Zend Framework 3 / Laminas Doctrine Authentication Module

This module requires Doctrine Module and uses Doctrine ORM.

I also assume that you are familiar with Doctrine ORM, Doctrine Module, Zend Framework / Laminas and using the command line interface.

Table of contents

Installation Return to top

With Composer Return to top

  1. In the root of your application enter:
    Copy to clipboard $ composer require krytenuk/doctrine-auth

Post installation Return to top

  1. Ensure the module is enabled it in your `modules.config.php`file.
    <?php
        
    return [
            
    // ...
            
    'FwsDoctrineAuth',
        ];
  2. Copy `./vendor/krytenuk/doctrine-auth/config/doctrine.auth.config.local.php.dist` to `./config/autoload/doctrine.auth.config.local.php`.
  3. Copy `./vendor/krytenuk/doctrine-auth/config/doctrine.auth.acl.local.php.dist` to `./config/autoload/doctrine.auth.acl.local.php`.

Configuration Return to top

In your ./config/autoload/doctrine.auth.config.local.php

  • Under the ['doctrine']['connection'] key you need to setup your database connection
    Key Type Description Default Value
    host string Your database host localhost
    port integer Port used to connect to database 3306
    dbname string Name of the database  
    user string Database user  
    password string Database user password  

  • Under the ['doctrine']['authentication'] key you need to setup your users entity
    Key Type Description Default Value
    identity_class string This is the Doctrine users entity \FwsDoctrineAuth\Entity\BaseUsers
    identity_property string The column in the users entity in which to search e.g. username, emailAddress
    If you change this you must add the column to your users entity, this column must also be unique
    emailAddress
    credential_property string The users password column password

  • Under the ['doctrineAuth'] key you need set other values
    Key Type Description Default Value
    registrationForm string The form to use when registering a new user \FwsDoctrineAuth\Form\RegisterForm
    loginForm string The form to use when logging in a user \FwsDoctrineAuth\Form\LoginForm
    formElements array    
    identity_label string The identity label to use in the login and registration forms Email Address
    credential_label string The credential label to use in the login and registration forms Password
    allowRegistration boolean Allow new user registration (TRUE or FALSE) TRUE
    registrationCallback string Callback class to add custom registration code  
    autoRegistrationLogin boolean Allowe auto login after registration (TRUE or FALSE) FALSE
    userActiveAfterRegistration integer User active after registration (1 = active, 0 = inactive) 1

  • To setup laminas-session please see laminas-session

In your ./config/autoload/doctrine.auth.acl.local.php

This is the access control list config. Here you setup your user roles, rescources (modules, controllers) and which roles have access to which resources (permissions). This is the backbone of Doctrine Auth.

User roles Return to top

All users are assigned roles, guest being the default. Guest users can access only limited resources such as the login page. Once logged in a users role takes on the role that is assigned to them, usually by an admin user and can access other areas of the application (website) not accessable to guest users. Examples of user roles are, user, author and admin. User roles can also inheret permissions from their parent roles, for example if the admin role has the author role as it's parent it can access all the resources that the author can.

To setup your user roles

  • Under the ['doctrineAuthAcl']['roles'] key
    Key Type Description
    id string The unique role identifier
    parents array The roles parent role(s) if any
    Roles inheret access to resources from their parents
    redirect array    
    route string The route to redirect to after successful login
    If the user is already trying to access a restricted rescource then redirect to that if allowed. If not then redirect to this route.
    params array The routes params, see laminas-router for more info
    options array The routes options, see laminas-router for more info

  • Under the ['doctrineAuthAcl'] key
    Key Type Description Default Value
    defaultRegisterRole string The default role assigned to a user after successful registration user

Resources Return to top

Resources are basically the modules and controllers that makeup your application. Usually I create a module for all my user roles created above (guest is usually the "application" module), for example for an admin user role I would create an admin module. In here I will place all the controllers the admin role can access. You may also wish to create an auth module if you want to extend this "Doctrine Auth" module.

To set your resources

  • Under the ['doctrineAuthAcl']['resources'] key
    Key Type Description
    module string Your module
    controllers array An array of your modules controllers fully qualifired names e.g. \namespace\Controller\YourController::class

Permissions Return to top

Here we tell "Doctrine Auth" which user roles can access which resources set above. We can also specify individual actions within a controller.

