[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`中打開應用程序后,您將看到如下內容:

該應用程序分為兩個部分:前端和后端。前端是一個公共區域,訪客可以接收有關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',
]
);
```
## 后端安全
后端是一個私有區域,只有注冊用戶才能訪問。因此,有必要檢查只有注冊用戶才能訪問這些控制器。如果您沒有登錄到應用程序并嘗試訪問,例如,產品控制器(私有),您將看到如下屏幕:

每當有人嘗試訪問任何控制器/操作時,應用程序都會驗證當前角色(在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>© 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>
```
- 常規
- Welcome
- 貢獻
- 生成回溯
- 測試重現
- 單元測試
- 入門
- 安裝
- Web服務器設置
- WAMP
- XAMPP
- 教程
- 基礎教程
- 教程:創建一個簡單的REST API
- 教程:V?kuró
- 提升性能
- 教程:INVO
- 開發環境
- Phalcon Compose (Docker)
- Nanobox
- Phalcon Box (Vagrant)
- 開發工具
- Phalcon開發者工具的安裝
- Phalcon開發者工具的使用
- 調試應用程序
- 核心
- MVC應用
- 微應用
- 創建命令行(CLI)應用程序
- 依賴注入與服務定位
- MVC架構
- 服務
- 使用緩存提高性能
- 讀取配置
- 上下文轉義
- 類加載器
- 使用命名空間
- 日志
- 隊列
- 數據庫
- 數據庫抽象層
- Phalcon查詢語言(PHQL)
- ODM(對象文檔映射器)
- 使用模型
- 模型行為
- ORM緩存
- 模型事件
- 模型元數據
- 模型關系
- 模型事務
- 驗證模型
- 數據庫遷移
- 分頁
- 前端
- Assets管理
- 閃存消息
- 表單
- 圖像
- 視圖助手(標簽)
- 使用視圖
- Volt:模板引擎
- 業務邏輯
- 訪問控制列表(ACL)
- 注解解析器
- 控制器
- 調度控制器
- 事件管理器
- 過濾與清理
- 路由
- 在session中存儲數據
- 生成URL和路徑
- 驗證
- HTTP
- Cookies管理
- 請求環境
- 返回響應
- 安全
- 加密/解密
- 安全
- 國際化
- 國際化
- 多語言支持