Quick Start Guide

The best way to experience and learn CakePHP is to sit down and build something. To start off we’ll build a simple bookmarking application.

Bookmarker Tutorial

This tutorial will walk you through the creation of a simple bookmarking application (bookmarker). To start with, we’ll be installing CakePHP, creating our database, and using the tools CakePHP provides to get our application up fast.

Here’s what you’ll need:

  1. A database server. We’re going to be using MySQL server in this tutorial. You’ll need to know enough about SQL in order to create a database: CakePHP will be taking the reins from there. Since we’re using MySQL, also make sure that you have pdo_mysql enabled in PHP.
  2. Basic PHP knowledge.

Before starting you should make sure that you have got an up to date PHP version:

php -v

You should at least have got installed PHP 5.6.0 (CLI) or higher. Your webserver’s PHP version must also be of 5.6.0 or higher, and should best be the same version your command line interface (CLI) PHP version is of. If you’d like to see the completed application, checkout cakephp/bookmarker. Let’s get started!

Getting CakePHP

The easiest way to install CakePHP is to use Composer. Composer is a simple way of installing CakePHP from your terminal or command line prompt. First, you’ll need to download and install Composer if you haven’t done so already. If you have cURL installed, it’s as easy as running the following:

curl -s https://getcomposer.org/installer | php

Or, you can download composer.phar from the Composer website.

Then simply type the following line in your terminal from your installation directory to install the CakePHP application skeleton in the bookmarker directory:

php composer.phar create-project --prefer-dist cakephp/app bookmarker

If you downloaded and ran the Composer Windows Installer, then type the following line in your terminal from your installation directory (ie. C:\wamp\www\dev\cakephp3):

composer self-update && composer create-project --prefer-dist cakephp/app bookmarker

The advantage to using Composer is that it will automatically complete some important set up tasks, such as setting the correct file permissions and creating your config/app.php file for you.

There are other ways to install CakePHP. If you cannot or don’t want to use Composer, check out the Installation section.

Regardless of how you downloaded and installed CakePHP, once your set up is completed, your directory setup should look something like the following:

/bookmarker
    /bin
    /config
    /logs
    /plugins
    /src
    /tests
    /tmp
    /vendor
    /webroot
    .editorconfig
    .gitignore
    .htaccess
    .travis.yml
    composer.json
    index.php
    phpunit.xml.dist
    README.md

Now might be a good time to learn a bit about how CakePHP’s directory structure works: check out the CakePHP Folder Structure section.

Checking our Installation

We can quickly check that our installation is correct, by checking the default home page. Before you can do that, you’ll need to start the development server:

bin/cake server

Note

For Windows, the command needs to be bin\cake server (note the backslash).

This will start PHP’s built-in webserver on port 8765. Open up http://localhost:8765 in your web browser to see the welcome page. All the bullet points should be checkmarks other than CakePHP being able to connect to your database. If not, you may need to install additional PHP extensions, or set directory permissions.

Creating the Database

Next, let’s set up the database for our bookmarking application. If you haven’t already done so, create an empty database for use in this tutorial, with a name of your choice, e.g. cake_bookmarks. You can execute the following SQL to create the necessary tables:

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
    created DATETIME,
    modified DATETIME
);

CREATE TABLE bookmarks (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(50),
    description TEXT,
    url TEXT,
    created DATETIME,
    modified DATETIME,
    FOREIGN KEY user_key (user_id) REFERENCES users(id)
);

CREATE TABLE tags (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    created DATETIME,
    modified DATETIME,
    UNIQUE KEY (title)
);

CREATE TABLE bookmarks_tags (
    bookmark_id INT NOT NULL,
    tag_id INT NOT NULL,
    PRIMARY KEY (bookmark_id, tag_id),
    FOREIGN KEY tag_key(tag_id) REFERENCES tags(id),
    FOREIGN KEY bookmark_key(bookmark_id) REFERENCES bookmarks(id)
);

