Documentation

Table Of Contents

Previous topic

< Tutorial 3: Securing INVO

Next topic

Tutorial 5: Customizing INVO >

This Page

Tutorial 4: Travailler avec le CRUD

Backends usually provide forms to allow users to manipulate data. Continuing the explanation of INVO, we now address the creation of CRUDs, a very common task that Phalcon will facilitate you using forms, validations, paginators and more.

La plupart des options qui manipulent des données (companies, products et types de products), ont été développés en utilisant un CRUD (create/read/update/delete) basique et commun. Chaque CRUD contient les fichiers suivants :

invo/
    app/
        controllers/
            ProductsController.php
        models/
            Products.php
        forms/
            ProductsForm.php
        views/
            products/
                edit.volt
                index.volt
                new.volt
                search.volt

Chaque contrôleur a les actions suivantes :

<?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)
    {
        // ...
    }
}

Formulaire de recherche

Tous les CRUD commencent avec le formulaire de recherche. Ce formulaire montre tous les champs que la table products possède, permettant à l’utilisateur de filtrer ses recherches. La tâche “products” est liée à la table “products_types”. Dans notre cas, nous avons déjà demandé des enregistrements de cette table, afin de faciliter la recherche dans ce champ :

<?php

/**
 * The start action, it shows the "search" view
 */
public function indexAction()
{
    $this->persistent->searchParams = null;

    $this->view->form = new ProductsForm();
}

An instance of the ProductsForm form (app/forms/ProductsForm.php) is passed to the view. This form defines the fields that are visible to the user:

<?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);
    }
}

The form is declared using an object-oriented scheme based on the elements provided by the forms component. Every element follows almost the same structure:

<?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);

Other elements are also used in this form:

<?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" => "",
    ]
);

Note that ProductTypes::find() contains the data necessary to fill the SELECT tag using Phalcon\Tag::select(). Once the form is passed to the view, it can be rendered and presented to the user:

{{ 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() }}

This produces the following 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>

When the form is submitted, the “search” action is executed in the controller performing the search based on the data entered by the user.

Exécuter une recherche

L’action de recherche a un double comportement. Quand on y accéde avec POST, cela fait une recherche basé sur les données que l’on a envoyé à partir du formulaire. Mais quand on y accéde via GET cela change la page courante dans le paginateur. Pour différencier la méthode (GET ou POST), nous utilisons le composant Request :

<?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
    }

    // ...
}

Avec l’aide de Phalcon\Mvc\Model\Criteria ,nous pouvons créer les conditions de recherche basé sur les types de données envoyé via le formulaire :

<?php

$query = Criteria::fromInput(
    $this->di,
    "Products",
    $this->request->getPost()
);

Cette méthode vérifie quelle valeur est différente de “” (chaine vide) et “null” et les prends en compte pour créer les critères de recherche :

  • Si le champs de données est “text” ou similaire (char, varchar, text, etc.). L’opérateur “like” sera utilisé pour filtrer les résultats.
  • Si le type de donnée est différent, l’opérateur “=” sera utilisé.

De plus, “Criteria” ignore toutes les variables $_POST qui ne correspondent à aucun champs de la table. Les valeurs seront automatiquement échappées en utilisant les paramètres liés (bond parameters).

Maintenant, on va stoquer les paramètres dans le “sac” de session du contrôleur :

<?php

$this->persistent->searchParams = $query->getParams();

Un sac de session est un attribut particulier dans un contrôleur qui est sauvegardé entre les requêtes. Quand on y accède, cet attribut injecte un service Phalcon\Session\Bag qui est indépendant de chaque contrôleur.

Puis, basé sur les paramètres passé, on génère la requête :

<?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",
        ]
    );
}

Si la recherche ne retourne aucun produit, on transfert l’utilisateur à l’action index. Si la recherche retourne des résultats, on créé un paginateur pour se déplacer à travers les pages facilement :

<?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();

Enfin, on passe la page retournée à la vue:

<?php

$this->view->page = $page;

Dans la vue (app/views/products/search.volt), on affiche le résultat correspondant à la page actuelle :

{% 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 %}

There are many things in the above example that worth detailing. First of all, active items in the current page are traversed using a Volt’s ‘for’. Volt provides a simpler syntax for a PHP ‘foreach’.

{% for product in page.items %}

Which in PHP is the same as:

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

The whole ‘for’ block provides the following:

{% 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 %}

Now you can go back to the view and find out what every block is doing. Every field in “product” is printed accordingly:

<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>

As we seen before using product.id is the same as in PHP as doing: $product->id, we made the same with product.name and so on. Other fields are rendered differently, for instance, let’s focus in product.productTypes.name. To understand this part, we have to check the Products model (app/models/Products.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,
            ]
        );
    }

    // ...
}

A model can have a method called initialize(), this method is called once per request and it serves the ORM to initialize a model. In this case, “Products” is initialized by defining that this model has a one-to-many relationship to another model called “ProductTypes”.

<?php

$this->belongsTo(
    "product_types_id",
    "ProductTypes",
    "id",
    [
        "reusable" => true,
    ]
);

Which means, the local attribute “product_types_id” in “Products” has an one-to-many relation to the “ProductTypes” model in its attribute “id”. By defining this relationship we can access the name of the product type by using:

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

The field “price” is printed by its formatted using a Volt filter:

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

In plain PHP, this would be:

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

Printing whether the product is active or not uses a helper implemented in the model:

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

This method is defined in the model.

Créer et modifier des entrées

Voyons comment le CRUD créé et modifie des entrées. A partir des vues “new” et “edit”, la donnée entrée par l’utilisateur est envoyé à l’action “create” et “save” qui exécute l’action de créer ou de modifier les produits.

Dans la page de création, on récupère les données envoyés et on leur assigne une nouvelle instance de produit :

<?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");

    // ...
}

Les données sont filtrés avant d’être assignés à l’objet $product. Ce filtrage est optionnel, l’ORM échappe les données entrées et caste les données en fonction des types des champs:

<?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);

Quand on sauvegarde, nous saurons si la donnée est conforme aux règles et validations implémentés dans le form ProductsForm (app/forms/ProductsForm.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",
        ]
    );
}

Finally, if the form does not return any validation message we can save the product instance:

<?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",
    ]
);

Maintenant, dans le cas de la modification de produit, on doit présenter les données à éditer à l’utilisateur en pré-remplissant les champs:

<?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,
            ]
        );
    }
}

L’helper “setDefault” entre les valeurs du produit dans les champs qui portent le même nom comme valeur par défaut. Grace à cela, l’utilisateur peut changer n’importe quelle valeur et ensuite envoyer ses modifications à la base de données avec l’action “save”:

<?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",
        ]
    );
}

We have seen how Phalcon lets you create forms and bind data from a database in a structured way. In next chapter, we will see how to add custom HTML elements like a menu.

Follow along: