💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 教程4: 使用CRUD(Tutorial 4: Working with the CRUD) 后台通常提供表单来允许用户提交数据. 继续对INVO的解释, 我们现在处理CRUD的创建, 一个非常常见的操作任务, Phalcon将会帮助你使用表单, 校验, 分页和更多. 在INVO(公司, 产品和产品类型)中大部分选项操作数据都是使用一个基础的常见的[CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete)(创建, 读取, 更新和删除)开发的. 每个CRUD包含以下文件: ~~~ invo/ app/ controllers/ ProductsController.php models/ Products.php forms/ ProductsForm.php views/ products/ edit.volt index.volt new.volt search.volt ~~~ 每个控制器都有以下方法: ~~~ <?php class ProductsController extends ControllerBase { /** * 开始操作, 它展示"search"视图 */ public function indexAction() { // ... } /** * 基于从"index"发送过来的条件处理"search" * 返回一个分页结果 */ public function searchAction() { // ... } /** * 展示创建一个"new"(新)产品的视图 */ public function newAction() { // ... } /** * 展示编辑一个已存在"edit"(编辑)产品的视图 */ public function editAction() { // ... } /** * 基于"new"方法中输入的数据创建一个产品 */ public function createAction() { // ... } /** * 基于"edit"方法中输入的数据更新一个产品 */ public function saveAction() { // ... } /** * 删除一个已存在的产品 */ public function deleteAction($id) { // ... } } ~~~ ## 表单搜索(The Search Form) 每个 CRUD 都开始于一个搜索表单. 这个表单展示了表(products)中的每个字段, 允许用户为一些字段创建一个搜索条件. 表 “products” 和表 “products\_types” 是关系表. 既然这样, 我们先前查询表中的记录以便于字段的搜索: ~~~ <?php /** * 开始操作, 它展示"search"视图 */ public function indexAction() { $this->persistent->searchParams = null; $this->view->form = new ProductsForm(); } ~~~ ProductsForm 表单的实例 (app/forms/ProductsForm.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 { /** * 初始化产品表单 */ 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); } } ~~~ 表单是使用面向对象的方式声明的, 基于[forms](http://docs.iphalcon.cn/reference/forms.html)组件提供的元素. 每个元素都遵循近乎相同的结构: ~~~ <?php // 创建一个元素 $name = new Text("name"); // 设置它的label $name->setLabel("Name"); // 在验证元素之前应用这些过滤器 $name->setFilters( [ "striptags", "string", ] ); // 应用此验证 $name->addValidators( [ new PresenceOf( [ "message" => "Name is required", ] ) ] ); // 增加元素到表单 $this->add($name); ~~~ 在表单中其它元素也是这样使用: ~~~ <?php // 增加一个隐藏input到表单 $this->add( new Hidden("id") ); // ... $productTypes = ProductTypes::find(); // 增加一个HTML Select (列表) 到表单 // 数据从"product_types"中填充 $type = new Select( "profilesId", $productTypes, [ "using" => [ "id", "name", ], "useEmpty" => true, "emptyText" => "...", "emptyValue" => "", ] ); ~~~ 注意,`ProductTypes::find()`包含的必须的数据 使用`Phalcon\Tag::select()`来填充 SELECT 标签. 一旦表单传递给视图, 它会进行渲染并呈现给用户: ~~~ {{ 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: ~~~ <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”操作是基于用户输入的数据执行搜索的. ## 执行搜索(Performing a Search) “search”操作有两个行为. 当通过POST访问, 它基于表单发送的数据执行搜索, 但是当通过GET访问它会在分页中移动当前的页数. 为了区分HTTP方法,我们使用[Request](http://docs.iphalcon.cn/reference/request.html)组件进行校验: ~~~ <?php /** * 基于从"index"发送过来的条件处理"search" * 返回一个分页结果 */ public function searchAction() { if ($this->request->isPost()) { // 创建查询条件 } else { // 使用已存在的条件分页 } // ... } ~~~ 在[Phalcon\\Mvc\\Model\\Criteria](http://docs.iphalcon.cn/api/Phalcon_Mvc_Model_Criteria.html)的帮助下, 我们基于从表单发送来的数据类型和值创建智能的搜索条件: ~~~ <?php $query = Criteria::fromInput( $this->di, "Products", $this->request->getPost() ); ~~~ 这个方法验证值 “” (空字符串) 和值 null 的区别并考虑到这些来创建搜索条件: * 如果字段日期类型是text或者类似的(char, varchar, text, 等等) 它会使用一个SQL “like” 操作符来过滤结果. * 如果日期类型不是text或者类似的, 它将会使用操作符”=”. 另外, “Criteria” 会忽略`$_POST`所有不与表中的任何字段相匹配的变量. 值会自动避免使用”参数绑定”. 现在, 我们在控制器的会话袋里存储产品参数: ~~~ <?php $this->persistent->searchParams = $query->getParams(); ~~~ 会话袋在控制器里面是一个特殊的属性, 在使用 session 服务的不同请求之间依然存在. 当访问的时候, 这个属性会注入一个[Phalcon\\Session\\Bag](http://docs.iphalcon.cn/api/Phalcon_Session_Bag.html)实例, 对于每个控制器来说, 这是独立的. 然后, 基于内置的参数我们执行查询: ~~~ <?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", ] ); } ~~~ 如果搜索不返回一些产品, 我们再一次转发用户到 index 方法. 让我们模拟搜索返回结果, 然后我们创建一个分页来轻松的浏览他们: ~~~ <?php use Phalcon\Paginator\Adapter\Model as Paginator; // ... $paginator = new Paginator( [ "data" => $products, // 分页的数据 "limit" => 5, // 每页的行数 "page" => $numberPage, // 查看的指定页 ] ); // 获取分页中当前页面 $page = $paginator->getPaginate(); ~~~ 最后我们通过返回的页面来浏览: ~~~ <?php $this->view->page = $page; ~~~ 在视图里面 (app/views/products/search.volt), 我们在当前页面循环相应的结果, 在当前页面给用户展示每一行记录: ~~~ {% 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’ 提供了一个简单的语法. ~~~ {% for product in page.items %} ~~~ 对于 PHP 来说也是一样: ~~~ <?php foreach ($page->items as $product) { ?> ~~~ 完整的 ‘for’ 提供了以下: ~~~ {% 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”中的每个字段都有相应的输出: ~~~ <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`是等价的, we made the same with`product.name`and so on. 其它字段都表现的有些不同, 例如, 让我们看下`product.productTypes.name`. 要理解这部分, 我们必须看一下 Products 模型 (app/models/Products.php): ~~~ <?php use Phalcon\Mvc\Model; /** * 产品 */ class Products extends Model { // ... /** * 产品初始化 */ public function initialize() { $this->belongsTo( "product_types_id", "ProductTypes", "id", [ "reusable" => true, ] ); } // ... } ~~~ 一个模型有一个名为`initialize()`的方法, 这个方法在每次请求的时候调用一次兵器它服务ORM去初始化一个模型. 在这种情况话, “Products” 通过定义这个模型跟另外一个叫做 “ProductTypes” 的模型有一对多的关系从而初始化. ~~~ <?php $this->belongsTo( "product_types_id", "ProductTypes", "id", [ "reusable" => true, ] ); ~~~ 它的意思是, “Products” 的本地属性”product\_types\_id” 跟 “ProductTypes” 模型里面的属性 “id” 是一对多的关系. 通过定义这个关系我们可以通过如下方法来访问产品类型的名字: ~~~ <td>{{ product.productTypes.name }}</td> ~~~ 字段 “price” 使用一个 Volt 过滤器来格式化输出: ~~~ <td>{{ "%.2f"|format(product.price) }}</td> ~~~ 在原生PHP中, 它将是这样的: ~~~ <?php echo sprintf("%.2f", $product->price) ?> ~~~ 使用模型中已经实现的帮助者函数来输出产品是否是有效的: ~~~ <td>{{ product.getActiveDetail() }}</td> ~~~ 这个方法在模型中定义了. ## 创建和更新记录(Creating and Updating Records) 现在, 让我们看看 CRUD 如何创建和更新记录. 从 “new” 和 “edit” 视图, 通过用户输入的数据发送 “create” 和 “save” 方法从而分别执行 “creating” 和 “updating” 产品的方法. 在创建的情况下, 我们提取提交的数据然后分配它们到一个新的 “Products” 实例: ~~~ <?php /** * 基于在 "new" 方法中输入的数据创建一个产品 */ 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 // ... $name = new Text("name"); $name->setLabel("Name"); // 过滤 name $name->setFilters( [ "striptags", "string", ] ); // 验证 name $name->addValidators( [ new PresenceOf( [ "message" => "Name is required", ] ) ] ); $this->add($name); ~~~ 当保存的时候, 我们就会知道 ProductsForm (app/forms/ProductsForm.php) 表单提交的数据是否否则业务规则和实现的验证: ~~~ <?php // ... $form = new ProductsForm(); $product = new Products(); // V验证输入 $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 // ... 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 /** * 基于它的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 /** * 在 "edit"方法中基于输入的数据更新一个产品 */ 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", ] ); } ~~~ 我们已经看到 Phalcon 如何以一种结构化的方式让你创建表单和从一个数据库中绑定数据. 在下一章, 我们将会看到如何添加自定义 HTML 元素, 比如一个菜单.