教程4: 使用CRUD(Tutorial 4: Working with the CRUD)

后台通常提供表单来允许用户提交数据. 继续对INVO的解释, 我们现在处理CRUD的创建, 一个非常常见的操作任务, Phalcon将会帮助你使用表单, 校验, 分页和更多.

在INVO(公司, 产品和产品类型)中大部分选项操作数据都是使用一个基础的常见的 CRUD (创建, 读取, 更新和删除)开发的. 每个CRUD包含以下文件:

  1. invo/
  2. app/
  3. controllers/
  4. ProductsController.php
  5. models/
  6. Products.php
  7. forms/
  8. ProductsForm.php
  9. views/
  10. products/
  11. edit.volt
  12. index.volt
  13. new.volt
  14. search.volt

每个控制器都有以下方法:

  1. <?php
  2. class ProductsController extends ControllerBase
  3. {
  4. /**
  5. * 开始操作, 它展示"search"视图
  6. */
  7. public function indexAction()
  8. {
  9. // ...
  10. }
  11. /**
  12. * 基于从"index"发送过来的条件处理"search"
  13. * 返回一个分页结果
  14. */
  15. public function searchAction()
  16. {
  17. // ...
  18. }
  19. /**
  20. * 展示创建一个"new"(新)产品的视图
  21. */
  22. public function newAction()
  23. {
  24. // ...
  25. }
  26. /**
  27. * 展示编辑一个已存在"edit"(编辑)产品的视图
  28. */
  29. public function editAction()
  30. {
  31. // ...
  32. }
  33. /**
  34. * 基于"new"方法中输入的数据创建一个产品
  35. */
  36. public function createAction()
  37. {
  38. // ...
  39. }
  40. /**
  41. * 基于"edit"方法中输入的数据更新一个产品
  42. */
  43. public function saveAction()
  44. {
  45. // ...
  46. }
  47. /**
  48. * 删除一个已存在的产品
  49. */
  50. public function deleteAction($id)
  51. {
  52. // ...
  53. }
  54. }

表单搜索(The Search Form)

每个 CRUD 都开始于一个搜索表单. 这个表单展示了表(products)中的每个字段, 允许用户为一些字段创建一个搜索条件. 表 “products” 和表 “products_types” 是关系表. 既然这样, 我们先前查询表中的记录以便于字段的搜索:

  1. <?php
  2. /**
  3. * 开始操作, 它展示"search"视图
  4. */
  5. public function indexAction()
  6. {
  7. $this->persistent->searchParams = null;
  8. $this->view->form = new ProductsForm();
  9. }

ProductsForm 表单的实例 (app/forms/ProductsForm.php)传递给了视图. 这个表单定义了用户可见的字段:

  1. <?php
  2. use Phalcon\Forms\Form;
  3. use Phalcon\Forms\Element\Text;
  4. use Phalcon\Forms\Element\Hidden;
  5. use Phalcon\Forms\Element\Select;
  6. use Phalcon\Validation\Validator\Email;
  7. use Phalcon\Validation\Validator\PresenceOf;
  8. use Phalcon\Validation\Validator\Numericality;
  9. class ProductsForm extends Form
  10. {
  11. /**
  12. * 初始化产品表单
  13. */
  14. public function initialize($entity = null, $options = [])
  15. {
  16. if (!isset($options["edit"])) {
  17. $element = new Text("id");
  18. $element->setLabel("Id");
  19. $this->add(
  20. $element
  21. );
  22. } else {
  23. $this->add(
  24. new Hidden("id")
  25. );
  26. }
  27. $name = new Text("name");
  28. $name->setLabel("Name");
  29. $name->setFilters(
  30. [
  31. "striptags",
  32. "string",
  33. ]
  34. );
  35. $name->addValidators(
  36. [
  37. new PresenceOf(
  38. [
  39. "message" => "Name is required",
  40. ]
  41. )
  42. ]
  43. );
  44. $this->add($name);
  45. $type = new Select(
  46. "profilesId",
  47. ProductTypes::find(),
  48. [
  49. "using" => [
  50. "id",
  51. "name",
  52. ],
  53. "useEmpty" => true,
  54. "emptyText" => "...",
  55. "emptyValue" => "",
  56. ]
  57. );
  58. $this->add($type);
  59. $price = new Text("price");
  60. $price->setLabel("Price");
  61. $price->setFilters(
  62. [
  63. "float",
  64. ]
  65. );
  66. $price->addValidators(
  67. [
  68. new PresenceOf(
  69. [
  70. "message" => "Price is required",
  71. ]
  72. ),
  73. new Numericality(
  74. [
  75. "message" => "Price is required",
  76. ]
  77. ),
  78. ]
  79. );
  80. $this->add($price);
  81. }
  82. }

