企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] # 教程:INVO 在第二篇教程中,我们将解释一个更完整的应用程序,以便更深入地了解使用Phalcon进行开发。INVO是我们创建的示例应用程序之一。INVO是一个小型网站,允许用户生成发票并执行其他任务,例如管理客户和产品。您可以从[Github](https://github.com/phalcon/invo)克隆其代码。 INVO是使用客户端框架[Bootstrap](http://getbootstrap.com/)制作的。虽然应用程序不会生成实际的发票,但它仍然可以作为一个示例来说明框架的工作原理。 ## 项目结构 在文档根目录中克隆项目后,您将看到以下结构: ```bash invo/ app/ config/ controllers/ forms/ library/ logs/ models/ plugins/ views/ cache/ volt/ docs/ public/ css/ fonts/ js/ schemas/ ``` 如您所知,Phalcon没有为应用程序开发强加特定的文件结构。该项目具有简单的MVC结构和更目录public。 在浏览器`http://localhost/invo`中打开应用程序后,您将看到如下内容: ![](https://docs.phalconphp.com/images/content/tutorial-invo-1.png) 该应用程序分为两个部分:前端和后端。前端是一个公共区域,访客可以接收有关INVO的信息并请求联系信息。后端是一个管理区域,注册用户可以在其中管理其产品和客户。 ## 路由 INVO使用与Router组件内置的标准路由。这些路由符合以下模式:`/:controller/:action/:params`。这意味着URI的第一部分是控制器,第二部分是控制器动作,其余部分是参数。 以下路由`/session/register`执行控制器`SessionController`及其操作`registerAction`。 ## 配置 INVO有一个配置文件,用于设置应用程序中的常规参数。该文件位于`app/config/config.ini`,并加载到应用程序引导程序的第一行(`public/index.php`): ```php <?php use Phalcon\Config\Adapter\Ini as ConfigIni; // ... // Read the configuration $config = new ConfigIni( APP_PATH . 'app/config/config.ini' ); ``` Phalcon Config(`Phalcon\Config`)允许我们以面向对象的方式操作文件。在这个例子中,我们使用ini文件进行配置,但Phalcon也有其他文件类型的`适配器`。配置文件包含以下设置: ```ini [database] host = localhost username = root password = secret name = invo [application] controllersDir = app/controllers/ modelsDir = app/models/ viewsDir = app/views/ pluginsDir = app/plugins/ formsDir = app/forms/ libraryDir = app/library/ baseUri = /invo/ ``` Phalcon没有任何预定义的设置约定。章节可以帮助我们根据需要组织选项。在此文件中,稍后将使用两个部分:`application`和`database`。 ## 自动加载 引导文件(`public/index.php`)中出现的第二部分是自动加载器: ```php <?php /** * Auto-loader configuration */ require APP_PATH . 'app/config/loader.php'; ``` 自动加载器注册一组目录,应用程序将在其中查找最终需要的类。 ```php <?php $loader = new Phalcon\Loader(); // We're a registering a set of directories taken from the configuration file $loader->registerDirs( [ APP_PATH . $config->application->controllersDir, APP_PATH . $config->application->pluginsDir, APP_PATH . $config->application->libraryDir, APP_PATH . $config->application->modelsDir, APP_PATH . $config->application->formsDir, ] ); $loader->register(); ``` 请注意,上面的代码已注册配置文件中定义的目录。唯一没有注册的目录是viewsDir,因为它包含HTML + PHP文件但没有类。另外,请注意我们使用一个名为APP_PATH的常量。此常量在bootstrap(`public/index.php`)中定义,以允许我们引用项目的根目录: ```php <?php // ... define( 'APP_PATH', realpath('..') . '/' ); ``` ## 注册服务 引导程序中需要的另一个文件是(`app/config/services.php`)。该文件允许我们组织INVO使用的服务。 ```php <?php /** * Load application services */ require APP_PATH . 'app/config/services.php'; ``` 使用闭包来实现服务注册,以便延迟加载所需的组件: ```php <?php use Phalcon\Mvc\Url as UrlProvider; // ... /** * The URL component is used to generate all kind of URLs in the application */ $di->set( 'url', function () use ($config) { $url = new UrlProvider(); $url->setBaseUri( $config->application->baseUri ); return $url; } ); ``` 我们稍后会深入讨论这个文件。 ## 处理请求 如果我们跳到文件的末尾(`public/index.php`),请求最终由`Phalcon\Mvc\Application`处理,它初始化并执行使应用程序运行所需的所有内容: ```php <?php use Phalcon\Mvc\Application; // ... $application = new Application($di); $response = $application->handle(); $response->send(); ``` ## 依赖注入 在上面代码块的第一行中,Application类构造函数接收变量`$di`作为参数。这个变量的目的是什么?Phalcon是一个高度分离的框架,因此我们需要一个充当胶水的组件,以使一切工作在一起。该组件是`Phalcon\Di`.它是一个服务容器,它还执行依赖注入和服务定位,实例化应用程序所需的所有组件。 在容器中注册服务的方法有很多种。在INVO中,大多数服务已使用匿名函数/闭包进行注册。由于这个原因,对象以惰性方式实例化,减少了应用程序所需的资源。 例如,在以下摘录中注册会话服务。只有在应用程序需要访问会话数据时才会调用匿名函数: ```php <?php use Phalcon\Session\Adapter\Files as Session; // ... // Start the session the first time a component requests the session service $di->set( 'session', function () { $session = new Session(); $session->start(); return $session; } ); ``` 在这里,我们可以自由更改适配器,执行额外的初始化等等。请注意,该服务是使用名称`session`注册的。这是一种允许框架识别服务容器中的活动服务的约定。 请求可以使用许多服务,并且单独注册每个服务可能是一项繁琐的任务。因此,该框架提供了一个名为`Phalcon\Di\FactoryDefault`的`Phalcon\Di`变体,其任务是注册提供全栈框架的所有服务。 ```php <?php use Phalcon\Di\FactoryDefault; // ... // The FactoryDefault Dependency Injector automatically registers the // right services providing a full-stack framework $di = new FactoryDefault(); ``` 它将大多数服务与框架提供的组件作为标准注册。如果我们需要覆盖某些服务的定义,我们可以像上面使用`session`或`url`一样重新设置它。这就是变量`$di`存在的原因。 ## 登录应用程序 登录工具将允许我们处理后端控制器。后端控制器和前端控制器之间的分离是合乎逻辑的。所有控制器都位于同一目录(`app/controllers/`)中。 要进入系统,用户必须拥有有效的用户名和密码。用户存储在数据库`invo`中的表 `users`中。 在我们开始会话之前,我们需要在应用程序中配置与数据库的连接。在服务容器中使用连接信息设置名为`db`的服务。与自动加载器一样,我们再次从配置文件中获取参数以配置服务: ```php <?php use Phalcon\Db\Adapter\Pdo\Mysql as DbAdapter; // ... // Database connection is created based on parameters defined in the configuration file $di->set( 'db', function () use ($config) { return new DbAdapter( [ 'host' => $config->database->host, 'username' => $config->database->username, 'password' => $config->database->password, 'dbname' => $config->database->name, ] ); } ); ``` 在这里,我们返回一个MySQL连接适配器的实例。如果需要,您可以执行额外操作,例如添加记录器,分析器或更改适配器,根据需要进行设置。 以下简单表单(`app/views/session/index.volt`)请求登录信息。我们删除了一些HTML代码,以使示例更简洁: ```twig {{ form('session/start') }} <fieldset> <div> <label for='email'> Username/Email </label> <div> {{ text_field('email') }} </div> </div> <div> <label for='password'> Password </label> <div> {{ password_field('password') }} </div> </div> <div> {{ submit_button('Login') }} </div> </fieldset> {{ endForm() }} ``` 我们开始使用`Volt`,而不是使用原始PHP作为上一个教程。这是一个内置的模板引擎,受`Jinja`的启发,提供了一种更简单友好的语法来创建模板。在你熟悉`Volt`之前不会花太多时间。 `SessionController::startAction`函数(`app/controllers/SessionController.php`)的任务是验证表单中输入的数据,包括检查数据库中的有效用户: ```php <?php class SessionController extends ControllerBase { // ... private function _registerSession($user) { $this->session->set( 'auth', [ 'id' => $user->id, 'name' => $user->name, ] ); } /** * This action authenticate and logs a user into the application */ public function startAction() { if ($this->request->isPost()) { // Get the data from the user $email = $this->request->getPost('email'); $password = $this->request->getPost('password'); // Find the user in the database $user = Users::findFirst( [ "(email = :email: OR username = :email:) AND password = :password: AND active = 'Y'", 'bind' => [ 'email' => $email, 'password' => sha1($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( [ 'controller' => 'invoices', 'action' => 'index', ] ); } $this->flash->error( 'Wrong email/password' ); } // Forward to the login form again return $this->dispatcher->forward( [ 'controller' => 'session', 'action' => 'index', ] ); } } ``` 为简单起见,我们使用`sha1`在数据库中存储密码哈希,但是,在实际应用中不建议使用此算法,而是使用`bcrypt`。 请注意,在控制器中访问多个公共属性,如:`$this->flash`,`$this->request` 或`$this->session`。这些是早期服务容器中定义的服务(`app/config/services.php`)。当它们第一次被访问时,它们作为控制器的一部分被注入。这些服务是`共享`的,这意味着无论我们调用它们的位置如何,我们总是访问同一个实例。例如,这里我们调用`session`服务,然后将用户身份存储在变量`auth`中: ```php <?php $this->session->set( 'auth', [ 'id' => $user->id, 'name' => $user->name, ] ); ``` 本节的另一个重要方面是如何将用户验证为有效用户,首先我们验证请求是否已使用方法`POST`进行: ```php <?php if ($this->request->isPost()) { // ... } ``` 然后,我们从表单中接收参数: ```php <?php $email = $this->request->getPost('email'); $password = $this->request->getPost('password'); ``` 现在,我们必须检查是否有一个用户具有相同的用户名或电子邮件和密码: ```php <?php $user = Users::findFirst( [ "(email = :email: OR username = :email:) AND password = :password: AND active = 'Y'", 'bind' => [ 'email' => $email, 'password' => sha1($password), ] ] ); ``` 注意,使用'bound parameters',占位符`:email:`和`:password:`放置在值应该的位置,然后使用参数`bind`绑定值。这可以安全地替换这些列的值,而不会有SQL注入的风险。 如果用户有效,我们会在会话中注册并将他/她转发到dashboard: ```php <?php if ($user !== false) { $this->_registerSession($user); $this->flash->success( 'Welcome ' . $user->name ); return $this->dispatcher->forward( [ 'controller' => 'invoices', 'action' => 'index', ] ); } ``` 如果用户不存在,我们会再次将用户转发回显示表单的操作: ```php <?php return $this->dispatcher->forward( [ 'controller' => 'session', 'action' => 'index', ] ); ``` ## 后端安全 后端是一个私有区域,只有注册用户才能访问。因此,有必要检查只有注册用户才能访问这些控制器。如果您没有登录到应用程序并尝试访问,例如,产品控制器(私有),您将看到如下屏幕: ![](https://docs.phalconphp.com/images/content/tutorial-invo-2.png) 每当有人尝试访问任何控制器/操作时,应用程序都会验证当前角色(在session中)是否可以访问它,否则它会显示如上所述的消息并将流转发到主页。 现在让我们看看应用程序如何实现这一点。首先要知道的是,有一个名为`Dispatcher`的组件。它被告知`Routing`组件找到的路由。然后,它负责加载适当的控制器并执行相应的操作方法。 通常,框架会自动创建`Dispatcher`。在我们的例子中,我们希望在执行所需操作之前执行验证,检查用户是否可以访问它。为此,我们通过在引导程序中创建函数来替换组件: ```php <?php use Phalcon\Mvc\Dispatcher; // ... /** * MVC dispatcher */ $di->set( 'dispatcher', function () { // ... $dispatcher = new Dispatcher(); return $dispatcher; } ); ``` 我们现在可以完全控制应用程序中使用的Dispatcher。框架中的许多组件触发允许我们修改其内部操作流程的事件。由于依赖注入组件充当组件的粘合剂,因此名为`EventsManager`的新组件允许我们拦截组件生成的事件,将事件路由到侦听器。 ### 事件管理 `EventsManager`允许我们将侦听器附加到特定类型的事件。我们现在感兴趣的类型是“dispatch”。以下代码过滤`Dispatcher`生成的所有事件: ```php <?php use Phalcon\Mvc\Dispatcher; use Phalcon\Events\Manager as EventsManager; $di->set( 'dispatcher', function () { // Create an events manager $eventsManager = new EventsManager(); // Listen for events produced in the dispatcher using the Security plugin $eventsManager->attach( 'dispatch:beforeExecuteRoute', new SecurityPlugin() ); // Handle exceptions and not-found exceptions using NotFoundPlugin $eventsManager->attach( 'dispatch:beforeException', new NotFoundPlugin() ); $dispatcher = new Dispatcher(); // Assign the events manager to the dispatcher $dispatcher->setEventsManager($eventsManager); return $dispatcher; } ); ``` 当触发一个名为`beforeExecuteRoute`的事件时,将通知以下插件: ```php <?php /** * Check if the user is allowed to access certain action using the SecurityPlugin */ $eventsManager->attach( 'dispatch:beforeExecuteRoute', new SecurityPlugin() ); ``` 触发`beforeException`时,会通知其他插件: ```php <?php /** * Handle exceptions and not-found exceptions using NotFoundPlugin */ $eventsManager->attach( 'dispatch:beforeException', new NotFoundPlugin() ); ``` SecurityPlugin是一个位于(`app/plugins/SecurityPlugin.php`)的类。此类实现`beforeExecuteRoute`之前的方法。这与Dispatcher中生成的事件之一相同: ```php <?php use Phalcon\Events\Event; use Phalcon\Mvc\User\Plugin; use Phalcon\Mvc\Dispatcher; class SecurityPlugin extends Plugin { // ... public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher) { // ... } } ``` 钩子事件总是接收第一个参数,该参数包含所生成事件的上下文信息(`$event`),第二个参数是生成事件本身的对象(`$dispatcher`)。插件扩展`Phalcon\Mvc\User\Plugin`类并不是必须的,但通过这样做,他们可以更轻松地访问应用程序中可用的服务。 现在,我们将验证当前会话中的角色,检查用户是否具有使用ACL列表的访问权限。如果用户没有访问权限,我们会重定向到主屏幕,如前所述: ```php <?php use Phalcon\Acl; use Phalcon\Events\Event; use Phalcon\Mvc\User\Plugin; use Phalcon\Mvc\Dispatcher; class SecurityPlugin 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) { // 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( [ 'controller' => 'index', 'action' => 'index', ] ); // Returning 'false' we tell to the dispatcher to stop the current operation return false; } } } ``` ### 获取ACL列表 在上面的例子中,我们使用`$this->getAcl()`方法获得了ACL。此方法也在插件中实现。现在我们将逐步解释如何构建访问控制列表(ACL): ```php <?php use Phalcon\Acl; use Phalcon\Acl\Role; use Phalcon\Acl\Adapter\Memory as AclList; // Create the ACL $acl = new AclList(); // The default action is DENY access $acl->setDefaultAction( Acl::DENY ); // Register two roles, Users is registered users // and guests are users without a defined identity $roles = [ 'users' => new Role('Users'), 'guests' => new Role('Guests'), ]; foreach ($roles as $role) { $acl->addRole($role); } ``` 现在,我们分别为每个区域定义资源。控制器名称是资源,其操作是对资源的访问: ```php <?php use Phalcon\Acl\Resource; // ... // Private area resources (backend) $privateResources = [ 'companies' => ['index', 'search', 'new', 'edit', 'save', 'create', 'delete'], 'products' => ['index', 'search', 'new', 'edit', 'save', 'create', 'delete'], 'producttypes' => ['index', 'search', 'new', 'edit', 'save', 'create', 'delete'], 'invoices' => ['index', 'profile'], ]; foreach ($privateResources as $resourceName => $actions) { $acl->addResource( new Resource($resourceName), $actions ); } // Public area resources (frontend) $publicResources = [ 'index' => ['index'], 'about' => ['index'], 'register' => ['index'], 'errors' => ['show404', 'show500'], 'session' => ['index', 'register', 'start', 'end'], 'contact' => ['index', 'send'], ]; foreach ($publicResources as $resourceName => $actions) { $acl->addResource( new Resource($resourceName), $actions ); } ``` ACL现在知道现有控制器及其相关操作。角色`Users`可以访问前端和后端的所有资源。角色`Guests` 只能访问公共区域: ```php <?php // 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 ); } } ``` ## 使用 CRUD 后端通常提供允许用户操作数据的表单。继续对INVO的解释,我们现在讨论CRUD的创建,这是Phalcon将使用表单,验证,分页器等方式为您提供的一项非常常见的任务。 大多数操纵INVO中数据的选项(公司,产品和产品类型)都是使用基本和通用 [CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete) (创建,读取,更新和删除)开发的。每个CRUD包含以下文件: ```bash invo/ app/ controllers/ ProductsController.php models/ Products.php forms/ ProductsForm.php views/ products/ edit.volt index.volt new.volt search.volt ``` 每个控制器都有以下操作: ```php <?php 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都以搜索表单开头。此表单显示表具有的每个字段(产品),允许用户为任何字段创建搜索条件。`products`表与表`products_types`有关系。在这种情况下,我们先前查询了此表中的记录,以便于按该字段进行搜索: ```php <?php /** * The start action, it shows the 'search' view */ public function indexAction() { $this->persistent->searchParams = null; $this->view->form = new ProductsForm(); } ``` `ProductsForm`表单的一个实例(`app/forms/ProductsForm.php`)被传递给视图。此表单定义了用户可见的字段: ```php <?php use Phalcon\Forms\Form; use Phalcon\Forms\Element\Text; use Phalcon\Forms\Element\Hidden; use Phalcon\Forms\Element\Select; use Phalcon\Validation\Validator\Email; use Phalcon\Validation\Validator\PresenceOf; use Phalcon\Validation\Validator\Numericality; class ProductsForm extends Form { /** * Initialize the products form */ public function initialize($entity = null, $options = []) { if (!isset($options['edit'])) { $element = new Text('id'); $element->setLabel('Id'); $this->add($element); } else { $this->add(new Hidden('id')); } $name = new Text('name'); $name->setLabel('Name'); $name->setFilters( [ 'striptags', 'string', ] ); $name->addValidators( [ new PresenceOf( [ 'message' => 'Name is required', ] ) ] ); $this->add($name); $type = new Select( 'profilesId', ProductTypes::find(), [ 'using' => [ 'id', 'name', ], 'useEmpty' => true, 'emptyText' => '...', 'emptyValue' => '', ] ); $this->add($type); $price = new Text('price'); $price->setLabel('Price'); $price->setFilters( [ 'float', ] ); $price->addValidators( [ new PresenceOf( [ 'message' => 'Price is required', ] ), new Numericality( [ 'message' => 'Price is required', ] ), ] ); $this->add($price); } } ``` 使用基于表单组件提供的元素的面向对象方案声明表单。每个元素都遵循几乎相同的结构: ```php <?php // Create the element $name = new Text('name'); // Set its label $name->setLabel('Name'); // Before validating the element apply these filters $name->setFilters( [ 'striptags', 'string', ] ); // Apply this validators $name->addValidators( [ new PresenceOf( [ 'message' => 'Name is required', ] ) ] ); // Add the element to the form $this->add($name); ``` 其他元素也以这种形式使用: ```php <?php // Add a hidden input to the form $this->add( new Hidden('id') ); // ... $productTypes = ProductTypes::find(); // Add a HTML Select (list) to the form // and fill it with data from 'product_types' $type = new Select( 'profilesId', $productTypes, [ 'using' => [ 'id', 'name', ], 'useEmpty' => true, 'emptyText' => '...', 'emptyValue' => '', ] ); ``` 请注意,`ProductTypes::find()`包含使用`Phalcon\Tag::select()`填充SELECT标记所需的数据。将表单传递给视图后,可以将其呈现并呈现给用户: ```twig {{ form('products/search') }} <h2> Search products </h2> <fieldset> {% for element in form %} <div class='control-group'> {{ element.label(['class': 'control-label']) }} <div class='controls'> {{ element }} </div> </div> {% endfor %} <div class='control-group'> {{ submit_button('Search', 'class': 'btn btn-primary') }} </div> </fieldset> {{ endForm() }} ``` 这会产生以下HTML: ```html <form action='/invo/products/search' method='post'> <h2> Search products </h2> <fieldset> <div class='control-group'> <label for='id' class='control-label'>Id</label> <div class='controls'> <input type='text' id='id' name='id' /> </div> </div> <div class='control-group'> <label for='name' class='control-label'>Name</label> <div class='controls'> <input type='text' id='name' name='name' /> </div> </div> <div class='control-group'> <label for='profilesId' class='control-label'>profilesId</label> <div class='controls'> <select id='profilesId' name='profilesId'> <option value=''>...</option> <option value='1'>Vegetables</option> <option value='2'>Fruits</option> </select> </div> </div> <div class='control-group'> <label for='price' class='control-label'>Price</label> <div class='controls'> <input type='text' id='price' name='price' /> </div> </div> <div class='control-group'> <input type='submit' value='Search' class='btn btn-primary' /> </div> </fieldset> </form> ``` 当提交表单时,在控制器中执行`search`动作,该控制器基于用户输入的数据执行搜索。 ## 执行搜索 `search` 操作有两种行为。当通过POST访问时,它会根据表单发送的数据执行搜索,但是当通过GET访问时,它会移动分页器中的当前页面。为区分HTTP方法,我们使用`Request`组件进行检查: ```php <?php /** * 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`的帮助下,我们可以根据表单发送的数据类型和值智能地创建搜索条件: ```php <?php $query = Criteria::fromInput( $this->di, 'Products', $this->request->getPost() ); ``` 此方法验证哪些值与''(空字符串)和null不同,并将它们考虑在内以创建搜索条件: * 如果字段数据类型是文本或类似(char,varchar,text等),它使用SQL的`like`运算符来过滤结果。 * 如果数据类型不是文本或类似的,它将使用 `=` 运算符。 此外,`Criteria`忽略所有与表中的任何字段都不匹配的`$_POST`变量。使用`绑定参数`自动转义值。 现在,我们将生成的参数存储在控制器的会话包中: ```php <?php $this->persistent->searchParams = $query->getParams(); ``` 会话包是控制器中的一个特殊属性,它在使用会话服务的请求之间保持不变。访问时,此属性会在每个控制器中注入一个独立的`Phalcon\Session\Bag`实例。 然后,基于构建的参数,我们执行查询: ```php <?php $products = Products::find($parameters); if (count($products) === 0) { $this->flash->notice( 'The search did not found any products' ); return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'index', ] ); } ``` 如果搜索未返回任何产品,我们会再次将用户转发给索引操作。让我们假装搜索返回结果,然后我们创建一个分页器来轻松浏览它们: ```php <?php use Phalcon\Paginator\Adapter\Model as Paginator; // ... $paginator = new Paginator( [ 'data' => $products, // Data to paginate 'limit' => 5, // Rows per page 'page' => $numberPage, // Active page ] ); // Get active page in the paginator $page = $paginator->getPaginate(); ``` 最后我们传递返回的页面到视图: ```php <?php $this->view->page = $page; ``` 在视图(`app/views/products/search.volt`)中,我们遍历与当前页面对应的结果,向用户显示当前页面中的每一行: ```twig {% for product in page.items %} {% if loop.first %} <table> <thead> <tr> <th>Id</th> <th>Product Type</th> <th>Name</th> <th>Price</th> <th>Active</th> </tr> </thead> <tbody> {% endif %} <tr> <td> {{ product.id }} </td> <td> {{ product.getProductTypes().name }} </td> <td> {{ product.name }} </td> <td> {{ '%.2f'|format(product.price) }} </td> <td> {{ product.getActiveDetail() }} </td> <td width='7%'> {{ link_to('products/edit/' ~ product.id, 'Edit') }} </td> <td width='7%'> {{ link_to('products/delete/' ~ product.id, 'Delete') }} </td> </tr> {% if loop.last %} </tbody> <tbody> <tr> <td colspan='7'> <div> {{ link_to('products/search', 'First') }} {{ link_to('products/search?page=' ~ page.before, 'Previous') }} {{ link_to('products/search?page=' ~ page.next, 'Next') }} {{ link_to('products/search?page=' ~ page.last, 'Last') }} <span class='help-inline'>{{ page.current }} of {{ page.total_pages }}</span> </div> </td> </tr> </tbody> </table> {% endif %} {% else %} No products are recorded {% endfor %} ``` 上面的例子中有很多东西值得详细说明。首先,使用Volt `for`来遍历当前页面中的活动项目。Volt为PHP `foreach`提供了更简单的语法。 ```twig {% for product in page.items %} ``` 与在PHP中的以下内容相同: ```php <?php foreach ($page->items as $product) { ?> ``` 整个`for` 块提供以下内容: ```twig {% for product in page.items %} {% if loop.first %} Executed before the first product in the loop {% endif %} Executed for every product of page.items {% if loop.last %} Executed after the last product is loop {% endif %} {% else %} Executed if page.items does not have any products {% endfor %} ``` 现在,您可以返回视图并查看每个块正在执行的操作。`product`中的每个字段都相应打印: ```twig <tr> <td> {{ product.id }} </td> <td> {{ product.productTypes.name }} </td> <td> {{ product.name }} </td> <td> {{ '%.2f'|format(product.price) }} </td> <td> {{ product.getActiveDetail() }} </td> <td width='7%'> {{ link_to('products/edit/' ~ product.id, 'Edit') }} </td> <td width='7%'> {{ link_to('products/delete/' ~ product.id, 'Delete') }} </td> </tr> ``` 正如我们之前看到的那样,使用`product.id`与PHP中的相同:`$product->id`,我们使用`product.name`进行相同的操作。其他字段的呈现方式不同,例如,让我们将焦点放在`product.productTypes.name`中。要理解这一部分,我们必须查看Product模型(`app/models/Products.php`): ```php <?php use Phalcon\Mvc\Model; /** * Products */ class Products extends Model { // ... /** * Products initializer */ public function initialize() { $this->belongsTo( 'product_types_id', 'ProductTypes', 'id', [ 'reusable' => true, ] ); } // ... } ``` 模型可以有一个名为`initialize()`的方法,每个请求调用一次该方法,并为ORM初始化模型。在这种情况下,通过定义此模型与另一个名为“ProductTypes”的模型具有一对多关系来初始化“Products”。 ```php <?php $this->belongsTo( 'product_types_id', 'ProductTypes', 'id', [ 'reusable' => true, ] ); ``` 这意味着,`Products`中的本地属性`product_types_id`在其属性`id`中与`ProductTypes`模型具有一对多的关系。通过定义此关系,我们可以使用以下方法访问产品类型的名称: ```twig <td>{{ product.productTypes.name }}</td> ``` 使用Volt过滤器格式化字段`price` : ```twig <td>{{ '%.2f'|format(product.price) }}</td> ``` 在普通的PHP中,这将是: ```php <?php echo sprintf('%.2f', $product->price) ?> ``` 打印产品是否处于活动状态使用模型中实现的帮助程序: ```php <td>{{ product.getActiveDetail() }}</td> ``` 此方法在模型中定义。 ## 创建和更新记录 现在让我们看看CRUD如何创建和更新记录。在`new`视图和`edit`视图中,用户输入的数据将分别发送到执行创建和更新产品操作的`create`和`save` 操作。 在创建案例中,我们恢复提交的数据并将其分配给新的`Products`实例: ```php <?php /** * Creates a product based on the data entered in the 'new' action */ public function createAction() { if (!$this->request->isPost()) { return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'index', ] ); } $form = new ProductsForm(); $product = new Products(); $product->id = $this->request->getPost('id', 'int'); $product->product_types_id = $this->request->getPost('product_types_id', 'int'); $product->name = $this->request->getPost('name', 'striptags'); $product->price = $this->request->getPost('price', 'double'); $product->active = $this->request->getPost('active'); // ... } ``` 还记得我们在产品表单中定义的过滤器吗?在分配给对象`$product`之前过滤数据。这种过滤是可选的;ORM还会转义输入数据并根据列类型执行其他转换: ```php <?php // ... $name = new Text('name'); $name->setLabel('Name'); // Filters for name $name->setFilters( [ 'striptags', 'string', ] ); // Validators for name $name->addValidators( [ new PresenceOf( [ 'message' => 'Name is required', ] ) ] ); $this->add($name); ``` 保存时,我们将知道数据是否符合以`ProductsForm`形式(`app/forms/ProductsForm.php`)形式实现的业务规则和验证: ```php <?php // ... $form = new ProductsForm(); $product = new Products(); // Validate the input $data = $this->request->getPost(); if (!$form->isValid($data, $product)) { $messages = $form->getMessages(); foreach ($messages as $message) { $this->flash->error($message); } return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'new', ] ); } ``` 最后,如果表单没有返回任何验证消息,我们可以保存产品实例: ```php <?php // ... if ($product->save() === false) { $messages = $product->getMessages(); foreach ($messages as $message) { $this->flash->error($message); } return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'new', ] ); } $form->clear(); $this->flash->success( 'Product was created successfully' ); return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'index', ] ); ``` 现在,在更新产品的情况下,我们必须首先向用户显示当前在编辑记录中的数据: ```php <?php /** * Edits a product based on its id */ public function editAction($id) { if (!$this->request->isPost()) { $product = Products::findFirstById($id); if (!$product) { $this->flash->error( 'Product was not found' ); return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'index', ] ); } $this->view->form = new ProductsForm( $product, [ 'edit' => true, ] ); } } ``` 通过将模型作为第一个参数传递,找到的数据绑定到表单。由于这个原因,用户可以更改任何值,然后通过 `save` 操作将其发送回数据库: ```php <?php /** * Updates a product based on the data entered in the 'edit' action */ public function saveAction() { if (!$this->request->isPost()) { return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'index', ] ); } $id = $this->request->getPost('id', 'int'); $product = Products::findFirstById($id); if (!$product) { $this->flash->error( 'Product does not exist' ); return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'index', ] ); } $form = new ProductsForm(); $data = $this->request->getPost(); if (!$form->isValid($data, $product)) { $messages = $form->getMessages(); foreach ($messages as $message) { $this->flash->error($message); } return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'new', ] ); } if ($product->save() === false) { $messages = $product->getMessages(); foreach ($messages as $message) { $this->flash->error($message); } return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'new', ] ); } $form->clear(); $this->flash->success( 'Product was updated successfully' ); return $this->dispatcher->forward( [ 'controller' => 'products', 'action' => 'index', ] ); } ``` ## 用户组件 应用程序的所有UI元素和视觉样式主要通过[Bootstrap](http://getbootstrap.com/)实现。某些元素(例如导航栏)会根据应用程序的状态而更改。例如,在右上角,如果用户登录到应用程序,则`Log in / Sign Up`链接将更改为`Log out`。 应用程序的这一部分在组件`Elements`(`app/library/Elements.php`)中实现。 ```php <?php use Phalcon\Mvc\User\Component; class Elements extends Component { public function getMenu() { // ... } public function getTabs() { // ... } } ``` 此类扩展了`Phalcon\Mvc\User\Component`。它不是用于扩展具有此类的组件,但它有助于更​​快地访问应用程序服务。现在,我们将在服务容器中注册我们的第一个用户组件: ```php <?php // Register a user component $di->set( 'elements', function () { return new Elements(); } ); ``` 作为视图中的控制器,插件或组件,该组件还可以访问容器中注册的服务,只需访问与以前注册的服务同名的属性: ```twig <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> {{ elements.getMenu() }} </div> </div> </div> <div class='container'> {{ content() }} <hr> <footer> <p>&copy; Company 2017</p> </footer> </div> ``` 重要的是: ```twig {{ elements.getMenu() }} ``` ## 动态更改标题 当您在一个选项和另一个选项之间浏览时,会看到标题会动态更改,指示我们当前的工作位置。这是在每个控制器初始化程序中实现的: ```php <?php class ProductsController extends ControllerBase { public function initialize() { // Set the document title $this->tag->setTitle( 'Manage your product types' ); parent::initialize(); } // ... } ``` 注意,也调用方法`parent::initialize()` ,它向标题添加更多数据: ```php <?php use Phalcon\Mvc\Controller; class ControllerBase extends Controller { protected function initialize() { // Prepend the application name to the title $this->tag->prependTitle('INVO | '); } // ... } ``` 最后,标题打印在主视图中(app/views/index.volt): ```php <!DOCTYPE html> <html> <head> <?php echo $this->tag->getTitle(); ?> </head> <!-- ... --> </html> ```