The CakeDC organization behind CakePHP has created the Users plugin, that has all the features a usual web app has - user login, registration and so on. However, the plugin by default assumes that the user logins with a username and password. For simplicity, some websites might prefer that the user logins with their email as their username.

The documentation for the Users plugin shows how the user can be made to login with their email, but the user still has to create a username during registration. Fortunately, by extending the plugin, it’s quite easy to remove all mentions of username and make everything work with just an email address as the login - the username field can even be removed from the database.

This guide is written for CakePHP 3.5.x and the plugin version 6.0.0.

Login with email and password

To begin, follow the documentation referenced above and change the authentication to use the email field and modify the login template to ask for email instead of username. (To overwrite the templates, copy them from vendor/cakedc/users/src/Template/Users to src/Template/Plugin/CakeDC/Users/Users/).

// config/bootstrap.php, below the plugin loading
Configure::write('Auth.authenticate.Form.fields.username', 'email');
// src/Template/Plugin/CakeDC/Users/Users/login.ctp, inside the form
    <?= $this->Form->control('email', ['required' => true]) ?>
    <?= $this->Form->control('password', ['required' => true]) ?>

In addition, you have to go through all other templates and edit them so that there are no references to username, either remove or replace with email as appropriate (fe. remove in add.ctp and register.ctp, use email instead in profile.ctp).

Also make sure that in config/users.php or wherever you configure the plugin that the Users.Email.required is set to true.

Database migrations

If you used the provided migrations to create the Users table, note that the username was set to NOT NULL so you need to either create a migration that either completely removes the column or allows null values for it. By default, the database also allows null values for email field, so you might want to do the exact opposite for it. Or you can just manually edit the table to do both. A sample migration is given below.

// config/Migrations/20XXXXXXXXXX_RemoveUsernameFromUsers.php
<?php
use Migrations\AbstractMigration;

class ExtendCakeDcUsers extends AbstractMigration
{
    public function change()
    {
        $table = $this->table('users');
            ->changeColumn('username', 'string', [
                'default' => null,
                'null' => true
                ])
            ->changeColumn('email', 'string', [
                'null' => false
                ])
            ->update();
    }
}

As mentioned you can just removeColumn('username') but this would be an irreversible migration so you would need to define up() and down() migrations for that.

Extend the Users model

Your Users model will very likely have relationships to other tables, so in practice you will have to extend the plugin’s Users model for your own purposes. The plugin’s own documentation about extending uses src/Model/Table/MyUsersTable.php and src/Model/Entity/MyUser.php but you can use src/Model/Table/UsersTable.php and src/Model/Entity/User.php by using the as keyword when importing the plugin’s classes.

// src/Model/Table/UsersTable.php
<?php
namespace App\Model\Table;

use Cake\Validation\Validator;

use CakeDC\Users\Model\Table\UsersTable as CakeUsersTable;

class UsersTable extends CakeUsersTable
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setDisplayField('email');

        $this->removeBehavior('Password'); // Actually removes CakeDC/Users.Password behavior
        $this->addBehavior('Password'); // Adds src/Model/Behavior/PasswordBehavior.php (see below for details)

        // your database relationships, fe. $this->hasMany('DogPictures', [...
    }

    /**
     * Removes username validation rule from parent
     *
     * @param \Cake\Validation\Validator $validator Validator
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator)
    {
        parent::validationDefault($validator);

        $validator->remove('username');

        return $validator;
    }

There’s a lot going on in our extended UserTable. We need to override some of the plugin’s methods that rely on username field existing. First, we setDisplayField('email') and override validationDefault() to remove validations relating to username field. In addition, we have to extend the PasswordBehavior so we have to remove the plugin’s own that searches users by username and add our own that does not.

// src/Model/Entity/User.php
use CakeDC\Users\Model\Entity\User as CakeUser;

class User extends CakeUser
{
    protected function _getUsername()
    {
        return $this->email;
    }

    protected function _setUsername($value)
    {
        return $this->email;
    }
}

By overriding the getter and setter for username in the model, all operations for that field will be effectively no-ops that will return (but not modify!) the email address instead. Because the plugin separates first and last name, you could also define _getFullName() as explained in CakePHP’s documentation on virtual properties.

By default, the plugin’s PasswordBehavior allows the user to recover their account if they remember their username or email. However, in our version, we want to only search for email. Fortunately, it is very easy to extend the behavior so that this happens.

// src/Model/Behavior/PasswordBehavior.php
<?php
namespace App\Model\Behavior;

use CakeDC\Users\Model\Behavior\PasswordBehavior as CakePasswordBehavior;

class PasswordBehavior extends CakePasswordBehavior
{

    /**
     * Override base PasswordBehavior not to look at Username
     *
     * @param string $reference reference can be only email
     * @return mixed user entity if found
     */
    protected function _getUser($reference)
    {
        return $this->_table->findByEmail($reference)->first();
    }
}

The original method will use findByUsernameOrEmail which is not safe in our version (and will fail if you removed the username field from the database table). Make sure you updated the template to reflect this change. (This will affect localizations.)

// src/Template/Plugin/CakeDC/Users/Users/request_reset_password.ctp
    <legend><?= __d('CakeDC/Users', 'Please enter your email to reset your password') ?></legend>

And finally, remember to change the plugin’s configuration in config/users.php to use your subclassed model instead.

// config/users.php
        'table' => 'Users', // defaults to CakeDC.Users/Users

Done

Now you should have the plugin overridden safely so that any call to the username property in the model will return email instead, and all other places were the plugin wants to validate the fields contents or use it in a feature are subclassed so that it doesn’t. In addition, these changes should be somewhat future-proof unless the internals of the plugin change a lot in a future upgrade.