Tutorial - Vökuró


Vokuro - 图1

Vökuró

Vökuró is a sample application, showcasing a typical web application written in Phalcon. This application focuses on:

  • User Login (security)
  • User Signup (security)
  • User Permissions
  • User management

You can use Vökuró as a starting point for your application and enhance it further to meet your needs. By no means this is a perfect application and it does not fit all needs.

This tutorial assumes that you are familiar with the concepts of the Model View Controller design pattern. (see References at the end of this tutorial)

Note the code below has been formatted to increase readability

Installation

Downloading

In order to install the application, you can either clone or download it from GitHub. You can visit the GitHub page, download the application and then unzip it to a directory on your machine. Alternatively you can use git clone:

  1. git clone https://github.com/phalcon/vokuro

Extensions

There are some prerequisites for the Vökuró to run. You will need to have PHP >= 7.2 installed on your machine and the following extensions:

  • ctype
  • curl
  • dom
  • json
  • iconv
  • mbstring
  • memcached
  • opcache
  • openssl
  • pdo
  • pdo_mysql
  • psr
  • session
  • simplexml
  • xml
  • xmlwriterPhalcon needs to be installed. Head over to the installation page if you need help with installing Phalcon. Note that Phalcon v4 requires the PSR extension to be installed and loaded before Phalcon. To install PSR you can check the php-psr GitHub page.

Finally, you will also need to ensure that you have updated the composer packages (see section below).

Run

If all the above requirements are satisfied, you can run the application using PHP’s built in web server by issuing the following command on a terminal:

  1. php -S localhost:8080 -t public/ .htrouter.php

The above command will start serving the site for localhost at the port 8080. You can change those settings to suit your needs. Alternatively you can set up your site in Apache or nginX using a virtual host. Please consult the relevant documentation on how to set up a virtual host for these web servers.

Docker

In the resources folder you will find a Dockerfile which allows you to quickly set up the environment and run the application. To use the Dockerfile we need to decide the name of our dockerized application. For the purposes of this tutorial, we will use phalcon-tutorial-vokuro.

From the root of the application we need to compile the project (you only need to do this once):

  1. $ docker build -t phalcon-tutorial-vokuro -f resources/Dockerfile .

and then run it

  1. $ docker run -it --rm phalcon-tutorial-vokuro bash

This will enter us in the dockerized environment. To check the PHP version:

  1. [email protected]:/code $ php -v
  2. PHP 7.3.9 (cli) (built: Sep 12 2019 10:08:33) ( NTS )
  3. Copyright (c) 1997-2018 The PHP Group
  4. Zend Engine v3.3.9, Copyright (c) 1998-2018 Zend Technologies
  5. with Zend OPcache v7.3.9, Copyright (c) 1999-2018, by Zend Technologies

and Phalcon:

  1. [email protected]:/code $ php -r 'echo Phalcon\Version::get();'
  2. 4.0.0

You now have a dockerized environment with all the necessary components to run Vökuró.

Nanobox

In the resources folder you will also find a boxfile.yml file that allows you to use nanobox in order to set up the environment quickly. All you have to do is copy the file to the root of your directory and run nanobox run php-server. Once the application is set up for the first time, you will be able to navigate to the IP address presented on screen and work with the application.

For more information on how to set up nanobox, check our [Environments Nanobox][environments-nanobox] page as well as the Nanobox Guides page

In this tutorial, we assume that your application has been downloaded or cloned in a directory called vokuro.

Structure

Looking at the structure of the application we have the following:

  1. vokuro/
  2. .ci
  3. configs
  4. db
  5. migrations
  6. seeds
  7. public
  8. resources
  9. src
  10. Controllers
  11. Forms
  12. Models
  13. Phalcon
  14. Plugins
  15. Providers
  16. tests
  17. themes
  18. vokuro
  19. var
  20. cache
  21. acl
  22. metaData
  23. session
  24. volt
  25. logs
  26. vendor
DirectoryDescription
.ciFiles necessary for setting services for the CI
configsConfiguration files
dbHolds the migrations for the databsae
publicEntry point for the application, css, js, images
resourcesDocker/nanobox files for setting the application
srcWhere the application lives (controllers, forms etc.)
src/ControllersControllers
src/FormsForms
src/ModelsDatabase Models
src/PluginsPlugins
src/ProvidersProviders: setting services in the DI container
testsTests
themesThemes/views for easy customization
themes/vokuroDefault theme for the application
varVarious supporting files
var/cacheCache files
var/logsLogs
vendorVendor/composer based libraries

Configuration

.env

Vökuró uses the popular Dotenv library by Vance Lucas. The library utilizes a .env file located in your root folder, which holds configuration parameters such as the database server host, username, password etc. There is a .env.example file that comes with Vökuró that you can copy and rename to .env and then edit it to match your environment. You need to do this first so that your application can run properly.

The available options are:

OptionDescription
APP_CRYPT_SALTRandom and long string that is used by the Phalcon\Crypt component to produce passwords and any additional security features
APP_BASE_URIUsually / if your web server points directly to the Vökuró directory. If you have installed Vökuró in a sub directory, you can adjust the base URI
APP_PUBLIC_URLThe public URL of the application. This is used for the emails.
DB_ADAPTERThe database adapter. The available adapters are: mysql, pgsql, sqlite. Please ensure that the relevant extensions for the database are installed in your system.
DB_HOSTThe database host
DB_PORTThe database port
DB_USERNAMEThe database username
DB_PASSWORDThe database password
DB_NAMEThe database name
MAIL_FROM_NAMEThe FROM name when sending emails
MAIL_FROM_EMAILThe FROM email when sending emails
MAIL_SMTP_SERVERThe SMTP server
MAIL_SMTP_PORTThe SMTP port
MAIL_SMTP_SECURITYThe SMTP security (e.g. tls)
MAIL_SMTP_USERNAMEThe SMTP username
MAIL_SMTP_PASSWORDThe SMTP password
CODECEPTION_URLThe Codeception server for tests. If you run the tests locally this should be 127.0.0.1
CODECEPTION_PORTThe Codeception port