表单是使用面向对象的方式声明的, 基于 forms 组件提供的元素. 每个元素都遵循近乎相同的结构:

  1. <?php
  2. // 创建一个元素
  3. $name = new Text("name");
  4. // 设置它的label
  5. $name->setLabel("Name");
  6. // 在验证元素之前应用这些过滤器
  7. $name->setFilters(
  8. [
  9. "striptags",
  10. "string",
  11. ]
  12. );
  13. // 应用此验证
  14. $name->addValidators(
  15. [
  16. new PresenceOf(
  17. [
  18. "message" => "Name is required",
  19. ]
  20. )
  21. ]
  22. );
  23. // 增加元素到表单
  24. $this->add($name);

在表单中其它元素也是这样使用:

  1. <?php
  2. // 增加一个隐藏input到表单
  3. $this->add(
  4. new Hidden("id")
  5. );
  6. // ...
  7. $productTypes = ProductTypes::find();
  8. // 增加一个HTML Select (列表) 到表单
  9. // 数据从"product_types"中填充
  10. $type = new Select(
  11. "profilesId",
  12. $productTypes,
  13. [
  14. "using" => [
  15. "id",
  16. "name",
  17. ],
  18. "useEmpty" => true,
  19. "emptyText" => "...",
  20. "emptyValue" => "",
  21. ]
  22. );

注意, ProductTypes::find() 包含的必须的数据 使用 Phalcon\Tag::select() 来填充 SELECT 标签. 一旦表单传递给视图, 它会进行渲染并呈现给用户:

  1. {{ form("products/search") }}
  2. <h2>
  3. Search products
  4. </h2>
  5. <fieldset>
  6. {% for element in form %}
  7. <div class="control-group">
  8. {{ element.label(["class": "control-label"]) }}
  9. <div class="controls">
  10. {{ element }}
  11. </div>
  12. </div>
  13. {% endfor %}
  14. <div class="control-group">
  15. {{ submit_button("Search", "class": "btn btn-primary") }}
  16. </div>
  17. </fieldset>
  18. {{ endForm() }}

这会生成下面的HTML:

  1. <form action="/invo/products/search" method="post">
  2. <h2>
  3. Search products
  4. </h2>
  5. <fieldset>
  6. <div class="control-group">
  7. <label for="id" class="control-label">Id</label>
  8. <div class="controls">
  9. <input type="text" id="id" name="id" />
  10. </div>
  11. </div>
  12. <div class="control-group">
  13. <label for="name" class="control-label">Name</label>
  14. <div class="controls">
  15. <input type="text" id="name" name="name" />
  16. </div>
  17. </div>
  18. <div class="control-group">
  19. <label for="profilesId" class="control-label">profilesId</label>
  20. <div class="controls">
  21. <select id="profilesId" name="profilesId">
  22. <option value="">...</option>
  23. <option value="1">Vegetables</option>
  24. <option value="2">Fruits</option>
  25. </select>
  26. </div>
  27. </div>
  28. <div class="control-group">
  29. <label for="price" class="control-label">Price</label>
  30. <div class="controls">
  31. <input type="text" id="price" name="price" />
  32. </div>
  33. </div>
  34. <div class="control-group">
  35. <input type="submit" value="Search" class="btn btn-primary" />
  36. </div>
  37. </fieldset>
  38. </form>

当表单提交的时候, 控制器里面的”search”操作是基于用户输入的数据执行搜索的.

执行搜索(Performing a Search)

“search”操作有两个行为. 当通过POST访问, 它基于表单发送的数据执行搜索, 但是当通过GET访问它会在分页中移动当前的页数. 为了区分HTTP方法,我们使用 Request 组件进行校验:

  1. <?php
  2. /**
  3. * 基于从"index"发送过来的条件处理"search"
  4. * 返回一个分页结果
  5. */
  6. public function searchAction()
  7. {
  8. if ($this->request->isPost()) {
  9. // 创建查询条件
  10. } else {
  11. // 使用已存在的条件分页
  12. }
  13. // ...
  14. }

