Spec-Zone.ru › Phalcon 3

Урок 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API