Laminas: Part 2 – Customizing LmcUser

In the tutorial Laminas: Part 1 – User authentication using LmcUser, we used the package LM-Commons/LmcUser, to add user authentication to a Laminas MVC application.

Off the shelf, LmcUser provides very basic building blocks such as a very basic User entity and very simple views to login, logout and register.

In this tutorial, let’s customize the building blocks of LmcUser such that you understand how to tailor it to suit your application needs. In fact, we will not modify the LmcUser code base itself as LmcUser provides methods to extend its classes and services.

In this tutorial, we will learned:

  • How to customize the User entity class
  • How to customize the register form and other forms
  • How to customize the user page view or any other views

Getting started

This tutorial is based on the example application developed in the Laminas: Part 1 – User authentication using LmcUser.

It is not mandatory to have completed that tutorial in order to follow this tutorial. A complete version of the LmcUser tutorial application is available in the https://github.com/visto9259/lmc-user-tutorial/tree/lmcuser-tutorial-part1 repository. To install, just clone it and run the PHP server locally:

$ git clone --branch lmcuser-tutorial-part1 https://github.com/visto9259/lmc-user-tutorial
$ cd lmc-user-tutorial
$ composer install
$ composer development-enable
$ composer serve

The application will start at localhost:8080.

This example application has one user already defined. You can log in using john@example.com with the password 123456.

Setting things up

In order to keep things tidy, let’s create a new module, called User, where we will hold all our customizations. As mentioned in the introduction, customizations to the LmcUser building block are mainly “overrides” to the LmcUser code base.

If you previously built the Album application upon which the Laminas: Part 1 – User authentication using LmcUser is based, then you should know how to add a new module to a Laminas MVC application. But just in case you skipped the tutorial, let’s do it again.

Create a module directory structure for the User module:

/module
   /Album
   /Application
   /User
      /config
      /src
      /view

Update the autoloader in composer.json to add the User namespace:

    "autoload": {
        "psr-4": {
            "Application\\": "module/Application/src/",
            "Album\\": "module/Album/src/",
            "User\\": "module/User/src/"
        }
    },

Update the Composer autoloading rules:

$ composer dump-autoload

Create a empty module config file module.config.php in the config folder:

<?php

return [

];

Create a Module class file in the module/User/src folder:

<?php

namespace User;

use Laminas\ModuleManager\Feature\ConfigProviderInterface;

class Module implements ConfigProviderInterface
{
    public function getConfig()
    {
        return include __DIR__ . "/../config/module.config.php";
    }
}

And lastly, add the new module to the list of modules to load in /config/modules.config.php.

<?php

/**
 * List of enabled modules for this application.
 *
 * This should be an array of module namespaces used in the application.
 */
return [
    'Laminas\Mvc\Plugin\FlashMessenger',
    'Laminas\Mvc\Plugin\Prg',
    'Laminas\Session',
    'Laminas\Navigation',
    'Laminas\Form',
    'Laminas\I18n',
    'Laminas\InputFilter',
    'Laminas\Filter',
    'Laminas\Hydrator',
    'Laminas\Db',
    'Laminas\Router',
    'Laminas\Validator',
    'Application',
    'Album',
    'LmcUser',
    'User',
];

Make sure that the User module entry is after the LmcUser module entry. The reason why this is important, is because the Module Manager will create an overall configuration by aggregating all the configurations provided by the modules in the order in which the modules are listed in modules.config.php. If there are duplicate configuration entries, the last entry will override the previous entry. We will use this feature to override some of the LmcUser services.

You can learn more about configuration in the Advanced Configuration Tricks section of the Laminas MVC Tutorial.

Customizing the User entity class

The first simple customization is to modify the User entity class used by LmcUser. By default, LmcUser uses the class LmcUser\Entity\User to hold user properties:

namespace LmcUser\Entity;

class User implements UserInterface
{
    /**
     * @var int
     */
    protected $id;

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

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

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

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

    /**
     * @var int
     */
    protected $state;

    /* .... */
    /* public methods */

}