Phalcon\Mvc\Model\Criteria 的帮助下, 我们基于从表单发送来的数据类型和值创建智能的搜索条件:

  1. <?php
  2. $query = Criteria::fromInput(
  3. $this->di,
  4. "Products",
  5. $this->request->getPost()
  6. );

这个方法验证值 “” (空字符串) 和值 null 的区别并考虑到这些来创建搜索条件:

  • 如果字段日期类型是text或者类似的(char, varchar, text, 等等) 它会使用一个SQL “like” 操作符来过滤结果.
  • 如果日期类型不是text或者类似的, 它将会使用操作符”=”.

另外, “Criteria” 会忽略 $_POST 所有不与表中的任何字段相匹配的变量. 值会自动避免使用”参数绑定”.

现在, 我们在控制器的会话袋里存储产品参数:

  1. <?php
  2. $this->persistent->searchParams = $query->getParams();

会话袋在控制器里面是一个特殊的属性, 在使用 session 服务的不同请求之间依然存在. 当访问的时候, 这个属性会注入一个 Phalcon\Session\Bag 实例, 对于每个控制器来说, 这是独立的.

然后, 基于内置的参数我们执行查询:

  1. <?php
  2. $products = Products::find($parameters);
  3. if (count($products) === 0) {
  4. $this->flash->notice(
  5. "The search did not found any products"
  6. );
  7. return $this->dispatcher->forward(
  8. [
  9. "controller" => "products",
  10. "action" => "index",
  11. ]
  12. );
  13. }

如果搜索不返回一些产品, 我们再一次转发用户到 index 方法. 让我们模拟搜索返回结果, 然后我们创建一个分页来轻松的浏览他们:

  1. <?php
  2. use Phalcon\Paginator\Adapter\Model as Paginator;
  3. // ...
  4. $paginator = new Paginator(
  5. [
  6. "data" => $products, // 分页的数据
  7. "limit" => 5, // 每页的行数
  8. "page" => $numberPage, // 查看的指定页
  9. ]
  10. );
  11. // 获取分页中当前页面
  12. $page = $paginator->getPaginate();

最后我们通过返回的页面来浏览:

  1. <?php
  2. $this->view->page = $page;

在视图里面 (app/views/products/search.volt), 我们在当前页面循环相应的结果, 在当前页面给用户展示每一行记录:

  1. {% for product in page.items %}
  2. {% if loop.first %}
  3. <table>
  4. <thead>
  5. <tr>
  6. <th>Id</th>
  7. <th>Product Type</th>
  8. <th>Name</th>
  9. <th>Price</th>
  10. <th>Active</th>
  11. </tr>
  12. </thead>
  13. <tbody>
  14. {% endif %}
  15. <tr>
  16. <td>
  17. {{ product.id }}
  18. </td>
  19. <td>
  20. {{ product.getProductTypes().name }}
  21. </td>
  22. <td>
  23. {{ product.name }}
  24. </td>
  25. <td>
  26. {{ "%.2f"|format(product.price) }}
  27. </td>
  28. <td>
  29. {{ product.getActiveDetail() }}
  30. </td>
  31. <td width="7%">
  32. {{ link_to("products/edit/" ~ product.id, "Edit") }}
  33. </td>
  34. <td width="7%">
  35. {{ link_to("products/delete/" ~ product.id, "Delete") }}
  36. </td>
  37. </tr>
  38. {% if loop.last %}
  39. </tbody>
  40. <tbody>
  41. <tr>
  42. <td colspan="7">
  43. <div>
  44. {{ link_to("products/search", "First") }}
  45. {{ link_to("products/search?page=" ~ page.before, "Previous") }}
  46. {{ link_to("products/search?page=" ~ page.next, "Next") }}
  47. {{ link_to("products/search?page=" ~ page.last, "Last") }}
  48. <span class="help-inline">{{ page.current }} of {{ page.total_pages }}</span>
  49. </div>
  50. </td>
  51. </tr>
  52. </tbody>
  53. </table>
  54. {% endif %}
  55. {% else %}
  56. No products are recorded
  57. {% endfor %}

在上面的例子中有很多东西值得详细介绍. 首先, 当前页面的记录是使用 Volt 的 ‘for’ 循环出来的. Volt 对 PHP 的 ‘foreach’ 提供了一个简单的语法.

  1. {% for product in page.items %}