To set your permissions

  • Under the ['doctrineAuthAcl']['permissions'] key
    Key Type Description
    type \Laminas\Permissions\Acl\Acl::TYPE_ALLOW or
    \Laminas\Permissions\Acl\Acl::TYPE_DENY
    Set permission type, either allow access or deny access to the module/controller specified below
    role string Your users role id defined ealier
    resource string A previously defined resource, either a module or controller
    If you specify a module then all the controllers under it inherit this permission
    actions array If you specify a controller above then an array of actions can be specified
    If left blank then this permission applies to all actions

Other Settings Return to top

  • Under the ['doctrineAuthAcl'] key
    Key Type Description Default
    injectAclIntoNavigation boolean Inject ACL into Laminas navigation view helper plugin (TRUE or FALSE)
    This will only show the resources accessable to the current user when using Laminas Navigation
    TRUE

Extending the users entity Return to top

As standard FWS Doctrine Auth ships with a base users entity, \FwsDoctrineAuth\Entity\BaseUsers, which works out of the box.

<?php
    
namespace FwsDoctrineAuth\Entity;

use 
Doctrine\ORM\Mapping as ORM;
use 
FwsDoctrineAuth\Entity\UserRoles;
use 
DateTime;

/**
 * Users
 * @ORM\Entity(repositoryClass="FwsDoctrineAuth\Entity\Repository\UserRepository")
 * @ORM\Table(name="users", indexes={
 *   @ORM\Index(name="user_id", columns={"user_id"}),
 * })
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="type", type="string")
 * @author Garry Childs <info@freedomwebservices.net>
 */
class BaseUsers implements EntityInterface
{

    
/**
     * @var integer
     *
     * @ORM\Column(name="user_id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    
private $userId;

    
/**
     * identity_property in config
     * @var string
     *
     * @ORM\Column(name="email_address", type="string", length=60, nullable=true)
     */
    
private $emailAddress;

    
/**
     * credential_property in config
     * @var string
     *
     * @ORM\Column(name="password", type="string", length=60, nullable=false)
     */
    
private $password;
    
/**
     * @var integer
     *
     * @ORM\Column(name="user_active", type="integer", nullable=false)
     */
    
private $userActive;

    
/**
     * @var DateTime
     *
     * @ORM\Column(name="date_created", type="datetime")
     */
    
private $dateCreated;

    
/**
     * @var DateTime
     *
     * @ORM\Column(name="date_modified", type="datetime")
     */
    
private $dateModified;

    
/**
     * @var BaseUserRoles
     *
     * @ORM\ManyToOne(targetEntity="FwsDoctrineAuth\Entity\UserRoles")
     * @ORM\JoinColumns({
     *   @ORM\JoinColumn(name="user_role_id", referencedColumnName="user_role_id")
     * })
     */
    
private $userRole;
    
/* Getters & Setters */

However you may well need to add additional columns to this entity to suit your application. As such this entity is designed to be extended.

Below is an example of extending the users entity by addina a company name

Copy to clipboard <?php
    
namespace YourNamespace\Entity;

use 
FwsDoctrineAuth\Entity\BaseUsers;
use 
Doctrine\ORM\Mapping as ORM;

/**
 * Users
 * @ORM\Entity
 */
class Users extends BaseUsers
{

    
/**
     * @var string
     *
     * @ORM\Column(name="company_name", type="string", length=255, nullable=true)
     */
    
private $companyName;
    
    public function 
getCompanyName()
    {
        return 
$this->companyName;
    }

    public function 
setCompanyName($companyName)
    {
        
$this->companyName $companyName;
        return 
$this;
    }

}

Don't forget to update your identity_class in the authentication config

For more information on Doctrine Entities please visit here

Updating your module.config.php Return to top

In your module(s) where your doctrine entties are stored you must update your module.config.php file to tell doctrine where your entities are.

Copy to clipboard <?php

use Doctrine\ORM\Mapping\Driver\AnnotationDriver;

return [
    
'doctrine' => [
        
'driver' => [
            
__NAMESPACE__ '_driver' => [
                
'class' => AnnotationDriver::class,
                
'paths' => [__DIR__ '/../src/Entity'], // path to your entity directory
            
],
            
'orm_default' => [
                
'drivers' => [
                    
__NAMESPACE__ '\Entity' => __NAMESPACE__ '_driver'
                
],
            ],
        ],
    ],
];

Database setup Return to top

In order to use FWS Doctrine Auth you need to setup your database through Doctrine Modules CLI

New database Return to top

Ensure that all your user roles and Doctrine entities have been created before creating your database

  1. To create a new database in MySQL use. mysql> CREATE DATABASE `your_database`;
    mysql> CREATE USER `your_user`@`localhost` IDENTIFIED BY 'your_password';
    mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, DROP, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES ON `your_database`.* TO `your_user`@`localhost`;
  2. and to add the tables using the CLI.
    Copy to clipboard $ vendor\bin\doctrine-module orm:schema-tool:create

Existing database Return to top

Ensure you backup your database first.

