Neighbors

In CakePHP 2 there was a handy feature called "neighbors" where you could simply request the neighboring records (previous and next) for easy linking. This is not present since CakePHP 3, possibly due to the limited use of such a feature, however, with custom finders you can replicate the functionality yourself.

Create the Finder

In your model /src/Model/Table/UsersTable.php add the following custom finder:

use Cake\Database\Query;
...
  /**
   * Find neighbors method
   */
  public function findNeighbors(Query $query, array $options): Query
  {
    $id = $options['id'];
    $previous = $this->find()
      ->select(['id'])
      ->where(['id <' => $id])
      ->orderBy(['id' => 'DESC'])
      ->limit(1);
    $next = $this->find()
      ->select(['id'])
      ->where(['id >' => $id])
      ->orderBy(['id' => 'ASC'])
      ->limit(1);
    return $this->find()->select(['prev' => $previous, 'next' => $next]);
  }

Then you can simply call the function in your Controller.

  public function view($id = null)
  {
    ...
    $neighbors = $this->Users->find('neighbors', ['id' => $user->id])->first();
    $this->set(compact('user', 'neighbors'));
  }

Display the Neighbors

And lastly to utilize the neighbor ids in your View:

<div class="clearfix">
<?php
  if ($neighbors->prev) {
    echo $this->Html->link('Prev', ['action' => 'view', $neighbors->prev], ['class' => 'button']);
  }
  if ($neighbors->next) {
    echo $this->Html->link('Next', ['action' => 'view', $neighbors->next], ['class' => 'button']);
  }
?>
</div>

Problems

Now there is one last issue. If you utilized a slug instead of the id for your Users' view() function as we did in the previous tutorials, you're going to get an error when using the links. You can change your finder to select the slug field:

  public function view($slug = null)
  {
    $this->Authorization->skipAuthorization();
    if (is_numeric($slug)) {
      $query = $this->Users->findById($slug)
        ->contain(['PhoneNumbers', 'Documents']); // Contains the related data
    } else {
      $query = $this->Users->findBySlug($slug)
        ->contain(['PhoneNumbers']);
    }
    $user = $query->first();
    $neighbors = $this->Users->find('neighbors', ['id' => $user->id])->first();
    $this->set(compact('user', 'neighbors'));
  }

Or a change to the custom finder to select the slug, not the id:

  public function findNeighbors(Query $query, array $options): Query
  {
    ...
    $previous = $this->find()
      ->select(['slug'])
      ...
    $next = $this->find()
      ->select(['slug'])
    ...
  }

There are undoubtably more performant ways, such as using the lag() and lead() MySQL functions, but this method works, and is very straight-forward and pretty self-explanatory, which makes maintaining, reviewing, and updating easier.