You may have noticed that the bookmarks_tags table used a composite primary key. CakePHP supports composite primary keys almost everywhere, making it easier to build multi-tenanted applications.

The table and column names we used were not arbitrary. By using CakePHP’s naming conventions, we can leverage CakePHP better and avoid having to configure the framework. CakePHP is flexible enough to accommodate even inconsistent legacy database schemas, but adhering to the conventions will save you time.

Database Configuration

Next, let’s tell CakePHP where our database is and how to connect to it. For many, this will be the first and last time you will need to configure anything.

The configuration should be pretty straightforward: just replace the values in the Datasources.default array in the config/app.php file with those that apply to your setup. A sample completed configuration array might look something like the following:

return [
    // More configuration above.
    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            'username' => 'cakephp',
            'password' => 'AngelF00dC4k3~',
            'database' => 'cake_bookmarks',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
        ],
    ],
    // More configuration below.
];

Once you’ve saved your config/app.php file, you should see that ‘CakePHP is able to connect to the database’ section have a checkmark.

Note

A copy of CakePHP’s default configuration file is found in config/app.default.php.

Generating Scaffold Code

Because our database is following the CakePHP conventions, we can use the bake console application to quickly generate a basic application. In your command line run the following commands:

// On Windows you'll need to use bin\cake instead.
bin/cake bake all users
bin/cake bake all bookmarks
bin/cake bake all tags

This will generate the controllers, models, views, their corresponding test cases, and fixtures for our users, bookmarks and tags resources. If you’ve stopped your server, restart it and go to http://localhost:8765/bookmarks.

You should see a basic but functional application providing data access to your application’s database tables. Once you’re at the list of bookmarks, add a few users, bookmarks, and tags.

Note

If you see a Not Found (404) page, confirm that the Apache mod_rewrite module is loaded.

Adding Password Hashing

