Controller and Views

At this point, if you browse to your application, for example http://localhost/tutorial/users/, you'll see an error indicating that your UsersController cannot be found, and the debug output gives you a suggestion of a bare-bones framework for the controller. You can copy and paste that code or type it by hand, but that's exactly where we'll start for your Users Controller to retrieve data from the Model.

While the Model is used for database interactions, the Controller is used for data requests and processing. While some developers prefer "light controllers" and others "light models," I prefer to think of the Controller as the workhorse between data storage/retrieval (the Model) and data display (the View), as it is easier for me to wrap my head around that level of separation. As always, use what you think is the best tool for the job and try to do what makes sense to you, as you will typically be the person trying to debug your code in a year or three.

A simple controller contains the primary CRUD (Create/Read/Update/Delete) functions and requests the Model to execute those tasks.

Build a Basic Controller

For our users table the controller will be located in /src/Controller/UsersController.php and can look like this:

<?php
declare(strict_types=1);

namespace App\Controller;

class UsersController extends AppController
{
  private $roles = ['User' => 'User', 'Admin' => 'Admin', 'Disabled' => 'Disabled'];
  protected array $paginate = [
    'limit' => 30,
    'order' => ['first_name' => 'ASC', 'last_name' => 'ASC']
  ];
  
  /**
   * Display a list of records
   */
  public function index()
  {
    $query = $this->Users->find();
    $users = $this->paginate($query);
    $this->set(compact('users'));
  }
  
  /**
   * View a single record
   */    
  public function view($slug = null)
  {
    $query = $this->Users->findBySlug($slug);
    $user = $query->first();
    $this->set(compact('user'));
  }
  
  /**
   * Add a new record
   */
  public function add()
  {
    $user = $this->Users->newEmptyEntity();
    if ($this->request->is('post')) {
      $user = $this->Users->patchEntity($user, $this->request->getData());
      if ($this->Users->save($user)) {
        $this->Flash->success(__('The user has been saved.'));
        return $this->redirect(['action' => 'index']);
      } else {
        $this->Flash->error(__('The user could not be saved. Please, try again.'));
      }
    }
    $roles = $this->roles;
    $this->set(compact('user', 'roles'));
  }
  
  /**
   * Edit an existing record
   */  
  public function edit($id = null)
  {
    $user = $this->Users->get($id);
    if ($this->request->is(['patch', 'post', 'put'])) {
      if (empty($this->request->getData('password'))) {
        // if password is empty, unset password
        $data = $this->request->getData();
        unset($data['password']);
        $this->request = $this->request->withParsedBody($data);            
      }
      $user = $this->Users->patchEntity($user, $this->request->getData());
      if ($this->Users->save($user)) {
        $this->Flash->success(__('The user has been saved.'));
        return $this->redirect(['action' => 'index']);
      }
      $this->Flash->error(__('The user could not be saved. Please, try again.'));
    }
    $roles = $this->roles;
    $this->set(compact('user', 'roles'));
  }
  
  /**
   * Delete a record
   */  
  public function delete($id = null)
  {
    $this->request->allowMethod(['post', 'delete']);
    $user = $this->Users->get($id);
    if ($this->Users->delete($user)) {
      $this->Flash->success(__('The user has been deleted.'));
    } else {
      $this->Flash->error(__('The user could not be deleted. Please, try again.'));
    }
    return $this->redirect(['action' => 'index']);
  }
}

The roles array is the list of available roles, the values to be displayed and the keys to be stored in the records. The paginate variable overrides the default limit of 20, which I find too small, and sets the sort order. You cannot use virtual fields to sort paginated lists, but you can define your initial sort order to be the same as the virtual field.

You may have noticed in the edit() function we unset() the password field. If the user has not set a new password, we don't want to save a hashed null string to the database, overriding the current password. While this isn't explicitly necessary because our _setPassword() function checks for strlen() and the Model validation verifies that if there is content it must follow the rules we set, it isn't a bad idea to ensure an empty password isn't ever sent for data processing. This also demonstrates an example of manually modifying your data before sending it to the Model for database storage.

Create the Index View

Now if you browse to your application at http://localhost/tutorial/users you'll see the previous error is gone and  you now have a new error indicating that you are missing the view template for UsersController::index(). The Users Controller has retrieved the records from the database and is expecting a View to display that data, which is also set up to be paginated using CakePHP's default pagination.

Create the index View file at /templates/Users/index.php:

<div class="users index content">
  <?php echo $this->Html->link(__('New User'), ['action' => 'add'], ['class' => 'button float-right']) ?>
  <h3><?php echo __('Users') ?></h3>
  <div class="table-responsive">
    <table>
      <thead>
        <tr>
          <th><?php echo $this->Paginator->sort('first_name', 'Name') ?></th>
          <th><?php echo $this->Paginator->sort('username') ?></th>
          <th><?php echo $this->Paginator->sort('email') ?></th>
          <th><?php echo $this->Paginator->sort('role') ?></th>
          <th class="actions"><?php echo __('Actions') ?></th>
        </tr>
      </thead>
      <tbody>
        <?php foreach ($users as $user): ?>
        <tr>
          <td><?php echo $this->Html->link($user->full_name, ['action' => 'view', $user->slug]) ?></td>
          <td><?php echo h($user->username) ?></td>
          <td><?php echo $this->Text->autoLinkEmails($user->email) ?></td>
          <td><?php echo h($user->role) ?></td>
          <td class="actions">
            <?php echo $this->Html->link(__('View'), ['action' => 'view', $user->slug]) ?>
            <?php echo $this->Html->link(__('Edit'), ['action' => 'edit', $user->id]) ?>
          </td>
        </tr>
        <?php endforeach; ?>
      </tbody>
    </table>
  </div>
  <div class="paginator">
    <ul class="pagination">
      <?php echo $this->Paginator->first('<< ' . __('first')) ?>
      <?php echo $this->Paginator->prev('< ' . __('previous')) ?>
      <?php echo $this->Paginator->numbers() ?>
      <?php echo $this->Paginator->next(__('next') . ' >') ?>
      <?php echo $this->Paginator->last(__('last') . ' >>') ?>
    </ul>
    <p>Page <?php echo $this->Paginator->counter() ?></p>
  </div>
</div>

Observe the first field listed in our table heading is first_name and not full_name. As I said previously, we cannot sort in the paginator by a virtual field. Also we've overridden the default label with "Name." Then, within the table, we display the virtual field full_name.

The h() function wrapping our variables is a convenience method for htmlspecialcharacters().

If you browse to the application now, you should see our list of users which should be seeded with any users you added.

Create the Add View

If you click the "New User" button, you'll see en error indicating that we're missing our view template for UsersController::add() so now we'll create the Add User view in /templates/Users/add.php:

<?php $this->assign('title', 'Add User'); // Set the title for the page ?>
<div class="row">
  <aside class="column">
    <div class="side-nav">
      <h4 class="heading"><?php echo __('Actions') ?></h4>
      <?php echo $this->Html->link(__('List Users'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
    </div>
  </aside>
  <div class="column column-80">
    <div class="users form content">
      <?php echo $this->Form->create($user) ?>
      <fieldset>
        <legend><?php echo __('Add User') ?></legend>
          <?php
            echo $this->Form->control('username', ['autofocus' => true]);
            echo $this->Form->control('first_name');
            echo $this->Form->control('last_name');
            echo $this->Form->control('password'); ?>
          <p class="helper">Passwords must be at least 8 characters and contain at least 1 number, 1 uppercase, 1 lowercase, and 1 special character</p>
          <?php
            echo $this->Form->control('confirm_password', ['type' => 'password']);
            echo $this->Form->control('email');
            echo $this->Form->control('role');
          ?>
      </fieldset>
      <?php echo $this->Form->button(__('Submit')) ?>
      <?php echo $this->Html->link(__('Cancel'), ['action' => 'index'], ['class' => 'button']); ?>
      <?php echo $this->Form->end() ?>
    </div>
  </div>
</div>

The CakePHP Form helper makes it easy to create a lot of the form fields for you automatically based on the database field type. In some cases, such as the role field, it realizes there is an array named $roles being passed to the View from the Controller (See the add() function above), and it automatically creates a select list form field utilizing that array.

Since confirm_password is not a field in the database, the form builder defaults to a text field, so utilizing the $options array we can tell CakePHP to render it as a password type field.

If you browsed to your user index page, you may have noticed that the page title is the CakePHP boilerplate plus "Users" and this will be the title for all pages under the /users/ path. We added some code at the top of this view to change the title for this page to a more explicit "Add User", but it's still prefixed by the long CakePHP boilerplate and hard to see our titles.

Update the default layout template

The layout templates are the primary wrapper HTML that our Views are displayed within, and are located in /templates/layout, depending on the output. For error pages, the error.php file is used. For our standard HTML pages, default.php is used. Let's change the page title so it's easier to see our page titles.

Edit /templates/default.php and swap the $cakeDescription with the fetched title variable.

  <title>
    <?= $this->fetch('title') ?>:
    <?= $cakeDescription ?>
  </title>

Now we can go to our application in the browser and add a user at http://localhost/users/add and observe our title change. Having at least one user is important before Authentication, so be sure to complete this step if you didn't seed any users.

Phone Numbers Controller

Now that you know how to create a Controller and views, practice creating the Phone Numbers Controller in /src/Controller/PhoneNumbersController.php and the index, add, and edit Views. Since we haven't defined our relationship to Users yet, we can't add a phone number, but we'll get there in Table Relationships.