Secciones

Tutorial - Vökuró


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

NOTE: 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.

NOTE: 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: Note the code below has been formatted to increase readability

Instalación

Descarga

In order to install the application, you can either clone or download it from GitHub. Puede visitar la página de GitHub, descargar la aplicación y luego descomprimirla a un directorio de su máquina. Alternativamente, puede usar git clone:

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

Extensiones

Hay algunos prerrequisitos para que Vökuró funcione. 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
  • xmlwriter

Se necesita instalar Phalcon. Diríjase a la página instalación si necesita ayuda con la instalación de Phalcon.

Finalmente, también necesitará asegurarse que ha actualizado los paquetes de composer (ver sección más abajo).

Ejecutar

Si todos los requerimientos anteriores son satisfechos, puede ejecutar la aplicación usando el servidor web integrado de PHP ejecutando el siguiente comando en un terminal:

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

El comando anterior empezará a servir el sitio para localhost en el puerto 8080. Puede cambiar estos ajustes para que cumplan sus necesidades. Alternativamente, puede configurar su sitio en Apache o nginX usando un virtual host. Por favor, consulte la documentación correspondiente de cómo configurar un virtual host para estos servidores web.

Docker

En la carpeta resources encontrará un Dockerfile que le permite configurar rápidamente el entorno y ejecutar la aplicación. Para usar el Dockerfile necesitamos decidir el nombre de nuestra aplicación dockerizada. Para los propósitos de este tutorial, usaremos phalcon-tutorial-vokuro.

Desde la raíz de la aplicación necesitamos compilar el proyecto (sólo necesita hacer esto una vez):

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

y ejecutarla

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

Esto nos introducirá en el entorno dockerizado. Para comprobar la versión PHP:

root@c7b43060b115:/code $ php -v

PHP 8.1.8 (cli) (built: Jul 12 2022 08:28:43) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.8, Copyright (c) Zend Technologies
    with Xdebug v3.1.5, Copyright (c) 2002-2022, by Derick Rethans

y Phalcon:

root@c7b43060b115:/code $ php -r 'echo Phalcon\Version::get();'

4.0.0

Ahora tiene un entorno dockerizado con todo los componentes necesarios para ejecutar Vökuró.

Nanobox

En la carpeta resources también encontrará un fichero boxfile.yml que le permite usar nanobox para configurar el entorno rápidamente. Todo lo que tiene que hacer es copiar el fichero en la raíz de su directorio y ejecutar nanobox run php-server. Una vez que la aplicación está configurada por primera vez, será capaz de navegar a la dirección IP presentada en pantalla y trabajar con la aplicación.

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

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

Estructura

Buscando en la estructura de la aplicación tenemos lo siguiente:

vokuro/
    .ci
    configs
    db
        migrations
        seeds
    public
    resources
    src
        Controllers
        Forms
        Models
        Phalcon
        Plugins
        Providers
    tests
    themes
        vokuro
    var
        cache
            acl
            metaData
            session
            volt
        logs
    vendor
Directorio Descripción
.ci Ficheros necesarios para configurar los servicios del CI
configs Ficheros de configuración
db Guarda las migraciones de la base de datos
public Punto de entrada para la aplicación, css, js, imágenes
resources Ficheros Docker/nanobox para configurar la aplicación
src Donde reside la aplicación (controladores, formularios, etc.)
src/Controllers Controladores
src/Forms Formularios
src/Models Modelos de la Base de Datos
src/Plugins Plugins
src/Providers Proveedores: configuración de servicios en el contenedor DI
tests Pruebas
themes Temas/vistas para una fácil personalización
themes/vokuro Tema predeterminado para la aplicación
var Varios ficheros de soporte
var/cache Ficheros de caché
var/logs Logs
vendor Librerías basadas en vendor/composer

Configuración

.env

Vökuró uses the popular Dotenv library by Vance Lucas. La librería usa un fichero .env ubicado en su carpeta raíz, que contiene parámetros de configuración como el nombre del servidor de base de datos, nombre de usuario, contraseña, etc. Hay un fichero .env.example que viene con Vökuró que puede copiar y renombrar a .env y luego editarlo para que coincida con su entorno. Necesita hacer esto primero para que su aplicación pueda funcionar correctamente.

Las opciones disponibles son:

Opción Descripción
APP_CRYPT_SALT Random and long string that is used by the Phalcon\Encryption\Crypt component to produce passwords and any additional security features
APP_BASE_URI Usualmente / si su servidor web apunta directamente al directorio Vökuró. Si tiene instalado Vökuró en un subdirectorio, puede ajustar la URI base
APP_PUBLIC_URL La URL pública de la aplicación. Se usa para los emails.
DB_ADAPTER El adaptador de base de datos. Los adaptadores disponibles son: mysql, pgsql, sqlite. Por favor, asegúrese de que tiene instaladas en su sistema las extensiones correspondientes para la base de datos.
DB_HOST El servidor de base de datos
DB_PORT El puerto de la base de datos
DB_USERNAME El nombre de usuario de la base de datos
DB_PASSWORD La contraseña de la base de datos
DB_NAME El nombre de la base de datos
MAIL_FROM_NAME El nombre DE al enviar emails
MAIL_FROM_EMAIL El email DE al enviar emails
MAIL_SMTP_SERVER El servidor SMTP
MAIL_SMTP_PORT El puerto SMTP
MAIL_SMTP_SECURITY La seguridad SMTP (ej. tls)
MAIL_SMTP_USERNAME El nombre de usuario del SMTP
MAIL_SMTP_PASSWORD La contraseña del SMTP
CODECEPTION_URL El servidor Codeception para pruebas. Si ejecuta las pruebas localmente este debería ser 127.0.0.1
CODECEPTION_PORT El puerto Codeception

