Laminas: Part 1 – User authentication using LmcUser

In this tutorial, we will go through the steps to add user authentication and registration to a Laminas MVC application using the LM-Commons/LmcUser package.

What is LmcUser?

LM-Commons/LmcUser is a generic user registration and authentication module for Laminas MVC applications. LmcUser provides the foundations for adding user authentication and registration to your Laminas site. It is designed to be very simple and easy to extend.

LM-Commons/LmcUser is available for download on GitHub or via Composer. Instructions to install and set up are in the README page of the repositoty and also on LM-Common’s website at lm-commons.github.io.

Getting started

This tutorial is not about how to set up a web application using the Laminas MVC framework and it will not go through the steps needed to set one up. The Laminas website has a tutorial to get started with Laminas MVC which starts with a skeleton application and builds upon it to add a module that provides a few extra pages to manages music albums.

This tutorial is built upon Laminas’ tutorial application and will add user registration and authentication to it. Therefore in order to follow this tutorial, you need to follow the steps in Laminas’ Album tutorial and get your application to the point where navigation has been added to the Album application.

In other words, you need to start at Getting Started with Laminas MVC Applications and walk through the tutorial until you reach the end of the step Using laminas-navigation in your Album Module. You can skip the step on Unit Testing a Laminas MVC application as it is not relevant to this tutorial. Obviously, you are encouraged to follow the Laminas tutorial to the end if you are not familiar with Laminas MVC application.

So go ahead, follow the tutorial and come back here once you have a functional Album application.

To lazy to follow the tutorial? Or you already know enough about Laminas MVC?

If you want to get going quickly, you can find a complete Album application, as it would be once you complete the tutorial steps as suggested above, in the visto9259/lmc-user-tutorial repository. Just clone it and set up the application:

$ git clone https://github.com/visto9259/lmc-user-tutorial --branch starting-point path/to/install
$ cd path/to/install
$ composer install
#
# Just to make sure
$ composer development-enable
#
$ composer serve

and then navigate to localhost:8080 using your browser and you should get the following page:

If you decided to install the tutorial from the repo, then it is important to remember that the Album application uses SQLite for its database and this tutorial will also use SQLite as a simple database for user accounts. The database is located in the file /data/laminastutorial.db

Installing the LmcUser package and initial setup

Use Composer to install LmcUser into the application and all its dependencies.

$ composer require lm-commons/lmc-user

Assuming you followed the Getting Started tutorial, you will be prompted by the laminas-component-installer plugin to inject LmcUser and its dependencies; be sure to select the option for either config/application.config.php or config/modules.config.php.

Then you need to setup the LmcUser global configuration file. There is a sample config file located in /vendor/lm-commons/lmc-user/config that you can copy to the autoload directory:

$ cp vendor/lm-commons/lmc-user/config/lmcuser.global.php.dist config/autoload/lmcuser.global.php

Let’s leave the configuration file untouched for now as the LmcUser module has defaults for each config parameters that provides a working solution after installation. We will modify it later.

Setting up the user database

LmcUser needs a table in the database to store user accounts.

As you may recall from the Laminas MVC tutorial, the Album application uses SQLite as its database engine and a default database adapter was configured in autoload/global.php config file. By default, LmcUser uses the default database adapter and there is no immediate need to set a separate adapter. The SQLite database for the Album application is located in data/laminastutorial.db. and we will add the user table to it.

There is a SQLite script to create the table in /vendor/lm-commons/lmc-user/data called schema.sqlite.sql:

CREATE TABLE user
(
    user_id       INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    username      VARCHAR(255) DEFAULT NULL UNIQUE,
    email         VARCHAR(255) DEFAULT NULL UNIQUE,
    display_name  VARCHAR(50) DEFAULT NULL,
    password      VARCHAR(128) NOT NULL,
    state         SMALLINT
);

Add the user table to the database:

#
# Depending on your configuration, you may have to use the sqlite3 command instead
#
$ sqlite data/laminastutorial.db < vendor/lm-commons/lmc-user/data/schema.sqlite.sql

OR

$ sqlite3 data/laminastutorial.db < vendor/lm-commons/lmc-user/data/schema.sqlite.sql

At this point, we have a mimimally functional user authentication. Navigate to localhost:8080/user and you should get the following page:


So what exactly happened here? The module is set up such that navigating to /user will check if the user is logged in. If not, then it will redirect to /user/login and prompt the user for its credentials. By default, LmcUser uses an email as the user identifier.