Once the configuration file is in place, visiting the IP address will present a screen similar to this:

Vokuro - 图2

Database

You also need to initialize the database. Vökuró uses the popular library Phinx by Rob Morgan (now the Cake Foundation). The library uses its own configuration file (phinx.php), but for Vökuró you don’t need to adjust any settings since phinx.php reads the .env file to retrieve the configuration settings. This allows you to set your configuration parameters in one place.

We will now need to run the migrations. To check the status of our database:

  1. /app $ ./vendor/bin/phinx status

You will see the following screen:

Vokuro - 图3

To initialize the database we need to run the migrations:

  1. /app $ ./vendor/bin/phinx migrate

The screen will show the operation:

Vokuro - 图4

And the status command will now show all green:

Vokuro - 图5

Config

acl.php

Looking at the config/ folder, you will notice four files. There is no need for you to change these files to start the application but if you wish to customize it, this is the place to visit. The acl.php file returns an array of routes that controls which routes are visible to only logged in users.

The current setup will require a user to be logged in, if they visit these routes:

  • users/index
  • users/search
  • users/edit
  • users/create
  • users/delete
  • users/changePassword
  • profiles/index
  • profiles/search
  • profiles/edit
  • profiles/create
  • profiles/delete
  • permissions/indexIf you use Vökuró as a starting point for your own application, you will need to modify this file to add or remove routes so as to ensure that your protected routes are behind the login mechanism.

Keeping the private routes in an array is efficient and easy to maintain for a small to medium application. Once your application starts growing, you might need to consider a different technique to keep your private routes such as the database with a caching mechanism.

config.php

This file holds all configuration parameters that Vökuró needs. Usually you will not need to change this file, since the elements of the array are set by the .env file and Dotenv. However, you might want to change the location of your logs or other paths, should you decide to change the directory structure.

One of the elements you might want to consider when working with Vökuró on your local machine is the useMail and set it to false. This will instruct Vökuró not to try to connect to a mail server and send an email when a user registers on the site.

providers.php

This file contains all the providers that Vökuró needs. This is a list of classes in the application, that registers the particular class in the DI container. If you need to register new components to your DI container, you can add them to the array of this file.

routes.php

This file contains the routes that Vökuró understands. The router already registers the default routes, so any routes defined in routes.php are specific ones. You can add any non standard routes you need, when customizing Vökuró, in this file. As a reminder, the default routes are:

  1. /:controller/:action/:parameters

Providers

As mentioned above, Vökuró uses classes called Providers in order to register services in the DI container. This is just one way to register services in the DI container, nothing stops you from putting all these registrations in a single file.

For Vökuró we decided to use one file per service as well as a providers.php (see above) as the registration configuration array for these services. This allows us to have much smaller chunks of code, organized in a separate file per service, as well as an array that allows us to register or unregister/disable a service without removing files. All we need to do is change the providers.php array.

The provider classes are located in src/Providers. Each of the provider classes implements the Phalcon\Di\ServiceProviderInterface interface. For more information, see the bootstrapping section below.

Composer

Vökuró uses composer to download and install supplemental PHP libraries. The libraries used are:

  1. "require": {
  2. "php": ">=7.2",
  3. "ext-openssl": "*",
  4. "ext-phalcon": "~4.0.0-beta.2",
  5. "robmorgan/phinx": "^0.11.1",
  6. "swiftmailer/swiftmailer": "^5.4",
  7. "vlucas/phpdotenv": "^3.4"
  8. }

If this is a fresh installation you can run

  1. composer install

of if you want to upgrade the existing installations of the above packages:

  1. composer update

For more information about composer, you can visit their documentation page.

Bootstrapping

Entry

The entry point of our application is public/index.php. This file contains the necessary code that bootstraps the application and runs it. It also serves as a single point of entry to our application, making things much easier for us when we want to trap errors, protect files etc.

Let’s look at the code:

  1. <?php
  2. use Vokuro\Application as VokuroApplication;
  3. error_reporting(E_ALL);
  4. $rootPath = dirname(__DIR__);
  5. try {
  6. require_once $rootPath . '/vendor/autoload.php';
  7. /**
  8. * Load .env configurations
  9. */
  10. Dotenv\Dotenv::create($rootPath)->load();
  11. /**
  12. * Run Vökuró!
  13. */
  14. echo (new VokuroApplication($rootPath))->run();
  15. } catch (Exception $e) {
  16. echo $e->getMessage(), '<br>';
  17. echo nl2br(htmlentities($e->getTraceAsString()));
  18. }

First of all we ensure that we have full error reporting. You can of course change this if you wish, or rework the code where error reporting is controlled by an entry in your .env file.

A try/catch block wraps all operations. This ensures that all errors are caught and displayed on screen.

NOTE You will need to rework the code to enhance security. Currently, if an error happens with the database, the catch code will echo on screen the database credentials with the exception. This code is intended as a tutorial not a full scale production application

We ensure that we have access to all the supporting libraries by loading composer’s autoloader. In the composer.json we have also defined the autoload entry, directing the autoloader to load any Vokuro namespaced classes from the src folder.

  1. "autoload": {
  2. "psr-4": {
  3. "Vokuro\\": "app/"
  4. },
  5. "files": [
  6. "app/Helpers.php"
  7. ]
  8. }

Then we load the environment variables as defined in our .env file by calling the

  1. Dotenv\Dotenv::create($rootPath)->load();

Finally, we run our application.

