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 Content Management application.

Content Management Tutorial

This tutorial will walk you through the creation of a simple CMS application. To start with, we’ll be installing CakePHP, creating our database, and building simple article management.

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, and run SQL snippets from the tutorial. CakePHP will handle building all the queries your application needs. 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’re using a supported PHP version:

php -v

You should at least have got installed PHP 8.1 (CLI) or higher. Your webserver’s PHP version must also be of 8.1 or higher, and should be the same version your command line interface (CLI) PHP is.

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, run 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 cms directory of the current working directory:

php composer.phar create-project --prefer-dist cakephp/app:5 cms

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):

composer self-update && composer create-project --prefer-dist cakephp/app:5.* cms

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 like the following, though other files may also be present:

cms/
  bin/
  config/
  plugins/
  resources/
  src/
  templates/
  tests/
  tmp/
  vendor/
  webroot/
  composer.json
  index.php
  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.

If you get lost during this tutorial, you can see the finished result on GitHub.

Tip

The bin/cake console utility can build most of the classes and data tables in this tutorial automatically. However, we recommend following along with the manual code examples to understand how the pieces fit together and how to add your application logic.

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:

cd /path/to/our/app

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 green chef hats other than CakePHP being able to connect to your database. If not, you may need to install additional PHP extensions, or set directory permissions.

Next, we will build our Database.

CMS Tutorial - Creating the Database

Now that we have CakePHP installed, let’s set up the database for our CMS application. If you haven’t already done so, create an empty database for use in this tutorial, with the name of your choice such as cake_cms. If you are using MySQL/MariaDB, you can execute the following SQL to create the necessary tables:

CREATE DATABASE cake_cms;

USE cake_cms;

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 articles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(255) NOT NULL,
    slug VARCHAR(191) NOT NULL,
    body TEXT,
    published BOOLEAN DEFAULT FALSE,
    created DATETIME,
    modified DATETIME,
    UNIQUE KEY (slug),
    FOREIGN KEY user_key (user_id) REFERENCES users(id)
) CHARSET=utf8mb4;

CREATE TABLE tags (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(191),
    created DATETIME,
    modified DATETIME,
    UNIQUE KEY (title)
) CHARSET=utf8mb4;

CREATE TABLE articles_tags (
    article_id INT NOT NULL,
    tag_id INT NOT NULL,
    PRIMARY KEY (article_id, tag_id),
    FOREIGN KEY tag_key(tag_id) REFERENCES tags(id),
    FOREIGN KEY article_key(article_id) REFERENCES articles(id)
);

INSERT INTO users (email, password, created, modified)
VALUES
('[email protected]', 'secret', NOW(), NOW());

INSERT INTO articles (user_id, title, slug, body, published, created, modified)
VALUES
(1, 'First Post', 'first-post', 'This is the first post.', 1, NOW(), NOW());

If you are using PostgreSQL, connect to the cake_cms database and execute the following SQL instead:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
    created TIMESTAMP,
    modified TIMESTAMP
);

CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(255) NOT NULL,
    slug VARCHAR(191) NOT NULL,
    body TEXT,
    published BOOLEAN DEFAULT FALSE,
    created TIMESTAMP,
    modified TIMESTAMP,
    UNIQUE (slug),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE tags (
    id SERIAL PRIMARY KEY,
    title VARCHAR(191),
    created TIMESTAMP,
    modified TIMESTAMP,
    UNIQUE (title)
);

CREATE TABLE articles_tags (
    article_id INT NOT NULL,
    tag_id INT NOT NULL,
    PRIMARY KEY (article_id, tag_id),
    FOREIGN KEY (tag_id) REFERENCES tags(id),
    FOREIGN KEY (article_id) REFERENCES articles(id)
);

INSERT INTO users (email, password, created, modified)
VALUES
('[email protected]', 'secret', NOW(), NOW());

INSERT INTO articles (user_id, title, slug, body, published, created, modified)
VALUES
(1, 'First Post', 'first-post', 'This is the first post.', TRUE, NOW(), NOW());

You may have noticed that the articles_tags table uses a composite primary key. CakePHP supports composite primary keys almost everywhere, allowing you to have simpler schemas that don’t require additional id columns.

The table and column names we used were not arbitrary. By using CakePHP’s naming conventions, we can leverage CakePHP more effectively and avoid needing to configure the framework. While CakePHP is flexible enough to accommodate almost any database schema, adhering to the conventions will save you time as you can leverage the convention-based defaults CakePHP provides.

