An example of developing a blog on Zend Framework 2. Part 3. Working with users

  • Tutorial
This is the third (last?) Part of the article devoted to the development of a simple application using Zend Framework 2. In the first part I examined the structure of ZendSkeletonApplication, in the second part I gave an example of the development of a simple module. This part is devoted to working with users, and I will also screw Twig template engine to the project.

Work with users


The code written in the previous parts allows you to create, edit and delete blog posts for all visitors to the site. This approach is unacceptable for any working site, so now it is time to solve the issues of registration / authorization and distribution of access rights to various features of the application.

Zf commons


A lot of modules have been written for the Zend framework that solve standard tasks; you can find them on a special site: modules.zendframework.com . Instead of developing my own bicycles to solve standard problems, I think it is more appropriate to use / adapt ready-made solutions for myself (at least ready-made solutions need to be studied before embarking on the development of a bicycle).

Among the many module developers, the ZF Commons team stands out, the guys from this team have developed a number of very useful modules that we will use in this project: github.com/ZF-Commons . Consider some of them that we need at this stage.

Zfcbase


The kernel that other ZF Commons modules depend on ( https://github.com/ZF-Commons/ZfcBase ).

Zfcuser


A module that implements user registration / authorization mechanisms, user profile and View helpers for use in templates ( https://github.com/ZF-Commons/ZfcUser ).

ZfcUserDoctrineORM


By default, ZfcUser works with the standard database mechanism, since our project uses Doctrine ORM, we also need the ZfcUserDoctrineORM module ( https://github.com/ZF-Commons/ZfcUserDoctrineORM ).

Zfctwig


Module for integration with Twig templating engine ( https://github.com/ZF-Commons/ZfcTwig ).

Bjyaututize


In addition to the modules from ZfCommons, I will use the BjyAuthorize module, which provides a convenient mechanism for distributing access rights. The logic of the module is simple and common among other frameworks. The module operates with concepts: user, role and guard.

The user may be authorized and not authorized. An authorized user can have one or more roles. Guard in this context is a controller / action to which we configure access rights for different roles.

Preparing for user configuration


Before you configure user experience, you must create an entity for the user and the roles that the Doctrine will use. Included with the BjyAuthorize module are examples of such entities, based on them I created the MyUser module.

The module does not contain anything original, you can see its code here: github.com/romka/zend-blog-example/tree/master/module/MyUser , in its structure it does not differ from the Application and MyBlog modules discussed above: it contains a config and 2 entity.

You should pay attention only to its config ( https://github.com/romka/zend-blog-example/blob/master/module/MyUser/config/module.config.php ):
return array(
    'doctrine' => array(
        'driver' => array(
            'zfcuser_entity' => array(
                'class' =>'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
                'paths' => array(__DIR__ . '/../src/MyUser/Entity')
            ),

            'orm_default' => array(
                'drivers' => array(
                    'MyUser\Entity' => 'zfcuser_entity',
                )
            )
        )
    ),

    'zfcuser' => array(
        // telling ZfcUser to use our own class
        'user_entity_class'       => 'MyUser\Entity\User',
        // telling ZfcUserDoctrineORM to skip the entities it defines
        'enable_default_entities' => false,
    ),

    'bjyauthorize' => array(
        // Using the authentication identity provider, which basically reads the roles from the auth service's identity
        'identity_provider' => 'BjyAuthorize\Provider\Identity\AuthenticationIdentityProvider',

        'role_providers'        => array(
            // using an object repository (entity repository) to load all roles into our ACL
            'BjyAuthorize\Provider\Role\ObjectRepositoryProvider' => array(
                'object_manager'    => 'doctrine.entity_manager.orm_default',
                'role_entity_class' => 'MyUser\Entity\Role',
            ),
        ),
    ),
);

In this config, we replace the zfcuser entity with our own, which is responsible for working with the user, and indicate to the BjyAuthorize module the entity responsible for working with roles.

The MyUser module needs to be added to application.config.php and then in the console execute the commands:
./vendor/bin/doctrine-module orm:schema-tool:update --force
./vendor/bin/doctrine-module orm:validate-schema

The first is to create tables in the database for entities created by the MyUser module, the second is to make sure that the first command worked correctly.

The final preparatory action will be to execute the request, which will create the appropriate roles:
INSERT INTO `role` 
    (`id`, `parent_id`, `roleId`) 
VALUES
    (1, NULL, 'guest'),
    (2, 1, 'user'),
    (3, 2, 'moderator'), 
    (4, 3, 'administrator');


Configuring ZfcUser, ZfcUserDoctrineORM and BjyAuthorize


First of all, you need to register new modules in the Composer's settings:
"zf-commons/zfc-base": "v0.1.2",
"zf-commons/zfc-user": "dev-master",
"zf-commons/zfc-user-doctrine-orm": "dev-master",
"doctrine/doctrine-orm-module": "0.7.*",
"bjyoungblood/bjy-authorize": "1.4.*"

execute update php composer.phar update and add new modules to application.config.php :
'ZfcBase',
'ZfcUser',
'ZfcUserDoctrineORM',
'BjyAuthorize',

Attention! The settings of some of these modules will be overridden by the settings of the self-written modules, so these modules must be added to the top of the list.

Now you need to copy the zfcuser.global.php.dist file from the vendor / zf-commons / zfc-user / config directory to config / autoload and rename it to zfcuser.global.php . In this configuration file, you need to set the value:
'table_name' => 'users',

as the user table is used by default for working with users.

In the same directory, you need to create the configuration file bjyauth.global.php containing the access rights settings for various roles. You can see the full version of this file on the github github.com/romka/zend-blog-example/blob/master/config/autoload/bjyauth.global.php , its most interesting part, which is responsible for the distribution of access rights to various controllers, given below:
'guards' => array(
    /* If this guard is specified here (i.e. it is enabled), it will block
     * access to all controllers and actions unless they are specified here.
     * You may omit the 'action' index to allow access to the entire controller
     */
    'BjyAuthorize\Guard\Controller' => array(
        array(
            'controller' => 'zfcuser',
            'action' => array('index', 'login', 'authenticate', 'register'),
            'roles' => array('guest'),
        ),
        array(
            'controller' => 'zfcuser',
            'action' => array('logout'),
            'roles' => array('user'),
        ),

        array('controller' => 'Application\Controller\Index', 'roles' => array()),

        array(
            'controller' => 'MyBlog\Controller\BlogPost',
            'action' => array('index', 'view'),
            'roles' => array('guest', 'user'),
        ),

        array(
            'controller' => 'MyBlog\Controller\BlogPost',
            'action' => array('add', 'edit', 'delete'),
            'roles' => array('administrator'),
        ),
    ),
),

It can be seen from the config that we made access to the index and view actions for all users, and add / edit / delete actions only to users with the administrator role. Now it’s easy to verify this by clicking on the link / blog / add - error 403 will be returned.
Now we can register using the link / user / register and assign administrator rights to our user with an SQL query:
INSERT INTO user_role_linker (user_id, role_id) VALUES (1, 4);

(yes, the ZfcUser module does not provide an admin panel for managing user roles).

After authorization, information on the role of the current user will be displayed at the bottom of the page in the developer toolbar and add / edit / delete actions will no longer return error 403.
A noticeable drawback of the current state of the project is that links to edit / delete blog posts are displayed to all users, despite the fact that anonymous people have no rights to perform such actions. Module Bjyaututize contains View-plug the isAllowed , which makes it easy to fix the problem. Add the following lines to the templates:
if ($this->isAllowed('controller/MyBlog\Controller\BlogPost:edit')) {
    // some code here
}

where it is necessary to check the availability of access rights to the corresponding controller / action, this will allow not displaying in the template links that are not accessible to the current user.

In a similar way, in the indexAction () action for admins, a full list of blog posts can be displayed, not just published ones:
if ($this->isAllowed('controller/MyBlog\Controller\BlogPost:edit')) {
    $posts = $objectManager
        ->getRepository('\MyBlog\Entity\BlogPost')
        ->findBy(array(), array('created' => 'DESC'));
}
else {
    $posts = $objectManager
        ->getRepository('\MyBlog\Entity\BlogPost')
        ->findBy(array('state' => 1), array('created' => 'DESC'));
}

The project in its current form is available in the Github repository with the configured_user tag : github.com/romka/zend-blog-example/tree/configured_user .

Twig


In my practice, I used several different template engines and I consider the Python Jinja 2 to be the most convenient of those that I had to work with. The Twig PHP templating engine was originally developed by Armin Ronacher, the author of Jinja 2, and then Fabien Potencier, the developer of the Symfony framework, took up his support and development .

One of the key differences between Twig and the template engine built into the Zend Framework is that you can’t use PHP code in Twig templates; instead, the template engine has its own syntax for implementing loops, conditional statements, etc. Twig templates are compiled into PHP code and as a result do not lose performance in PHP code.

Thanks to such features as template inheritance, macros, a filter system, etc. Twig templates are compact and easy to read.

Installation


To install Twig, it is enough to follow the standard steps: add a line to composer.json , run php composer.phar update and add the module to application.config.php .

Now, to the modules that will use this template engine, add the following lines to the configuration file in the view_manager section:
'strategies' => array(
    'ZfcTwigViewStrategy',
),

and Twig will be ready to use. Moreover, both template engines (Twig and default) can be used together, that is, part of the templates can be implemented on one template, part on the other.

Twig Templates


The template inheritance mentioned above means that we can create a default layout.twig template with approximately the following contents:
<html>
<head>
    <title>
        {% block title %}Default title{% endblock title %}
    </title>

    {% block script %}
    <script type="text/javascript" src="/js/jquery.min.js"></script>
    {% endblock script %}
</head>
<body>
<div class="content">
    {% block content %}{{ content|raw }}{% endblock content %}
</div>
<div class="sidebar">
    {% block sidebar %}{{ sidebar|raw }}{% endblock sidebar %}
</div>
</body>
</html>

Next, we can create a template that will inherit from layout.twig, in which we will redefine only the changed parts of the template:
{% extends 'layout/layout.twig' %}

{% block script %}
    {{ parent() }}
    <script type="text/javascript" src="some-additional-file.js"></script>
{% endblock script %}


{% block content %}
Custom content
{% endblock content %}

By default, the block overridden in the successor template replaces the block in the parent template, but pay attention to the line {{parent ()}} in the script block, its use means that the contents of the same block from the parent template will be loaded into this block.

Now let's rewrite the templates using the new template engine. I started with the standard layout.phtml template from Zend Skeleton Application, you can find it in the MyBlog module in the view / layout directory github.com/romka/zend-blog-example/blob/master/module/MyBlog/view/layout/layout. twig .

Pay attention to how much more compact it has become, for example, using view-helpers, now instead of:
<?php
    echo $this->url('blog', array('action' => 'edit'));
?>

you can call:
{{ url('blog', {'action': 'edit'}) }}

instead:
<?php
    echo $this->showMessages();
?>

just:
{{ showMessages() }}

After processing the main template, we will deal with the forms. First of all, in the view directory of the module, create the macros subdirectory and the forms.twig file with the following contents:
{% macro input(name, value, type, label, size, messages) %}
    {% if type != 'hidden' %}
        <div class="form-element-{{ name }}">
    {% endif %}

    {% if label %}
        {{ label }}:
    {% endif %}

    {% if type == 'textarea' %}
        <textarea name="{{ name }}" size="{{ size|default(20) }}" {% if messages|length > 0 %}class="error"{% endif %}/>{{ value|e }}</textarea>
    {% elseif type == 'checkbox' %}
        <input type="{{ type }}" name="{{ name }}" value="1"{% if value == true %} checked="checked"{% endif %} {% if messages|length > 0 %}class="error"{% endif %}/>
    {% else %}
        <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" {% if messages|length > 0 %}class="error"{% endif %}/>
    {% endif %}

    {% if type != 'hidden' %}
        </div>
    {%  endif %}
    {% if messages|length > 0 %}
        <ul>
            {% for m in messages %}
                <li>{{ m }}</li>
            {% endfor %}
        </ul>
    {% endif %}
{% endmacro %}

This macro will be used to display form fields. At the input, it receives the parameters of the field; at the output, it returns the html markup.

Now you can delete the existing add.phtml template and replace it with a new add.twig with the following content:
{% extends 'layout/layout.twig' %}
{% import 'macros/forms.twig' as forms %}

{% block content %}
    <h1>{{ title }}</h1>

    <form method="{{ form.attributes.method }}" action="{{ url('blog', {'action': 'add'}) }}">
        {% for element in form %}
            {{ forms.input(element.attributes.name, element.value, element.attributes.type, element.label, 20, element.messages) }}
        {% endfor %}
    </form>
{% endblock content %}

Similarly, I redid the rest of the templates and deleted now the module * .phtml templates that became unnecessary: github.com/romka/zend-blog-example/tree/master/module/MyBlog/view/my-blog/blog .

Conclusion


On this I would like to end. I did not touch on a lot of important points, such as logging, caching, Dependency Injection, writing tests, etc., etc., but all these issues are beyond the scope of the introductory article. But I hope that for beginners to learn Zend Framework 2, this article will help become a useful starting point.

I wrote all 3 parts of this article before the publication of the first part and at the time of completion of the text I planned to finish it. After reading the comments, I decided to slightly improve the application:
  • use REST, instead of checking for the type of the GET / POST request,
  • transfer part of tasks to hooks before actions,
  • transfer part of the tasks to the doctrine hooks,
  • get rid of magic constants,
  • transfer configs to yaml,
  • replace some of the calls with DI (?).

It will take some time to prepare these changes, I hope to publish the fourth part of the article soon.