Application

All the application logic is wrapped in the Vokuro\Application class. Let’s see how this is done:

  1. <?php
  2. declare(strict_types=1);
  3. namespace Vokuro;
  4. use Exception;
  5. use Phalcon\Application\AbstractApplication;
  6. use Phalcon\Di\DiInterface;
  7. use Phalcon\Di\FactoryDefault;
  8. use Phalcon\Di\ServiceProviderInterface;
  9. use Phalcon\Mvc\Application as MvcApplication;
  10. /**
  11. * Vökuró Application
  12. */
  13. class Application
  14. {
  15. const APPLICATION_PROVIDER = 'bootstrap';
  16. /**
  17. * @var AbstractApplication
  18. */
  19. protected $app;
  20. /**
  21. * @var DiInterface
  22. */
  23. protected $di;
  24. /**
  25. * Project root path
  26. *
  27. * @var string
  28. */
  29. protected $rootPath;
  30. /**
  31. * @param string $rootPath
  32. *
  33. * @throws Exception
  34. */
  35. public function __construct(string $rootPath)
  36. {
  37. $this->di = new FactoryDefault();
  38. $this->app = $this->createApplication();
  39. $this->rootPath = $rootPath;
  40. $this->di->setShared(self::APPLICATION_PROVIDER, $this);
  41. $this->initializeProviders();
  42. }
  43. /**
  44. * Run Vökuró Application
  45. *
  46. * @return string
  47. * @throws Exception
  48. */
  49. public function run(): string
  50. {
  51. return (string) $this
  52. ->app
  53. ->handle($_SERVER['REQUEST_URI'])
  54. ->getContent()
  55. ;
  56. }
  57. /**
  58. * Get Project root path
  59. *
  60. * @return string
  61. */
  62. public function getRootPath(): string
  63. {
  64. return $this->rootPath;
  65. }
  66. /**
  67. * @return AbstractApplication
  68. */
  69. protected function createApplication(): AbstractApplication
  70. {
  71. return new MvcApplication($this->di);
  72. }
  73. /**
  74. * @throws Exception
  75. */
  76. protected function initializeProviders(): void
  77. {
  78. $filename = $this->rootPath
  79. . '/configs/providers.php';
  80. if (!file_exists($filename) || !is_readable($filename)) {
  81. throw new Exception(
  82. 'File providers.php does not exist or is not readable.'
  83. );
  84. }
  85. $providers = include_once $filename;
  86. foreach ($providers as $providerClass) {
  87. /** @var ServiceProviderInterface $provider */
  88. $provider = new $providerClass;
  89. $provider->register($this->di);
  90. }
  91. }
  92. }

The constructor of the class first creates a new DI container and store it in a local property. We are using the Phalcon\Di\FactoryDefault one, which has a lot of services already registered for us.

We then create a new Phalcon\Mvc\Application and store it in a property also. We also store the root path because it is useful throughout the application.

We then register this class (the Vokuro\Application) in the Di container using the name bootstrap. This allows us to have access to this class from any part of our application through the Di container.

The last thing we do is to register all the providers. Although the Phalcon\Di\FactoryDefault object has a lot of services already registered for us, we still need to register providers that suit the needs of our application. As mentioned above, each provider class implements the Phalcon\Di\ServiceProviderInterface interface, so we can load each class and call the register() method with the Di container to register each service. We therefore first load the configuration array config/providers.php and then loop through the entries and register each provider in turn.

The available providers are:

ProviderDescription
AclProviderPermissons
AuthProviderAuthentication
ConfigProviderConfiguration values
CryptProviderEncryption
DbProviderDatabase access
DispatcherProviderDispatcher - what controller to call for what URL
FlashProviderFlash messages for feedback to the user
LoggerProviderLogger for errors and other information
MailProviderMail support
ModelsMetadataProviderMetadata for models
RouterProviderRoutes
SecurityProviderSecurity
SessionBagProviderSession data
SessionProviderSession data
UrlProviderURL handling
ViewProviderViews and view engine

run() will now handle the REQUEST_URI, handle it and return the content back. Internally the application will calculate the route based on the request, and dispatch the relevant controller and view before returning the result of this operation back to the user as a response.

Database

As mentioned above, Vökuró can be installed with MariaDB/MySQL/Aurora, PostgreSql or SQLite as the database store. For the purposes of this tutorial, we are using MariaDB. The tables that the application uses are:

TableDescription
emailconfirmationsEmail confirmations for registration
failed_loginsFailed login attempts
password_changesWhen a password was changed and by whom
permissionsPermission matrix
phinxlogPhinx migration table
profilesProfile for each user
remember_tokens_Remember Me functionality tokens
reset_passwordsReset password tokens table
success_loginsSuccessful login attempts
usersUsers

Models

Following the Model-View-Controller pattern, Vökuró has one model per database table (excluding the phinxlog). The models allow us to interact with the database tables in an easy object oriented manner. The models are located in the /src/Models directory, and each model defines the relevant fields, source table as well as any relationships between the model and others. Some models also implement validation rules to ensure that data is stored properly in the database.

  1. <?php
  2. declare(strict_types=1);
  3. namespace Vokuro\Models;
  4. use Phalcon\Mvc\Model;
  5. /**
  6. * SuccessLogins
  7. *
  8. * This model registers successfully logins registered users have made
  9. */
  10. class SuccessLogins extends Model
  11. {
  12. /**
  13. * @var integer
  14. */
  15. public $id;
  16. /**
  17. * @var integer
  18. */
  19. public $usersId;
  20. /**
  21. * @var string
  22. */
  23. public $ipAddress;
  24. /**
  25. * @var string
  26. */
  27. public $userAgent;
  28. public function initialize()
  29. {
  30. $this->belongsTo(
  31. 'usersId',
  32. Users::class,
  33. 'id',
  34. [
  35. 'alias' => 'user',
  36. ]
  37. );
  38. }
  39. }

