Article

Introducing CRUD

Last time we worked on our Events application, we left off having just baked a basic CakePHP Model/Controller/View. We are going to expand on it in this post, but first I am going to introduce you to the FriendsOfCake Crud plugin. If you've done any major work in a MVC framework you would've found that probably 80% of the controller code you write is almost exactly the same across your controllers. The Crud plugin aims to reduce the amount of code you need to write by abstracting most of this into a common plugin that you can use over and over again.

First, we need to bring up our application again, so as before run vagrant up followed by vagrant ssh and then cd /var/www. As it's been a while since we last did this, let's quickly update our dependencies (Since at this stage CakePHP 3 is still under active development) by running

Check that you have the Bake plugin installed, Bake was recently pulled out of the CakePHP 3 core into a separate plugin. If you have it installed, then plugins/Bake will exist. If it does not exist then you need to run composer require --dev cakephp/bake.You also need to load the Bake plugin, open config/bootstrap_cli.php and add the following line to it Cake\Core\Plugin::load('Bake');​

While you are waiting for that to finish, open up src/Controller/EventsController.php so long.

<?php
namespace App\Controller;

use App\Controller\AppController;

/**
 * Events Controller
 *
 * @property App\Model\Table\EventsTable $Events
 */
class EventsController extends AppController {
/**
 * Index method
 *
 * @return void
 */
    public function index() {
        $this->set('events', $this->paginate($this->Events));
    }

/**
 * View method
 *
 * @param string $id
 * @return void
 * @throws \Cake\Network\Exception\NotFoundException
 */
    public function view($id = null) {
        $event = $this->Events->get($id, [
            'contain' => []
        ]);
        $this->set('event', $event);
    }

/**
 * Add method
 *
 * @return void
 */
    public function add() {
        $event = $this->Events->newEntity($this->request->data);
        if ($this->request->is('post')) {
            if ($this->Events->save($event)) {
                $this->Flash->success('The event has been saved.');
                return $this->redirect(['action' => 'index']);
            } else {
                $this->Flash->error('The event could not be saved. Please, try again.');
            }
        }
        $this->set(compact('event'));
    }

/**
 * Edit method
 *
 * @param string $id
 * @return void
 * @throws \Cake\Network\Exception\NotFoundException
 */
    public function edit($id = null) {
        $event = $this->Events->get($id, [
            'contain' => []
        ]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $event = $this->Events->patchEntity($event, $this->request->data);
            if ($this->Events->save($event)) {
                $this->Flash->success('The event has been saved.');
                return $this->redirect(['action' => 'index']);
            } else {
                $this->Flash->error('The event could not be saved. Please, try again.');
            }
        }
        $this->set(compact('event'));
    }

/**
 * Delete method
 *
 * @param string $id
 * @return void
 * @throws \Cake\Network\Exception\NotFoundException
 */
    public function delete($id = null) {
        $event = $this->Events->get($id);
        $this->request->allowMethod(['post', 'delete']);
        if ($this->Events->delete($event)) {
            $this->Flash->success('The event has been deleted.');
        } else {
            $this->Flash->error('The event could not be deleted. Please, try again.');
        }
        return $this->redirect(['action' => 'index']);
    }

}

Most of the code you see there will be repeated in other controllers, but for different table and entity names. This is what the Crud plugin aims to reduce. We'll start by installing Crud, run composer require friendsofcake/crud dev-cake3 on the vagrant box.

/var/www $ composer require friendsofcake/crud dev-cake3

Once that's done, we enable the plugin in our CakePHP bootstrap, and then we'll modify our controller file. Open config/bootstrap.php and simply add Plugin::load('Crud'); towards the bottom. Replace the contents of src/Controller/EventsController.php with:

<?php
namespace App\Controller;

use App\Controller\AppController;

/**
 * Events Controller
 *
 * @property App\Model\Table\EventsTable $Events
 */
class EventsController extends AppController {

    use \Crud\Controller\ControllerTrait;

    public $components = [
        'Crud.Crud' => [
            'actions' => [
                'Crud.Index',
                'Crud.View',
                'Crud.Add',
                'Crud.Edit',
                'Crud.Delete',
            ],
        ],
    ];
}

If you go look at http://events.dev/events it should look (and work) exactly the same as it did previously!

If you get an errorController not found error, open config/routes.php and change the $routes->fallbacks(); line to be $routes->fallbacks('InflectedRoute');

Adding a users table

One important thing that any event calendar needs, is the ability to invite people as guests. We are going to add a users table, and then link our Events up to Users so that we can invite users to events. We'll start by creating a migration for the users database table and a table to link events with users. We'll then bake a Users table and controller along with some basic views. Finally we'll modify our Events controller to load a list of users and modify the event add view to allow us to select multiple users to invite. In the next post, we'll go a step further and add authentication and basic authorization to our app.

