Урок 4: Работа с CRUD
Бэкэнды обычно предоставляют формы для манипулирования данными. Продолжая объяснение INVO, мы теперь рассмотрим создание CRUD, очень распространённую задачу, которую Phalcon позволит вам выполнить, используя формы, валидации, пагинаторы и многое другое.
Большинство опций для манипулирования данными в INVO (компании, продукты и типы продуктов) были разработаны с использованием базового и распространённого CRUD (Создание, Чтение, Обновление и Удаление). Каждый CRUD содержит следующие файлы:
invo/
app/
controllers/
ProductsController.php
models/
Products.php
forms/
ProductsForm.php
views/
products/
edit.volt
index.volt
new.volt
search.volt
Каждый контроллер имеет следующие действия:
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->form = new ProductsForm();
}
Экземпляр формы ProductsForm (app/forms/ProductsForm.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);
}
}
Форма объявляется с использованием объектно-ориентированной схемы, основанной на элементах, предоставленных компонентом формы. Каждый элемент имеет почти одинаковую структуру:
// 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);
В этой форме также используются другие элементы:
// 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() содержит данные, необходимые для заполнения тега SELECT, используя Phalcon\Tag::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>
При отправке формы выполняется действие «поиск» в контроллере, выполняющем поиск на основе данных, введённых пользователем.
Выполнение поиска
Действие «поиск» имеет два поведения. При обращении по POST оно выполняет поиск на основе данных, отправленных из формы, а при обращении по GET оно перемещает текущую страницу в пагинаторе. Для различения методов HTTP мы проверяем это с помощью компонента Запрос:
/**
* 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",
$this->request->getPost()
);
Этот метод проверяет, какие значения отличаются от «» (пустая строка) и 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->dispatcher->forward(
[
"controller" => "products",
"action" => "index",
]
);
}
Если поиск не возвращает никаких продуктов, мы перенаправляем пользователя снова на действие индекса. Предположим, поиск вернул результаты, тогда мы создаём пагинатор для удобной навигации по ним:
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();
Наконец, передаём возвращённую страницу в представление:
$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. Volt обеспечивает более простой синтаксис для PHP-цикла foreach.
{% for product in page.items %}
Что в PHP эквивалентно:
<?php foreach ($page->items as $product) { ?>
Весь блок цикла обеспечивает следующее:
{% 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 %}
Теперь вы можете вернуться к представлению и понять, что делает каждый блок. Каждое поле в «продукте» выводится соответствующим образом:
<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. Чтобы понять эту часть, нам нужно проверить модель Products (app/models/Products.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 для инициализации модели. В данном случае «Products» инициализируется определением того, что эта модель имеет отношение один-ко-многим с другой моделью под названием «ProductTypes».
$this->belongsTo(
"product_types_id",
"ProductTypes",
"id",
[
"reusable" => true,
]
);
Это означает, что локальный атрибут «product_types_id» в «Products» имеет отношение один-ко-многим к модели «ProductTypes» в её атрибуте «id». Определив это отношение, мы можем получить имя типа продукта, используя:
<td>{{ product.productTypes.name }}</td>
Поле «цена» выводится с использованием отформатированного фильтра Volt:
<td>{{ "%.2f"|format(product.price) }}</td>
В обычном PHP это будет выглядеть так:
<?php echo sprintf("%.2f", $product->price) ?>
Вывод активен ли продукт или нет, использует помощник, реализованный в модели:
<td>{{ product.getActiveDetail() }}</td>
Этот метод определён в модели.
Создание и обновление записей
Теперь давайте посмотрим, как CRUD создаёт и обновляет записи. Из представлений «новый» и «редактировать» данные, введённые пользователем, отправляются в действия «создать» и «сохранить», которые выполняют действия «создание» и «обновление» продуктов соответственно.
В случае создания мы восстанавливаем отправленные данные и назначаем их новому экземпляру «Products»:
/**
* 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");
// ...
}
Помните фильтры, которые мы определили в форме Products? Данные фильтруются перед назначением объекту $product. Этот фильтр необязателен; ORM также экранирует входные данные и выполняет дополнительную приведение типов в соответствии с типами столбцов:
// ...
$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):
// ...
$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",
]
);
}
Наконец, если форма не возвращает никаких сообщений об ошибках валидации, мы можем сохранить экземпляр продукта:
// ...
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",
]
);
Теперь, в случае обновления продукта, мы должны сначала представить пользователю данные, которые в данный момент находятся в редактируемой записи:
/**
* 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,
]
);
}
}
Найденные данные привязываются к форме путём передачи модели в качестве первого параметра. Благодаря этому пользователь может изменить любое значение, а затем отправить его обратно в базу данных через действие «сохранить»:
/**
* 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",
]
);
}
Мы увидели, как Phalcon позволяет создавать формы и связывать данные из базы данных структурированным способом. В следующей главе мы увидим, как добавить пользовательские HTML-элементы, такие как меню.
© 2011–2017 Phalcon Framework Team
Licensed under the Creative Commons Attribution License 3.0.
https://docs.phalconphp.com/en/latest/reference/tutorial-invo-3.html