In the model above, we have defined all the fields of the table as public properties for easy access:

  1. echo $successLogin->ipAddress;

If you notice, the property names map exactly the case (upper/lower) of the field names in the relevant table.

In the initialize() method, we also define a relationship between this model and the Users model. We assign the fields (local/remote) as well as an alias for this relationship. We can therefore access the user related to a record of this model as follows:

  1. echo $successLogin->user->name;

Feel free to open each model file and identify the relationships between the models. Check our documentation for the difference between various types of relationships

Controllers

Again following the Model-View-Controller pattern, Vökuró has one controller to handle a specific parent route. This means that the AboutController handles the /about route. All controllers are located in the /src/Cotnrollers directory.

The default controller is IndexController. All controller classes have the suffix Controller. Each controller has methods suffixed with Action and the default action is indexAction. Therefore if you visit the site with just the URL, the IndexController will be called and the indexAction will be executed.

After that, unless you have registered specific routes, the default routes (automatically registered) will try to match:

  1. /profiles/search

to

  1. /src/Controllers/SearchController.php -> searchAction

The available controllers, actions and routes for Vökuró are:

ControllerActionRouteDescription
Aboutindex/aboutShows the about page
Indexindex/Default action - home page
Permissionsindex/permissionsView/change permissions for a profile level
Privacyindex/privacyView the privacy page
Profilesindex/profilesView profiles default page
Profilescreate/profiles/createCreate profile
Profilesdelete/profiles/deleteDelete profile
Profilesedit/profiles/editEdit profile
Profilessearch/profiles/searchSearch profiles
Sessionindex/sessionSession default action
SessionforgotPassword/session/forgotPasswordForget password
Sessionlogin/session/loginLogin
Sessionlogout/session/logoutLogout
Sessionsignup/session/signupSignup
Termsindex/termsView the terms page
UserControlconfirmEmail/confirmConfirm email
UserControlresetPassword/reset-passwordReset password
Usersindex/usersUsers default screen
UserschangePassword/users/changePasswordChange user password
Userscreate/users/createCreate user
Usersdelete/users/deleteDelete user
Usersedit/users/editEdit user

Views

The last element of the Model-View-Controller pattern is the views. Vökuró uses Volt as the view engine for its views.

Generally, one would expect to see a views folder under the /src folder. Vökuró uses a slightly different approach, storing all the view files under /themes/vokuro.

The views directory contains directories that map to each controller. Inside each of those directories, .volt files are mapped to each action. So for example the route:

  1. /profiles/create

maps to:

  1. ProfilesController -> createAction

and the view is located:

  1. /themes/vokuro/profiles/create.volt

The available views are:

ControllerActionViewDescription
Aboutindex/about/index.voltShows the about page
Indexindex/index/index.voltDefault action - home page
Permissionsindex/permissions/index.voltView/change permissions for a profile level
Privacyindex/privacy/index.voltView the privacy page
Profilesindex/profiles/index.voltView profiles default page
Profilescreate/profiles/create.voltCreate profile
Profilesdelete/profiles/delete.voltDelete profile
Profilesedit/profiles/edit.voltEdit profile
Profilessearch/profiles/search.voltSearch profiles
Sessionindex/session/index.voltSession default action
SessionforgotPassword/session/forgotPassword.voltForget password
Sessionlogin/session/login.voltLogin
Sessionlogout/session/logout.voltLogout
Sessionsignup/session/signup.voltSignup
Termsindex/terms/index.voltView the terms page
Usersindex/users/index.voltUsers default screen
UserschangePassword/users/changePassword.voltChange user password
Userscreate/users/create.voltCreate user
Usersdelete/users/delete.voltDelete user
Usersedit/users/edit.voltEdit user

The /index.volt file contains the main layout of the page, including stylesheets, javascript references etc. The /layouts directory contains different layouts that are used in the application, for instance a public one if the user is not logged in, and a private one for logged in users. The individual views are injected into the layouts and construct the final page.

Components

There are several components that we use in Vökuró, offering functionality throughout the application. All these components are located in the /src/Plugins directory.

Acl

Vokuro\Plugins\Acl\Acl is a component that implements an Access Control List for our application. The ACL controls which user has access to which resources. You can read more about ACL in our dedicated page.

In this component, We define the resources that are considered private. These are held in an internal array with controller as the key and action as the value, and identify which controller/actions require authentication. It also holds human readable descriptions for actions used throughout the application.

The component exposes the following methods:

MethodReturnsDescription
getActionDescription($action)stringReturns the action description according to its simplified name
getAcl()ACL objectReturns the ACL list
getPermissions(Profiles $profile)arrayReturns the permissions assigned to a profile
getResources()arrayReturns all the resources and their actions available
isAllowed($profile, $controller, $action)boolChecks if the current profile is allowed to access a resource
isPrivate($controllerName)boolChecks if a controller is private or not
rebuild()ACL objectRebuilds the access list into a file

Auth

Vokuro\Plugins\Auth\Auth is a component that manages authentication and offers identity management in Vökuró.

The component exposes the following methods:

MethodDescription
check($credentials) Checks the user credentials
saveSuccessLogin($user)Creates the remember me environment settings the related cookies and generating tokens
registerUserThrottling($userId)Implements login throttling. Reduces the effectiveness of brute force attacks
createRememberEnvironment(Users $user)Creates the remember me environment settings the related cookies and generating tokens
hasRememberMe(): boolCheck if the session has a remember me cookie
loginWithRememberMe(): ResponseLogs on using the information in the cookies
checkUserFlags(Users $user)Checks if the user is banned/inactive/suspended
getIdentity(): array / nullReturns the current identity
getName(): stringReturns the name of the user
remove()Removes the user identity information from session
authUserById($id)Authenticates the user by his/her id
getUser(): UsersGet the entity related to user in the active identity
findFirstByToken($token): int / nullReturns the current token user
deleteToken(int $userId)Delete the current user token in session

