Database and Model

Now that your application is up and running, it is time to build your data store and interface with that data. We have two options here: We can create the database tables using Migrations or directly in the database, either through the database interface, a web interface, or an IDE interface. We'll cover creating the tables directly in the database here as it can be faster, however, Migrations have a lot of benefits, not the least of which is allowing you to commit your schema and any changes to source control.

Create Your Users Database Table

Most applications require some form of user authentication and identification. To begin, create your users database table. The fields are mostly self-explanatory, however, the passkey and timeout fields are part of a password reset functionality that I add to save myself time later on, as users constantly forget their passwords.

CREATE TABLE `users` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL,
  `password` varchar(61) COLLATE utf8mb4_unicode_ci NOT NULL,
  `email` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
  `first_name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
  `last_name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
  `slug` varchar(65) COLLATE utf8mb4_unicode_ci NOT NULL,
  `role` varchar(12) COLLATE utf8mb4_unicode_ci NOT NULL,
  `passkey` varchar(29) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `timeout` timestamp NULL DEFAULT NULL,
  `modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`),
  UNIQUE KEY `slug` (`slug`),
  UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB;

Once your table is created, you could use the built-in cake command line scripting to "bake" your Model, Views, and Controller. This will save you a lot of time, however, you won't learn much if all I covered was bin/cake bake all users. Just know that this feature is available and is covered thoroughly in the CakePHP documentation: https://book.cakephp.org/5/en/index.html and the code we'll  be creating is based on the basic templates so it is easier to follow the baked code if you ever use bake.

Create Your Table Class

The CakePHP Model is how the application interacts with the database table. There should be one model per table, made up of the Table and Entity objects. For our users table you will therefore need to create two files. The first one is the Users Table class named /src/Model/Table/UsersTable.php and the User Entity class named /src/Model/Entity/User.php. If you do not have the model files for a table, CakePHP will dynamically create a model for you with basic functionality.

Table objects are used to interface with collections of objects (i.e. tables). While much of the following class is self-explanatory, I've added some comments to clarify a few of the lines below.

/src/Model/Table/UsersTable.php

<?php
declare(strict_types = 1);

namespace App\Model\Table;

use Cake\ORM\Query\SelectQuery;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class UsersTable extends Table
{
  public function initialize(array $config): void
  {
    parent::initialize($config);
    $this->setTable('users'); // Name of the table in the database, if absent convention assumes lowercase version of file prefix
    $this->setDisplayField('full_name'); // field or virtual field used for default display in associated models, if absent 'id' is assumed
    $this->setPrimaryKey('id'); // Primary key field(s) in table, if absent convention assumes 'id' field
    $this->addBehavior('Timestamp'); // Allows your model to automatically timestamp records on creation/modification with the created/modified fields in your table
  }
}

Automatically Create the Slug

If you've never used a "slug" before, slugs are url-safe values that uniquely identify a record and typically look better than sequential numbers.  Instead of urls ending in the default /users/view/356 they can be /users/view/betty-white which not only looks better and is easier to remember, but is also beneficial for SEO. This is a basic example that we will expand on later.

In the Users Table class we just created add the following imports:

use Cake\Event\EventInterface;
use Cake\Utility\Text;

And add the following function:

public function beforeSave(EventInterface $event, $entity, $options)
{
  $entity->slug = $this->getSlug($entity->full_name);
}

/**
 * Return slugged version of passed in string
 */
protected function getSlug(string $name): string
{
  $sluggedTitle = Text::slug($name);
  // lowercase the slug for consistency
  return mb_strtolower($sluggedTitle);
}

The beforeSave() function is called before an entity is saved and the purpose of our function is to set the entity's slug field. There is an issue with this function as it doesn't catch duplicates, however, we'll correct that in Unique Slugs.

Create the Entity Class

The Entity provides interface to individual objects (i.e. rows or records). You should NEVER store passwords as plain text. The authentication plugin we installed previously provides a default password hasher which makes it very easy to encrypt passwords for storage and comparison. In addition to mutators (functions that mutate values before saving), you can define accessor functions so you can create virtual fields, or for quick variable checks.

/src/Model/Entity/User.php

<?php
declare(strict_types = 1);

namespace App\Model\Entity;

use Cake\ORM\Entity;
use Authentication\PasswordHasher\DefaultPasswordHasher;

class User extends Entity
{
  // Fields that can be mass assigned using newEntity() or patchEntity();
  // If you change your table, remember to update your entity class otherwise
  // you won't be able to store data in the new field
  protected array $_accessible = [
    'username' => true,
    'password' => true,
    'email' => true,
    'first_name' => true,
    'last_name' => true,
    'slug' => true,
    'role' => true,
    'modified' => true,
    'created' => true,
    'passkey' => true,
    'timeout' => true,
  ];
  
  // Fields excluded from JSON versions of the entity
  protected array $_hidden = [
    'passkey',
    'password',
  ];
  
  // Mutator to hash the user's password before saving and before validation
  protected function _setPassword(string $password): ?string
  {
    if (strlen($password) > 0) {
      $hasher = new DefaultPasswordHasher();
      return $hasher->hash($password);
    }
  }
  
  // Accessor for virtual field full_name
  protected function _getFullName()
  {
    return $this->first_name . ' ' . $this->last_name;
  }
  
  // Accessor for quick admin role check
  protected function _getIsAdmin()
  {
    return $this->role === 'Admin';
  }
  
  // Access for quick disabled account check
  protected function _getIsDisabled()
  {
    return $this->role === 'Disabled';
  }
}