We'll begin with our database migration. We'll create a basic users table, and a events_users which will be used to link events with users.

/var/www $ bin/cake migrations create AddUsersTable

This creates an empty file in config/Migrations. Open up the file, and replace the contents with:

<?php

use Phinx\Migration\AbstractMigration;

class AddUsersTable extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * http://docs.phinx.org/en/latest/migrations.html#the-change-method
     *
     * Uncomment this method if you would like to use it.
     */
    public function change()
    {
        $usersTable = $this->table('users');
        $usersTable
            ->addColumn('name', 'string')
            ->addColumn('email', 'string')
            ->addColumn('created', 'datetime')
            ->addColumn('modified', 'datetime')
            ->create();

        $eventsUsersTable = $this->table('events_users');
        $eventsUsersTable
            ->addColumn('user_id', 'integer')
            ->addColumn('event_id', 'integer')
            ->addColumn('created', 'datetime')
            ->addColumn('modified', 'datetime')
            ->create();            
    }
}

Then run the migration:

/var/www $ bin/cake migrations migrate

Now we bake our Users table and views, and the EventsUsers table.

/var/www $ bin/cake bake model Users
/var/www $ bin/cake bake model EventsUsers
/var/www $ bin/cake bake view Users

And finally, we will manually create a UsersController as the CRUD plugin does not currently have a Bake template, create src/Controller/UsersController.php and put the following in it:

<?php
namespace App\Controller;

use App\Controller\AppController;

/**
 * Users Controller
 *
 * @property App\Model\Table\UsersTable $Users
 */
class UsersController extends AppController {

    use \Crud\Controller\ControllerTrait;

    public $components = [
        'Crud.Crud' => [
            'actions' => [
                'Crud.Index',
                'Crud.View',
                'Crud.Add',
                'Crud.Edit',
                'Crud.Delete',
            ],
        ],
    ];
}

Save the file, and navigate to http://events.dev/users, you'll see a familiar interface, but for your users.

Linking users with events

The next step is to link our new Users model with the existing Events model. Open src/Model/Table/UsersTable.php and take a look at the initialize method.

public function initialize(array $config) {
    $this->table('users');
    $this->displayField('name');
    $this->primaryKey('id');
    $this->addBehavior('Timestamp');
    $this->belongsToMany('Events', [
        'alias' => 'Events',
        'foreignKey' => 'user_id',
        'targetForeignKey' => 'event_id',
        'joinTable' => 'events_users'
    ]);
}

The $this->belongsToMany call tells CakePHP that the Users table belongs to many Events using the events_users table to join them. CakePHP has a number of different type of associations, you can read more about them in the CakePHP book. The reason why this exists, is that Bake tries to automatically determine associations as long as you follow the CakePHP naming conventions. You'll also notice List Events and New Event links in your view, these were also added by Bake. Of course, our Events table doesn't have a association with Users since the users table didn't exist when we baked our events. Open src/Model/Table/EventsTable.php and add the following to the initialize() method:

$this->belongsToMany('Users', [
    'alias' => 'Users',
    'foreignKey' => 'event_id',
    'targetForeignKey' => 'user_id',
    'joinTable' => 'events_users'
]);

Bake would also have tried to add a field to the Users form to allow linking of Events to Users, however, we want to do it the other way round (Link Users to an Event). Open src/Template/Users/add.ctp and remove the echo $this->Form->input('events._ids', ['options' => $events]); line, then open src/Template/Users/edit.ctp and remove the same line.

Now we can go and add a field to our events form. Open src/Template/Events/add.ctp and add echo $this->Form->input('users._ids', ['options' => $users]); just after the echo $this->Form->input('all_day'); line. Do the same thing in src/Template/Events/edit.ctp. What this does is create a select form field using the contents of the $users variable. The special _users.ids key tells CakePHP that this will be used to link BelongsToMany users to the event you are creating or editing. If you try to add a new event now, you will get a undefined variable error because we haven't declared what $users is yet.

Open src/Controller/EventsController.php and add the following two methods:

public function add() {
    $this->set('users', $this->Events->Users->find('list'));

    return $this->Crud->execute();
}

public function edit($id) {
    $this->Crud->on('beforeFind', function (\Cake\Event\Event $event) {
        $event->subject->query->contain(['Users']);
    });

    $this->set('users', $this->Events->Users->find('list'));

    return $this->Crud->execute();
}

Finally, open src/Model/Entity/Event.php and change the $_accessible property to look like:

protected $_accessible = [
    'title' => true,
    'description' => true,
    'start' => true,
    'end' => true,
    'all_day' => true,
    'users' => true
];

You'll now be able to create a user, then create an event and select the user. Finally, if you edit the event you'll see your user is already selected. Let's break down what we just did.