Very basic indeed! And it is probably not suitable for your needs.

For example, what if we wanted to add a tagline property where we could store a simple tagline for the user. For example, the tagline could be “I love the Beatles”. How can we extend LmcUser to also have a tagline property?

Modifying the User entity class

The User entity class that LmcUser uses is configured by the user_entity_class key in the lmc_user configuration which is located in the config/autoload/lmcuser.global.php file. This defaults to the \LmcUser\Entity\User class:

/* config/autoload/lmcuser.global.php */
<?php

/**
 * LmcUser Configuration
 *
 * If you have a ./config/autoload/ directory set up for your project, you can
 * drop this config file in it and change the values as you wish.
 */

$settings = [

    /* ... */

    /**
     * User Model Entity Class
     *
     * Name of Entity class to use. Useful for using your own entity class
     * instead of the default one provided. Default is LmcUser\Entity\User.
     * The entity class should implement LmcUser\Entity\UserInterface
     */
    //'user_entity_class' => \LmcUser\Entity\User::class,

    /* the rest of the file... */
]

So let’s modify the User entity to add a tagline property. One simple method is to create a new User entity class that extends \LmcUser\Entity\User. You also need to add methods to populate the tagline property. LmcUser uses a setter/getter hydrator by default and therefore you need to add getTagline and setTagline methods.

Note: Let’s keep things simple for now by adding a simple scalar type property, such as a string, to the user entity class. The reason is that the built-in mapper and hydrator can easily handle simple scalar type properties. To handle more complex properties such as an array of string, which would be more suitable, for example, for roles, then we also need to work on new mappers and hydrators. We will address these changes in a separate tutorial.

Create a new User entity class that extends the \LmcUser\Entity\User class in module/User/src/Entity/User.php:

<?php
/* src/User/Entity/User.php */

namespace User\Entity;

class User extends \LmcUser\Entity\User
{
    protected ?string $tagline = null;

    public function getTagline(): ?string
    {
        return $this->tagline;
    }

    public function setTagline(?string $tagline): User
    {
        $this->tagline = $tagline;
        return $this;
    }
}

Before we can use this new User entity class, you need to modify the user table in the laminastutorial.db database to add a tagline column. Add a simple varchar column called tagline:

$ sqlite3 data/laminastutorial.db
sqlite> ALTER TABLE user ADD COLUMN tagline varchar(100);
sqlite> .quit

Let’s add a tagline to the user john@example.com and validate that it has been added:

$ sqlite3 data/laminastutorial.db
sqlite> UPDATE user SET tagline='I love the Beatles' WHERE email='john@example.com';
sqlite> SELECT * FROM user WHERE email='john@example.com';
0||john@example.com||$2y$14$MGuj.nnEGvBMoK6riwhrk.AZKYpTKE4KPvwP8c0KwMlHETSVVjb2O||I love the Beatles
sqlite> .quit

Now we can configure LmcUser to use the new User entity class. Modify the user_entity_class configuration entry in the /config/autoload/lmcuser.global.php file:

/* config/autoload/lmcuser.global.php */
<?php

/**
 * LmcUser Configuration
 *
 * If you have a ./config/autoload/ directory set up for your project, you can
 * drop this config file in it and change the values as you wish.
 */

$settings = [

    /* ... */

    /**
     * User Model Entity Class
     *
     * Name of Entity class to use. Useful for using your own entity class
     * instead of the default one provided. Default is LmcUser\Entity\User.
     * The entity class should implement LmcUser\Entity\UserInterface
     */
    'user_entity_class' => \User\Entity\User::class,

    /* the rest of the file... */
]

Now, if you reload the application, it should still work as expected.

How can you access the new tagline property?

Remember from the Laminas: Part 1 – User authentication using LmcUser, that there are plugins to access the user identity:

  • In controllers, $this->lmcUserAuthenticaion()->getIdentity() will return the new \User\Entity\User object
  • In views, $this->lmcUserIdentity() will return the new \User\Entity\User object

Let’s put this new property to some use.