Database Configuration

Next, let’s tell CakePHP where our database is and how to connect to it. Replace the values in the Datasources.default array in your config/app_local.php file with those that apply to your setup. A sample completed configuration array might look something like the following:

<?php
// config/app_local.php
return [
    // More configuration above.
    'Datasources' => [
        'default' => [
            'host' => 'localhost',
            'username' => 'cakephp',
            'password' => 'AngelF00dC4k3~',
            'database' => 'cake_cms',
            'url' => env('DATABASE_URL', null),
        ],
    ],
    // More configuration below.
];

Once you’ve saved your config/app_local.php file, you should see that the ‘CakePHP is able to connect to the database’ section has a green chef hat.

Note

The file config/app_local.php is a local override of the file config/app.php used to configure your development environment quickly.

Migrations

The SQL statements to create the tables for this tutorial can also be generated using the Migrations Plugin. Migrations provide a platform-independent way to run queries so the subtle differences between MySQL, PostgreSQL, SQLite, etc. don’t become obstacles.

bin/cake bake migration CreateUsers email:string password:string created modified
bin/cake bake migration CreateArticles user_id:integer title:string slug:string[191]:unique body:text published:boolean created modified
bin/cake bake migration CreateTags title:string[191]:unique created modified
bin/cake bake migration CreateArticlesTags article_id:integer:primary tag_id:integer:primary created modified

Note

Some adjustments to the generated code might be necessary. For example, the composite primary key on articles_tags will be set to auto-increment both columns:

$table->addColumn('article_id', 'integer', [
    'autoIncrement' => true,
    'default' => null,
    'limit' => 11,
    'null' => false,
]);
$table->addColumn('tag_id', 'integer', [
    'autoIncrement' => true,
    'default' => null,
    'limit' => 11,
    'null' => false,
]);

Remove those lines to prevent foreign key problems. Once adjustments are done:

bin/cake migrations migrate

Likewise, the starter data records can be done with seeds.

bin/cake bake seed Users
bin/cake bake seed Articles

Fill the seed data above into the new UsersSeed and ArticlesSeed classes, then:

bin/cake migrations seed

Read more about building migrations and data seeding: Migrations

With the database built, we can now build Models.

CMS Tutorial - Creating the Articles Controller

With our model created, we need a controller for our articles. Controllers in CakePHP handle HTTP requests and execute business logic contained in model methods, to prepare the response. We’ll place this new controller in a file called ArticlesController.php inside the src/Controller directory. Here’s what the basic controller should look like:

<?php
// src/Controller/ArticlesController.php

namespace App\Controller;

class ArticlesController extends AppController
{
}

Now, let’s add an action to our controller. Actions are controller methods that have routes connected to them. For example, when a user requests www.example.com/articles/index (which is also the same as www.example.com/articles), CakePHP will call the index method of your ArticlesController. This method should query the model layer, and prepare a response by rendering a Template in the View. The code for that action would look like this:

<?php
// src/Controller/ArticlesController.php

namespace App\Controller;

class ArticlesController extends AppController
{
    public function index()
    {
        $articles = $this->paginate($this->Articles);
        $this->set(compact('articles'));
    }
}

By defining function index() in our ArticlesController, users can now access the logic there by requesting www.example.com/articles/index. Similarly, if we were to define a function called foobar(), users would be able to access that at www.example.com/articles/foobar. You may be tempted to name your controllers and actions in a way that allows you to obtain specific URLs. Resist that temptation. Instead, follow the CakePHP Conventions creating readable, meaningful action names. You can then use Routing to connect the URLs you want to the actions you’ve created.

Our controller action is very simple. It fetches a paginated set of articles from the database, using the Articles Model that is automatically loaded via naming conventions. It then uses set() to pass the articles into the Template (which we’ll create soon). CakePHP will automatically render the template after our controller action completes.

Create the Article List Template

Now that we have our controller pulling data from the model, and preparing our view context, let’s create a view template for our index action.

CakePHP view templates are presentation-flavored PHP code that is inserted inside the application’s layout. While we’ll be creating HTML here, Views can also generate JSON, CSV or even binary files like PDFs.

A layout is presentation code that is wrapped around a view. Layout files contain common site elements like headers, footers and navigation elements. Your application can have multiple layouts, and you can switch between them, but for now, let’s just use the default layout.