  1. On an existing database use.
    Copy to clipboard $ vendor\bin\doctrine-module orm:schema-tool:update --force
    To synchronize your entities with your database tables

For more information on Doctrine CLI see Doctrine Module and Doctrine Console

Once your database tables are setup.

  1. Populate the user roles
    Ensure that you run this command after adding new user roles
    Copy to clipboard $ vendor\bin\doctrine-module doctrine-auth:init
  2. you can also truncate the user roles table prior to populating
    use this with caution
    Copy to clipboard $ vendor\bin\doctrine-module doctrine-auth:init --truncate

Extending the registration form Return to top

The registration form simply collects the users email address and password, you may wish to add more fields to populate your users entity

Firstly add the columns to your users entity. Create methods "addElements()" for your elements and "addInputFilterSpecification()" for your input filters to your registration form, then add the fields to your registration form. Both the form field name and entity variable must be the same for auto population to work. Ensure that your registration form extends the default \FwsDoctrineAuth\Form\RegisterForm

Copy to clipboard _("Your company name must not contain more than %max% characters"), Validator\StringLength::TOO_SHORT => _("Your company name must contain more than %min% characters"), ], ], ], ], ], ]; } }"> <?php

namespace Auth\Form;

use 
FwsDoctrineAuth\Form\RegisterForm AS FwsDoctrineAuthRegisterForm;
use 
Laminas\Form\Element\Text;
use 
Laminas\Validator;

/**
 * Description of Register
 *
 * @author Garry Childs <info@freedomwebservices.net>
 */
class RegisterForm extends FwsDoctrineAuthRegisterForm
{

    public function 
addElements()
    {
        
$this->add([
            
'name' => 'companyName',
            
'type' => Text::class,
            
'attributes' => [
                
'size' => 16,
                
'maxlength' => 255,
            ],
            
'options' => [
                
'label' => _('Company'),
                
'label_attributes' => ['class' => 'required'],
            ],
        ]);
    }

    public function 
addInputFilterSpecification()
    {
        return [
            
'companyName' => [
                
'required' => FALSE,
                
'filters' => [
                    [
'name' => 'StripTags'],
                    [
'name' => 'StringTrim'],
                ],
                
'validators' => [
                    [
                        
'name' => Validator\StringLength::class,
                        
'options' => [
                            
'encoding' => 'UTF-8',
                            
'min' => 3,
                            
'max' => $this->get('companyName')->getAttribute('maxlength'),
                            
'messages' => [
                                
Validator\StringLength::INVALID => _("Your company name must contain between %min% and %max% characters"),
                                
Validator\StringLength::TOO_LONG => _("Your company name must not contain more than %max% characters"),
                                
Validator\StringLength::TOO_SHORT => _("Your company name must contain more than %min% characters"),
                            ],
                        ],
                    ],
                ],
            ],
        ];
    }

}

Next add your form to the registrationForm key in doctrine.auth.config.local.php

And finally register your form in your module.config.php

Copy to clipboard <?php

namespace YourNamespace;

use 
YourNamespace\Form\RegisterForm;
use 
FwsDoctrineAuth\Form\Service\RegisterFormFactory;

return [
    
'form_elements' => [
        
'factories' => [
            
RegisterForm::class => RegisterFormFactory::class,
        ],
    ],
];

The login form can be extended in a similar fashon if required

Changing the registration and login form view script Return to top

To override the default view scripts you must add the following to your module.config.php

<?php
                    
'view_manager' => array(
    
'template_path_stack' => array(
        
'fws-doctrine-auth' => __DIR__ '/../view'
    
),
),

Make sure that your module is loaded after FwsDoctrineAuth. In your modules.config.php use:

<?php
    
return [
    
// ...
    
'FwsDoctrineAuth',
    
'YourModule',
    
// ...
];

In view folder of your module create a "fws-doctrine-auth" folder and in that create an "index" folder.

Now create your login.phtml file or register.phtml file. Inside your view script the form can be found in variable $this->form or just $form.

Refer to each built-in view script to see how the form is configured and rendered.

Here is an examle of a custom register form.