How does it know if the user is logged in? The current user, when logged in, is stored in a PHP session. You may have noticed that, when installing LmcUser, the laminas/laminas-session component was also installed and added to the modules configuration file. This component add session management behind the scenes. Session management and configuration is beyond the scope of this tutorial.

Register as new user

In order to log in, we obviously need to create a user. LmcUser provides a simple view to register new users.

Before we register, let’s make a small change to the default LmcUser configuration to enable the use of a display name for the user. This parameter is disabled by default. Make this change to the 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 = [

     /* .... */

     /**
     * Enable Display Name
     *
     * Enables a display name field on the registration form, which is persisted
     * in the database. Default value is false.
     *
     * Accepted values: boolean true or false
     */
    'enable_display_name' => true,

     /* .... */

Navigate to localhost:8080/user/register or follow the Sign up! link on the Sign in page. On the register page, enter a user email and a password. No validation is performed other than ensuring that the email has the valid format of an email.

For this tutorial, let’s use john@example.com as the Email, John for the Display Name and 123456 as the Password.


Once the registration is completed, you will be taken to the user page (/user). This is the default behavior and it can be changed. We will see how in a few minutes.


That’s it! But that’s the minimum LmcUser can do. As you can see, it does not provide much by default and it can be easily customized and extended to suit your application needs. The LmcUser Wiki provides more detailed instructions but, no worries, we will cover those in this series of tutorials.

Configuring LmcUser

Most of the configuration for LmcUser is done through the config/autoload/lmcuser.global.php file which was copied earlier from vendor/lm-commons/lmc-user/config/lmcuser.global.php.dist.

Note: As with any other Laminas MVC application, the content of this configuration file is merged into the global 'config' under the 'lmc_user' key. This is explained in more detailed in the Laminas MVC Tutorial.

Let’s look at the configuration options:

  • user_entity_class – Name of User Model Entity class to use. Default is LmcUser\Entity\User::class
    Useful for using your own entity class instead of the default one provided. You can create your own class by extending LmcUser\Entity\User or by creating an entire new class. The entity class must implement the LmcUser\Entity\UserInterface interface.
  • enable_username – Boolean value, enables username field on the registration form. Default is false.
  • auth_identity_fields – Array value, specifies which fields a user can use as the 'identity' field when logging in. Acceptable values: username, email. The default is ['email'].
  • enable_display_name – Boolean value, enables a display name field on the registration form. Default value is false.
  • enable_registration – Boolean value, Determines if a user should be allowed to register. Default value is true.
  • login_after_registration – Boolean value, automatically logs the user in after they successfully register. Default value is true.
  • login_form_timeout – Integer value, specify the timeout for the CSRF security field of the login form in seconds. Default value is 300 seconds.
  • use_login_form_csrf – Boolean value which enables the use of a Cross-Site Request Forgery (CSRF) field in the the login form to added security. Defaults to true.
  • user_form_timeout – Integer value, specify the timeout for the CSRF security field of the registration form in seconds. Default value is 300 seconds.
  • login_redirect_route String value, name of a route in the application which the user will be redirected to after a successful login. The default is lmcuser which maps to /user.
  • use_redirect_parameter_if_present – Boolean value, if a redirect GET parameter is specified, the user will be redirected to the specified URL if authentication is successful (if present, a GET parameter will override the login_redirect_route specified above). The default is true.
  • logout_redirect_route String value, name of a route in the application which the user will be redirected to after logging out. It defaults to 'lmcuser/login' which maps to '/user/login'.
  • password_cost – This should be an integer between 4 and 31. The number represents the base-2 logarithm of the iteration count used for hashing. Default is 14 (about 10 hashes per second on an i5).
  • enable_user_state – Boolean value, enable user state usage. Default is true. When enable, the user state will be checked against the allowed login states defined in allowed_login_states
  • default_user_state – Integer value, default user state upon registration. Defaults to 1.
  • allowed_login_states – Array value, states which are allowing user to login. When user tries to login, is his/her state which be checked againts the values in the array. Include null if you want user’s with no state to login as well. Defaults to [null, 1].
  • table_name – Name on the user table. Defaults to 'user'.
  • use_registration_form_captcha – Boolean value, determines if a captcha should be utilized on the user registration form. Default value is false.
  • laminas_db_adapter – This is the dependency injection alias for the database adapter to use for the user table. Defaults to \Laminas\Db\Adapter\Adapter::class.

That’s a lot of information to process. It will be easier to explain through uses cases. We already covered the enable_display_name parameter.

Logging in, registering and redirecting

Let’s say that we would like, when the user logs in, that he is redirected to the Albums page instead the user page. And let’s also say that, when the user logs out, that he should be redirected to the home page.

As you recall from the tutorial, the album page matches the 'album' route ('/album') and the home page matches the 'home' route. Therefore we need to make the following changes to the lmcuser.global.php file, but before you do, log out from the application, using the [Sign out] link from the user page or by navigating to localhost:8080/user/logout.

Note: you do not have to log out to make the changes. This is just to ensure a better flow in the tutorial.

<?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 = [
     /* .... */
     /**
     * Login Redirect Route
     *
     * Upon successful login the user will be redirected to the entered route
     *
     * Default value: 'lmcuser'
     * Accepted values: A valid route name within your application
     *
     */
    'login_redirect_route' => 'album',

    /**
     * Logout Redirect Route
     *
     * Upon logging out the user will be redirected to the entered route
     *
     * Default value: 'lmcuser/login'
     * Accepted values: A valid route name within your application
     */
    'logout_redirect_route' => 'home',

    /*  the rest of the configuration */
];
    /* the rest of the file */

Now log in into the application and you will be redirected to the album page. Log out and you will be redirected to the home page. Right now, we do not have a way to log out other then navigating to the user page (localhost:8080/user) and sign out from there, or by navigating directly to the logout page (localhost:8080/user/logout).

Now, once logged out, navigate to the Album page from the home page. You can still view the page which is something that we do not want. We would rather allow access to the album page, and the add/edit/delete pages, to a logged in user. This is the next use case.

Restricting access to the album views to a logged in user

LmcUser provides a Controller plugin to check the status of the user. The $this->lmcUserAuthentication() plugin allows you to take an action based on user status and in our case, to redirect if the user is not logged in. Where should we redirect the user? A good pattern would be to redirect the user to the login page which in turn will redirect to the Album page.

Add the following to the Album Controller index action:

    public function indexAction()
    {
        // Check if we have a user identity
        
        if (!$this->lmcUserAuthentication()->hasIdentity()) {
        
            // If not, then redirect to the 'login' route
            return $this->redirect()->toRoute('lmcuser/login');
        }

        return new ViewModel([
            'albums' => $this->table->fetchAll(),
        ]);
    }

Let’s break this down.

First step in the controller is to check if we have a user identity.

$this->lmcUserAuthentication()->hasIdentity() returns true if the user is logged in, false otherwise.

If false, then we cannot continue executing the controller and we want to redirect to the login page.
return $this->redirect()->toRoute('lmcuser/login'); will redirect to the login page.

Note: the Laminas MVC Controller redirect() plugin is documented here.

Go ahead and try it. Log out of the application and try navigating to the Album page. You will be redirected to the login page. Log in and you will be redirected back to the Album page.

Let’s do this with the other actions in the AlbumController:

    public function addAction()
    {
        if (!$this->lmcUserAuthentication()->hasIdentity()) {
            return $this->redirect()->toRoute(UserController::ROUTE_LOGIN);
        }
        
        $form = new AlbumForm();
        $form->get('submit')->setValue('Add');
        /* 
          ...
        */
    }

    public function editAction()
    {
        if (!$this->lmcUserAuthentication()->hasIdentity()) {
            return $this->redirect()->toRoute(UserController::ROUTE_LOGIN);
        }

        $id = (int) $this->params()->fromRoute('id', 0);
        /* 
          ...
        */
    }

    public function deleteAction()
    {
        if (!$this->lmcUserAuthentication()->hasIdentity()) {
            return $this->redirect()->toRoute(UserController::ROUTE_LOGIN);
        }
        /* 
          ...
        */
    }

A few minor changes from the code that we inserted in the indexAction().

  • We are using UserController::ROUTE_LOGIN instead of /lmcuser/login in the $this->redirect()plugin.

But why do we need to bother checking if the user is logged in or not for the add, edit and delete actions since we need reach the index action first? This is because, the user can navigate directly to the add, edit, delete pages.

Try it. Log out of the application, then navigate directly to localhost:8080/album/add. You should be redirected to the login page. Try with the edit and delete actions as well.

Note: There is a better approach to restricting access to pages to logged in users than checking the user status in each action in each controller. You can use Role Based Access Control (RBAC). The LmcMvcRbac package can provide the RBAC functionality such that user status is checked before the action controller gets dispatched. This will be the topic of a separate tutorial.

Using the user identity in controllers and views

In the above steps, we have seen how we can use a controller plugin to check if a user is logged in. The $this->lmcUserAuthentication() plugin has the method getIdentity() that returns an object of the LmcUser\User\Entity class if a user is logged in or null if no user is logged in.

Note: The getIdentity() method will return an object of the class configured by the user_entity_class parameter in the lmcuser.global.php configuration file. By defautl, the user entity class is LmcUser\User\Entity.

Now let’s assume that the Album page should only be displaying the albums that belongs to the logged in user and we also want the Album page to show the user’s name in the page heading (ie. replace “My albums” by “John’s albums”.

Let’s do the easy part first which is to display the user’s name in the Album page.

LmcUser provides View Helpers to display user information. The helpers are:

  • $this->lmcUserIdentity(): this will return the user entity object of the logged in user which allows you to use any property of the object in the view. The user entity object has methods properties like getDisplayName() to get the Display Name and getEmail() to get the user’s email.
  • $this->lmcUserDisplayName(): this will return the display name. It is functionally equivalent to $this->lmcUserIdentity()->getDisplayName().

Let’s go ahead and modify the Album index view by adding the following lines to the module/Album/view/album/album/index.phtml view:

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

$displayName = $this->lmcUserDisplayName();
$title = $displayName() . '\'s albums';
$this->headTitle($title);
?>
<h1><?= $this->escapeHtml($title) ?></h1>
<p>
    <a href="<?= $this->url('album/add') ?>">Add new album</a>
</p>

<table class="table">
    <tr>
        <th>Title</th>
        <th>Artist</th>
        <th>&nbsp;</th>
    </tr>
    <?php foreach ($albums as $album) : ?>
        <tr>
            <td><?= $this->escapeHtml($album->title) ?></td>
            <td><?= $this->escapeHtml($album->artist) ?></td>
            <td>
                <a href="<?= $this->url('album/edit', ['id' => $album->id]) ?>">Edit</a>
                <a href="<?= $this->url('album/delete', ['id' => $album->id]) ?>">Delete</a>
            </td>
        </tr>
    <?php endforeach; ?>
</table>

The Album should now look like this:

Let’s get a little more sophisticated and add a “mailto” link to the user’ displayname and we will use the $this->LmcUserIdentity() plugin for that. The user identity object returned by the plugin implements the LmcUser\Entity\UserInterface interface which provides a few methods to retrieve identity details. The getEmail() method returns the user’s email.

Let’s modify the view again:

<?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 albums</h1>
<p>
    <a href="<?= $this->url('album/add') ?>">Add new album</a>
</p>

<table class="table">
    <tr>
        <th>Title</th>
        <th>Artist</th>
        <th>&nbsp;</th>
    </tr>
    <?php foreach ($albums as $album) : ?>
        <tr>
            <td><?= $this->escapeHtml($album->title) ?></td>
            <td><?= $this->escapeHtml($album->artist) ?></td>
            <td>
                <a href="<?= $this->url('album', ['action' => 'edit', 'id' => $album->id]) ?>">Edit</a>
                <a href="<?= $this->url('album', ['action' => 'delete', 'id' => $album->id]) ?>">Delete</a>
            </td>
        </tr>
    <?php endforeach; ?>
</table>

The page should now look like this:

The more complicated part is to only display the albums that belongs to the user. This will require us to first modify the Album application to add the user identity in the album record in order for us to select album based on the user.

Let’s modily the album table in the database to add the user email column and let’s update a few records with the user’s email. Create the following SQL file in data/add_user_to_album.sql:

ALTER TABLE album ADD COLUMN user_email varchar(100);
UPDATE album SET user_email='john@example.com' WHERE title='In My Dreams';
UPDATE album SET user_email='john@example.com' WHERE title='21';

Then execute the script:

#
# Depending on your configuration, you may have to use the sqlite3 command instead
#
$ cat data/add_user_to_album.sql | sqlite data/laminastutorial.db

OR

$ cat data/add_user_to_album.sql | sqlite3 data/laminastutorial.db

Now that the database has been updated, let’s make the following changes in the following files:

In module/Album/src/Model/Album.php, add the user_email property:

class Album
{
    public $id;
    public $artist;
    public $title;
    public $user_email;
    private $inputFilter;

    public function exchangeArray(object|array $array): void
    {
        $this->id     = ! empty($array['id']) ? $array['id'] : null;
        $this->artist = ! empty($array['artist']) ? $array['artist'] : null;
        $this->title  = ! empty($array['title']) ? $array['title'] : null;
        $this->user_email = !empty($array['user_email']) ? $array['user_email'] : null;
    }

    public function getArrayCopy(): array
    {
        return [
            'id'     => $this->id,
            'artist' => $this->artist,
            'title'  => $this->title,
            'user_email' => $this->user_email,
        ];
    }
   /* ... */
}

In module/Album/src/Model/AlbumTable.php, add a $where option when fetching albums:

    public function fetchAll($where = [])
    {
        return $this->tableGateway->select($where);
    }

    public function saveAlbum(Album $album)
    {
        $data = [
            'artist' => $album->artist,
            'title'  => $album->title,
            'user_email' => $album->user_email,
        ];

        $id = (int) $album->id;

        if ($id === 0) {
            $this->tableGateway->insert($data);
            return;
        }

        try {
            $this->getAlbum($id);
        } catch (RuntimeException $e) {
            throw new RuntimeException(sprintf(
                'Cannot update album with identifier %d; does not exist',
                $id
            ));
        }

        $this->tableGateway->update($data, ['id' => $id]);
    }

Now let’s modify the Album controller to use the user identity. The lmcUserAuthentication() plugin has a getIdentity() method that returns the user identity object of the logged in user or null otherwise. Let’s use the plugin to get the email of the logged in user that we will use as a where clause when fetching albums from the database.

/module/Album/src/Controller/AlbumController.php:

use LmcUser\Entity\UserInterface;
    
   /* ... */

    public function indexAction()
    {
        // Check if we have a user identity
        if (!$this->lmcUserAuthentication()->hasIdentity()) {
            // If not, then redirect to the 'login' route
            return $this->redirect()->toRoute('lmcuser/login');
        }
        /** @var UserInterface $user */
        $user = $this->lmcUserAuthentication()->getIdentity();

        return new ViewModel([
            'albums' => $this->table->fetchAll(['user_email' => $user->getEmail()]),
        ]);
    }

Now, the Album page should only show the albums belonging to John.

Now let’s modify the add action of the Album controller (/module/Album/src/Controller/AlbumController.php) to add the user’s email in the album model before updating the database:

    public function addAction()
    {
        if (!$this->lmcUserAuthentication()->hasIdentity()) {
            return $this->redirect()->toRoute(UserController::ROUTE_LOGIN);
        }

        $form = new AlbumForm();
        $form->get('submit')->setValue('Add');

        $request = $this->getRequest();

        if (! $request->isPost()) {
            return ['form' => $form];
        }

        $album = new Album();
        $form->setInputFilter($album->getInputFilter());
        $form->setData($request->getPost());

        if (! $form->isValid()) {
            return ['form' => $form];
        }

        /** @var UserInterface $user */
        // Get the user identity
        $user = $this->lmcUserAuthentication()->getIdentity();
        $data = $form->getData();
        // Add the user's email to the album data
        $data['user_email'] = $user->getEmail();

        $album->exchangeArray($data);
        $this->table->saveAlbum($album);
        return $this->redirect()->toRoute('album');
    }

For the edit action, we need to check that the selected album belongs to the user. This can happen if the user navigates directly to the edit page of an album that does not belong to him. If the album does belong to the user, let’s redirect to the Album page (ideally, we add an error message but let’s keep it simple):

    public function editAction()
    {
        if (!$this->lmcUserAuthentication()->hasIdentity()) {
            return $this->redirect()->toRoute(UserController::ROUTE_LOGIN);
        }

        $id = (int) $this->params()->fromRoute('id', 0);

        if (0 === $id) {
            return $this->redirect()->toRoute('album', ['action' => 'add']);
        }

        // Retrieve the album with the specified id. Doing so raises
        // an exception if the album is not found, which should result
        // in redirecting to the landing page.
        try {
            $album = $this->table->getAlbum($id);
        } catch (\Exception $e) {
            return $this->redirect()->toRoute('album', ['action' => 'index']);
        }

        /** @var UserInterface $user */
        $user = $this->lmcUserAuthentication()->getIdentity();

        // Check that the album belongs to the user and if not redirect to the album page
        // This can happen if the user navigated to the edit page for an album he does not own
        if ($album->user_email != $user->getEmail()) {
            return $this->redirect()->toRoute('album', ['action' => 'index']);
        }

        $form = new AlbumForm();
        $form->bind($album);
        $form->get('submit')->setAttribute('value', 'Edit');

        $request = $this->getRequest();
        $viewData = ['id' => $id, 'form' => $form];

        if (! $request->isPost()) {
            return $viewData;
        }

        $form->setInputFilter($album->getInputFilter());
        $form->setData($request->getPost());

        if (! $form->isValid()) {
            return $viewData;
        }

        try {
            $this->table->saveAlbum($album);
        } catch (\Exception $e) {
        }

        // Redirect to album list
        return $this->redirect()->toRoute('album', ['action' => 'index']);
    }

Let’s do a similar check for the delete action:

    public function deleteAction()
    {
        if (!$this->lmcUserAuthentication()->hasIdentity()) {
            return $this->redirect()->toRoute(UserController::ROUTE_LOGIN);
        }

        $id = (int) $this->params()->fromRoute('id', 0);
        if (!$id) {
            return $this->redirect()->toRoute('album');
        }

        // Get the album first
        try {
            $album = $this->table->getAlbum($id);
        } catch (\Exception $e) {
            return $this->redirect()->toRoute('album', ['action' => 'index']);
        }

        /** @var UserInterface $user */
        $user = $this->lmcUserAuthentication()->getIdentity();

        // Does the album belong to the user?
        if ($album->user_email != $user->getEmail()) {
            return $this->redirect()->toRoute('album', ['action' => 'index']);
        }

        $request = $this->getRequest();
        if ($request->isPost()) {
            $del = $request->getPost('del', 'No');

            if ($del == 'Yes') {
                $id = (int) $request->getPost('id');
                $this->table->deleteAlbum($id);
            }

            // Redirect to list of albums
            return $this->redirect()->toRoute('album');
        }

        return [
            'id'    => $id,
            'album' => $this->table->getAlbum($id),
        ];
    }

So let’s try all of this:

1) Add an album by clicking the ‘Add new album’ link in the Album page and enter “Revolver” as the abum title and “The Rolling Stones” as the artist. You should now see the new album in the Album page. But wait… Revolver is not a Rolling Stones album… The Beatles made this great album!

2) Click on the ‘Edit’ link of the Revolver album and update the Artist with “The Beatles” and click Edit. The album info should now be updated in the Album page. But you know what? Revolver is not part of our album collection, so let’s delete it.