Una vez que el fichero de configuración está en su lugar, al visitar la dirección IP se presentará en pantalla algo similar a esto:

Base de Datos

También necesita inicializar la base de datos. Vökuró uses the popular library Phinx by Rob Morgan (now the Cake Foundation). La librería usa su propio fichero de configuración (phinx.php), pero para Vökuró no necesita realizar ningún ajuste ya que phinx.php lee el fichero .env para recuperar los ajustes de configuración. Esto le permite establecer sus parámetros de configuración en un solo lugar.

Ahora necesitaremos ejecutar las migraciones. Para comprobar el estado de nuestra base de datos:

/app $ ./vendor/bin/phinx status

Verá la siguiente pantalla:

Para inicializar la base de datos necesita ejecutar las migraciones:

/app $ ./vendor/bin/phinx migrate

La pantalla mostrará la operación:

Y el comando status ahora mostrará todo verde:

Configuración

acl.php

Buscando en la carpeta config/, observará cuatro ficheros. No hay necesidad de cambiar estos ficheros para iniciar la aplicación, pero si desea personalizarla, éste es el lugar a visitar. The acl.php file returns an array of routes that controls which routes are visible to only logged-in users.

La configuración actual requerirá que un usuario se conecte, si visita estas rutas:

  • users/index
  • users/search
  • users/edit
  • users/create
  • users/delete
  • users/changePassword
  • profiles/index
  • profiles/search
  • profiles/edit
  • profiles/create
  • profiles/delete
  • permissions/index

If you use Vökuró as a starting point for your own application, you will need to modify this file to add or remove routes to ensure that your protected routes are behind the login mechanism.

NOTE: Keeping the private routes in an array is efficient and easy to maintain for a small to medium application. Una vez su aplicación empieza a crecer, podría necesitar considerar una técnica diferente para mantener sus rutas privadas como la base de datos con un mecanismo de caché.

config.php

Este fichero mantiene todos los parámetros de configuración que necesita Vökuró. Usually you will not need to change this file, since the elements of the array are set by the .env file and Dotenv. Sin embargo, podría querer cambiar la localización de sus logs u otras rutas, si decide cambiar la estructura de directorios.

Uno de los elementos que podría querer considerar cuando trabaje con Vökuró en su máquina local es useMail y establecerlo a false. Esto indicará a Vökuró que no intente conectar a un servidor de correo y enviar un email cuando un usuario se registre en el sitio.

providers.php

Este fichero contiene todos los proveedores que necesita Vökuró. Esto es una lista de clases de la aplicación, que registra la clase particular en el contenedor DI. Si necesita registrar nuevos componentes en su contenedor DI, puede añadirlos al vector de este fichero.

routes.php

Este fichero contiene las rutas que Vökuró entiende. El enrutador ya registra las rutas predeterminadas, por lo que cualquier ruta definida en routes.php es específica. You can add any non-standard routes you need, when customizing Vökuró, in this file. Como recordatorio, las rutas predeterminadas son:

/:controller/:action/:parameters

Proveedores

Como se ha mencionado antes, Vökuró usa clases llamadas Proveedores para registrar servicios en el contenedor DI. Esto sólo es una forma de registrar servicios en el contenedor DI, nada le impide poner todos estos registros en un solo fichero.

Para Vökuró decidimos usar un fichero por servicio así como un providers.php (ver arriba) como vector de configuración de registro para estos servicios. Esto nos permite tener trozos de código mucho más pequeños, organizados en ficheros separados por servicio, así como un vector que nos permite registrar o desregistrar/desactivar un servicio sin eliminar ficheros. Todo lo que necesitamos hacer es cambiar el vector providers.php.

Las clases proveedor se encuentran en src/Providers. Each of the provider classes implements the Phalcon\Di\ServiceProviderInterface interface. Para más información, ver la sección de arranque a continuación.

Composer

Vökuró uses composer to download and install supplemental PHP libraries. Las librerías usadas son:

Mirando en composer.json los paquetes requeridos son:

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

Si es una instalación nueva puede ejecutar

composer install

o si desea actualizar las instalaciones existentes de los paquetes anteriores:

composer update

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

Arranque

Entrada

El punto de entrada a nuestra aplicación es public/index.php. Este fichero contiene el código necesario que inicia la aplicación y la ejecuta. También sirve como punto único de entrada de nuestra aplicación, haciendo las cosas mucho más fáciles para nosotros cuando queremos capturar errores, proteger ficheros, etc.

Veamos el código:

<?php

use Vokuro\Application as VokuroApplication;

error_reporting(E_ALL);
$rootPath = dirname(__DIR__);

try {
    require_once $rootPath . '/vendor/autoload.php';

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

    echo (new VokuroApplication($rootPath))->run();
} catch (Exception $e) {
    echo $e->getMessage(), '<br>';
    echo nl2br(htmlentities($e->getTraceAsString()));
}

En primer lugar, nos aseguramos que tenemos el reporte de errores al máximo. Por supuesto, puede cambiar esto si lo desea, o sustituir el código donde se controla el reporte de errores por una entrada en su fichero .env.

Un bloque try/catch envuelve todas las operaciones. Esto asegura que todos los errores sean capturados y mostrados por pantalla.

NOTE You will need to rework the code to enhance security. Actualmente, si ocurre algún error con la base de datos, el código catch mostrará en pantalla las credenciales de la base de datos con la excepción. This code is intended as a tutorial not a full scale production application

