Урок 2: Объяснение INVO
В этом втором руководстве мы объясним более полное приложение, чтобы углубить разработку с помощью Phalcon. INVO — одно из приложений, созданных нами в качестве примера. INVO — небольшой веб-сайт, который позволяет пользователям генерировать счета-фактуры и выполнять другие задачи, такие как управление клиентами и продуктами. Вы можете клонировать его код из Github.
Кроме того, INVO был создан с использованием Bootstrap в качестве фреймворка для клиентской части. Хотя приложение не генерирует счета-фактуры, оно по-прежнему служит примером для понимания того, как работает фреймворк.
Структура проекта
После клонирования проекта в корень вашего документа вы увидите следующую структуру:
invo/
app/
app/config/
app/controllers/
app/library/
app/models/
app/plugins/
app/views/
public/
public/bootstrap/
public/css/
public/js/
schemas/
Как вам известно, Phalcon не навязывает определённой структуры файлов для разработки приложений. Этот проект предоставляет простую структуру MVC и корень для общедоступных документов.
После открытия приложения в вашем браузере по адресу http://localhost/invo вы увидите нечто подобное:
Приложение разделено на две части: фронтенд, который представляет собой общедоступную часть, где посетители могут получить информацию об INVO и запросить контактную информацию. Вторая часть — бэкенд, административная область, где зарегистрированный пользователь может управлять своими продуктами и клиентами.
Маршрутизация
INVO использует стандартный маршрут, встроенный в компонент Router. Эти маршруты соответствуют следующему шаблону: /:controller/:action/:params. Это означает, что первая часть URI — контроллер, вторая — действие, а остальная часть — параметры.
Следующий маршрут /session/register выполняет контроллер SessionController и его действие registerAction.
Конфигурация
INVO имеет файл конфигурации, который устанавливает общие параметры приложения. Этот файл читается в первых строках файла загрузки (public/index.php):
//Read the configuration
$config = new Phalcon\Config\Adapter\Ini('../app/config/config.ini');
Phalcon\Config позволяет нам управлять файлом объектно-ориентированным способом. Файл конфигурации содержит следующие настройки:
[database] host = localhost username = root password = secret name = invo [application] controllersDir = /../app/controllers/ modelsDir = /../app/models/ viewsDir = /../app/views/ pluginsDir = /../app/plugins/ libraryDir = /../app/library/ baseUri = /invo/ ;[metadata] ;adapter = "Apc" ;suffix = my-suffix ;lifetime = 3600
Phalcon не имеет предопределённых конвенций. Разделы помогают нам организовать параметры должным образом. В этом файле есть три раздела, которые будут использованы позднее.
Автозагрузчики
Вторая часть, которая появляется в файле загрузки (public/index.php), — автозагрузчик. Автозагрузчик регистрирует набор директорий, в которых приложение будет искать классы, которые ему в конечном итоге понадобятся.
$loader = new \Phalcon\Loader();
$loader->registerDirs(
array(
$config->application->controllersDir,
$config->application->pluginsDir,
$config->application->libraryDir,
$config->application->modelsDir,
)
)->register();
Обратите внимание, что вышеприведенный код зарегистрировал директории, которые были определены в файле конфигурации. Единственная директория, которая не зарегистрирована, это viewsDir, потому что она содержит файлы HTML + PHP, но не классы.
Обработка запроса
Если мы перейдём к концу файла, запрос в конечном итоге обрабатывается Phalcon\Mvc\Application, который инициализирует и выполняет всё необходимое для запуска приложения:
$app = new \Phalcon\Mvc\Application($di); echo $app->handle()->getContent();
Внедрение зависимостей
Посмотрите на первую строку блока кода выше. Конструктор класса Application получает переменную $di в качестве аргумента. Какова цель этой переменной? Phalcon — высокодекомпозированный фреймворк, поэтому нам нужен компонент, который действует как клей, чтобы всё работало вместе. Этот компонент — Phalcon\DI. Это контейнер сервисов, который также выполняет внедрение зависимостей, создавая все компоненты по мере необходимости приложения.
Существует множество способов регистрации сервисов в контейнере. В INVO большинство сервисов зарегистрированы с использованием анонимных функций. Благодаря этому, объекты создаются ленивым способом, уменьшая ресурсы, необходимые приложению.
Например, в следующем фрагменте регистрируется сервис сессий. Анонимная функция будет вызвана только тогда, когда приложению потребуется доступ к данным сессии:
//Start the session the first time a component requests the session service
$di->set('session', function() {
$session = new Phalcon\Session\Adapter\Files();
$session->start();
return $session;
});
Здесь у нас есть свобода менять адаптер, выполнять дополнительную инициализацию и многое другое. Обратите внимание, что сервис был зарегистрирован под именем «session». Это соглашение, которое позволит фреймворку идентифицировать активный сервис в контейнере сервисов.
Запрос может использовать множество сервисов, и регистрация каждого сервиса по отдельности может быть трудоёмкой задачей. Поэтому фреймворк предоставляет вариант Phalcon\DI под названием Phalcon\DI\FactoryDefault, задача которого — регистрировать все сервисы, обеспечивая полнофункциональный фреймворк.
// The FactoryDefault Dependency Injector automatically registers the // right services providing a full-stack framework $di = new \Phalcon\DI\FactoryDefault();
Он регистрирует большинство сервисов с компонентами, предоставляемыми фреймворком по умолчанию. Если нам нужно переопределить определение какого-либо сервиса, мы можем просто установить его снова, как мы сделали выше с «session». Именно поэтому существует переменная $di.
Вход в приложение
Функциональность «Вход» позволит нам работать с контроллерами бэкенда. Разделение контроллеров бэкенда и фронтенда — лишь логично. Все контроллеры находятся в одной директории (app/controllers/).
Для входа в систему пользователи должны иметь действительное имя пользователя и пароль. Пользователи хранятся в таблице «users» в базе данных «invo».
Прежде чем мы сможем начать сеанс, нам нужно настроить подключение к базе данных в приложении. Сервис под названием «db» настроен в контейнере сервисов с информацией о подключении. Как и с автозагрузчиком, мы снова получаем параметры из файла конфигурации для настройки сервиса:
// Database connection is created based on parameters defined in the configuration file
$di->set('db', function() use ($config) {
return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
"host" => $config->database->host,
"username" => $config->database->username,
"password" => $config->database->password,
"dbname" => $config->database->name
));
});
Здесь мы возвращаем экземпляр адаптера подключения MySQL. При необходимости вы можете выполнить дополнительные действия, такие как добавление логирования, профилирования или изменение адаптера, настраивая его по своему желанию.
Следующая простая форма (app/views/session/index.phtml) запрашивает информацию для входа. Мы удалили некоторые HTML-коды, чтобы пример был более лаконичным:
<?php echo $this->tag->form('session/start') ?>
<label for="email">Username/Email</label>
<?php echo $this->tag->textField(array("email", "size" => "30")) ?>
<label for="password">Password</label>
<?php echo $this->tag->passwordField(array("password", "size" => "30")) ?>
<?php echo $this->tag->submitButton(array('Login')) ?>
</form>
Функция SessionController::startAction (app/controllers/SessionController.php) отвечает за валидацию данных, введённых в форму, включая проверку на наличие действительного пользователя в базе данных:
class SessionController extends ControllerBase
{
// ...
private function _registerSession($user)
{
$this->session->set('auth', array(
'id' => $user->id,
'name' => $user->name
));
}
public function startAction()
{
if ($this->request->isPost()) {
//Receiving the variables sent by POST
$email = $this->request->getPost('email', 'email');
$password = $this->request->getPost('password');
$password = sha1($password);
//Find the user in the database
$user = Users::findFirst(array(
"email = :email: AND password = :password: AND active = 'Y'",
"bind" => array('email' => $email, 'password' => $password)
));
if ($user != false) {
$this->_registerSession($user);
$this->flash->success('Welcome ' . $user->name);
//Forward to the 'invoices' controller if the user is valid
return $this->dispatcher->forward(array(
'controller' => 'invoices',
'action' => 'index'
));
}
$this->flash->error('Wrong email/password');
}
//Forward to the login form again
return $this->dispatcher->forward(array(
'controller' => 'session',
'action' => 'index'
));
}
}
Для простоты мы использовали «sha1» для хранения хэшей паролей в базе данных, однако этот алгоритм не рекомендуется в реальных приложениях. Используйте «bcrypt» вместо него.
Обратите внимание, что в контроллере обращается к нескольким общедоступным атрибутам, таким как: $this->flash, $this->request или $this->session. Это сервисы, определённые в контейнере сервисов ранее. При первом обращении к ним они внедряются как часть контроллера.
Эти сервисы общие, что означает, что мы всегда обращаемся к одному и тому же экземпляру независимо от места, где мы их вызываем.
Например, здесь мы вызываем сервис «session», а затем сохраняем идентификатор пользователя в переменной «auth»:
$this->session->set('auth', array(
'id' => $user->id,
'name' => $user->name
));
Защита бэкенда
Бэкенд — это закрытая область, к которой имеют доступ только зарегистрированные пользователи. Поэтому необходимо проверить, что только зарегистрированные пользователи имеют доступ к этим контроллерам. Если вы не вошли в систему и попытаетесь получить доступ, например, к контроллеру продуктов (который является закрытым), вы увидите экран, подобный этому:
Каждый раз, когда кто-то пытается получить доступ к любому контроллеру/действию, приложение проверяет, имеет ли текущая роль (в сессии) доступ к нему, в противном случае оно отображает сообщение, как выше, и перенаправляет поток на главную страницу.
Теперь давайте выясним, как приложение достигает этого. Первое, что нужно знать, это то, что есть компонент под названием Dispatcher. Он получает информацию о маршруте, найденном компонентом Routing. Затем он отвечает за загрузку соответствующего контроллера и выполнение соответствующего метода действия.
Обычно фреймворк создаёт Dispatcher автоматически. В нашем случае мы хотим выполнить проверку перед выполнением требуемого действия, проверяя, имеет ли пользователь к нему доступ или нет. Для этого мы заменили компонент, создав функцию в файле загрузки:
$di->set('dispatcher', function() use ($di) {
$dispatcher = new Phalcon\Mvc\Dispatcher();
return $dispatcher;
});
Теперь мы имеем полный контроль над Dispatcher, используемым в приложении. Многие компоненты фреймворка вызывают события, которые позволяют нам изменить их внутренний поток работы. Так как компонент Dependency Injector выступает в качестве клея для компонентов, новый компонент под названием EventsManager позволяет перехватывать события, генерируемые компонентом, маршрутизируя события к слушателям.
Управление событиями
EventsManager позволяет нам присоединять слушателей к определённому типу события. Тип, который нас сейчас интересует, — «dispatch». Следующий код фильтрует все события, генерируемые Dispatcher:
$di->set('dispatcher', function() use ($di) {
//Obtain the standard eventsManager from the DI
$eventsManager = $di->getShared('eventsManager');
//Instantiate the Security plugin
$security = new Security($di);
//Listen for events produced in the dispatcher using the Security plugin
$eventsManager->attach('dispatch', $security);
$dispatcher = new Phalcon\Mvc\Dispatcher();
//Bind the EventsManager to the Dispatcher
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
});
Плагин Security — класс, расположенный в (app/plugins/Security.php). Этот класс реализует метод «beforeDispatch». Это то же самое имя, что и одно из событий, генерируемых в Dispatcher:
use Phalcon\Events\Event,
Phalcon\Mvc\User\Plugin,
Phalcon\Mvc\Dispatcher,
Phalcon\Acl;
class Security extends Plugin
{
// ...
public function beforeDispatch(Event $event, Dispatcher $dispatcher)
{
// ...
}
}
События-хуки всегда получают первый параметр, содержащий контекстную информацию о событии ($event), и второй — объект, который сгенерировал событие ($dispatcher). Необязательно, чтобы плагины расширяли класс Phalcon\Mvc\User\Plugin, но делая это, они получают более простой доступ к доступным в приложении сервисам.
Теперь мы проверяем роль в текущей сессии, проверяя, имеет ли пользователь доступ, используя список ACL. Если у пользователя нет доступа, мы перенаправляем его на главную страницу, как описано ранее:
use Phalcon\Events\Event,
Phalcon\Mvc\User\Plugin,
Phalcon\Mvc\Dispatcher,
Phalcon\Acl;
class Security extends Plugin
{
// ...
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher)
{
//Check whether the "auth" variable exists in session to define the active role
$auth = $this->session->get('auth');
if (!$auth) {
$role = 'Guests';
} else {
$role = 'Users';
}
//Take the active controller/action from the dispatcher
$controller = $dispatcher->getControllerName();
$action = $dispatcher->getActionName();
//Obtain the ACL list
$acl = $this->getAcl();
//Check if the Role have access to the controller (resource)
$allowed = $acl->isAllowed($role, $controller, $action);
if ($allowed != Acl::ALLOW) {
//If he doesn't have access forward him to the index controller
$this->flash->error("You don't have access to this module");
$dispatcher->forward(
array(
'controller' => 'index',
'action' => 'index'
)
);
//Returning "false" we tell to the dispatcher to stop the current operation
return false;
}
}
}
Предоставление списка ACL
В вышеуказанном примере мы получили ACL с помощью метода $this->_getAcl(). Этот метод также реализован в плагине. Сейчас мы объясним поэтапно, как мы построили список контроля доступа (ACL):
//Create the ACL
$acl = new Phalcon\Acl\Adapter\Memory();
//The default action is DENY access
$acl->setDefaultAction(Phalcon\Acl::DENY);
//Register two roles, Users is registered users
//and guests are users without a defined identity
$roles = array(
'users' => new Phalcon\Acl\Role('Users'),
'guests' => new Phalcon\Acl\Role('Guests')
);
foreach ($roles as $role) {
$acl->addRole($role);
}
Теперь мы определяем ресурсы для каждой области соответственно. Имена контроллеров — ресурсы, а их действия — доступа к ресурсам:
//Private area resources (backend)
$privateResources = array(
'companies' => array('index', 'search', 'new', 'edit', 'save', 'create', 'delete'),
'products' => array('index', 'search', 'new', 'edit', 'save', 'create', 'delete'),
'producttypes' => array('index', 'search', 'new', 'edit', 'save', 'create', 'delete'),
'invoices' => array('index', 'profile')
);
foreach ($privateResources as $resource => $actions) {
$acl->addResource(new Phalcon\Acl\Resource($resource), $actions);
}
//Public area resources (frontend)
$publicResources = array(
'index' => array('index'),
'about' => array('index'),
'session' => array('index', 'register', 'start', 'end'),
'contact' => array('index', 'send')
);
foreach ($publicResources as $resource => $actions) {
$acl->addResource(new Phalcon\Acl\Resource($resource), $actions);
}
Теперь ACL знает о существующих контроллерах и связанных с ними действиях. Роль «Users» имеет доступ ко всем ресурсам как фронтенда, так и бэкенда. Роль «Guests» имеет доступ только к общедоступной области:
//Grant access to public areas to both users and guests
foreach ($roles as $role) {
foreach ($publicResources as $resource => $actions) {
$acl->allow($role->getName(), $resource, '*');
}
}
//Grant access to private area only to role Users
foreach ($privateResources as $resource => $actions) {
foreach ($actions as $action) {
$acl->allow('Users', $resource, $action);
}
}
Ура!, список ACL теперь завершён.
Компоненты пользователя
Все элементы пользовательского интерфейса и визуальный стиль приложения в основном достигнуты с помощью Bootstrap. Некоторые элементы, такие как навигационная панель, меняются в зависимости от состояния приложения. Например, в верхнем правом углу ссылка «Вход / Регистрация» меняется на «Выход», если пользователь вошёл в приложение.
Эта часть приложения реализована в компоненте «Elements» (app/library/Elements.php).
use Phalcon\Mvc\User\Component;
class Elements extends Component
{
public function getMenu()
{
//...
}
public function getTabs()
{
//...
}
}
Этот класс расширяет Phalcon\Mvc\User\Component, не обязательно расширять компонент этим классом, но он помогает быстрее получить доступ к сервисам приложения. Теперь мы регистрируем этот класс в контейнере сервисов:
//Register an user component
$di->set('elements', function(){
return new Elements();
});
Как контроллеры, плагины или компоненты внутри представления, этот компонент также имеет доступ к сервисам, зарегистрированным в контейнере, просто обращаясь к атрибуту с тем же именем, что и ранее зарегистрированный сервис:
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="#">INVO</a>
<?php echo $this->elements->getMenu() ?>
</div>
</div>
</div>
<div class="container">
<?php echo $this->getContent() ?>
<hr>
<footer>
<p>© Company 2012</p>
</footer>
</div>
Важный момент:
<?php echo $this->elements->getMenu() ?>
Работа с CRUD
Большинство опций, манипулирующих данными (компании, продукты и типы продуктов), были разработаны с использованием базового и распространённого CRUD (Создание, Чтение, Обновление и Удаление). Каждый CRUD содержит следующие файлы:
invo/
app/
app/controllers/
ProductsController.php
app/models/
Products.php
app/views/
products/
edit.phtml
index.phtml
new.phtml
search.phtml
Каждый контроллер имеет следующие действия:
class ProductsController extends ControllerBase
{
/**
* The start action, it shows the "search" view
*/
public function indexAction()
{
//...
}
/**
* Execute the "search" based on the criteria sent from the "index"
* Returning a paginator for the results
*/
public function searchAction()
{
//...
}
/**
* Shows the view to create a "new" product
*/
public function newAction()
{
//...
}
/**
* Shows the view to "edit" an existing product
*/
public function editAction()
{
//...
}
/**
* Creates a product based on the data entered in the "new" action
*/
public function createAction()
{
//...
}
/**
* Updates a product based on the data entered in the "edit" action
*/
public function saveAction()
{
//...
}
/**
* Deletes an existing product
*/
public function deleteAction($id)
{
//...
}
}
Форма поиска
Каждый CRUD начинается с формы поиска. Эта форма отображает каждое поле таблицы (продукты), позволяя пользователю создать критерии поиска по любому полю. Таблица «продукты» имеет отношение к таблице «типы_продуктов». В этом случае мы предварительно запросили записи в этой таблице, чтобы упростить поиск по этому полю:
/**
* The start action, it shows the "search" view
*/
public function indexAction()
{
$this->persistent->searchParams = null;
$this->view->productTypes = ProductTypes::find();
}
Все «типы продуктов» запрашиваются и передаются в представление в качестве локальной переменной «productTypes». Затем в представлении (app/views/index.phtml) мы отображаем тег «select», заполненный этими результатами:
<div>
<label for="product_types_id">Product Type</label>
<?php echo $this->tag->select(array(
"product_types_id",
$productTypes,
"using" => array("id", "name"),
"useDummy" => true
)) ?>
</div>
Обратите внимание, что $productTypes содержит данные, необходимые для заполнения тега SELECT с помощью Phalcon\Tag::select. После отправки формы выполняется действие «search» в контроллере, выполняющем поиск на основе данных, введённых пользователем.
Выполнение поиска
Действие «search» имеет двойное поведение. При обращении по POST оно выполняет поиск на основе данных, отправленных из формы. Но при обращении по GET оно перемещает текущую страницу в пагинаторе. Для различения методов HTTP мы используем компонент Request:
/**
* Execute the "search" based on the criteria sent from the "index"
* Returning a paginator for the results
*/
public function searchAction()
{
if ($this->request->isPost()) {
//create the query conditions
} else {
//paginate using the existing conditions
}
//...
}
С помощью Phalcon\Mvc\Model\Criteria мы можем интеллектуально создавать условия поиска на основе типов данных и значений, отправленных из формы:
$query = Criteria::fromInput($this->di, "Products", $_POST);
Этот метод проверяет, какие значения отличаются от «» (пустая строка) и null, и учитывает их при создании критериев поиска:
- Если тип данных поля — текст или аналогичный (char, varchar, text и т. д.), он использует оператор SQL «like» для фильтрации результатов.
- Если тип данных не текст или аналогичный, он использует оператор «=».
Кроме того, «Criteria» игнорирует все переменные $_POST, которые не соответствуют ни одному полю в таблице. Значения автоматически экранируются с помощью «связанных параметров».
Теперь мы сохраняем сгенерированные параметры в сессионном пакете контроллера:
$this->persistent->searchParams = $query->getParams();
Сессионный пакет — это специальный атрибут в контроллере, который сохраняется между запросами. При обращении к этому атрибуту впрыскивается сервис Phalcon\Session\Bag, независимый в каждом контроллере.
Затем, на основе построенных параметров, мы выполняем запрос:
$products = Products::find($parameters);
if (count($products) == 0) {
$this->flash->notice("The search did not found any products");
return $this->forward("products/index");
}
Если поиск не возвращает ни одного продукта, мы перенаправляем пользователя на действие index снова. Предположим, что поиск вернул результаты, тогда мы создаём пагинатор для удобной навигации по ним:
$paginator = new Phalcon\Paginator\Adapter\Model(array(
"data" => $products, //Data to paginate
"limit" => 5, //Rows per page
"page" => $numberPage //Active page
));
//Get active page in the paginator
$page = $paginator->getPaginate();
Наконец, мы передаём возвращённую страницу в представление:
$this->view->setVar("page", $page);
В представлении (app/views/products/search.phtml) мы просматриваем результаты, соответствующие текущей странице:
<?php foreach ($page->items as $product) { ?>
<tr>
<td><?= $product->id ?></td>
<td><?= $product->getProductTypes()->name ?></td>
<td><?= $product->name ?></td>
<td><?= $product->price ?></td>
<td><?= $product->active ?></td>
<td><?= $this->tag->linkTo("products/edit/" . $product->id, 'Edit') ?></td>
<td><?= $this->tag->linkTo("products/delete/" . $product->id, 'Delete') ?></td>
</tr>
<?php } ?>
Создание и обновление записей
Теперь давайте посмотрим, как CRUD создаёт и обновляет записи. Из представлений «new» и «edit» данные, введённые пользователем, отправляются в действия «create» и «save», которые выполняют действия «создания» и «обновления» продуктов соответственно.
В случае создания мы восстанавливаем отправленные данные и присваиваем их новой инстанции «продукты»:
/**
* Creates a product based on the data entered in the "new" action
*/
public function createAction()
{
$products = new Products();
$products->id = $this->request->getPost("id", "int");
$products->product_types_id = $this->request->getPost("product_types_id", "int");
$products->name = $this->request->getPost("name", "striptags");
$products->price = $this->request->getPost("price", "double");
$products->active = $this->request->getPost("active");
//...
}
Данные фильтруются перед присвоением объекту. Этот фильтр необязателен, ORM экранирует входные данные и выполняет дополнительное преобразование в соответствии с типами столбцов.
При сохранении мы узнаем, соответствуют ли данные бизнес-правилам и валидации, реализованным в модели Products:
/**
* Creates a product based on the data entered in the "new" action
*/
public function createAction()
{
//...
if (!$products->create()) {
//The store failed, the following messages were produced
foreach ($products->getMessages() as $message) {
$this->flash->error((string) $message);
}
return $this->forward("products/new");
} else {
$this->flash->success("Product was created successfully");
return $this->forward("products/index");
}
}
Теперь, в случае обновления продукта, мы должны сначала отобразить пользователю данные, которые в настоящее время находятся в редактируемой записи:
/**
* Shows the view to "edit" an existing product
*/
public function editAction($id)
{
//...
$product = Products::findFirstById($id);
$this->tag->setDefault("id", $product->id);
$this->tag->setDefault("product_types_id", $product->product_types_id);
$this->tag->setDefault("name", $product->name);
$this->tag->setDefault("price", $product->price);
$this->tag->setDefault("active", $product->active);
}
Помощник «setDefault» задаёт значение по умолчанию в форме для атрибута с тем же именем. Благодаря этому пользователь может изменить любое значение и затем отправить его обратно в базу данных через действие «save»:
/**
* Updates a product based on the data entered in the "edit" action
*/
public function saveAction()
{
//...
//Find the product to update
$id = $this->request->getPost("id");
$product = Products::findFirstById($id);
if (!$product) {
$this->flash->error("products does not exist " . $id);
return $this->forward("products/index");
}
//... assign the values to the object and store it
}
Динамическое изменение заголовка
При переходе между вариантами вы увидите, что заголовок динамически изменяется, указывая, с чем мы работаем в данный момент. Это достигается в инициализаторе каждого контроллера:
class ProductsController extends ControllerBase
{
public function initialize()
{
//Set the document title
$this->tag->setTitle('Manage your product types');
parent::initialize();
}
//...
}
Обратите внимание, что метод parent::initialize() также вызывается, он добавляет больше данных в заголовок:
class ControllerBase extends Phalcon\Mvc\Controller
{
protected function initialize()
{
//Prepend the application name to the title
$this->tag->prependTitle('INVO | ');
}
//...
}
Наконец, заголовок отображается в главном представлении (app/views/index.phtml):
<!DOCTYPE html>
<html>
<head>
<?php echo $this->tag->getTitle() ?>
</head>
<!-- ... -->
</html>
Заключение
Этот учебник охватывает множество аспектов создания приложений с помощью Phalcon. Надеюсь, он поможет вам узнать больше и извлечь больше пользы из фреймворка.
© 2011–2016 Phalcon Framework Team
Licensed under the Creative Commons Attribution License 3.0.
https://docs.phalconphp.com/en/2.0.0/reference/tutorial-invo.html