First, we created an add and edit method in our controller because we want to override the default Crud behaviour for them. In the add method, we first fetch an associative array of all users in the database and set the $users variable in views. Because Users is associated with Events, you can access it with $this->Events->Users (Likewise, if Users was associated with say Comments, you'd be able to do $this->Events->Users->Comments). Finally, we run the normal Crud handler with $this->Crud->execute();. The edit method does the same as the add method, except we wish to include the current users in our Event entity (Otherwise, the edit form would not show previously selected users). The Crud plugin provides a number of events (beforeFind, afterFind, beforeRender, etc.) to help us hook into the process, and modify it if needed. Here we add a handler for the beforeFind event which then includes Users into the query (using contain()).

The change in the Event entity was to add 'users' => true to the $_accessible property. The $_accessible property is used by CakePHP to determine which properties are allowed to be mass assigned (by, for example, a form).

The final thing we'll do before the end of the post, is to show a list of related users when you view an event. First, we need to load the related users in our view action, so open src/Controller/EventsController.php and add the following method:

public function view($id) {
    $this->Crud->on('beforeFind', function (\Cake\Event\Event $event) {
        $event->subject->query->contain(['Users']);
    });

    return $this->Crud->execute();
}

Then, open src/Template/Events/view.ctp and replace the contents with:

<div class="actions columns large-2 medium-3">
    <h3><?= __('Actions') ?></h3>

    <ul class="side-nav">
        <li><?= $this->Html->link(__('Edit Event'), ['action' => 'edit', $event->id]) ?></li>
        <li><?= $this->Form->postLink(__('Delete Event'), ['action' => 'delete', $event->id], ['confirm' => __('Are you sure you want to delete # {0}?', $event->id)]) ?></li>
        <li><?= $this->Html->link(__('List Events'), ['action' => 'index']) ?>*   <?= $this->Html->link(__('New Event'), ['action' => 'add']) ?></li>
    </ul>
</div>
<div class="events view large-10 medium-9 columns">
    <h2><?= h($event->title) ?></h2>
    <div class="row">
        <div class="large-5 columns strings">
            <h6 class="subheader"><?= __('Title') ?></h6>
            <p><?= h($event->title) ?></p>
        </div>
        <div class="large-2 columns numbers end">
            <h6 class="subheader"><?= __('Id') ?></h6>
            <p><?= $this->Number->format($event->id) ?></p>
        </div>
        <div class="large-2 columns dates end">
            <h6 class="subheader"><?= __('Start') ?></h6>
            <p><?= h($event->start) ?></p>
            <h6 class="subheader"><?= __('End') ?></h6>
            <p><?= h($event->end) ?></p>
            <h6 class="subheader"><?= __('Created') ?></h6>
            <p><?= h($event->created) ?></p>
            <h6 class="subheader"><?= __('Modified') ?></h6>
            <p><?= h($event->modified) ?></p>
        </div>
        <div class="large-2 columns booleans end">
            <h6 class="subheader"><?= __('All Day') ?></h6>
            <p><?= $event->all_day ? __('Yes') : __('No'); ?></p>
        </div>
    </div>
    <div class="row texts">
        <div class="columns large-9">
            <h6 class="subheader"><?= __('Description') ?></h6>
            <?= $this->Text->autoParagraph(h($event->description)); ?>
        </div>
    </div>
    <div class="related row">
        <div class="column large-12">
        <h4 class="subheader"><?= __('Invited users') ?></h4>
        <?php if (!empty($event->users)): ?>
            <ul>
                <?php foreach ($event->users as $user): ?>
                    <li><?= h($user->name) ?> (<?= h($user->email) ?>)</li>
                <?php endforeach; ?>
            </ul>
        <?php else: ?>
            <p>Nobody has been invited to attend this event</p>
        <?php endif; ?>
        </div>
    </div>  
</div>

The interesting bit that we added is:

<div class="related row">
    <div class="column large-12">
    <h4 class="subheader"><?= __('Invited users') ?></h4>
    <?php if (!empty($event->users)): ?>
        <ul>
            <?php foreach ($event->users as $user): ?>
                <li><?= h($user->name) ?> (<?= h($user->email) ?>)</li>
            <?php endforeach; ?>
        </ul>
    <?php else: ?>
        <p>Nobody has been invited to attend this event</p>
    <?php endif; ?>
    </div>
</div>  

Which does a simple loop over the $event->users property and outputs a list of users. Now go and create some users, then create an event and select some of the users you've created. If you view the event, you should now see something like,

That is enough for this post. In the next post, we'll expand on our users table and add user authentication (Login), user registration, changing password, and forgot password. We'll also look at capturing the user who created an event, and only allow users to edit or delete events they created.

I have created a git repository for the system which you can follow along with if you wish. You can find it at https://github.com/dakota/events.

Comments