Tutorial 4: Using CRUDs ======================= Backends usually provides 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. Working with the CRUD --------------------- Most options that manipulate data in INVO (companies, products and types of products), were developed using a basic and common CRUD_ (Create, Read, Update and Delete). Each CRUD contains the following files: .. code-block:: bash invo/ app/ controllers/ ProductsController.php models/ Products.php forms/ ProductsForm.php views/ products/ edit.volt index.volt new.volt search.volt Each controller has the following actions: .. code-block:: php persistent->searchParams = null; $this->view->form = new ProductsForm; } An instance of the form ProductsForm (app/forms/ProductsForm.php) is passed to the view. This form defines the fields that are visible to the user: .. code-block:: php add($element->setLabel("Id")); } else { $this->add(new Hidden("id")); } $name = new Text("name"); $name->setLabel("Name"); $name->setFilters(array('striptags', 'string')); $name->addValidators( array( new PresenceOf( array( 'message' => 'Name is required' ) ) ) ); $this->add($name); $type = new Select( 'profilesId', ProductTypes::find(), array( 'using' => array('id', 'name'), 'useEmpty' => true, 'emptyText' => '...', 'emptyValue' => '' ) ); $this->add($type); $price = new Text("price"); $price->setLabel("Price"); $price->setFilters(array('float')); $price->addValidators( array( new PresenceOf( array( 'message' => 'Price is required' ) ), new Numericality( array( '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(array('striptags', 'string')); // Apply this validators $name->addValidators( array( new PresenceOf( array( '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")); // ... // Add a HTML Select (list) to the form // and fill it with data from "product_types" $type = new Select( 'profilesId', ProductTypes::find(), array( 'using' => array('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") }}
This produces the following HTML: .. code-block:: html

Search products

When the form is submitted, the action "search" is executed in the controller performing the search based on the data entered by the user. Performing a Search ^^^^^^^^^^^^^^^^^^^ The action "search" has a dual behavior. When accessed via POST, it performs a search based on the data sent from the form. But when accessed via GET it moves the current page in the paginator. To differentiate one from another HTTP method, we check it using the :doc:`Request ` component: .. code-block:: php request->isPost()) { // Create the query conditions } else { // Paginate using the existing conditions } // ... } With the help of :doc:`Phalcon\\Mvc\\Model\\Criteria <../api/Phalcon_Mvc_Model_Criteria>`, we can create the search conditions intelligently based on the data types and values sent from the form: .. code-block:: php di, "Products", $this->request->getPost()); This method verifies which values are different from "" (empty string) and null and takes them into account to create the search criteria: * If the field data type is text or similar (char, varchar, text, etc.) It uses an SQL "like" operator to filter the results. * If the data type is not text or similar, it'll use the operator "=". Additionally, "Criteria" ignores all the :code:`$_POST` variables that do not match any field in the table. Values are automatically escaped using "bound parameters". Now, we store the produced parameters in the controller's session bag: .. code-block:: php persistent->searchParams = $query->getParams(); A session bag, is a special attribute in a controller that persists between requests using the session service. When accessed, this attribute injects a :doc:`Phalcon\\Session\\Bag <../api/Phalcon_Session_Bag>` instance that is independent in each controller. Then, based on the built params we perform the query: .. code-block:: php flash->notice("The search did not found any products"); return $this->forward("products/index"); } If the search doesn't return any product, we forward the user to the index action again. Let's pretend the search returned results, then we create a paginator to navigate easily through them: .. 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(); Finally we pass the returned page to view: .. code-block:: php view->page = $page; In the view (app/views/products/search.volt), we traverse the results corresponding to the current page, showing every row in the current page to the user: .. 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 product.id is the same as in PHP as doing: :code:`$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 model Products (app/models/Products.php): .. code-block:: php belongsTo( 'product_types_id', 'ProductTypes', 'id', array( '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". .. code-block:: php belongsTo( 'product_types_id', 'ProductTypes', 'id', array( 'reusable' => true ) ); Which means, the local attribute "product_types_id" in "Products" has an one-to-many relation to the model "ProductTypes" in its attribute "id". By defining this relation 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) }} What in PHP 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. Creating and Updating Records ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Now let's see how the CRUD creates and updates records. From the "new" and "edit" views the data entered by the user are sent to the actions "create" and "save" that perform actions of "creating" and "updating" products respectively. In the creation case, we recover the data submitted and assign them to a new "products" instance: .. code-block:: php request->isPost()) { return $this->forward("products/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"); // ... } Remember the filters we defined in the Products form? Data is filtered before being assigned to the object :code:`$product`. This filtering is optional, also the ORM escapes the input data and performs additional casting according to the column types: .. code-block:: php setLabel("Name"); // Filters for name $name->setFilters(array('striptags', 'string')); // Validators for name $name->addValidators( array( new PresenceOf( array( 'message' => 'Name is required' ) ) ) ); $this->add($name); When saving we'll know whether the data conforms to the business rules and validations implemented in the form ProductsForm (app/forms/ProductsForm.php): .. code-block:: php request->getPost(); if (!$form->isValid($data, $product)) { foreach ($form->getMessages() as $message) { $this->flash->error($message); } return $this->forward('products/new'); } Finally, if the form does not return any validation message we can save the product instance: .. code-block:: php save() == false) { foreach ($product->getMessages() as $message) { $this->flash->error($message); } return $this->forward('products/new'); } $form->clear(); $this->flash->success("Product was created successfully"); return $this->forward("products/index"); Now, in the case of product updating, first we must present to the user the data that is currently in the edited record: .. code-block:: php request->isPost()) { $product = Products::findFirstById($id); if (!$product) { $this->flash->error("Product was not found"); return $this->forward("products/index"); } $this->view->form = new ProductsForm($product, array('edit' => true)); } } The data found is bound to the form passing the model as first parameter. Thanks to this, the user can change any value and then sent it back to the database through to the "save" action: .. code-block:: php request->isPost()) { return $this->forward("products/index"); } $id = $this->request->getPost("id", "int"); $product = Products::findFirstById($id); if (!$product) { $this->flash->error("Product does not exist"); return $this->forward("products/index"); } $form = new ProductsForm; $data = $this->request->getPost(); if (!$form->isValid($data, $product)) { foreach ($form->getMessages() as $message) { $this->flash->error($message); } return $this->forward('products/new'); } if ($product->save() == false) { foreach ($product->getMessages() as $message) { $this->flash->error($message); } return $this->forward('products/new'); } $form->clear(); $this->flash->success("Product was updated successfully"); return $this->forward("products/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: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete