Tutorial 2: Explaining INVO =========================== In this second tutorial, we'll explain a more complete application in order to deepen the development with Phalcon. INVO is one of the applications we have created as samples. INVO is a small website that allows their users to generate invoices, and do other tasks as manage their customers and products. You can clone its code from Github_. Also, INVO was made with `Twitter Bootstrap`_ as client-side framework. Although the application does not generate invoices still it serves as an example to understand how the framework works. Project Structure ------------------ Once you clone the project in your document root you'll see the following structure: .. code-block:: bash invo/ app/ app/config/ app/controllers/ app/library/ app/models/ app/plugins/ app/views/ public/ public/bootstrap/ public/css/ public/js/ schemas/ As you know, Phalcon does not impose a particular file structure for application development. This project provides a simple MVC structure and a public document root. Once you open the application in your browser http://localhost/invo you'll something like this: .. figure:: ../_static/img/invo-1.png :align: center The application is divided in two parts, a frontend, that is a public part where visitors can receive information about INVO and request contact information. The second part is the backend, an administrative area where a registered user can manage his/her products and customers. Routing ------- INVO uses the standard route that is built-in with the Router component. These routes matches the following pattern: /:controller/:action/:params. This means that the first part of an URI is the controller, the second the action and the rest are the parameters. The following route /session/register executes the controller SessionController and its action registerAction. Configuration ------------- INVO has a configuration file that sets general parameters in the application. This file is read in the first lines of the bootstrap file (public/index.php): .. code-block:: php ` allows us to manipulate the file in an object-oriented way. The configuration file contains the following settings: .. code-block:: ini [database] host = localhost username = root password = secret name = invo [application] controllersDir = /../app/controllers/ modelsDir = /../app/models/ viewsDir = /../app/views/ pluginsDir = /../app/plugins/ libraryDir = /../app/library/ baseUri = /invo/ ;[metadata] ;adapter = "Apc" ;suffix = my-suffix ;lifetime = 3600 Phalcon hasn't any pre-defined convention settings. Sections help us to organize the options as appropriate. In this file there are three sections to be used later. Autoloaders ----------- A second part that appears in the bootstrap file (public/index.php) is the autoloader. The autoloader registers a set of directories where the application will look for the classes that it eventually will need. .. code-block:: php registerDirs( array( $config->application->controllersDir, $config->application->pluginsDir, $config->application->libraryDir, $config->application->modelsDir, ) )->register(); Note that what has been done is registing the directories that were defined in the configuration file. The only directory that is not registered is the viewsDir, because it contains no classes but html + php files. Handling the Request -------------------- Let's go much further, at the end of the file, the request is finally handled by Phalcon\\Mvc\\Application, this class initializes and executes all the necessary to make the application run: .. code-block:: php handle()->getContent(); Dependency Injection -------------------- Look at the first line of the code block above, the variable $app is receiving another variable $di in its constructor. What is the purpose of that variable? Phalcon is a highly decoupled framework, so we need a component that acts as glue to make everything work together. That component is Phalcon\\DI. It is a service container that also performs dependency injection, instantiating all components, as they are needed by the application. There are many ways of registering services in the container. In INVO most services have been registered using anonymous functions. Thanks to this, the objects are instantiated in a lazy way, reducing the resources needed by the application. For instance, in the following excerpt, the session service is registered, the anonymous function will only be called when the application requires access to the session data: .. code-block:: php set('session', function() { $session = new Phalcon\Session\Adapter\Files(); $session->start(); return $session; }); Here, we have the freedom to change the adapter, perform additional initialization and much more. Note that the service was registered using the name "session". This is a convention that will allow the framework to identify the active service in the services container. A request can use many services, register each service one to one can be a cumbersome task. For that reason, the framework provides a variant of Phalcon\\DI called Phalcon\\DI\\FactoryDefault whose task is to register all services providing a full-stack framework. .. code-block:: php set('db', function() use ($config) { return new \Phalcon\Db\Adapter\Pdo\Mysql(array( "host" => $config->database->host, "username" => $config->database->username, "password" => $config->database->password, "dbname" => $config->database->name )); }); Here, we return an instance of the MySQL connection adapter. If needed, you could do extra actions such as adding a logger, a profiler or change the adapter, setting up it as you want. Back then, the following simple form (app/views/session/index.phtml) requests the logon information. We've removed some HTML code to make the example more concise: .. code-block:: html+php "30")) ?> "30")) ?> The SessionController::startAction (app/controllers/SessionController.phtml) has the task of validate the data entered checking for a valid user in the database: .. code-block:: php session->set('auth', array( 'id' => $user->id, 'name' => $user->name )); } public function startAction() { if ($this->request->isPost()) { //Receiving the variables sent by POST $email = $this->request->getPost('email', 'email'); $password = $this->request->getPost('password'); $password = sha1($password); //Find for the user in the database $user = Users::findFirst(array( "email = :email: AND password = :password: AND active = 'Y'", "bind" => array('email' => $email, 'password' => $password) )); if ($user != false) { $this->_registerSession($user); $this->flash->success('Welcome ' . $user->name); //Forward to the 'invoices' controller if the user is valid return $this->dispatcher->forward(array( 'controller' => 'invoices', 'action' => 'index' )); } $this->flash->error('Wrong email/password'); } //Forward to the login form again return $this->dispatcher->forward(array( 'controller' => 'session', 'action' => 'index' )); } } For simplicity, we have used "sha1_" to store the password hashes in the database, however, this algorithm is not recommended in real applications, use " :doc:`bcrypt `" instead. Note that multiple public attributes are accessed in the controller like: $this->flash, $this->request or $this->session. These are services defined in services container from earlier. When they're accessed the first time, are injected as part of the controller. These services are shared, which means that we are always accessing the same instance regardless of the place where we invoke them. For instance, here we invoke the "session" service and then we store the user identity in the variable "auth": .. code-block:: php session->set('auth', array( 'id' => $user->id, 'name' => $user->name )); Securing the Backend -------------------- The backend is a private area where only registered users have access. Therefore, it is necessary to check that only registered users have access to these controllers. If you aren't logged in the application and you try to access, for example, the products controller (that is private) you will see a screen like this: .. figure:: ../_static/img/invo-2.png :align: center Every time someone attempts to access any controller/action, the application verifies that the current role (in session) has access to it, otherwise it displays a message like the above and forwards the flow to the home page. Now let's find out how the application accomplishes this. The first thing to know is that there is a component called :doc:`Dispatcher `. It is informed about the route found by the :doc:`Routing ` component. Then, it is responsible for loading the appropriate controller and execute the corresponding action method. Normally, the framework creates the Dispatcher automatically. In our case, we want to perform a verification before executing the required action, checking if the user has access to it or not. To achieve this, we have replaced the component by creating a function in the bootstrap: .. code-block:: php set('dispatcher', function() use ($di) { $dispatcher = new Phalcon\Mvc\Dispatcher(); return $dispatcher; }); We now have total control over the Dispatcher used in the application. Many components in the framework trigger events that allow us to modify their internal flow of operation. As the dependency Injector component acts as glue for components, a new component called :doc:`EventsManager ` aids us to intercept the events produced by a component routing the events to listeners. Events Management ^^^^^^^^^^^^^^^^^ A :doc:`EventsManager ` allows us to attach listeners to a particular type of event. The type that interest us now is "dispatch", the following code filters all events produced by the Dispatcher: .. code-block:: php set('dispatcher', function() use ($di) { //Obtain the standard eventsManager from the DI $eventsManager = $di->getShared('eventsManager'); //Instantiate the Security plugin $security = new Security($di); //Listen for events produced in the dispatcher using the Security plugin $eventsManager->attach('dispatch', $security); $dispatcher = new Phalcon\Mvc\Dispatcher(); //Bind the EventsManager to the Dispatcher $dispatcher->setEventsManager($eventsManager); return $dispatcher; }); The Security plugin is a class located at (app/plugins/Security.php). This class implements the method "beforeExecuteRoute". This is the same name as one of the events produced in the Dispatcher: .. code-block:: php session->get('auth'); if (!$auth) { $role = 'Guests'; } else { $role = 'Users'; } //Take the active controller/action from the dispatcher $controller = $dispatcher->getControllerName(); $action = $dispatcher->getActionName(); //Obtain the ACL list $acl = $this->_getAcl(); //Check if the Role have access to the controller (resource) $allowed = $acl->isAllowed($role, $controller, $action); if ($allowed != Phalcon\Acl::ALLOW) { //If he doesn't have access forward him to the index controller $this->flash->error("You don't have access to this module"); $dispatcher->forward( array( 'controller' => 'index', 'action' => 'index' ) ); //Returning "false" we tell to the dispatcher to stop the current operation return false; } } } Providing an ACL list ^^^^^^^^^^^^^^^^^^^^^ In the above example we have obtained the ACL using the method $this->_getAcl(). This method is also implemented in the Plugin. Now we are going to explain step-by-step how we built the access control list (ACL): .. code-block:: php setDefaultAction(Phalcon\Acl::DENY); //Register two roles, Users is registered users //and guests are users without a defined identity $roles = array( 'users' => new Phalcon\Acl\Role('Users'), 'guests' => new Phalcon\Acl\Role('Guests') ); foreach ($roles as $role) { $acl->addRole($role); } Now we define the resources for each area respectively. Controller names are resources and their actions are accesses for the resources: .. code-block:: php array('index', 'search', 'new', 'edit', 'save', 'create', 'delete'), 'products' => array('index', 'search', 'new', 'edit', 'save', 'create', 'delete'), 'producttypes' => array('index', 'search', 'new', 'edit', 'save', 'create', 'delete'), 'invoices' => array('index', 'profile') ); foreach ($privateResources as $resource => $actions) { $acl->addResource(new Phalcon\Acl\Resource($resource), $actions); } //Public area resources (frontend) $publicResources = array( 'index' => array('index'), 'about' => array('index'), 'session' => array('index', 'register', 'start', 'end'), 'contact' => array('index', 'send') ); foreach ($publicResources as $resource => $actions) { $acl->addResource(new Phalcon\Acl\Resource($resource), $actions); } The ACL now have knowledge of the existing controllers and their related actions. Role "Users" has access to all the resources of both frontend and backend. The role "Guests" only has access to the public area: .. code-block:: php $actions) { $acl->allow($role->getName(), $resource, '*'); } } //Grant access to private area only to role Users foreach ($privateResources as $resource => $actions) { foreach ($actions as $action) { $acl->allow('Users', $resource, $action); } } Hooray!, the ACL is now complete. User Components --------------- All the UI elements and visual style of the application has been achieved mostly through `Twitter Bootstrap`_. Some elements, such as the navigation bar changes according to the state of the application. For example, in the upper right corner, the link "Log in / Sign Up" changes to "Log out" if an user is logged into the application. This part of the application is implemented in the component "Elements" (app/library/Elements.php). .. code-block:: php set('elements', function(){ return new Elements(); }); As controllers, plugins or components within a view, this component also has access to the services registered in the container and by just accessing an attribute with the same name as a previously registered service: .. code-block:: html+php
getContent() ?>