3) Click on the ‘Delete’ link of the Revolver album and confirm its deletion.

Those actions are the trivial actions that normally works. We added code to prevent other users to edit and delete our albums. So let’s try to do this on the Born To Die album from Lana Del Rey. The id of this album should be 4 but let’s make sure using SQLite:

$ sqlite3
sqlite> open /data/laminastutorial.db
sqlite> SELECT * FROM album WHERE title="Born To Die";
4|Lana Del Rey|Born To Die|
sqlite> .quit

Try to navigate to localhost:8080/album/edit/4, you be redirected to the Album page. Try the same with localhost:8080/album/delete/4, you should also be redirected to the Album page.

We are almost done. We are missing some items in the navigation bar to make it easier to login and logout instead of having to navigate to the login page. This will let us explore another small LmcUser View Helper.

Updating the navigation bar based on user status

So our use case here is the following:

  • If the user is not logged in, we should have a login link in the navigation bar that takes the user to the login page
  • If the user is logged in, we should have a logout link in the navigation bar that logs out the user. At the same time, why don’t we also show the user’s name in the navigation bar if the user is logged in and provide a link to the user page.

As with many applications, let’s add the login/logout actions and user info to the right side of the navigation bar. We need to check if the user is logged in order to display the right navigation item.

We will make changes to the application layout view. In the Laminas MVC tutorial, you saw that navigation items can be added to Application module configuration file. For sake of simplicity, we will not use the navigation helper but add the markup directly to the layout file.