Nos aseguramos que tenemos acceso a todas las librerías de soporte cargando el autocargador de composer. En composer.json también tenemos definida la entrada autoload, dirigiendo al autocargador a cargar cualquier clase con espacio de nombres Vokuro desde la carpeta src.

"autoload": {
    "psr-4": {
        "Vokuro\\": "app/"
    },
    "files": [
        "app/Helpers.php"
    ]
}

Entonces cargaremos las variables de entorno definidas en nuestro fichero .env llamando

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

Finalmente, ejecutamos nuestra aplicación.

Application

Toda la lógica de aplicación está envuelta en la clase Vokuro\Application. Veamos como se hace esto:

<?php
declare(strict_types=1);

namespace Vokuro;

use Exception;
use Phalcon\Application\AbstractApplication;
use Phalcon\Di\DiInterface;
use Phalcon\Di\FactoryDefault;
use Phalcon\Di\ServiceProviderInterface;
use Phalcon\Mvc\Application as MvcApplication;

class Application
{
    const APPLICATION_PROVIDER = 'bootstrap';

    /**
     * @var AbstractApplication
     */
    protected $app;

    /**
     * @var DiInterface
     */
    protected $di;

    /**
     * @var string
     */
    protected $rootPath;

    /**
     * @param string $rootPath
     *
     * @throws Exception
     */
    public function __construct(string $rootPath)
    {
        $this->di       = new FactoryDefault();
        $this->app      = $this->createApplication();
        $this->rootPath = $rootPath;

        $this->di->setShared(self::APPLICATION_PROVIDER, $this);

        $this->initializeProviders();
    }

    /**
     * @return string
     * @throws Exception
     */
    public function run(): string
    {
        return (string) $this
            ->app
            ->handle($_SERVER['REQUEST_URI'])
            ->getContent()
        ;
    }

    /**
     * @return string
     */
    public function getRootPath(): string
    {
        return $this->rootPath;
    }

    /**
     * @return AbstractApplication
     */
    protected function createApplication(): AbstractApplication
    {
        return new MvcApplication($this->di);
    }

    /**
     * @throws Exception
     */
    protected function initializeProviders(): void
    {
        $filename = $this->rootPath 
                 . '/configs/providers.php';
        if (!file_exists($filename) || !is_readable($filename)) {
            throw new Exception(
                'File providers.php does not exist or is not readable.'
            );
        }

        $providers = include_once $filename;
        foreach ($providers as $providerClass) {
            /** @var ServiceProviderInterface $provider */
            $provider = new $providerClass;
            $provider->register($this->di);
        }
    }
}

El constructor de la clase crea primero un nuevo contenedor DI y lo almacena en una propiedad local. Estamos usando Phalcon\Di\FactoryDefault, que tiene muchos servicios ya registrados por nosotros.

Entonces creamos un nuevo Phalcon\Mvc\Application y lo almacenamos también en una propiedad. También almacenamos la ruta raíz porque es útil a lo largo de la aplicación.

Luego registramos est clase (Vokuro\Application) en el contenedor Di usando el nombre bootstrap. Esto nos permite tener acceso a esta clase desde cualquier parte de nuestra aplicación a través del contenedor Di.

Lo último que hacemos es registrar todos los proveedores. Aunque el objeto Phalcon\Di\FactoryDefault tiene muchos servicios ya registrados para nosotros, todavía necesitamos registrar proveedores que cubran las necesidades de nuestra aplicación. 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. Por lo tanto, primero cargamos el vector de configuración config/providers.php y luego iteramos sobre las entradas y registramos cada proveedor por orden.

Los proveedores disponibles son:

Proveedor Descripción
AclProvider Permisos
AuthProvider Autenticación
ConfigProvider Valores de configuración
CryptProvider Encriptación
DbProvider Acceso a base de datos
DispatcherProvider Despachador - qué controlador llamar para qué URL
FlashProvider Mensajes flash para retroalimentar al usuario
LoggerProvider Registrador de errores y otra información
MailProvider Soporte de email
ModelsMetadataProvider Metadatos para modelos
RouterProvider Rutas
SecurityProvider Seguridad
SessionBagProvider Datos de sesión
SessionProvider Datos de sesión
UrlProvider Gestión de URL
ViewProvider Vistas y motor de vistas

run() ahora gestionará REQUEST_URI, y devolverá el contenido de vuelta. Internamente, la aplicación calculará la ruta basándose en la petición, y despachará el controlador y vista correspondientes, antes de devolver el resultado de esta operación de vuelta al usuario como respuesta.

Base de Datos

Como hemos mencionado anteriormente, Vökuró se puede instalar con MariaDB/MySQL/Aurora, PostgreSql o SQLite como almacén de base de datos. Para los propósitos de este tutorial, usamos MariaDB. Las tablas que usa la aplicación son:

Tabla Descripción
email_confirmations Confirmaciones de email de registro
failed_logins Intentos de inicio de sesión fallidos
password_changes Cuando cambia una contraseña y por quién
permissions Vector de permisos
phinxlog Tabla de migraciones de Phinx
profiles Perfil de cada usuario
remember_tokens Remember Me functionality tokens
reset_passwords Tabla de tokens de reseteo de contraseñas
success_logins Intentos de inicio de sesión correctos
users Usuarios

Modelos

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. Los modelos se localizan en el directorio /src/Models, y cada modelo define los campos relevantes, tabla origen así como cualquier relación entre este modelo y los otros. Algunos modelos también implementan reglas de validación para asegurarse que los datos se almacenan correctamente en la base de datos.

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;