When you created your users (by visiting http://localhost:8765/users), you probably noticed that the passwords were stored in plain text. This is pretty bad from a security point of view, so let’s get that fixed.

This is also a good time to talk about the model layer in CakePHP. In CakePHP, we separate the methods that operate on a collection of objects, and a single object into different classes. Methods that operate on the collection of entities are put in the Table class, while features belonging to a single record are put on the Entity class.

For example, password hashing is done on the individual record, so we’ll implement this behavior on the entity object. Because, we want to hash the password each time it is set, we’ll use a mutator/setter method. CakePHP will call convention based setter methods any time a property is set in one of your entities. Let’s add a setter for the password. In src/Model/Entity/User.php add the following:

namespace App\Model\Entity;

use Cake\Auth\DefaultPasswordHasher; //include this line
use Cake\ORM\Entity;

class User extends Entity
{

    // Code from bake.

    protected function _setPassword($value)
    {
        $hasher = new DefaultPasswordHasher();
        return $hasher->hash($value);
    }
}

Now update one of the users you created earlier, if you change their password, you should see a hashed password instead of the original value on the list or view pages. CakePHP hashes passwords with bcrypt by default. You can also use sha1 or md5 if you’re working with an existing database.

Note

If the password doesn’t get hashed, make sure you followed the same case for the password member of the class while naming the setter function

Getting Bookmarks with a Specific Tag

Now that we’re storing passwords safely, we can build out some more interesting features in our application. Once you’ve amassed a collection of bookmarks, it is helpful to be able to search through them by tag. Next we’ll implement a route, controller action, and finder method to search through bookmarks by tag.

Ideally, we’d have a URL that looks like http://localhost:8765/bookmarks/tagged/funny/cat/gifs. This would let us find all the bookmarks that have the ‘funny’, ‘cat’ or ‘gifs’ tags. Before we can implement this, we’ll add a new route. Your config/routes.php should look like:

<?php
use Cake\Routing\Route\DashedRoute;
use Cake\Routing\Router;

Router::defaultRouteClass(DashedRoute::class);

// New route we're adding for our tagged action.
// The trailing `*` tells CakePHP that this action has
// passed parameters.
Router::scope(
    '/bookmarks',
    ['controller' => 'Bookmarks'],
    function ($routes) {
        $routes->connect('/tagged/*', ['action' => 'tags']);
    }
);

Router::scope('/', function ($routes) {
    // Connect the default home and /pages/* routes.
    $routes->connect('/', [
        'controller' => 'Pages',
        'action' => 'display', 'home'
    ]);
    $routes->connect('/pages/*', [
        'controller' => 'Pages',
        'action' => 'display'
    ]);

    // Connect the conventions based default routes.
    $routes->fallbacks();
});

The above defines a new ‘route’ which connects the /bookmarks/tagged/ path, to BookmarksController::tags(). By defining routes, you can isolate how your URLs look, from how they are implemented. If we were to visit http://localhost:8765/bookmarks/tagged, we would see a helpful error page from CakePHP informing you that the controller action does not exist. Let’s implement that missing method now. In src/Controller/BookmarksController.php add the following:

public function tags()
{
    // The 'pass' key is provided by CakePHP and contains all
    // the passed URL path segments in the request.
    $tags = $this->request->getParam('pass');

    // Use the BookmarksTable to find tagged bookmarks.
    $bookmarks = $this->Bookmarks->find('tagged', [
        'tags' => $tags
    ]);

    // Pass variables into the view template context.
    $this->set([
        'bookmarks' => $bookmarks,
        'tags' => $tags
    ]);
}

To access other parts of the request data, consult the Request section.

Creating the Finder Method

In CakePHP we like to keep our controller actions slim, and put most of our application’s logic in the models. If you were to visit the /bookmarks/tagged URL now you would see an error that the findTagged() method has not been implemented yet, so let’s do that. In src/Model/Table/BookmarksTable.php add the following:

// The $query argument is a query builder instance.
// The $options array will contain the 'tags' option we passed
// to find('tagged') in our controller action.
public function findTagged(Query $query, array $options)
{
    $bookmarks = $this->find()
        ->select(['id', 'url', 'title', 'description']);

    if (empty($options['tags'])) {
        $bookmarks->leftJoinWith('Tags', function ($q) {
            return $q->where(['Tags.title IS ' => null]);
        });
    } else {
        $bookmarks->innerJoinWith('Tags', function ($q) use ($options) {
            return $q->where(['Tags.title IN' => $options['tags']]);
        });
    }

    return $bookmarks->group(['Bookmarks.id']);
}

We just implemented a custom finder method. This is a very powerful concept in CakePHP that allows you to package up re-usable queries. Finder methods always get a Query Builder object and an array of options as parameters. Finders can manipulate the query and add any required conditions or criteria. When complete, finder methods must return a modified query object. In our finder we’ve leveraged the distinct() and matching() methods which allow us to find distinct bookmarks that have a ‘matching’ tag. The matching() method accepts an anonymous function that receives a query builder as its argument. Inside the callback we use the query builder to define conditions that will filter bookmarks that have specific tags.

Creating the View

Now if you visit the /bookmarks/tagged URL, CakePHP will show an error letting you know that you have not made a view file. Next, let’s build the view file for our tags() action. In src/Template/Bookmarks/tags.ctp put the following content:

<h1>
    Bookmarks tagged with
    <?= $this->Text->toList(h($tags)) ?>
</h1>

<section>
<?php foreach ($bookmarks as $bookmark): ?>
    <article>
        <!-- Use the HtmlHelper to create a link -->
        <h4><?= $this->Html->link($bookmark->title, $bookmark->url) ?></h4>
        <small><?= h($bookmark->url) ?></small>

        <!-- Use the TextHelper to format text -->
        <?= $this->Text->autoParagraph(h($bookmark->description)) ?>
    </article>
<?php endforeach; ?>
</section>

In the above code we use the Html and Text helpers to assist in generating our view output. We also use the h shortcut function to HTML encode output. You should remember to always use h() when outputting user data to prevent HTML injection issues.

The tags.ctp file we just created follows the CakePHP conventions for view template files. The convention is to have the template use the lower case and underscored version of the controller action name.

You may notice that we were able to use the $tags and $bookmarks variables in our view. When we use the set() method in our controller, we set specific variables to be sent to the view. The view will make all passed variables available in the templates as local variables.

You should now be able to visit the /bookmarks/tagged/funny URL and see all the bookmarks tagged with ‘funny’.

So far, we’ve created a basic application to manage bookmarks, tags and users. However, everyone can see everyone else’s tags. In the next chapter, we’ll implement authentication and restrict the visible bookmarks to only those that belong to the current user.

Now continue to Bookmarker Tutorial Part 2 to continue building your application or dive into the documentation to learn more about what CakePHP can do for you.

Bookmarker Tutorial Part 2

After finishing the first part of this tutorial you should have a very basic bookmarking application. In this chapter we’ll be adding authentication and restricting the bookmarks each user can see/modify to only the ones they own.

Adding Login

In CakePHP, authentication is handled by Components. Components can be thought of as ways to create reusable chunks of controller code related to a specific feature or concept. Components can also hook into the controller’s event life-cycle and interact with your application that way. To get started, we’ll add the AuthComponent to our application. We’ll pretty much want every method to require authentication, so we’ll add AuthComponent in our AppController:

// In src/Controller/AppController.php
namespace App\Controller;

use Cake\Controller\Controller;

class AppController extends Controller
{
    public function initialize()
    {
        $this->loadComponent('Flash');
        $this->loadComponent('Auth', [
            'authenticate' => [
                'Form' => [
                    'fields' => [
                        'username' => 'email',
                        'password' => 'password'
                    ]
                ]
            ],
            'loginAction' => [
                'controller' => 'Users',
                'action' => 'login'
            ],
            'unauthorizedRedirect' => $this->referer() // If unauthorized, return them to page they were just on
        ]);

        // Allow the display action so our pages controller
        // continues to work.
        $this->Auth->allow(['display']);
    }
}

We’ve just told CakePHP that we want to load the Flash and Auth components. In addition, we’ve customized the configuration of AuthComponent, as our users table uses email as the username. Now, if you go to any URL you’ll be kicked to /users/login, which will show an error page as we have not written that code yet. So let’s create the login action:

// In src/Controller/UsersController.php
public function login()
{
    if ($this->request->is('post')) {
        $user = $this->Auth->identify();
        if ($user) {
            $this->Auth->setUser($user);
            return $this->redirect($this->Auth->redirectUrl());
        }
        $this->Flash->error('Your username or password is incorrect.');
    }
}

And in src/Template/Users/login.ctp add the following:

<h1>Login</h1>
<?= $this->Form->create() ?>
<?= $this->Form->input('email') ?>
<?= $this->Form->input('password') ?>
<?= $this->Form->button('Login') ?>
<?= $this->Form->end() ?>

Now that we have a simple login form, we should be able to log in with one of the users that has a hashed password.

Note

If none of your users have hashed passwords, comment the loadComponent('Auth') line. Then go and edit the user, saving a new password for them.

You should now be able to log in. If not, make sure you are using a user that has a hashed password.

Adding Logout

Now that people can log in, you’ll probably want to provide a way to log out as well. Again, in the UsersController, add the following code:

public function initialize()
{
    parent::initialize();
    $this->Auth->allow(['logout']);
}

public function logout()
{
    $this->Flash->success('You are now logged out.');
    return $this->redirect($this->Auth->logout());
}

This code whitelists the logout action as a public action, and implements the logout method. Now you can visit /users/logout to log out. You should then be sent to the login page.

Enabling Registrations

If you aren’t logged in and you try to visit /users/add you will be kicked to the login page. We should fix that as we want to allow people to sign up for our application. In the UsersController add the following:

public function initialize()
{
    parent::initialize();
    // Add logout to the allowed actions list.
    $this->Auth->allow(['logout', 'add']);
}

The above tells AuthComponent that the add() action does not require authentication or authorization. You may want to take the time to clean up the Users/add.ctp and remove the misleading links, or continue on to the next section. We won’t be building out user editing, viewing or listing in this tutorial so they will not work as AuthComponent will deny you access to those controller actions.

Restricting Bookmark Access

Now that users can log in, we’ll want to limit the bookmarks they can see to the ones they made. We’ll do this using an ‘authorization’ adapter. Since our requirements are pretty simple, we can write some simple code in our BookmarksController. But before we do that, we’ll want to tell the AuthComponent how our application is going to authorize actions. In your AppController add the following:

public function isAuthorized($user)
{
    return false;
}

Also, add the following to the configuration for Auth in your AppController:

'authorize' => 'Controller',

Your initialize() method should now look like:

public function initialize()
{
    $this->loadComponent('Flash');
    $this->loadComponent('Auth', [
        'authorize'=> 'Controller',//added this line
        'authenticate' => [
            'Form' => [
                'fields' => [
                    'username' => 'email',
                    'password' => 'password'
                ]
            ]
        ],
        'loginAction' => [
            'controller' => 'Users',
            'action' => 'login'
        ],
        'unauthorizedRedirect' => $this->referer()
    ]);

    // Allow the display action so our pages controller
    // continues to work.
    $this->Auth->allow(['display']);
}

We’ll default to denying access, and incrementally grant access where it makes sense. First, we’ll add the authorization logic for bookmarks. In your BookmarksController add the following:

public function isAuthorized($user)
{
    $action = $this->request->getParam('action');

    // The add and index actions are always allowed.
    if (in_array($action, ['index', 'add', 'tags'])) {
        return true;
    }
    // All other actions require an id.
    if (!$this->request->getParam('pass.0')) {
        return false;
    }

    // Check that the bookmark belongs to the current user.
    $id = $this->request->getParam('pass.0');
    $bookmark = $this->Bookmarks->get($id);
    if ($bookmark->user_id == $user['id']) {
        return true;
    }
    return parent::isAuthorized($user);
}

Now if you try to view, edit or delete a bookmark that does not belong to you, you should be redirected back to the page you came from. If no error message is displayed, add the following to your layout:

// In src/Template/Layout/default.ctp
<?= $this->Flash->render() ?>

You should now see the authorization error messages.

Fixing List view and Forms

While view and delete are working, edit, add and index have a few problems:

  1. When adding a bookmark you can choose the user.
  2. When editing a bookmark you can choose the user.
  3. The list page shows bookmarks from other users.

Let’s tackle the add form first. To begin with remove the input('user_id') from src/Template/Bookmarks/add.ctp. With that removed, we’ll also update the add() action from src/Controller/BookmarksController.php to look like:

public function add()
{
    $bookmark = $this->Bookmarks->newEntity();
    if ($this->request->is('post')) {
        $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->getData());
        $bookmark->user_id = $this->Auth->user('id');
        if ($this->Bookmarks->save($bookmark)) {
            $this->Flash->success('The bookmark has been saved.');
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error('The bookmark could not be saved. Please, try again.');
    }
    $tags = $this->Bookmarks->Tags->find('list');
    $this->set(compact('bookmark', 'tags'));
    $this->set('_serialize', ['bookmark']);
}

By setting the entity property with the session data, we remove any possibility of the user modifying which user a bookmark is for. We’ll do the same for the edit form and action. Your edit() action from src/Controller/BookmarksController.php should look like:

public function edit($id = null)
{
    $bookmark = $this->Bookmarks->get($id, [
        'contain' => ['Tags']
    ]);
    if ($this->request->is(['patch', 'post', 'put'])) {
        $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->getData());
        $bookmark->user_id = $this->Auth->user('id');
        if ($this->Bookmarks->save($bookmark)) {
            $this->Flash->success('The bookmark has been saved.');
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error('The bookmark could not be saved. Please, try again.');
    }
    $tags = $this->Bookmarks->Tags->find('list');
    $this->set(compact('bookmark', 'tags'));
    $this->set('_serialize', ['bookmark']);
}

List View

Now, we only need to show bookmarks for the currently logged in user. We can do that by updating the call to paginate(). Make your index() action from src/Controller/BookmarksController.php look like:

public function index()
{
    $this->paginate = [
        'conditions' => [
            'Bookmarks.user_id' => $this->Auth->user('id'),
        ]
    ];
    $this->set('bookmarks', $this->paginate($this->Bookmarks));
    $this->set('_serialize', ['bookmarks']);
}

We should also update the tags() action and the related finder method, but we’ll leave that as an exercise you can complete on your own.

Improving the Tagging Experience

Right now, adding new tags is a difficult process, as the TagsController disallows all access. Instead of allowing access, we can improve the tag selection UI by using a comma separated text field. This will let us give a better experience to our users, and use some more great features in the ORM.

Adding a Computed Field

Because we’ll want a simple way to access the formatted tags for an entity, we can add a virtual/computed field to the entity. In src/Model/Entity/Bookmark.php add the following:

use Cake\Collection\Collection;

protected function _getTagString()
{
    if (isset($this->_properties['tag_string'])) {
        return $this->_properties['tag_string'];
    }
    if (empty($this->tags)) {
        return '';
    }
    $tags = new Collection($this->tags);
    $str = $tags->reduce(function ($string, $tag) {
        return $string . $tag->title . ', ';
    }, '');
    return trim($str, ', ');
}

This will let us access the $bookmark->tag_string computed property. We’ll use this property in inputs later on. Remember to add the tag_string property to the _accessible list in your entity, as we’ll want to ‘save’ it later on.

In src/Model/Entity/Bookmark.php add the tag_string to $_accessible this way:

protected $_accessible = [
    'user_id' => true,
    'title' => true,
    'description' => true,
    'url' => true,
    'user' => true,
    'tags' => true,
    'tag_string' => true,
];

Updating the Views

With the entity updated we can add a new input for our tags. In src/Template/Bookmarks/add.ctp and src/Template/Bookmarks/edit.ctp, replace the existing tags._ids input with the following:

echo $this->Form->input('tag_string', ['type' => 'text']);

Persisting the Tag String

Now that we can view existing tags as a string, we’ll want to save that data as well. Because we marked the tag_string as accessible, the ORM will copy that data from the request into our entity. We can use a beforeSave() hook method to parse the tag string and find/build the related entities. Add the following to src/Model/Table/BookmarksTable.php:

public function beforeSave($event, $entity, $options)
{
    if ($entity->tag_string) {
        $entity->tags = $this->_buildTags($entity->tag_string);
    }
}

protected function _buildTags($tagString)
{
    // Trim tags
    $newTags = array_map('trim', explode(',', $tagString));
    // Remove all empty tags
    $newTags = array_filter($newTags);
    // Reduce duplicated tags
    $newTags = array_unique($newTags);

    $out = [];
    $query = $this->Tags->find()
        ->where(['Tags.title IN' => $newTags]);

    // Remove existing tags from the list of new tags.
    foreach ($query->extract('title') as $existing) {
        $index = array_search($existing, $newTags);
        if ($index !== false) {
            unset($newTags[$index]);
        }
    }
    // Add existing tags.
    foreach ($query as $tag) {
        $out[] = $tag;
    }
    // Add new tags.
    foreach ($newTags as $tag) {
        $out[] = $this->Tags->newEntity(['title' => $tag]);
    }
    return $out;
}

While this code is a bit more complicated than what we’ve done so far, it helps to showcase how powerful the ORM in CakePHP is. You can manipulate query results using the Collections methods, and handle scenarios where you are creating entities on the fly with ease.

Wrapping Up

We’ve expanded our bookmarking application to handle authentication and basic authorization/access control scenarios. We’ve also added some nice UX improvements by leveraging the FormHelper and ORM capabilities.

Thanks for taking the time to explore CakePHP. Next, you can complete the Blog Tutorial, learn more about the Database Access & ORM, or you can peruse the Using CakePHP.