© Company 2012

The important part is: .. code-block:: html+php elements->getMenu() ?> Working with the CRUD --------------------- Most options that manipulate data (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/ app/controllers/ ProductsController.php app/models/ Products.php app/views/ products/ edit.phtml index.phtml new.phtml search.phtml Each controller has the following actions: .. code-block:: php persistent->searchParams = null; $this->view->productTypes = ProductTypes::find(); } All the "product types" are queried and passed to the view as a local variable "productTypes". Then, in the view (app/views/index.phtml) we show a "select" tag filled with those results: .. code-block:: html+php
array("id", "name"), "useDummy" => true )) ?>
Note that $productTypes contains the data necessary to fill the SELECT tag using Phalcon\\Tag::select. Once 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", $_POST); 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 $_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. When accessed, this attribute injects a :doc:`Phalcon\\Session\\Bag <../api/Phalcon_Session_Bag>` service 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->setVar("page", $page); In the view (app/views/products/search.phtml), we traverse the results corresponding to the current page: .. code-block:: html+php items as $product) { ?> id ?> getProductTypes()->name ?> name ?> price ?> active ?> id, 'Edit') ?> id, 'Delete') ?> 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 id = $this->request->getPost("id", "int"); $products->product_types_id = $this->request->getPost("product_types_id", "int"); $products->name = $this->request->getPost("name", "striptags"); $products->price = $this->request->getPost("price", "double"); $products->active = $this->request->getPost("active"); //... } Data is filtered before being assigned to the object. This filtering is optional, the ORM escapes the input data and performs additional casting according to the column types. When saving we'll know whether the data conforms to the business rules and validations implemented in the model Products: .. code-block:: php create()) { //The store failed, the following messages were produced foreach ($products->getMessages() as $message) { $this->flash->error((string) $message); } return $this->forward("products/new"); } else { $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 id); Tag::setDefault("product_types_id", $product->product_types_id); Tag::setDefault("name", $product->name); Tag::setDefault("price", $product->price); Tag::setDefault("active", $product->active); } The "setDefault" helper sets a default value in the form on the attribute with the same name. 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->getPost("id")); if (!$product) { $this->flash->error("products does not exist " . $id); return $this->forward("products/index"); } //... assign the values to the object and store it } Changing the Title Dynamically ------------------------------ When you browse between one option and another will see that the title changes dynamically indicating where we are currently working. This is achieved in each controller initializer: .. code-block:: php Conclusion ---------- This tutorial covers many more aspects of building applications with Phalcon, hope you have served to learn more and get more out of the framework. .. _Github: https://github.com/phalcon/invo .. _CRUD: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete .. _Twitter Bootstrap: http://bootstrap.github.com/ .. _sha1: http://php.net/manual/en/function.sha1.php .. _bcrypt: http://stackoverflow.com/questions/4795385/how-do-you-use-bcrypt-for-hashing-passwords-in-php