class SuccessLogins extends Model
{
    /**
     * @var integer
     */
    public $id;

    /**
     * @var integer
     */
    public $usersId;

    /**
     * @var string
     */
    public $ipAddress;

    /**
     * @var string
     */
    public $userAgent;

    public function initialize()
    {
        $this->belongsTo(
            'usersId', 
            Users::class, 
            'id', 
            [
                'alias' => 'user',
            ]
        );
    }
}

En el modelo anterior, hemos definido todos los campos de la tabla como propiedades públicas para un acceso fácil:

echo $successLogin->ipAddress;

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

En el método initialize(), también definimos las relaciones entre este modelo y el modelo Users. Asignamos los campos (local/remoto) así como un alias para esta relación. Por lo tanto, ahora podemos acceder al usuario relacionado con un registro de este modelo como sigue:

echo $successLogin->user->name;

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

Controladores

Again following the Model-View-Controller pattern, Vökuró has one controller to handle a specific parent route. Esto significa que AboutController gestiona la ruta /about. Todos los controladores se localizan en el directorio /src/Cotnrollers.

El controlador predeterminado es IndexController. Todas las clases controlador tienen el sufijo Controller. Cada controlador tiene métodos con el sufijo Action y la acción predeterminada es indexAction. Therefore, if you visit the site with just the URL, the IndexController will be called and the indexAction will be executed.

Después de eso, a no ser que tenga rutas específicas registradas, las rutas predeterminadas (automáticamente registradas) intentarán encajar:

/profiles/search

a

/src/Controllers/ProfilesController.php -> searchAction

Los controladores, acciones y rutas disponibles para Vökuró son:

Controlador Acción Ruta Descripción
About index /about Muestra la página about
Índice index / Acción predeterminada - página de inicio
Permisos index /permissions Ver/cambiar permisos para un nivel perfil
Privacy index /privacy Ver la página de privacidad
Profiles index /profiles Ver página predeterminada de perfiles
Profiles create /profiles/create Crear perfil
Profiles delete /profiles/delete Eliminar perfil
Profiles edit /profiles/edit Editar perfil
Profiles search /profiles/search Buscar perfiles
Session index /session Acción predeterminada de sesión
Session forgotPassword /session/forgotPassword Ha olvidado la contraseña
Session login /session/login Inicio de sesión
Session logout /session/logout Cerrar sesión
Session signup /session/signup Registro
Terms index /terms Ver la página de términos
UserControl confirmEmail /confirm Confirmar email
UserControl resetPassword /reset-password Resetear contraseña
Usuarios index /users Pantalla predeterminada de usuarios
Usuarios changePassword /users/changePassword Cambiar contraseña de usuario
Usuarios create /users/create Crear usuario
Usuarios delete /users/delete Eliminar usuario
Usuarios edit /users/edit Editar usuario

Vistas

The last element of the Model-View-Controller pattern is the views. Vökuró usa Volt como el motor de vista para sus vistas.

NOTE: Generally, one would expect to see a views folder under the /src folder. Vökuró usa un enfoque diferente, almacenando todas los ficheros de las vistas bajo /themes/vokuro.

El directorio de vistas contiene directorios que mapean a cada controlador. Dentro de cada uno de esos directorios, se mapean ficheros .volt a cada acción. Así por ejemplo la ruta:

/profiles/create

mapea a:

ProfilesController -> createAction

y la vista se localiza en:

/themes/vokuro/profiles/create.volt

Las vistas disponibles son:

Controlador Acción Vistas Descripción
About index /about/index.volt Muestra la página about
Índice index /index/index.volt Acción predeterminada - página de inicio
Permisos index /permissions/index.volt Ver/cambiar permisos para un nivel perfil
Privacy index /privacy/index.volt Ver la página de privacidad
Profiles index /profiles/index.volt Ver página predeterminada de perfiles
Profiles create /profiles/create.volt Crear perfil
Profiles delete /profiles/delete.volt Eliminar perfil
Profiles edit /profiles/edit.volt Editar perfil
Profiles search /profiles/search.volt Buscar perfiles
Session index /session/index.volt Acción predeterminada de sesión
Session forgotPassword /session/forgotPassword.volt Ha olvidado la contraseña
Session login /session/login.volt Inicio de sesión
Session logout /session/logout.volt Cerrar sesión
Session signup /session/signup.volt Registro
Terms index /terms/index.volt Ver la página de términos
Usuarios index /users/index.volt Pantalla predeterminada de usuarios
Usuarios changePassword /users/changePassword.volt Cambiar contraseña de usuario
Usuarios create /users/create.volt Crear usuario
Usuarios delete /users/delete.volt Eliminar usuario
Usuarios edit /users/edit.volt Editar usuario

El fichero /index.volt contiene el diseño principal de la página, incluyendo hojas de estilo, referencias javascript, 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. Las vistas individuales se inyectan en los diseños y construyen la página final.

Components

Hay varios componentes que usamos en Vökuró, ofreciendo funcionalidad a lo largo de la aplicación. Todos estos componentes se localizan en el directorio /src/Plugins.

Acl

Vokuro\Plugins\Acl\Acl is a component that implements an Access Control List for our application. La ACL controla qué usuario tiene acceso a qué recurso. Puede leer más sobre ACL en nuestra página dedicada.

In this component, We define the resources that are considered private. Estos se mantienen en un vector interno con el controlador como clave y la acción como el valor, e identificamos qué controlador/acción requiere autenticación. It also holds human-readable descriptions for actions used throughout the application.