<h2><?php echo $this->translate('Custom Register Form'); ?></h2>
<?php
if ($this->errorMessage) {
    echo 
'<p class="error">' $this->errorMessage '</p>' PHP_EOL;
}
$this->form->setAttribute('action'$this->url('doctrine-auth/default', array('action' => 'register')));
$this->formElementErrors()->setAttributes(array('class' => 'errors'));
$this->form->prepare();
echo 
$this->form()->openTag($this->form);
?>
<dl class="zend_form">
    <dt><?php echo $this->formLabel($this->form->get('companyName')); ?></dt>
    <dd><?php echo $this->formText($this->form->get('companyName')) . $this->formElementErrors($this->form->get('companyName')); ?></dd>
    
    <dt><?php echo $this->formLabel($this->form->get('emailAddress')); ?></dt>
    <dd><?php echo $this->formText($this->form->get('emailAddress')) . $this->formElementErrors($this->form->get('emailAddress')); ?></dd>
    
    <dt><?php echo $this->formLabel($this->form->get('password')); ?></dt>
    <dd><?php echo $this->formPassword($this->form->get('password')) . $this->formElementErrors($this->form->get('password')); ?></dd>
    
    <dd><?php echo $this->formHidden($this->form->get('csrf')) . $this->formElementErrors($this->form->get('csrf')); ?>
    <?php echo $this->formSubmit($this->form->get('submit')); ?></dd>
</dl>
<?php echo $this->form()->closeTag(); ?>

Adding custom code to registration Return to top

Whilst adding fields to your registration form having the same name as the property in your users entity will auto populate the entity, there are times that you may need to add custom code to the registration process. There is a config key, registrationCallback , which enables you to specify an invokeable class that contains this code. An example can be seen below.

Copy to clipboard <?php

namespace Auth\Model;

use 
FwsDoctrineAuth\Entity\BaseUsers;
use 
Laminas\Form\FormInterface;
use 
Laminas\Stdlib\Parameters;

class 
Registration
{

    public function 
__invoke(BaseUsers $userEntityFormInterface $formParameters $postData)
    {
        
// Your code here
    
}

}

Here you have access to the user entity, form and the post data from the browser. You don't need to return anything, after this the users entity is saved.

Laminas Navigation intergration Return to top

Doctrine Auth comes with laminas-navigation intergration builtin. This simply injects the ACL into the Laminas Navigation view helper showing only the links to resources the current user is allowed to view in your navigation menus.

For example in your navigation config you can use

Copy to clipboard <?php

use FwsDoctrineAuth\Controller\IndexController;
use 
Application\Controller\IndexController as ApplicationIndexController;

return array(
    
'navigation' => array(
        
'default' => array(
            array(
                
'label' => _('Home'),
                
'route' => 'home',
                
'resource' => ApplicationIndexController::class,
                
'privilege' => 'index',
            ),
            array(
                
'label' => _('Login'),
                
'route' => 'doctrine-auth/default',
                
'action' => 'login',
                
'resource' => IndexController::class,
                
'privilege' => 'login',
            ),
            array(
                
'label' => _('Logout'),
                
'route' => 'doctrine-auth/default',
                
'action' => 'logout',
                
'resource' => IndexController::class,
                
'privilege' => 'logout',
            ),
        ),
    ),
);

In your layout.phtml or a view.phtml script

Copy to clipboard <?php echo $this->navigation('Laminas\Navigation\Default')->menu(); ?>

By default when used in your layout.phtml or a view.phtml file and is viewed by a guest user then they will only see the login link as they are not allowed to access logout. However if an admin views this then all they will see is the logout link as they don't have permission to access login (they are already logged in!). Both users will see the home link.

Routing Return to top

Using one route for multiple controllers

In order to use the controller parameter to route to a multipule controllers from a single route, you need to add an aliases to map them to the fully qualified controller names. Below is an example of this, for example going to a url of www.yourdomain.com/user/somecontroller/some-action should goto a controller named \User\Controllers\somecontrollerController and dispatch the someActionAction() method.

Copy to clipboard <?php

namespace User;

use 
Laminas\ServiceManager\Factory\InvokableFactory;
use 
User\Controller;
use 
Laminas\Router\Http\Segment;

return [
    
'controllers' => array(
        
'aliases' => array(
            
'somecontroller' => Controller\SomecontrollerController ::class, // controller route aliases here
        
),
        
'factories' => array(
            
Controller\SomecontrollerController ::class => InvokableFactory::class,
        ),
    ),
    
'router' => [
        
'routes' => [
            
'user' => [
                
'type' => Segment::class,
                
'options' => [
                    
'route' => '/user[/:controller[/:action]]',
                    
'constraints' => [
                        
'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                        
'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
                    ],
                    
'defaults' => [
                        
'controller' => Controller\IndexController::class,
                        
'action' => 'index',
                    ],
                ],
            ],
        ],
    ],
];
Donate