对于 PHP 来说也是一样:

  1. <?php foreach ($page->items as $product) { ?>

完整的 ‘for’ 提供了以下:

  1. {% for product in page.items %}
  2. {% if loop.first %}
  3. Executed before the first product in the loop
  4. {% endif %}
  5. Executed for every product of page.items
  6. {% if loop.last %}
  7. Executed after the last product is loop
  8. {% endif %}
  9. {% else %}
  10. Executed if page.items does not have any products
  11. {% endfor %}

现在你可以返回到页面找出每个块都在做什么. 在”product”中的每个字段都有相应的输出:

  1. <tr>
  2. <td>
  3. {{ product.id }}
  4. </td>
  5. <td>
  6. {{ product.productTypes.name }}
  7. </td>
  8. <td>
  9. {{ product.name }}
  10. </td>
  11. <td>
  12. {{ "%.2f"|format(product.price) }}
  13. </td>
  14. <td>
  15. {{ product.getActiveDetail() }}
  16. </td>
  17. <td width="7%">
  18. {{ link_to("products/edit/" ~ product.id, "Edit") }}
  19. </td>
  20. <td width="7%">
  21. {{ link_to("products/delete/" ~ product.id, "Delete") }}
  22. </td>
  23. </tr>

正如我们看到的, 在之前使用 product.id 和在PHP中使用 $product->id 是等价的, we made the same with product.name and so on. 其它字段都表现的有些不同, 例如, 让我们看下 product.productTypes.name. 要理解这部分, 我们必须看一下 Products 模型 (app/models/Products.php):

  1. <?php
  2. use Phalcon\Mvc\Model;
  3. /**
  4. * 产品
  5. */
  6. class Products extends Model
  7. {
  8. // ...
  9. /**
  10. * 产品初始化
  11. */
  12. public function initialize()
  13. {
  14. $this->belongsTo(
  15. "product_types_id",
  16. "ProductTypes",
  17. "id",
  18. [
  19. "reusable" => true,
  20. ]
  21. );
  22. }
  23. // ...
  24. }

一个模型有一个名为 initialize() 的方法, 这个方法在每次请求的时候调用一次兵器它服务ORM去初始化一个模型. 在这种情况话, “Products” 通过定义这个模型跟另外一个叫做 “ProductTypes” 的模型有一对多的关系从而初始化.

  1. <?php
  2. $this->belongsTo(
  3. "product_types_id",
  4. "ProductTypes",
  5. "id",
  6. [
  7. "reusable" => true,
  8. ]
  9. );

它的意思是, “Products” 的本地属性”product_types_id” 跟 “ProductTypes” 模型里面的属性 “id” 是一对多的关系. 通过定义这个关系我们可以通过如下方法来访问产品类型的名字:

  1. <td>{{ product.productTypes.name }}</td>

字段 “price” 使用一个 Volt 过滤器来格式化输出:

  1. <td>{{ "%.2f"|format(product.price) }}</td>

在原生PHP中, 它将是这样的:

  1. <?php echo sprintf("%.2f", $product->price) ?>

使用模型中已经实现的帮助者函数来输出产品是否是有效的:

  1. <td>{{ product.getActiveDetail() }}</td>

这个方法在模型中定义了.

创建和更新记录(Creating and Updating Records)

现在, 让我们看看 CRUD 如何创建和更新记录. 从 “new” 和 “edit” 视图, 通过用户输入的数据发送 “create” 和 “save” 方法从而分别执行 “creating” 和 “updating” 产品的方法.

在创建的情况下, 我们提取提交的数据然后分配它们到一个新的 “Products” 实例:

  1. <?php
  2. /**
  3. * 基于在 "new" 方法中输入的数据创建一个产品
  4. */
  5. public function createAction()
  6. {
  7. if (!$this->request->isPost()) {
  8. return $this->dispatcher->forward(
  9. [
  10. "controller" => "products",
  11. "action" => "index",
  12. ]
  13. );
  14. }
  15. $form = new ProductsForm();
  16. $product = new Products();
  17. $product->id = $this->request->getPost("id", "int");
  18. $product->product_types_id = $this->request->getPost("product_types_id", "int");
  19. $product->name = $this->request->getPost("name", "striptags");
  20. $product->price = $this->request->getPost("price", "double");
  21. $product->active = $this->request->getPost("active");
  22. // ...
  23. }

还记得我们在产品表单中定义的过滤器吗? 数据在开始分配到 $product 对象前进行过滤. 这个过滤器是可选的; ORM同样也会转义输入的数据和根据列类型执行附加的转换:

  1. <?php
  2. // ...
  3. $name = new Text("name");
  4. $name->setLabel("Name");
  5. // 过滤 name
  6. $name->setFilters(
  7. [
  8. "striptags",
  9. "string",
  10. ]
  11. );
  12. // 验证 name
  13. $name->addValidators(
  14. [
  15. new PresenceOf(
  16. [
  17. "message" => "Name is required",
  18. ]
  19. )
  20. ]
  21. );
  22. $this->add($name);

当保存的时候, 我们就会知道 ProductsForm (app/forms/ProductsForm.php) 表单提交的数据是否否则业务规则和实现的验证:

  1. <?php
  2. // ...
  3. $form = new ProductsForm();
  4. $product = new Products();
  5. // V验证输入
  6. $data = $this->request->getPost();
  7. if (!$form->isValid($data, $product)) {
  8. $messages = $form->getMessages();
  9. foreach ($messages as $message) {
  10. $this->flash->error($message);
  11. }
  12. return $this->dispatcher->forward(
  13. [
  14. "controller" => "products",
  15. "action" => "new",
  16. ]
  17. );
  18. }

最后, 如果表单没有返回任何验证消息, 我们就可以保存产品实例了:

  1. <?php
  2. // ...
  3. if ($product->save() === false) {
  4. $messages = $product->getMessages();
  5. foreach ($messages as $message) {
  6. $this->flash->error($message);
  7. }
  8. return $this->dispatcher->forward(
  9. [
  10. "controller" => "products",
  11. "action" => "new",
  12. ]
  13. );
  14. }
  15. $form->clear();
  16. $this->flash->success(
  17. "Product was created successfully"
  18. );
  19. return $this->dispatcher->forward(
  20. [
  21. "controller" => "products",
  22. "action" => "index",
  23. ]
  24. );

现在, 在更新产品的情况下, 我们必须先将当前编辑的记录展示给用户:

  1. <?php
  2. /**
  3. * 基于它的id编辑一个产品
  4. */
  5. public function editAction($id)
  6. {
  7. if (!$this->request->isPost()) {
  8. $product = Products::findFirstById($id);
  9. if (!$product) {
  10. $this->flash->error(
  11. "Product was not found"
  12. );
  13. return $this->dispatcher->forward(
  14. [
  15. "controller" => "products",
  16. "action" => "index",
  17. ]
  18. );
  19. }
  20. $this->view->form = new ProductsForm(
  21. $product,
  22. [
  23. "edit" => true,
  24. ]
  25. );
  26. }
  27. }

通过将模型作为第一个参数传递过去找出被绑定到表单的数据. 多亏了这个, 用户可以改变任何值, 然后通过 “save” 方法发送它到数据库:

  1. <?php
  2. /**
  3. * 在 "edit"方法中基于输入的数据更新一个产品
  4. */
  5. public function saveAction()
  6. {
  7. if (!$this->request->isPost()) {
  8. return $this->dispatcher->forward(
  9. [
  10. "controller" => "products",
  11. "action" => "index",
  12. ]
  13. );
  14. }
  15. $id = $this->request->getPost("id", "int");
  16. $product = Products::findFirstById($id);
  17. if (!$product) {
  18. $this->flash->error(
  19. "Product does not exist"
  20. );
  21. return $this->dispatcher->forward(
  22. [
  23. "controller" => "products",
  24. "action" => "index",
  25. ]
  26. );
  27. }
  28. $form = new ProductsForm();
  29. $data = $this->request->getPost();
  30. if (!$form->isValid($data, $product)) {
  31. $messages = $form->getMessages();
  32. foreach ($messages as $message) {
  33. $this->flash->error($message);
  34. }
  35. return $this->dispatcher->forward(
  36. [
  37. "controller" => "products",
  38. "action" => "new",
  39. ]
  40. );
  41. }
  42. if ($product->save() === false) {
  43. $messages = $product->getMessages();
  44. foreach ($messages as $message) {
  45. $this->flash->error($message);
  46. }
  47. return $this->dispatcher->forward(
  48. [
  49. "controller" => "products",
  50. "action" => "new",
  51. ]
  52. );
  53. }
  54. $form->clear();
  55. $this->flash->success(
  56. "Product was updated successfully"
  57. );
  58. return $this->dispatcher->forward(
  59. [
  60. "controller" => "products",
  61. "action" => "index",
  62. ]
  63. );
  64. }

我们已经看到 Phalcon 如何以一种结构化的方式让你创建表单和从一个数据库中绑定数据. 在下一章, 我们将会看到如何添加自定义 HTML 元素, 比如一个菜单.