We will make a simple modification to the Album page to show the user’s tagline. This may not be the most useful usage of the new tagline property but it is sufficient for this tutorial.

Modify the Album index view to display the user’s tagline, if present. In module/Album/view/album/album/index.phtml, make the highlighted changes:

<?php
// module/Album/view/album/album/index.phtml:

$displayName = $this->lmcUserDisplayName();
$title = $displayName . '\'s albums';
$this->headTitle($title);
$userIdentity = $this->lmcUserIdentity();
$mailTo = 'mailto:' . $userIdentity->getEmail();
?>
<h1><a href="<?= $mailTo;?>"><?= $displayName;?></a>&apos;s album</h1>
<?php if ($this->lmcUserIdentity()->getTagline()): ?>
<p><?=$this->lmcUserIdentity()->getTagline()?>.</p>
<?php endif; ?>
<p>
    <a href="<?= $this->url('album', ['action' => 'add']) ?>">Add new album</a>
</p>

/* ... the rest of the file */

The page should now look like this:

Customizing the user register form

Now that we have added the tagline property to the User entity class, we need for the user to be able to enter it during registration.

Note: LmcUser v3 does not currently provide building blocks to modify an existing user. This should be an enhancements for future versions. Therefore, for the purpose of this tutorial, we will add the capability to set the tagline during user registration.

LmcUser uses a form to capture user data during the registration process. When navigating to the /user/register URL, the register action of the User controller is executed. The action instantiate a register form that is passed as a parameter to the register view.

This register form is not hardcoded in the register action. Instead, the action instantiate the form by requesting it through the Service Manager using the name 'lmcuser_register_form'. LmcUser configures the Service Manager, when requesting 'lmcuser_register_form', to invoke the factory LmcUser\Factory\Form\Register that creates a LmcUser\Form\Register object with a few validations. This process may look complicated to create a simple register form but it allows you to “override” this process by your own factory and form if you want to.

The register view will then use this register form and simply iterate through the form’s elements to create the HTML for the form. You can override the register view as well and we will cover this later.

From here, in order to add the tagline field in the form generated by the register view, there are two options:

  1. Override the factory that creates the register form to create your own form. This is however complicated since you need to ensure that your factory creates a form that has all the elements required by the register action, that the form’s data hydrator is set properly, etc. To use this option, you will need to review the LmcUser code to undertand what it does in order to replicate the same behavior.
  2. Use a delegate factory to “decorate” the register form with the new field. “Decorating” in this context is simply to add a new element to the form to capture the tagline. This may sound complicated but this is the simplest option considering that the new element to add is a simple text field.

Note: Using delegate factories is not covered by the Laminas Album tutorial. Delegators are documented in the Laminas Service Manager Reference.

Let’s create a delegator that will add the new tagline element to the register form and add this delegator to the Service Manager configuration.

Create the delegator class in the file module/User/src/Form/RegisterFormDelegatorFactory.php:

<?php

namespace User\Form;

use Laminas\Form\FormInterface;
use Laminas\ServiceManager\Factory\DelegatorFactoryInterface;
use LmcUser\Form\Register;
use Psr\Container\ContainerInterface;

class RegisterFormDelegatorFactory implements DelegatorFactoryInterface
{

    /**
     * @inheritDoc
     */
    public function __invoke(ContainerInterface $container, $name, callable $callback, ?array $options = null): Register
    {
        // Create the instance
        /** @var Register $form */
        $form = call_user_func($callback, $options);

        // Add the tagline element. The name of the element must match the property in the User entity
        $form->add([
            'name' => 'tagline',
            'type' => \Laminas\Form\Element\Text::class,
            'options' => [
                'label' => 'Tagline'
            ],
        ]);

        // We could get fancy and add filtering and validation if we wanted to
        return $form;
    }
}

So what’s happening here? The delegator is invoked by the Service Manager. The delegator uses the callback function $callback to instantiate the object referenced by '$name‘. The object returned by the callback is the register form (\LmcUser\Form\Register). Lastly, we add a simple text input element to the form and return the “decorated” form.