CakePHP’s template files are stored in templates inside a folder named after the controller they correspond to. So we’ll have to create a folder named ‘Articles’ in this case. Add the following code to your application:

<!-- File: templates/Articles/index.php -->

<h1>Articles</h1>
<table>
    <tr>
        <th>Title</th>
        <th>Created</th>
    </tr>

    <!-- Here is where we iterate through our $articles query object, printing out article info -->

    <?php foreach ($articles as $article): ?>
    <tr>
        <td>
            <?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
        </td>
        <td>
            <?= $article->created->format(DATE_RFC850) ?>
        </td>
    </tr>
    <?php endforeach; ?>
</table>

In the last section we assigned the ‘articles’ variable to the view using set(). Variables passed into the view are available in the view templates as local variables which we used in the above code.

You might have noticed the use of an object called $this->Html. This is an instance of the CakePHP HtmlHelper. CakePHP comes with a set of view helpers that make tasks like creating links, forms, and pagination buttons. You can learn more about Helpers in their chapter, but what’s important to note here is that the link() method will generate an HTML link with the given link text (the first parameter) and URL (the second parameter).

When specifying URLs in CakePHP, it is recommended that you use arrays or named routes. These syntaxes allow you to leverage the reverse routing features CakePHP offers.

At this point, you should be able to point your browser to http://localhost:8765/articles/index. You should see your list view, correctly formatted with the title and table listing of the articles.

Create the View Action

If you were to click one of the ‘view’ links in our Articles list page, you’d see an error page saying that action hasn’t been implemented. Lets fix that now:

// Add to existing src/Controller/ArticlesController.php file

public function view($slug = null)
{
    $article = $this->Articles->findBySlug($slug)->firstOrFail();
    $this->set(compact('article'));
}

While this is a simple action, we’ve used some powerful CakePHP features. We start our action off by using findBySlug() which is a Dynamic Finder. This method allows us to create a basic query that finds articles by a given slug. We then use firstOrFail() to either fetch the first record, or throw a \Cake\Datasource\Exception\RecordNotFoundException.

Our action takes a $slug parameter, but where does that parameter come from? If a user requests /articles/view/first-post, then the value ‘first-post’ is passed as $slug by CakePHP’s routing and dispatching layers. If we reload our browser with our new action saved, we’d see another CakePHP error page telling us we’re missing a view template; let’s fix that.

Create the View Template

Let’s create the view for our new ‘view’ action and place it in templates/Articles/view.php

<!-- File: templates/Articles/view.php -->

<h1><?= h($article->title) ?></h1>
<p><?= h($article->body) ?></p>
<p><small>Created: <?= $article->created->format(DATE_RFC850) ?></small></p>
<p><?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?></p>

You can verify that this is working by trying the links at /articles/index or manually requesting an article by accessing URLs like /articles/view/first-post.

Adding Articles

With the basic read views created, we need to make it possible for new articles to be created. Start by creating an add() action in the ArticlesController. Our controller should now look like:

<?php
// src/Controller/ArticlesController.php
namespace App\Controller;

use App\Controller\AppController;

class ArticlesController extends AppController
{
    public function index()
    {
        $articles = $this->paginate($this->Articles);
        $this->set(compact('articles'));
    }

    public function view($slug)
    {
        $article = $this->Articles->findBySlug($slug)->firstOrFail();
        $this->set(compact('article'));
    }