Mail

Vokuro\Plugins\Mail\Mail is a wrapper to Swift Mailer. It exposes two methods send() and getTemplate() which allow you to get a template from the views and populate it with data. The resulting HTML can then be used in the send() method along with the recipient and other parameters to send the email message.

Note that this component is used only if useMail is enabled in your .env file. You will also need to ensure that the SMTP server and credentials are valid.

Sign Up

Controller

In order to access all the areas of Vökuró you need to have an account. Vökuró allows you to sign up to the site by clicking the Create an Account button.

What this will do is navigate you to the /session/signup URL, which in turn will call the SessionController and signupAction. Let’s have a look what is going on in the signupAction:

  1. <?php
  2. declare(strict_types=1);
  3. namespace Vokuro\Controllers;
  4. use Phalcon\Flash\Direct;
  5. use Phalcon\Http\Request;
  6. use Phalcon\Mvc\Dispatcher;
  7. use Phalcon\Security;
  8. use Phalcon\Mvc\View;
  9. use Vokuro\Forms\SignUpForm;
  10. use Vokuro\Models\Users;
  11. /**
  12. * @property Dispatcher $dispatcher
  13. * @property Direct $flash
  14. * @property Request $request
  15. * @property Security $security
  16. * @property View $view
  17. */
  18. class SessionController extends ControllerBase
  19. {
  20. /**
  21. * Allow a user to signup to the system
  22. */
  23. public function signupAction()
  24. {
  25. $form = new SignUpForm();
  26. // ....
  27. $this->view->setVar('form', $form);
  28. }
  29. }

The workflow of the application is:

  • Visit /session/signup
    • Create form, send form to the view, render the form
  • Submit data (not post)
    • Form shows again, nothing else happens
  • Submit data (post)
    • Errors
      • Form validators have errors, send the form tothe view, render the form (errors will show)
    • No errors
      • Data is sanitized
      • New Model created
      • Data saved in the database
        • Error
          • Show message on screen and refresh the form
        • Success
          • Record saved
          • Show confirmation on screen
          • Send email (if applicable)

Form

In order to have validation for user supplied data, we are utilizing the Phalcon\Forms\Form and Phalcon\Validation* classes. These classes allow us to create HTML elements and attach validators to them. The form is then passed to the view, where the actual HTML elements are rendered on the screen.

When the user submits information, we send the posted data back to the form and the relevant validators validate the input and return any potential error messages.

All the forms for Vökuró are located in /src/Forms

First we create a SignUpForm object. In that object we define all the HTML elements we need with their respective validators:

  1. <?php
  2. declare(strict_types=1);
  3. namespace Vokuro\Forms;
  4. use Phalcon\Forms\Element\Check;
  5. use Phalcon\Forms\Element\Hidden;
  6. use Phalcon\Forms\Element\Password;
  7. use Phalcon\Forms\Element\Submit;
  8. use Phalcon\Forms\Element\Text;
  9. use Phalcon\Forms\Form;
  10. use Phalcon\Validation\Validator\Confirmation;
  11. use Phalcon\Validation\Validator\Email;
  12. use Phalcon\Validation\Validator\Identical;
  13. use Phalcon\Validation\Validator\PresenceOf;
  14. use Phalcon\Validation\Validator\StringLength;
  15. class SignUpForm extends Form
  16. {
  17. /**
  18. * @param string|null $entity
  19. * @param array $options
  20. */
  21. public function initialize(
  22. string $entity = null,
  23. array $options = []
  24. ) {
  25. $name = new Text('name');
  26. $name->setLabel('Name');
  27. $name->addValidators(
  28. [
  29. new PresenceOf(
  30. [
  31. 'message' => 'The name is required',
  32. ]
  33. ),
  34. ]
  35. );
  36. $this->add($name);
  37. // Email
  38. $email = new Text('email');
  39. $email->setLabel('E-Mail');
  40. $email->addValidators(
  41. [
  42. new PresenceOf(
  43. [
  44. 'message' => 'The e-mail is required',
  45. ]
  46. ),
  47. new Email(
  48. [
  49. 'message' => 'The e-mail is not valid',
  50. ]
  51. ),
  52. ]
  53. );
  54. $this->add($email);
  55. // Password
  56. $password = new Password('password');
  57. $password->setLabel('Password');
  58. $password->addValidators(
  59. [
  60. new PresenceOf(
  61. [
  62. 'message' => 'The password is required',
  63. ]
  64. ),
  65. new StringLength(
  66. [
  67. 'min' => 8,
  68. 'messageMinimum' => 'Password is too short. ' .
  69. 'Minimum 8 characters',
  70. ]
  71. ),
  72. new Confirmation(
  73. [
  74. 'message' => "Password doesn't match " .
  75. "confirmation",
  76. 'with' => 'confirmPassword',
  77. ]
  78. ),
  79. ]
  80. );
  81. $this->add($password);
  82. // Confirm Password
  83. $confirmPassword = new Password('confirmPassword');
  84. $confirmPassword->setLabel('Confirm Password');
  85. $confirmPassword->addValidators(
  86. [
  87. new PresenceOf(
  88. [
  89. 'message' => 'The confirmation password ' .
  90. 'is required',
  91. ]
  92. ),
  93. ]
  94. );
  95. $this->add($confirmPassword);
  96. // Remember
  97. $terms = new Check(
  98. 'terms',
  99. [
  100. 'value' => 'yes',
  101. ]
  102. );
  103. $terms->setLabel('Accept terms and conditions');
  104. $terms->addValidator(
  105. new Identical(
  106. [
  107. 'value' => 'yes',
  108. 'message' => 'Terms and conditions must be ' .
  109. 'accepted',
  110. ]
  111. )
  112. );
  113. $this->add($terms);
  114. // CSRF
  115. $csrf = new Hidden('csrf');
  116. $csrf->addValidator(
  117. new Identical(
  118. [
  119. 'value' => $this->security->getRequestToken(),
  120. 'message' => 'CSRF validation failed',
  121. ]
  122. )
  123. );
  124. $csrf->clear();
  125. $this->add($csrf);
  126. // Sign Up
  127. $this->add(
  128. new Submit(
  129. 'Sign Up',
  130. [
  131. 'class' => 'btn btn-success',
  132. ]
  133. )
  134. );
  135. }
  136. /**
  137. * Prints messages for a specific element
  138. *
  139. * @param string $name
  140. *
  141. * @return string
  142. */
  143. public function messages(string $name)
  144. {
  145. if ($this->hasMessagesFor($name)) {
  146. foreach ($this->getMessagesFor($name) as $message) {
  147. return $message;
  148. }
  149. }
  150. return '';
  151. }
  152. }