Note: Remember that the Laminas MVC Skeleton used in the tutorial uses Bootstrap for layout and rendering. The added markup is therefore based on Bootstrap classes.

In /module/Application/view/application/layout/layout.phtml, modify the markup where the navigation bar is added.

                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <?= $this->navigation('navigation')
                            ->menu()
                            ->setMinDepth(0)
                            ->setMaxDepth(0)
                            ->setUlClass('nav navbar-nav me-auto');
                    ?>
                    <?php $user = $this->lmcUserIdentity(); ?>
                    <?php if (!$user): ?>
                        <ul class="nav navbar-nav">
                            <li class="nav-item dropdown">
                                <a data-toggle="dropdown" href="#">Log in</a>
                                <div class="dropdown-menu">
                                    <?= $this->lmcUserLoginWidget();?>
                                </div>
                            </li>
                        </ul>
                    <?php else: ?>
                        <ul class="nav navbar-nav">
                            <li class="nav-item dropdown">
                                <a data-toggle="dropdown" href="#">Hi <?= $this->lmcUserDisplayName();?>!</a>
                                <div class="dropdown-menu">
                                    <a class="dropdown-item" href="<?= $this->url('lmcuser');?>">Profile</a>
                                    <a class="dropdown-item" href="<?= $this->url('lmcuser/logout');?>">Log out</a>
                                </div>
                            </li>
                        </ul>
                    <?php endif;?>
                </div>