El componente expone los siguientes métodos:

Método Devuelve Descripción
getActionDescription($action) string Devuelve la descripción de la acción según su nombre simplificado
getAcl() Objeto ACL Devuelve la lista ACL
getPermissions(Profiles $profile) array Devuelve los permisos asignados a un perfil
getResources() array Devuelve todos los recursos y sus acciones disponibles
isAllowed($profile, $controller, $action) bool Comprueba si el perfil actual tiene permitido acceder a un recurso
isPrivate($controllerName) bool Comprueba si un controlador es privado o no
rebuild() Objeto ACL Reconstruye la lista de acceso en un fichero

Auth

Vokuro\Plugins\Auth\Auth es un componente que gestiona la autenticación y ofrece gestión de identidad en Vökuró.

El componente expone los siguientes métodos:

Método Descripción
check($credentials) Comprueba las credenciales de usuario
saveSuccessLogin($user) Crea la configuración del entorno recuérdame, las cookies relacionadas y genera tokens
registerUserThrottling($userId) Implementa el bloqueo de acceso. Reduce la efectividad de ataques de fuerza bruta
createRememberEnvironment(Users $user) Crea la configuración del entorno recuérdame, las cookies relacionadas y genera tokens
hasRememberMe(): bool Comprueba si la sesión tiene una cookie recuérdame
loginWithRememberMe(): Response Inicia sesión utilizando la información en las cookies
checkUserFlags(Users $user) Comprueba si el usuario está baneado/inactivo/suspenddo
getIdentity(): array / null Devuelve la identidad actual
getName(): string Devuelve el nombre del usuario
remove() Elimina la información de identidad del usuario de la sesión
authUserById($id) Autentica al usuario por su id
getUser(): Users Obtiene la entidad relacionada con el usuario de la identidad activa
findFirstByToken($token): int / null Devuelve el usuario del token actual
deleteToken(int $userId) Elimina el token del usuario actual de la sesión

Mail

Vokuro\Plugins\Mail\Mail is a wrapper to Swift Mailer. Expone dos métodos send() y getTemplate() que le permite obtener una plantilla desde las vistas y rellenarla con datos. El HTML resultante se puede usar en el método send() junto con el destinatario y otros parámetros para enviar el mensaje de email.

NOTE: Note that this component is used only if useMail is enabled in your .env file. También necesitará asegurarse de que el servidor SMTP y las credenciales son válidos.

Registro

Controlador

Para poder acceder a todas las áreas de Vökuró necesita tener una cuenta. Vökuró le permite registrarse en el sitio haciendo click en el botón Crear una Cuenta.

Lo que esto hará es llevarle a la URL /session/signup, que a su vez llamará a SessionController y signupAction. Echemos un vistazo a lo que está ocurriendo en signupAction:

<?php
declare(strict_types=1);

namespace Vokuro\Controllers;

use Phalcon\Flash\Direct;
use Phalcon\Http\Request;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Security;
use Phalcon\Mvc\View;
use Vokuro\Forms\SignUpForm;
use Vokuro\Models\Users;

/**
 * @property Dispatcher $dispatcher
 * @property Direct     $flash
 * @property Request    $request
 * @property Security   $security
 * @property View       $view
 */
class SessionController extends ControllerBase
{
    public function signupAction()
    {
        $form = new SignUpForm();

        // ....

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

El flujo de trabajo de la aplicación es:

  • Visita /session/signup
    • Crea formulario, envía formulario a la vista, renderiza el formulario
  • Envía datos (no post)
    • El formulario se muestra de nuevo, no ocurre nada más
  • Envía datos (post)
    • Errores
      • Validadores del formulario tiene errores, envía el formulario a la vista, renderiza el formulario (se mostrarán los errores)
    • Sin errores
      • Se sanean los datos
      • Se crea nuevo modelo
      • Se guardan datos en la base de datos
        • Error
          • Se muestra mensaje en pantalla y actualiza el formulario
        • Éxito
          • Registro guardado
          • Muestra confirmación en pantalla
          • Envía email (si corresponde)

Form

In order to have validation for user supplied data, we are utilizing the Phalcon\Forms\Form and Phalcon\Filter\Validation* classes. Estas clases nos permiten crear elementos HTML y adjuntarles validadores. El formulario se pasa entonces a la vista, donde los elementos HTML actuales se renderizan en la pantalla.

Cuando el usuario envía la información, envía los datos publicados de vuelta al formulario y los validadores correspondientes validan la entrada y devuelven cualquier posible mensaje de error.

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

Primero creamos un objeto SignUpForm. En ese objeto definimos todos los elementos HTML que necesitamos con sus respectivos validadores:

<?php
declare(strict_types=1);

namespace Vokuro\Forms;

use Phalcon\Forms\Element\Check;
use Phalcon\Forms\Element\Hidden;
use Phalcon\Forms\Element\Password;
use Phalcon\Forms\Element\Submit;
use Phalcon\Forms\Element\Text;
use Phalcon\Forms\Form;
use Phalcon\Validation\Validator\Confirmation;
use Phalcon\Validation\Validator\Email;
use Phalcon\Validation\Validator\Identical;
use Phalcon\Validation\Validator\PresenceOf;
use Phalcon\Validation\Validator\StringLength;

class SignUpForm extends Form
{
    /**
     * @param string|null $entity
     * @param array       $options
     */
    public function initialize(
        string $entity = null, 
        array $options = []
    ) {
        $name = new Text('name');
        $name->setLabel('Name');
        $name->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The name is required',
                    ]
                ),
            ]
        );

        $this->add($name);

        $email = new Text('email');
        $email->setLabel('E-Mail');
        $email->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The e-mail is required',
                    ]
                ),
                new Email(
                    [
                        'message' => 'The e-mail is not valid',
                    ]
                ),
            ]
        );

        $this->add($email);

        $password = new Password('password');
        $password->setLabel('Password');
        $password->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The password is required',
                    ]
                ),
                new StringLength(
                    [
                        'min'            => 8,
                        'messageMinimum' => 'Password is too short. ' .
                                            'Minimum 8 characters',
                    ]
                ),
                new Confirmation(
                    [
                        'message' => "Password doesn't match " .
                                     "confirmation",
                        'with'    => 'confirmPassword',
                    ]
                ),
            ]
        );

        $this->add($password);

        $confirmPassword = new Password('confirmPassword');
        $confirmPassword->setLabel('Confirm Password');
        $confirmPassword->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The confirmation password ' .
                                     'is required',
                    ]
                ),
            ]
        );

        $this->add($confirmPassword);

        $terms = new Check(
            'terms', 
            [
                'value' => 'yes',
            ]
        );

        $terms->setLabel('Accept terms and conditions');
        $terms->addValidator(
            new Identical(
                [
                    'value'   => 'yes',
                    'message' => 'Terms and conditions must be ' .
                                 'accepted',
                ]
            )
        );

        $this->add($terms);

        $csrf = new Hidden('csrf');
        $csrf->addValidator(
            new Identical(
                [
                    'value'   => $this->security->getRequestToken(),
                    'message' => 'CSRF validation failed',
                ]
            )
        );
        $csrf->clear();

        $this->add($csrf);

        $this->add(
            new Submit(
                'Sign Up', 
                [
                    'class' => 'btn btn-success',
                ]
            )
        );
    }

    /**
     * @param string $name
     *
     * @return string
     */
    public function messages(string $name)
    {
        if ($this->hasMessagesFor($name)) {
            foreach ($this->getMessagesFor($name) as $message) {
                return $message;
            }
        }

        return '';
    }
}