In the initialize method we are setting up all the HTML elements we need. These elements are:

ElementTypeDescription
nameTextThe name of the user
emailTextThe email for the account
passwordPasswordThe password for the account
confirmPasswordPasswordPassword confirmation
termsCheckAccept the terms checkbox
csrfHiddenCSRF protection element
Sign UpSubmitSubmit button

Adding elements is pretty straight forward:

  1. <?php
  2. declare(strict_types=1);
  3. // Email
  4. $email = new Text('email');
  5. $email->setLabel('E-Mail');
  6. $email->addValidators(
  7. [
  8. new PresenceOf(
  9. [
  10. 'message' => 'The e-mail is required',
  11. ]
  12. ),
  13. new Email(
  14. [
  15. 'message' => 'The e-mail is not valid',
  16. ]
  17. ),
  18. ]
  19. );
  20. $this->add($email);

First we create a Text object and set its name to email. We also set the label of the element to E-Mail. After that we attach various validators on the element. These will be invoked after the user submits data, and that data is passed in the form.

As we see above, we attach the PresenceOf validator on the email element with a message The e-mail is required. The validator will check if the user has submitted data when they clicked the submit button and will produce the message if the validator fails. The validator checks the passed array (usually $_POST) and for this particular element it will check $_POST['email'].

We also attach the Email validator, which is responsible for checking for a valid email address. As you can see the validators belong in an array, so you can easily attach as many validators as you need on any particular element.

The last thing we do is to add the element in the form.

You will notice that the terms element does not have any validators attached to it, so our form will not check the contents of the element.

Special attention to the password and confirmPassword elements. You will notice that both elements are of type Password. The idea is that you need to type your password twice, and the passwords need to match in order to avoid errors.

The password field has two validators for content: PresenceOf i.e. it is required and StringLength: we need the password to be more than 8 characters. We also attach a third validator called Confirmation. This special validator ties the password element with the confirmPassword element. When it is triggered to validate it will check the contents of both elements and if they are not identical, the error message will appear i.e. the validation will fail.

View

Now that we have everything set up in our form, we pass the form to the view:

  1. $this->view->setVar('form', $form);