Let’s go through the added code.

First we get the user identify using the $this->lmcUserIdentity() helper.

If $user is false, then the user is not logged in. In this case, we want to have a Log in navigation item, but instead of navigating to the /user/login page, let’s toggle a dropdown that embeds a small login widget. The widget is provided by another LmcUser View Helper: $this->lmcUserLoginWidget().

If $user exists, then the user is logged in. In this case, we want to have navigation item that shows the user’s display name that will toggle a dropdown menu where we will have a Profile item that navigates to the user page (/user) and the Logout item.

Try the login and logout navigation items. When trying to log in, you will get the following widget page:

The rendering of the login widget in a navigation menu dropdown is not the greatest. This was to showcase the login widget. A better solution is to render the login form directly. $this->lmcUserLoginWidget(['render' => false])) will return a View Model instead of rendering it. We can then get the form itself which is contained in the loginForm variables of the View Model.

$this->lmcUserLoginWidget(['render' => false])->getVariable('loginForm');

We just need then to add the markup to render the form. However, we have to make sure that we render the form elements that the User Controller authentication expects to see otherwise the login process will fail. By default, the login form uses a hidden CSRF form element (security) that must absolutely be rendered.

Modify the layout as follows to render a simpler form that takes less real estate:

                    <?php if (!$user): ?>
                        <ul class="nav navbar-nav">
                            <li class="dropdown">
                                <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">Login in</a>
                                <div class="dropdown-menu">
                                    <?php /** @var \LmcUser\Form\Login $loginForm */?>
                                    <?php $loginForm = $this->lmcUserLoginWidget(['render' => false])->getVariable('loginForm');?>
                                    <?php $loginForm->setAttribute('action',$this->url('lmcuser/login'))?>
                                    <?= $this->form()->openTag($loginForm);?>
                                    <div class="form-group mb-1">
                                        <?php $loginForm->get('identity')->setAttributes([
                                                'class' =>'form-control',
                                                'placeholder' => 'Email',
                                            ]);?>
                                        <?= $this->formElement($loginForm->get('identity'));?>
                                    </div>
                                    <div class="form-group mb-1">
                                        <?php $loginForm->get('credential')->setAttributes([
                                                'class'=> 'form-control',
                                                'placeholder' => 'Password',
                                            ]);?>
                                        <?= $this->formElement($loginForm->get('credential'));?>
                                    </div>
                                    <?= $this->formElement($loginForm->get('security'));?>
                                    <?php $loginForm->get('submit')->setAttribute('class', 'btn btn-primary');?>
                                    <?= $this->formElement($loginForm->get('submit'));?>
                                    <?= $this->form()->closeTag();?>
                                </div>
                            </li>
                        </ul>

Now the login dropdown should look like this:

Wrapping up and next steps

That’s it! You have added user management to the Album application with a minimal level of access control.

This tutorial was a basic example of using LmcUser to manage user access. LmcUser can be extended and customized to suit your application needs. Some of the customizations that developers bring to their applications are overrding the login and register views to match their overall UI, extend the simple User Identity class, add listeners to perform additional processing on login/logout and registration, etc.

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

Leave a Comment

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