    public function add()
    {
        $article = $this->Articles->newEmptyEntity();
        if ($this->request->is('post')) {
            $article = $this->Articles->patchEntity($article, $this->request->getData());

            // Hardcoding the user_id is temporary, and will be removed later
            // when we build authentication out.
            $article->user_id = 1;

            if ($this->Articles->save($article)) {
                $this->Flash->success(__('Your article has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('Unable to add your article.'));
        }
        $this->set('article', $article);
    }
}

Note

You need to include the Flash component in any controller where you will use it. Often it makes sense to include it in your AppController, which is there already for this tutorial.

Here’s what the add() action does:

  • If the HTTP method of the request was POST, try to save the data using the Articles model.

  • If for some reason it doesn’t save, just render the view. This gives us a chance to show the user validation errors or other warnings.

Every CakePHP request includes a request object which is accessible using $this->request. The request object contains information regarding the request that was just received. We use the Cake\Http\ServerRequest::is() method to check that the request is a HTTP POST request.

Our POST data is available in $this->request->getData(). You can use the pr() or debug() functions to print it out if you want to see what it looks like. To save our data, we first ‘marshal’ the POST data into an Article Entity. The Entity is then persisted using the ArticlesTable we created earlier.

After saving our new article we use FlashComponent’s success() method to set a message into the session. The success method is provided using PHP’s magic method features. Flash messages will be displayed on the next page after redirecting. In our layout we have <?= $this->Flash->render() ?> which displays flash messages and clears the corresponding session variable. Finally, after saving is complete, we use Cake\Controller\Controller::redirect to send the user back to the articles list. The param ['action' => 'index'] translates to URL /articles i.e the index action of the ArticlesController. You can refer to Cake\Routing\Router::url() function on the API to see the formats in which you can specify a URL for various CakePHP functions.

Create Add Template

Here’s our add view template:

<!-- File: templates/Articles/add.php -->

<h1>Add Article</h1>
<?php
    echo $this->Form->create($article);
    // Hard code the user for now.
    echo $this->Form->control('user_id', ['type' => 'hidden', 'value' => 1]);
    echo $this->Form->control('title');
    echo $this->Form->control('body', ['rows' => '3']);
    echo $this->Form->button(__('Save Article'));
    echo $this->Form->end();
?>

We use the FormHelper to generate the opening tag for an HTML form. Here’s the HTML that $this->Form->create() generates:

<form method="post" action="/articles/add">

Because we called create() without a URL option, FormHelper assumes we want the form to submit back to the current action.

The $this->Form->control() method is used to create form elements of the same name. The first parameter tells CakePHP which field they correspond to, and the second parameter allows you to specify a wide array of options - in this case, the number of rows for the textarea. There’s a bit of introspection and conventions used here. The control() will output different form elements based on the model field specified, and use inflection to generate the label text. You can customize the label, the input or any other aspect of the form controls using options. The $this->Form->end() call closes the form.

Now let’s go back and update our templates/Articles/index.php view to include a new “Add Article” link. Before the <table>, add the following line:

<?= $this->Html->link('Add Article', ['action' => 'add']) ?>

Adding Simple Slug Generation

If we were to save an Article right now, saving would fail as we are not creating a slug attribute, and the column is NOT NULL. Slug values are typically a URL-safe version of an article’s title. We can use the beforeSave() callback of the ORM to populate our slug:

<?php
// in src/Model/Table/ArticlesTable.php
namespace App\Model\Table;

use Cake\ORM\Table;
// the Text class
use Cake\Utility\Text;
// the EventInterface class
use Cake\Event\EventInterface;

// Add the following method.

public function beforeSave(EventInterface $event, $entity, $options)
{
    if ($entity->isNew() && !$entity->slug) {
        $sluggedTitle = Text::slug($entity->title);
        // trim slug to maximum length defined in schema
        $entity->slug = substr($sluggedTitle, 0, 191);
    }
}

This code is simple, and doesn’t take into account duplicate slugs. But we’ll fix that later on.

Add Edit Action

Our application can now save articles, but we can’t edit them. Lets rectify that now. Add the following action to your ArticlesController:

// in src/Controller/ArticlesController.php

// Add the following method.

public function edit($slug)
{
    $article = $this->Articles
        ->findBySlug($slug)
        ->firstOrFail();

    if ($this->request->is(['post', 'put'])) {
        $this->Articles->patchEntity($article, $this->request->getData());
        if ($this->Articles->save($article)) {
            $this->Flash->success(__('Your article has been updated.'));

            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('Unable to update your article.'));
    }

    $this->set('article', $article);
}

This action first ensures that the user has tried to access an existing record. If they haven’t passed in an $slug parameter, or the article does not exist, a RecordNotFoundException will be thrown, and the CakePHP ErrorHandler will render the appropriate error page.

Next the action checks whether the request is either a POST or a PUT request. If it is, then we use the POST/PUT data to update our article entity by using the patchEntity() method. Finally, we call save(), set the appropriate flash message, and either redirect or display validation errors.

Create Edit Template

The edit template should look like this:

<!-- File: templates/Articles/edit.php -->

<h1>Edit Article</h1>
<?php
    echo $this->Form->create($article);
    echo $this->Form->control('user_id', ['type' => 'hidden']);
    echo $this->Form->control('title');
    echo $this->Form->control('body', ['rows' => '3']);
    echo $this->Form->button(__('Save Article'));
    echo $this->Form->end();
?>

This template outputs the edit form (with the values populated), along with any necessary validation error messages.

You can now update your index view with links to edit specific articles:

<!-- File: templates/Articles/index.php  (edit links added) -->

<h1>Articles</h1>
<p><?= $this->Html->link("Add Article", ['action' => 'add']) ?></p>
<table>
    <tr>
        <th>Title</th>
        <th>Created</th>
        <th>Action</th>
    </tr>

<!-- Here's where we iterate through our $articles query object, printing out article info -->

<?php foreach ($articles as $article): ?>
    <tr>
        <td>
            <?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
        </td>
        <td>
            <?= $article->created->format(DATE_RFC850) ?>
        </td>
        <td>
            <?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?>
        </td>
    </tr>
<?php endforeach; ?>

</table>

Update Validation Rules for Articles

Up until this point our Articles had no input validation done. Lets fix that by using a validator:

// src/Model/Table/ArticlesTable.php

// add this use statement right below the namespace declaration to import
// the Validator class
use Cake\Validation\Validator;

// Add the following method.
public function validationDefault(Validator $validator): Validator
{
    $validator
        ->notEmptyString('title')
        ->minLength('title', 10)
        ->maxLength('title', 255)

        ->notEmptyString('body')
        ->minLength('body', 10);

    return $validator;
}

The validationDefault() method tells CakePHP how to validate your data when the save() method is called. Here, we’ve specified that both the title, and body fields must not be empty, and have certain length constraints.

CakePHP’s validation engine is powerful and flexible. It provides a suite of frequently used rules for tasks like email addresses, IP addresses etc. and the flexibility for adding your own validation rules. For more information on that setup, check the Validation documentation.

Now that your validation rules are in place, use the app to try to add an article with an empty title or body to see how it works. Since we’ve used the Cake\View\Helper\FormHelper::control() method of the FormHelper to create our form elements, our validation error messages will be shown automatically.

Add Delete Action

Next, let’s make a way for users to delete articles. Start with a delete() action in the ArticlesController:

// src/Controller/ArticlesController.php

// Add the following method.

public function delete($slug)
{
    $this->request->allowMethod(['post', 'delete']);

    $article = $this->Articles->findBySlug($slug)->firstOrFail();
    if ($this->Articles->delete($article)) {
        $this->Flash->success(__('The {0} article has been deleted.', $article->title));

        return $this->redirect(['action' => 'index']);
    }
}

This logic deletes the article specified by $slug, and uses $this->Flash->success() to show the user a confirmation message after redirecting them to /articles. If the user attempts to delete an article using a GET request, allowMethod() will throw an exception. Uncaught exceptions are captured by CakePHP’s exception handler, and a nice error page is displayed. There are many built-in Exceptions that can be used to indicate the various HTTP errors your application might need to generate.

Warning

Allowing content to be deleted using GET requests is very dangerous, as web crawlers could accidentally delete all your content. That is why we used allowMethod() in our controller.

Because we’re only executing logic and redirecting to another action, this action has no template. You might want to update your index template with links that allow users to delete articles:

<!-- File: templates/Articles/index.php  (delete links added) -->

<h1>Articles</h1>
<p><?= $this->Html->link("Add Article", ['action' => 'add']) ?></p>
<table>
    <tr>
        <th>Title</th>
        <th>Created</th>
        <th>Action</th>
    </tr>

<!-- Here's where we iterate through our $articles query object, printing out article info -->

<?php foreach ($articles as $article): ?>
    <tr>
        <td>
            <?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
        </td>
        <td>
            <?= $article->created->format(DATE_RFC850) ?>
        </td>
        <td>
            <?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?>
            <?= $this->Form->postLink(
                'Delete',
                ['action' => 'delete', $article->slug],
                ['confirm' => 'Are you sure?'])
            ?>
        </td>
    </tr>
<?php endforeach; ?>

</table>

Using postLink() will create a link that uses JavaScript to do a POST request deleting our article.

Note

This view code also uses the FormHelper to prompt the user with a JavaScript confirmation dialog before they attempt to delete an article.

Tip

The ArticlesController can also be built with bake:

/bin/cake bake controller articles

However, this does not build the templates/Articles/*.php files.

With a basic articles management setup, we’ll create the basic actions for our Tags and Users tables.