Our view now needs to render the elements:

  1. {# ... #}
  2. {%
  3. set isEmailValidClass = form.messages('email') ?
  4. 'form-control is-invalid' :
  5. 'form-control'
  6. %}
  7. {# ... #}
  8. <h1 class="mt-3">Sign Up</h1>
  9. <form method="post">
  10. {# ... #}
  11. <div class="form-group row">
  12. {{
  13. form.label(
  14. 'email',
  15. [
  16. 'class': 'col-sm-2 col-form-label'
  17. ]
  18. )
  19. }}
  20. <div class="col-sm-10">
  21. {{
  22. form.render(
  23. 'email',
  24. [
  25. 'class': isEmailValidClass,
  26. 'placeholder': 'Email'
  27. ]
  28. )
  29. }}
  30. <div class="invalid-feedback">
  31. {{ form.messages('email') }}
  32. </div>
  33. </div>
  34. </div>
  35. {# ... #}
  36. <div class="form-group row">
  37. <div class="col-sm-10">
  38. {{
  39. form.render(
  40. 'csrf',
  41. [
  42. 'value': security.getToken()
  43. ]
  44. )
  45. }}
  46. {{ form.messages('csrf') }}
  47. {{ form.render('Sign Up') }}
  48. </div>
  49. </div>
  50. </form>
  51. <hr>
  52. {{ link_to('session/login', "&larr; Back to Login") }}

The variable that we set in our view for our SignUpForm object is called form. We therefore use it directly and call the methods of it. The syntax in Volt is slightly different. In PHP we would use $form->render() whereas in Volt we will use form.render().

The view contains a conditional at the top, checking whether there have been any errors in our form, and if there were, it attaches the is-invalid CSS class to the element. This class puts a nice red border by the element, highlighting the error and showing the message.

After that we have regular HTML tags with the relevant styling. In order to display the HTML code of each element we need to call render() on the form with the relevant element name. Also note that we also call form.label() with the same element name, so that we can create respective <label> tags.

At the end of the view we render the CSRF hidden field as well as the submit button Sign Up.

Post

As mentioned above, once the user fills the form and clicks the Sign Up button, the form will self post i.e. it will post the data on the same controller and action (in our case /session/signup). The action now needs to process this posted data:

  1. <?php
  2. declare(strict_types=1);
  3. namespace Vokuro\Controllers;
  4. use Phalcon\Flash\Direct;
  5. use Phalcon\Http\Request;
  6. use Phalcon\Mvc\Dispatcher;
  7. use Phalcon\Security;
  8. use Phalcon\Mvc\View;
  9. use Vokuro\Forms\SignUpForm;
  10. use Vokuro\Models\Users;
  11. /**
  12. * @property Dispatcher $dispatcher
  13. * @property Direct $flash
  14. * @property Request $request
  15. * @property Security $security
  16. * @property View $view
  17. */
  18. class SessionController extends ControllerBase
  19. {
  20. /**
  21. * Allow a user to signup to the system
  22. */
  23. public function signupAction()
  24. {
  25. $form = new SignUpForm();
  26. if (true === $this->request->isPost()) {
  27. if (false !== $form->isValid($this->request->getPost())) {
  28. $name = $this
  29. ->request
  30. ->getPost('name', 'striptags')
  31. ;
  32. $email = $this
  33. ->request
  34. ->getPost('email')
  35. ;
  36. $password = $this
  37. ->request
  38. ->getPost('password')
  39. ;
  40. $password = $this
  41. ->security
  42. ->hash($password)
  43. ;
  44. $user = new Users(
  45. [
  46. 'name' => $name,
  47. 'email' => $email,
  48. 'password' => $password,
  49. 'profilesId' => 2,
  50. ]
  51. );
  52. if ($user->save()) {
  53. return $this->dispatcher->forward([
  54. 'controller' => 'index',
  55. 'action' => 'index',
  56. ]);
  57. }
  58. foreach ($user->getMessages() as $message) {
  59. $this->flash->error((string) $message);
  60. }
  61. }
  62. }
  63. $this->view->setVar('form', $form);
  64. }
  65. }

If the user has submitted data, the following line will evaluate and we will be executing code inside the if statement:

  1. if (true === $this->request->isPost()) {

Here we are checking the request that came from the user, if it is a POST. Now that it is, we need to use the form validators and check if we have any errors. The Phalcon\Http\Request object, allows us to get that data easily by using:

  1. $this->request->getPost()

We now need to pass this posted data in the form and call isValid. This will fire all the validators for each element and if any of them fail, the form will populate the internal messages collection and return false

  1. if (false !== $form->isValid($this->request->getPost())) {

If everything is fine, we use again the Phalcon\Http\Request object to retrieve the submitted data but also sanitize them. The following example strips the tags from the submitted name string:

  1. $name = $this
  2. ->request
  3. ->getPost('name', 'striptags')
  4. ;

Note that we never store clear text passwords. Instead we use the Phalcon\Security component and call hash on it, to transform the supplied password to a one way hash and store that instead. This way, if someone compromises our database, at least they have no access to clear text passwords.

  1. $password = $this
  2. ->security
  3. ->hash($password)
  4. ;

We now need to store the supplied data in the database. We do that by creating a new Users model, pass the sanitized data into it and then call save:

  1. $user = new Users(
  2. [
  3. 'name' => $name,
  4. 'email' => $email,
  5. 'password' => $password,
  6. 'profilesId' => 2,
  7. ]
  8. );
  9. if ($user->save()) {
  10. return $this
  11. ->dispatcher
  12. ->forward(
  13. [
  14. 'controller' => 'index',
  15. 'action' => 'index',
  16. ]
  17. );
  18. }

If the $user->save() returns true, the user will be forwarded to the home page (index/index) and a success message will appear on screen.

Model

Relationships

Now we need to check the Users model, since there is some logic we have applied there, in particular the afterSave and beforeValidationOnCreate events.

The core method, the setup if you like happens in the inintialize method. That is the spot where we set all the relationships for the model. For the Users class we have several relationships defined. Why relationships you might ask? Phalcon offers an easy way to retrieve related data to a particular model.

If for instance we want to check all the successful logins for a particular user, we can do so with the following code snippet:

  1. <?php
  2. declare(strict_types=1);
  3. use Vokuro\Models\SuccessLogins;
  4. use Vokuro\Models\Users;
  5. $user = Users::findFirst(
  6. [
  7. 'conditions' => 'id = :id:',
  8. 'bind' => [
  9. 'id' => 7,
  10. ]
  11. ]
  12. );
  13. $logins = SuccessLogin::find(
  14. [
  15. 'conditions' => 'userId = :userId:',
  16. 'bind' => [
  17. 'userId' => 7,
  18. ]
  19. ]
  20. );

The above code gets the user with id 7 and then gets all the successful logins from the relevant table for that user.

Using relationships we can let Phalcon do all the heavy lifting for us. So the code above becomes:

  1. <?php
  2. declare(strict_types=1);
  3. use Vokuro\Models\SuccessLogins;
  4. use Vokuro\Models\Users;
  5. $user = Users::findFirst(
  6. [
  7. 'conditions' => 'id = :id:',
  8. 'bind' => [
  9. 'id' => 7,
  10. ]
  11. ]
  12. );
  13. $logins = $user->successLogins;
  14. $logins = $user->getRelated('successLogins');

The last two lines do exactly the same thing. It is a matter of preference which syntax you want to use. Phalcon will query the related table, filtering the related table with the id of the user.

For our Users table we define the following relationships:

NameSource fieldTarget fieldModel
passwordChangesidusersIdPasswordChanges
profileprofileIdidProfiles
resetPasswordsidusersIdResetPasswords
successLoginsidusersIdSuccessLogins
  1. <?php
  2. declare(strict_types=1);
  3. namespace Vokuro\Models;
  4. use Phalcon\Mvc\Model;
  5. use Phalcon\Validation;
  6. use Phalcon\Validation\Validator\Uniqueness;
  7. /**
  8. * All the users registered in the application
  9. */
  10. class Users extends Model
  11. {
  12. // ...
  13. public function initialize()
  14. {
  15. $this->belongsTo(
  16. 'profilesId',
  17. Profiles::class,
  18. 'id',
  19. [
  20. 'alias' => 'profile',
  21. 'reusable' => true,
  22. ]
  23. );
  24. $this->hasMany(
  25. 'id',
  26. SuccessLogins::class,
  27. 'usersId',
  28. [
  29. 'alias' => 'successLogins',
  30. 'foreignKey' => [
  31. 'message' => 'User cannot be deleted because ' .
  32. 'he/she has activity in the system',
  33. ],
  34. ]
  35. );
  36. $this->hasMany(
  37. 'id',
  38. PasswordChanges::class,
  39. 'usersId',
  40. [
  41. 'alias' => 'passwordChanges',
  42. 'foreignKey' => [
  43. 'message' => 'User cannot be deleted because ' .
  44. 'he/she has activity in the system',
  45. ],
  46. ]
  47. );
  48. $this->hasMany(
  49. 'id',
  50. ResetPasswords::class,
  51. 'usersId', [
  52. 'alias' => 'resetPasswords',
  53. 'foreignKey' => [
  54. 'message' => 'User cannot be deleted because ' .
  55. 'he/she has activity in the system',
  56. ],
  57. ]);
  58. }
  59. // ...
  60. }

As you can see in the defined relationships, we have a belongsTo and three hasMany. All relationships have an alias so that we can access them easier. The belongsTo relationship also has the reusable flag set to on. This means that if the relationship is called more than once in the same request, Phalcon would perform the database query only the first time and cache the resultset. Any subsequent calls will use the cached resultset.

Also notable is that we define specific messages for foreign keys. If the particular relationship is violated, the defined message will be raised.

Events

Phalcon\Mvc\Model is designed to fire specific events. These event methods can be located either in a listener or in the same model.

For the Users model, we attach code to the afterSave and beforeValidationOnCreate events.

  1. <?php
  2. declare(strict_types=1);
  3. namespace Vokuro\Models;
  4. use Phalcon\Mvc\Model;
  5. use Phalcon\Validation;
  6. use Phalcon\Validation\Validator\Uniqueness;
  7. /**
  8. * All the users registered in the application
  9. */
  10. class Users extends Model
  11. {
  12. public function beforeValidationOnCreate()
  13. {
  14. if (true === empty($this->password)) {
  15. $tempPassword = preg_replace(
  16. '/[^a-zA-Z0-9]/',
  17. '',
  18. base64_encode(openssl_random_pseudo_bytes(12))
  19. );
  20. $this->mustChangePassword = 'Y';
  21. $this->password = $this->getDI()
  22. ->getSecurity()
  23. ->hash($tempPassword)
  24. ;
  25. } else {
  26. $this->mustChangePassword = 'N';
  27. }
  28. if ($this->getDI()->get('config')->useMail) {
  29. $this->active = 'N';
  30. } else {
  31. $this->active = 'Y';
  32. }
  33. $this->suspended = 'N';
  34. $this->banned = 'N';
  35. }
  36. }

The beforeValidationOnCreate will fire every time we have a new record (Create), before any validations occur. We check if we have a defined password and if not, we will generate a random string, then hash that string using Phalcon\Security amd storing it in the password property. We also set the flag to change the password.

If the password is not empty, we just set the mustChangePassword field to N. Finally, we set some defaults on whether the user is active, suspended or banned. This ensures that our record is ready before it is inserted in the database.

  1. <?php
  2. declare(strict_types=1);
  3. namespace Vokuro\Models;
  4. use Phalcon\Mvc\Model;
  5. use Phalcon\Validation;
  6. use Phalcon\Validation\Validator\Uniqueness;
  7. /**
  8. * All the users registered in the application
  9. */
  10. class Users extends Model
  11. {
  12. public function afterSave()
  13. {
  14. if ($this->getDI()->get('config')->useMail) {
  15. if ($this->active == 'N') {
  16. $emailConfirmation = new EmailConfirmations();
  17. $emailConfirmation->usersId = $this->id;
  18. if ($emailConfirmation->save()) {
  19. $this->getDI()
  20. ->getFlash()
  21. ->notice(
  22. 'A confirmation mail has ' .
  23. 'been sent to ' . $this->email
  24. )
  25. ;
  26. }
  27. }
  28. }
  29. }
  30. }

The afterSave event fires right after a record is saved in the database. In this event we check if emails have been enabled (see .env file useMail setting), and if active we create a new record in the EmailConfirmations table and then save the record. Once everything is done, a notice will appear on screen.

Note that the EmailConfirmations model also has an afterCreate event, which is responsible for actually sending the email to the user.

Validation

The model also has the validate method which allows us to attach a validator to any number of fields in our model. For the Users table, we need the email to be unique. As such, we attach the Uniqueness validator to it. The validator will fire right before any save operation is performed on the model and the message will be returned back if the validation fails.

  1. <?php
  2. declare(strict_types=1);
  3. namespace Vokuro\Models;
  4. use Phalcon\Mvc\Model;
  5. use Phalcon\Validation;
  6. use Phalcon\Validation\Validator\Uniqueness;
  7. /**
  8. * All the users registered in the application
  9. */
  10. class Users extends Model
  11. {
  12. public function validation()
  13. {
  14. $validator = new Validation();
  15. $validator->add(
  16. 'email',
  17. new Uniqueness(
  18. [
  19. "message" => "The email is already registered",
  20. ]
  21. )
  22. );
  23. return $this->validate($validator);
  24. }
  25. }

Conclusion

Vökuró is a sample application that we use to demonstrate some of the features that Phalcon offers. It is definitely not a solution that will fit all needs. However you can use it as a starting point to develop your application.

References