En el método initialize estamos configurando todos los elementos HTML que necesitamos. Estos elementos son:

Elemento Tipo Descripción
name Text El nombre del usuario
email Text El email de la cuenta
password Password La contraseña para la cuenta
confirmPassword Password Confirmación de la contraseña
terms Check Acepta la casilla de términos
csrf Hidden Elemento de protección CSRF
Registro Submit Botón de enviar

Añadir elementos es bastante sencillo:

<?php
declare(strict_types=1);

$email = new Text('email');
$email->setLabel('E-Mail');
$email->addValidators(
    [
        new PresenceOf(
            [
                'message' => 'The e-mail is required',
            ]
        ),
        new Email(
            [
                'message' => 'The e-mail is not valid',
            ]
        ),
    ]
);

$this->add($email);

Primero creamos un objeto Text y establecemos su nombre a email. También establecemos la etiqueta del elemento a E-Mail. Después de eso adjuntamos varios validadores al elemento. Estos se invocarán después de que el usuario envíe los datos, y los datos se pasen en el formulario.

Como vemos arriba, adjuntamos el validador PresenceOf en el elemento email con un mensaje The e-mail is required. El validador comprobará si el usuario ha enviado los datos cuando hace click en el botón enviar y producirá el mensaje si el validador falla. El validador comprueba el vector pasado (normalmente $_POST) y para este elemento particular comprobará $_POST['email'].

También adjunta el validador Email, que es responsable de comprobar si una dirección de email es válida. Como puede ver los validadores residen en un vector, por lo que fácilmente puede adjuntar tantos validadores como necesite.

Lo último que hacemos es añadir el elemento en el formulario.

Tenga en cuenta que el elemento terms no tiene ningún validador adjunto a él, por lo que nuestro formulario no comprobará los contenidos del elemento.

Atención especial a los elementos password and confirmPassword. Notará que ambos elementos son del tipo Password. La idea es que necesita escribir la contraseña dos veces, y las contraseñas deben coincidir para evitar errores.

El campo password tiene dos validadores de contenido: PresenceOf es decir, es requerido y StringLength: necesitamos que la contraseña tenga más de 8 caracteres. También adjuntamos un tercer validador llamado Confirmation. Este validador especial vincula el elemento password con el elemento confirmPassword. Cuando se activa para validarlos comprueba los contenidos de ambos elementos y si no son idénticos, aparecerá el mensaje de error, y la validación fallará.

Vistas

Ahora que tenemos todo configurado en nuestro formulario, pasamos el formulario a la vista:

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

Our view now needs to render the elements:


{# ... #}
{% 
    set isEmailValidClass = form.messages('email') ? 
        'form-control is-invalid' : 
        'form-control' 
%}
{# ... #}

<h1 class="mt-3">Sign Up</h1>

<form method="post">
    {# ... #}

    <div class="form-group row">
        {{ 
            form.label(
                'email', 
                [
                    'class': 'col-sm-2 col-form-label'
                ]
            ) 
        }}
        <div class="col-sm-10">
            {{ 
                form.render(
                    'email', 
                    [
                        'class': isEmailValidClass, 
                        'placeholder': 'Email'
                    ]
                ) 
            }}
            <div class="invalid-feedback">
                {{ form.messages('email') }}
            </div>
        </div>
    </div>

    {# ... #}
    <div class="form-group row">
        <div class="col-sm-10">
            {{ 
                form.render(
                    'csrf', 
                    [
                        'value': security.getToken()
                    ]
                ) 
            }}
            {{ form.messages('csrf') }}

            {{ form.render('Sign Up') }}
        </div>
    </div>
</form>

<hr>

{{ link_to('session/login', "&larr; Back to Login") }}

La variable que establecemos en nuestra vista para nuestro objeto SignUpForm se llama form. Por lo tanto, la usamos directamente y llamamos a sus métodos. La sintaxis en Volt es ligeramente diferente. In PHP, we would use $form->render() whereas in Volt we will use form.render().

Las vistas contienen un condicional en la parte superior, que comprueba si ha habido algún error en nuestro formulario, en cuyo caso, adjunta la clase CSS is-invalid al elemento. Esta clase pone un bonito borde rojo en el elemento, resaltando el error y mostrando el mensaje.

Después de eso tenemos etiquetas HTML regulares con el estilo correspondiente. Para mostrar el código HTML de cada elemento necesitamos llamar render() sobre form con el nombre de elemento correspondiente. También tenga en cuenta que llamamos form.label() con el mismo nombre de elemento, para poder crear las etiquetas <label> correspondientes.

Al final de la vista renderizamos el campo oculto CSRF así como el botón de enviar 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). La acción ahora necesita procesar los datos publicados:

<?php
declare(strict_types=1);

namespace Vokuro\Controllers;

use Phalcon\Flash\Direct;
use Phalcon\Http\Request;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Security;
use Phalcon\Mvc\View;
use Vokuro\Forms\SignUpForm;
use Vokuro\Models\Users;

/**
 * @property Dispatcher $dispatcher
 * @property Direct     $flash
 * @property Request    $request
 * @property Security   $security
 * @property View       $view
 */
class SessionController extends ControllerBase
{
    public function signupAction()
    {
        $form = new SignUpForm();

        if (true === $this->request->isPost()) {
            if (false !== $form->isValid($this->request->getPost())) {
                $name     = $this
                    ->request
                    ->getPost('name', 'striptags')
                ;
                $email    = $this
                    ->request
                    ->getPost('email')
                ;
                $password = $this
                    ->request
                    ->getPost('password')
                ;
                $password = $this
                    ->security
                    ->hash($password)
                ;

                $user = new Users(
                    [
                        'name'       => $name,
                        'email'      => $email,
                        'password'   => $password,
                        'profilesId' => 2,
                    ]
                );

                if ($user->save()) {
                    return $this->dispatcher->forward([
                        'controller' => 'index',
                        'action'     => 'index',
                    ]);
                }

                foreach ($user->getMessages() as $message) {
                    $this->flash->error((string) $message);
                }
            }
        }

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

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

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

Aquí estamos comprobando la petición que llegó del usuario, si es un POST. Ahora que lo es, necesitamos usar los validadores del formulario y comprobar si tenemos algún error. El objeto Phalcon\Http\Request, nos permite obtener esos datos fácilmente usando:

$this->request->getPost()

Ahora necesitamos pasar estos datos publicados en el formulario y llamar isValid. Esto disparará todos los validadores de cada elemento y si alguno de ellos falla, el formulario rellenará la colección de mensajes internos y devolverá false

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

Si todo es correcto, usamos otra vez el objeto Phalcon\Http\Request para recuperar los datos publicados y además sanearlos. El siguiente ejemplo elimina las etiquetas de la cadena name enviada:

$name     = $this
    ->request
    ->getPost('name', 'striptags')
;

Tenga en cuenta que nunca almacenamos contraseñas en texto plano. 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. De esta forma, si alguien compromete nuestra base de datos, al menos no tendrá acceso a las contraseñas en texto plano.

$password = $this
    ->security
    ->hash($password)
;

Ahora necesitamos almacenar los datos proporcionados en la base de datos. Hacemos eso creando un nuevo modelo Users, le pasamos los datos saneados y llamamos a save:

$user = new Users(
    [
        'name'       => $name,
        'email'      => $email,
        'password'   => $password,
        'profilesId' => 2,
    ]
);

if ($user->save()) {
    return $this
        ->dispatcher
        ->forward(
            [
                'controller' => 'index',
                'action'     => 'index',
            ]
        );
}

Si $user->save() devuelve true, el usuario será enviado a la página de inicio (index/index) y le aparecerá en pantalla un mensaje de éxito.

Modelo

Relaciones

Ahora necesitamos comprobar el modelo Users, ya que hay alguna lógica que hemos aplicado aquí, en particular los eventos afterSave y beforeValidationOnCreate.

El método principal, la configuración si le gusta ocurre en el método initialize. Ese es el lugar donde establecemos todas las relaciones del modelo. Para la clase Users tenemos varias relaciones definidas. Podría preguntar, ¿porqué relaciones? Phalcon ofrece una forma fácil de recuperar datos relacionados con un modelo particular.

Si por ejemplo quiere comprobar todos los inicios de sesión correctos para un usuario particular, puede hacerlo con el siguiente fragmento de código:

<?php
declare(strict_types=1);

use Vokuro\Models\SuccessLogins;
use Vokuro\Models\Users;

$user = Users::findFirst(
    [
        'conditions' => 'id = :id:',
        'bind'       => [
            'id' => 7,
        ] 
    ]
);

$logins = SuccessLogin::find(
    [
        'conditions' => 'userId = :userId:',
        'bind'       => [
            'userId' => 7,
        ] 
    ]
);

El código anterior obtiene el usuario con id 7 y luego obtiene todos los inicios de sesión correctos de la tabla correspondiente para ese usuario.

Usando las relaciones podemos dejar que Phalcon haga todo el trabajo pesado por nosotros. Con lo que el código anterior sería:

<?php
declare(strict_types=1);

use Vokuro\Models\SuccessLogins;
use Vokuro\Models\Users;

$user = Users::findFirst(
    [
        'conditions' => 'id = :id:',
        'bind'       => [
            'id' => 7,
        ] 
    ]
);

$logins = $user->successLogins;

$logins = $user->getRelated('successLogins');

Las últimas dos líneas hacen exactamente lo mismo. Es una cuestión de preferencia qué sintaxis quiere usar. Phalcon consultará la tabla relacionada, filtrando la tabla relacionada con el id del usuario.

Para nuestra tabla Users definimos las siguientes relaciones:

Nombre Campo origen Campo destino Modelo
passwordChanges id usersId PasswordChanges
profile profileId id Profiles
resetPasswords id usersId ResetPasswords
successLogins id usersId SuccessLogins
<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    // ...

    public function initialize()
    {
        $this->belongsTo(
            'profilesId', 
            Profiles::class, 
            'id', 
            [
                'alias'    => 'profile',
                'reusable' => true,
            ]
        );

        $this->hasMany(
            'id', 
            SuccessLogins::class, 
            'usersId', 
            [
                'alias'      => 'successLogins',
                'foreignKey' => [
                    'message' => 'User cannot be deleted because ' .
                                 'he/she has activity in the system',
                ],
            ]
        );

        $this->hasMany(
            'id', 
            PasswordChanges::class, 
            'usersId', 
            [
                'alias'      => 'passwordChanges',
                'foreignKey' => [
                    'message' => 'User cannot be deleted because ' .
                                 'he/she has activity in the system',
                ],
            ]
        );

        $this->hasMany(
            'id', 
            ResetPasswords::class, 
            'usersId', [
            'alias'      => 'resetPasswords',
            'foreignKey' => [
                'message' => 'User cannot be deleted because ' .
                             'he/she has activity in the system',
            ],
        ]);
    }

    // ...
}

Como puede ver en las relaciones definidas, tenemos un belongsTo y tres hasMany. Todas las relaciones tienen un alias para que podamos acceder a ellas más fácilmente. La relación belongsTo también tiene el parámetro reusable activo. Esto significa que si la relación se llama más de una vez en la misma petición, Phalcon realizaría la consulta a la base de datos sólo la primera vez y cachearía el conjunto de resultados. Cualquier llamada posterior usará el conjunto de resultados cacheado.

Also, notable is that we define specific messages for foreign keys. Si se infringe la relación particular, se generará el mensaje definido.

Eventos

Phalcon\Mvc\Model está diseñado para disparar eventos específicos. Estos métodos de eventos se pueden localizar en un oyente o en el propio modelo.

Para el modelo Users, adjuntamos código a los eventos afterSave y beforeValidationOnCreate.

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    public function beforeValidationOnCreate()
    {
        if (true === empty($this->password)) {
            $tempPassword = preg_replace(
                '/[^a-zA-Z0-9]/', 
                '', 
                base64_encode(openssl_random_pseudo_bytes(12))
            );

            $this->mustChangePassword = 'Y';

            $this->password = $this->getDI()
                                   ->getSecurity()
                                   ->hash($tempPassword)
            ;
        } else {
            $this->mustChangePassword = 'N';
        }

        if ($this->getDI()->get('config')->useMail) {
            $this->active = 'N';
        } else {
            $this->active = 'Y';
        }

        $this->suspended = 'N';

        $this->banned = 'N';
    }
}

beforeValidationOnCreate disparará cada vez que tenemos un nuevo registro (Create), antes de que ocurra alguna validación. We check if we have a defined password and if not, we will generate a random string, then hash that string using Phalcon\Security and storing it in the password property. También activamos el parámetro para cambiar la contraseña.

Si la contraseña no está vacía, solo establecemos el campo mustChangePassword a N. Finalmente, establecemos algunos valores predeterminados sobre si el usuario está active (activo), suspended (suspendido) o banned (baneado). Esto asegura que nuestro registro está listo antes de ser insertado en la base de datos.

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    public function afterSave()
    {
        if ($this->getDI()->get('config')->useMail) {
            if ($this->active == 'N') {
                $emailConfirmation          = new EmailConfirmations();
                $emailConfirmation->usersId = $this->id;

                if ($emailConfirmation->save()) {
                    $this->getDI()
                         ->getFlash()
                         ->notice(
                            'A confirmation mail has ' .
                            'been sent to ' . $this->email
                        )
                    ;
                }
            }
        }
    }
}

El evento afterSave se dispara justo después de guardar un registro en la base de datos. En este evento comprobamos si los emails están habilitados (ver ajuste useMail del fichero .env), y si están activos creamos un nuevo registro en la tabla EmailConfirmations y guardamos el registro. Una vez que se ha hecho todo, aparecerá un aviso en pantalla.

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

Validación

El modelo también tiene el método validate que nos permite adjuntar un validador a cualquier número de campos de nuestro modelo. Para la tabla Users, necesitamos que el email sea único. 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 if the validation fails.

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    public function validation()
    {
        $validator = new Validation();

        $validator->add(
            'email', 
            new Uniqueness(
                [
                    "message" => "The email is already registered",
                ]
            )
        );

        return $this->validate($validator);
    }
}

Conclusión

Vökuró es una aplicación de ejemplo que usamos para demostrar algunas de las características que Phalcon ofrece. Definitivamente, no es una solución que cubra todas las necesidades. However, you can use it as a starting point to develop your application.

Referencias