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 : .. code-block:: bash 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 : .. code-block:: php 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: .. code-block:: php 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 :doc:`forms ` component. Every element follows almost the same structure: .. code-block:: php 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: .. code-block:: php 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 :code:`ProductTypes::find()` contains the data necessary to fill the SELECT tag using :code:`Phalcon\Tag::select()`. Once the form is passed to the view, it can be rendered and presented to the user: .. code-block:: html+jinja {{ form("products/search") }}

Search products

{% for element in form %}
{{ element.label(["class": "control-label"]) }}
{{ element }}
{% endfor %}
{{ submit_button("Search", "class": "btn btn-primary") }}
{{ endForm() }} This produces the following HTML: .. code-block:: html

Search products

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 :doc:`Request ` : .. code-block:: php request->isPost()) { // Create the query conditions } else { // Paginate using the existing conditions } // ... } Avec l'aide de :doc:`Phalcon\\Mvc\\Model\\Criteria <../api/Phalcon_Mvc_Model_Criteria>` ,nous pouvons créer les conditions de recherche basé sur les types de données envoyé via le formulaire : .. code-block:: php 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 :code:`$_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 : .. code-block:: php 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 :doc:`Phalcon\\Session\\Bag <../api/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 : .. code-block:: php 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 : .. code-block:: php $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: .. code-block:: php view->page = $page; Dans la vue (app/views/products/search.volt), on affiche le résultat correspondant à la page actuelle : .. code-block:: html+jinja {% for product in page.items %} {% if loop.first %} {% endif %} {% if loop.last %}
Id Product Type Name Price Active
{{ product.id }} {{ product.getProductTypes().name }} {{ product.name }} {{ "%.2f"|format(product.price) }} {{ product.getActiveDetail() }} {{ link_to("products/edit/" ~ product.id, "Edit") }} {{ link_to("products/delete/" ~ product.id, "Delete") }}
{{ 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") }} {{ page.current }} of {{ page.total_pages }}
{% 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'. .. code-block:: html+jinja {% for product in page.items %} Which in PHP is the same as: .. code-block:: php items as $product) { ?> The whole 'for' block provides the following: .. code-block:: html+jinja {% 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: .. code-block:: html+jinja {{ product.id }} {{ product.productTypes.name }} {{ product.name }} {{ "%.2f"|format(product.price) }} {{ product.getActiveDetail() }} {{ link_to("products/edit/" ~ product.id, "Edit") }} {{ link_to("products/delete/" ~ product.id, "Delete") }} As we seen before using :code:`product.id` is the same as in PHP as doing: :code:`$product->id`, we made the same with :code:`product.name` and so on. Other fields are rendered differently, for instance, let's focus in :code:`product.productTypes.name`. To understand this part, we have to check the Products model (app/models/Products.php): .. code-block:: php belongsTo( "product_types_id", "ProductTypes", "id", [ "reusable" => true, ] ); } // ... } A model can have a method called :code:`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". .. code-block:: php 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: .. code-block:: html+jinja {{ product.productTypes.name }} The field "price" is printed by its formatted using a Volt filter: .. code-block:: html+jinja {{ "%.2f"|format(product.price) }} In plain PHP, this would be: .. code-block:: php price) ?> Printing whether the product is active or not uses a helper implemented in the model: .. code-block:: php {{ product.getActiveDetail() }} 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 : .. code-block:: php 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 :code:`$product`. Ce filtrage est optionnel, l'ORM échappe les données entrées et caste les données en fonction des types des champs: .. code-block:: php 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): .. code-block:: php 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: .. code-block:: php 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: .. code-block:: php 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": .. code-block:: php 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. .. _CRUD: https://fr.wikipedia.org/wiki/CRUD