To add the delegator to the Service Manager, we need to modify the module/User/config/module.config.php file with the follwing changes:

<?php

return [
    'service_manager' => [
        'delegators' => [
            'lmcuser_register_form' => [
                \User\Factory\RegisterFormDelegatorFactory::class,
            ]
        ],
    ],
];

This configuraton tells the Service Manager to invoke the delegator \User\Factory\RegisterFormDelegatorFactory when instantiating the object/service 'lmcuser_register_form'.

Let’s try it out. Make sure that the user is logged out and go to localhost:8080/user/register. You should get the following:

Let’s create a new user with the username jane@example.com, password 123456, display name Jane and tageline I prefer the Stones.

You should get the following Album’s page:

Customizing the other forms

The same process can be used the other forms that LmcUser uses for login and to change the email and password. These forms are instantiated from the following aliases:

  • ‘lmcuser_login_form’
  • ‘lmcuser_change_password_form’
  • ‘lmcuser_change_email_form’

Customizing views

Up to now, we have added the new tagline property to the Album’s main page. It somewhat makes sense but it would make more sense to also have the tagline show up on the user’s profile page.

We can achieve this by “overriding” the view template used by the Index action of the User controller.

LmcUser is a typical Laminas MVC module. Each action in the User controller returns a View Model that gets maps to a template. LmcUser uses a view template stack configuration to assign the right template to each action:

    'view_manager' => [
        'template_path_stack' => [
            'lmcuser' => __DIR__ . '/../view',
        ],
    ],

where __DIR__ . '/../view' points to the top of the view directory structure at /vendor/lm-commons/lmc-user/view:

/vendor/
   lm-commons/
      lmc-user/
         view/
           lmc-user/
              user/
                index.phtml
                login.phtml
                ....

Laminas MVC will set the template for the index action of the User controller automatically to 'lmc-user/user/index' which, given the template stack above, will point to the file login.phtml.

Note: Why 'lmc-user/user/index' instead of 'lmcuser/user/index'? Laminas MVC uses name inflection to convert camel case names like LmcUser to lmc-user. Hence the reason why the view directory structure is lmc-user instead of lmcuser.

Let’s create our own template for the user index page. We will start from the existing template and adjust it to show the tagline. While we are at it, let’s make the view a little more beautiful by using some Bootstrap styles. Copy the index template vendor/lm-commons/lmc-user/view/lmc-user/user/index.phtml to module/User/view/lmc-user/user/index.phtml and modify it as follows:

<div class="row">
    <div class="col-1 m-1">
        <div><?php echo $this->gravatar($this->lmcUserIdentity()->getEmail()) ?></div>
    </div>
    <div class="col m-1">
        <h3><?php echo $this->translate('Hello'); ?>, <?php echo $this->escapeHtml($this->lmcUserDisplayName()) ?>!</h3>
        <p><?= $this->lmcUserIdentity()->getTagline();?></p>
        <a class="btn btn-primary" href="<?php echo $this->url('lmcuser/logout') ?>"><?php echo $this->translate('Sign Out'); ?></a>
    </div>
</div>

Then let’s also add a view template map in module/config/module.config.php to configure the View Manager to pick up our new template:

<?php

return [
    'service_manager' => [
        'delegators' => [
            'lmcuser_register_form' => [
                \User\Factory\RegisterFormDelegatorFactory::class,
            ]
        ],
    ],
    'view_manager' => [
        'template_map' => [
            'lmc-user/user/index' => __DIR__ . '/../view/lmc-user/user/index.phtml',
        ],
    ],
];

Let’s check it out. Navigate to localhost:8080/user and you should get the following:

Conclusion

In this tutorial, we learned:

  • How to customize the User entity class
  • How to customize the register form and other forms
  • How to customize the user page view or any other views

Next steps

These were simple customizations to show that LmcUser can be easily adapted to your needs.

In the next tutorials, we will go deeper in customizations:

Previous tutorials:

Leave a Comment

Your email address will not be published. Required fields are marked *