Table Relationships

When working with CakePHP it is important to remember "Convention over Configuration." If you start from the bottom, or back end, the database, and work your way forward correctly, CakePHP will do much of legwork for you, saving you tons of redundant and repetitive work.

If you don't, there are almost always ways to make things work the hard way. An example of this is the relationships between tables. If you name your database tables and fields following CakePHP convention, it becomes trivial to set up relationships between them.

Let's follow with our example. Our application contains "Users" which are stored in the users table. Each User in our application can have many phone numbers: a home phone, a work phone, a cell phone, etc., And while we could have had multiple phone number fields in the users table, this is not good database normalization, nor is it efficient storage of data. So we created a separate phone_numbers table to store the phone numbers (see Migrations).

Belongs To and Has Many

Another convention used to save time and energy is to identify relationships with a field that ties the tables together. Every user can have many phone numbers, and every phone number belongs to a user. This is typically referred to as a one-to-many relationship. To identify which user each phone number belongs to, the phone number records will contain a foreign key holding the unique identifier, or id, of the user. This field should be named after the foreign table and followed by "_id" indicating the id of the other table. In the case of users, this field would be user_id.

This isn't required, it just makes your job easier. You can use a different field name, you just have to identify that field explicitly in your Model.

Set the Table Association

Because we followed CakePHP's convention when creating our tables, we could now easily use bin/cake bake to create our models, controllers, and views from the console and CakePHP would automatically set up everything, including the table associations for us. But we won't learn anything from that so let's do it manually.

If you've been following along, you should already have your User and PhoneNumber models, controllers, and views created.

In our Phone Number Table Class /src/Model/Tables/PhoneNumbersTable.php we want to identify the "belongsTo" relationship with the users table.

  public function initialize(array $config): void
  {
    ...
    // Identify the relationship to the Users table
    $this->belongsTo('Users', [
      'foreignKey' => 'user_id', // default value
      'joinType' => 'INNER', // default value
    ]);
    // Could also be expressed as $this->belongsTo('Users'); if you followed convention
  }

Now, if you didn't, or can't, set your table name to phone_numbers, the primary key for the table to id, or the foreign key in the phone_numbers table to the assumed default, user_id, you can explicitly declare them.

  public function initialize(array $config): void
  {
    ...
    $this->setTable('different_table_name');
    $this->setPrimaryKey('not_id');
    $this->belongsTo('Users', [
      'foreignKey' => 'not_user_id'
    ]);
  }

Now, in the Users Table Class, /src/Model/Table/UsersTable.php, we define the "hasMany" relationship.

  public function initialize(array $config): void
  {
    ...
    $this->setTable('users');
    $this->hasMany('PhoneNumbers');
  }

Set the Controller Relationship

Now when getting our data for display, we need to ensure our SQL query contains the associated data of the phone numbers table. In /src/Controller/UsersController.php update the view function:

  public function view($slug = null)
  {
    $query = $this->Users->findBySlug($slug)
      ->contain(['PhoneNumbers']); // Contains the related data
    $user = $query->first();
    $this->set(compact('user'));
  }

It would be similar if the User request was still get($id) instead of findBySlug($slug):

  public function view($id = null)
  {
    $user = $this->Users->get($id, [
      'contain' => ['PhoneNumbers'],
    ]);
    $this->set(compact('user'));
  }

Now when the user record is loaded, it will also load any associated phone numbers with each user. You can do the same in the Phone Numbers Controller if you want to display the user data (e.g. full_name) in the phone number view.

  public function index()
  {
  	$query = $this->PhoneNumbers->find()
  	  ->contain(['Users']);
    $phoneNumbers = $this->paginate($query);
    $this->set(compact('phoneNumbers'));
  }

If you'll only be viewing the phone numbers in the user's display, you may not need or want to even bother with the index() and view() actions for the Phone Numbers Controller. But they can be handy for troubleshooting issues.

Display the Information in the View

Now that you are loading the phone numbers in the Users Controller, you can display the loaded information in the view.

Update your Users view /templates/Users/view.php to contain the phone numbers.

...
</table>
<div class="related">
  <?php echo $this->Html->link(__('New Phone Number'), ['controller' => 'phone_numbers', 'action' => 'add'], ['class' => 'button float-right']) ?>
  <h4><?php echo __('Phone Numbers') ?></h4>
  <?php if (!empty($user->phone_numbers)) : ?>
  <div class="table-responsive">
    <table>
      <tr>
        <th><?php echo __('Type') ?></th>
        <th><?php echo __('Number') ?></th>
        <th class="actions"><?php echo __('Actions') ?></th>
      </tr>
      <?php foreach ($user->phone_numbers as $phone_number) : ?>
      <tr>
        <td><?php echo h($phone_number->type) ?></td>
        <td><?php echo h($phone_number->phone_number) ?></td>
        <td class="actions">
          <?php echo $this->Form->postLink(__('Delete'), ['controller' => 'PhoneNumbers', 'action' => 'delete', $phone_number->id], ['confirm' => __('Are you sure you want to delete # {0}?', $phone_number->phone_number)]) ?>
        </td>
      </tr>
      <?php endforeach; ?>
    </table>
  </div>
  <?php endif; ?>
</div>
...

Now maybe the most complicated part is the add() and delete() controller functions and view, so we'll cover those next separately.

Display the User’s Name in the Phone Numbers View

Practice what you've learned by adding the Users data to the Phone Numbers index method and display the full name instead of the user's id in the index View.