1. php
  2. /frameworks
  3. /cakephp

CakePHP Framework

Introduction to CakePHP

CakePHP is a rapid development framework that follows the "Convention over Configuration" principle. It provides a structured, reusable way to create web applications with built-in ORM, flexible templating, and powerful scaffolding features.

Key Features

  1. Convention over Configuration: Minimal setup required
  2. Built-in ORM: Powerful database abstraction layer
  3. Rapid Development: Code generation and scaffolding
  4. Security Features: Built-in protection against common vulnerabilities
  5. Flexible Templating: Easy view management
  6. Active Community: Extensive plugin ecosystem

Installation and Setup

Installing CakePHP 4

# Using Composer
composer create-project --prefer-dist cakephp/app:~4.0 my_app_name

# Navigate to project
cd my_app_name

# Start development server
bin/cake server

Project Structure

my_app_name/
├── bin/
│   └── cake
├── config/
│   ├── app.php
│   └── routes.php
├── src/
│   ├── Controller/
│   ├── Model/
│   │   ├── Entity/
│   │   └── Table/
│   └── View/
├── templates/
│   ├── layout/
│   └── Pages/
├── tests/
├── tmp/
└── webroot/
    └── index.php

Conventions and Configuration

Naming Conventions

<?php
// Table: users
// Model: User (Entity), UsersTable (Table class)
// Controller: UsersController
// Views: src/Template/Users/

// Table: blog_posts
// Model: BlogPost, BlogPostsTable
// Controller: BlogPostsController
// Views: src/Template/BlogPosts/

Database Configuration

<?php
// config/app.php
return [
    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            'username' => 'my_app',
            'password' => 'secret',
            'database' => 'my_app',
            'encoding' => 'utf8mb4',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
        ],
    ],
];

Controllers

Basic Controller

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

class ArticlesController extends AppController
{
    public function index()
    {
        $articles = $this->Articles->find('all');
        $this->set(compact('articles'));
    }
    
    public function view($id = null)
    {
        $article = $this->Articles->get($id, [
            'contain' => ['Users', 'Comments']
        ]);
        $this->set(compact('article'));
    }
    
    public function add()
    {
        $article = $this->Articles->newEmptyEntity();
        
        if ($this->request->is('post')) {
            $article = $this->Articles->patchEntity($article, $this->request->getData());
            
            if ($this->Articles->save($article)) {
                $this->Flash->success(__('Article saved.'));
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('Unable to add article.'));
        }
        
        $users = $this->Articles->Users->find('list');
        $this->set(compact('article', 'users'));
    }
    
    public function edit($id = null)
    {
        $article = $this->Articles->get($id);
        
        if ($this->request->is(['patch', 'post', 'put'])) {
            $article = $this->Articles->patchEntity($article, $this->request->getData());
            
            if ($this->Articles->save($article)) {
                $this->Flash->success(__('Article updated.'));
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('Unable to update article.'));
        }
        
        $users = $this->Articles->Users->find('list');
        $this->set(compact('article', 'users'));
    }
    
    public function delete($id = null)
    {
        $this->request->allowMethod(['post', 'delete']);
        
        $article = $this->Articles->get($id);
        
        if ($this->Articles->delete($article)) {
            $this->Flash->success(__('Article deleted.'));
        } else {
            $this->Flash->error(__('Unable to delete article.'));
        }
        
        return $this->redirect(['action' => 'index']);
    }
}

RESTful API Controller

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

use App\Controller\AppController;
use Cake\Http\Exception\BadRequestException;
use Cake\Http\Exception\NotFoundException;

class ArticlesController extends AppController
{
    public function initialize(): void
    {
        parent::initialize();
        $this->loadComponent('RequestHandler');
    }
    
    public function index()
    {
        $articles = $this->Articles->find('all', [
            'contain' => ['Users']
        ]);
        
        $this->set([
            'articles' => $articles,
            '_serialize' => ['articles']
        ]);
    }
    
    public function view($id)
    {
        try {
            $article = $this->Articles->get($id, [
                'contain' => ['Users', 'Comments']
            ]);
        } catch (\Exception $e) {
            throw new NotFoundException(__('Article not found'));
        }
        
        $this->set([
            'article' => $article,
            '_serialize' => ['article']
        ]);
    }
    
    public function add()
    {
        $this->request->allowMethod(['post']);
        
        $article = $this->Articles->newEntity($this->request->getData());
        
        if ($this->Articles->save($article)) {
            $message = 'Article created successfully';
            $this->set([
                'message' => $message,
                'article' => $article,
                '_serialize' => ['message', 'article']
            ]);
        } else {
            throw new BadRequestException(__('Unable to create article'));
        }
    }
    
    public function edit($id)
    {
        $this->request->allowMethod(['patch', 'put']);
        
        $article = $this->Articles->get($id);
        $article = $this->Articles->patchEntity($article, $this->request->getData());
        
        if ($this->Articles->save($article)) {
            $message = 'Article updated successfully';
        } else {
            throw new BadRequestException(__('Unable to update article'));
        }
        
        $this->set([
            'message' => $message,
            'article' => $article,
            '_serialize' => ['message', 'article']
        ]);
    }
}

Models (ORM)

Entity Classes

<?php
// src/Model/Entity/Article.php
namespace App\Model\Entity;

use Cake\ORM\Entity;
use Cake\Utility\Text;

class Article extends Entity
{
    protected $_accessible = [
        'title' => true,
        'body' => true,
        'user_id' => true,
        'published' => true,
        'created' => true,
        'modified' => true
    ];
    
    protected $_hidden = ['user_id'];
    
    // Virtual fields
    protected function _getSlug()
    {
        return Text::slug(strtolower($this->title));
    }
    
    protected function _getFullTitle()
    {
        return $this->title . ' - Published';
    }
    
    // Custom setters
    protected function _setTitle($title)
    {
        return ucwords($title);
    }
    
    // Validation
    public function validationDefault($validator)
    {
        $validator->notEmptyString('title', 'Title is required')
                 ->minLength('title', 3, 'Title must be at least 3 characters')
                 ->maxLength('title', 255, 'Title cannot exceed 255 characters');
                 
        $validator->notEmptyString('body', 'Body is required')
                 ->minLength('body', 10, 'Body must be at least 10 characters');
                 
        return $validator;
    }
}

Table Classes

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

use Cake\ORM\Table;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;

class ArticlesTable extends Table
{
    public function initialize(array $config): void
    {
        parent::initialize($config);
        
        $this->setTable('articles');
        $this->setDisplayField('title');
        $this->setPrimaryKey('id');
        
        $this->addBehavior('Timestamp');
        
        // Associations
        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
            'joinType' => 'INNER'
        ]);
        
        $this->hasMany('Comments', [
            'foreignKey' => 'article_id'
        ]);
        
        $this->belongsToMany('Tags', [
            'foreignKey' => 'article_id',
            'targetForeignKey' => 'tag_id',
            'joinTable' => 'articles_tags'
        ]);
    }
    
    public function validationDefault(Validator $validator): Validator
    {
        $validator->integer('id')
                 ->allowEmptyString('id', null, 'create');
                 
        $validator->scalar('title')
                 ->maxLength('title', 255)
                 ->requirePresence('title', 'create')
                 ->notEmptyString('title');
                 
        $validator->scalar('body')
                 ->requirePresence('body', 'create')
                 ->notEmptyString('body');
                 
        $validator->boolean('published')
                 ->notEmptyString('published');
                 
        return $validator;
    }
    
    public function buildRules(RulesChecker $rules): RulesChecker
    {
        $rules->add($rules->existsIn(['user_id'], 'Users'));
        $rules->add($rules->isUnique(['title'], 'Title must be unique'));
        
        return $rules;
    }
    
    // Custom finder methods
    public function findPublished($query, $options)
    {
        return $query->where(['published' => true]);
    }
    
    public function findByUser($query, $options)
    {
        return $query->where(['user_id' => $options['user_id']]);
    }
    
    public function findRecent($query, $options)
    {
        return $query->order(['created' => 'DESC'])
                    ->limit(10);
    }
}

Advanced Queries

<?php
// In Controller
public function complexQuery()
{
    // Basic find with conditions
    $articles = $this->Articles->find()
        ->where(['published' => true])
        ->contain(['Users', 'Comments'])
        ->order(['created' => 'DESC'])
        ->limit(10);
    
    // Using custom finders
    $published = $this->Articles->find('published');
    $recent = $this->Articles->find('recent');
    $userArticles = $this->Articles->find('byUser', ['user_id' => 1]);
    
    // Complex query with joins and conditions
    $query = $this->Articles->find()
        ->select(['Articles.id', 'Articles.title', 'Users.username'])
        ->innerJoinWith('Users')
        ->where([
            'Articles.published' => true,
            'Articles.created >=' => new \DateTime('-30 days')
        ])
        ->order(['Articles.created' => 'DESC']);
    
    // Aggregations
    $stats = $this->Articles->find()
        ->select([
            'user_id',
            'count' => $query->func()->count('*'),
            'avg_length' => $query->func()->avg('LENGTH(body)')
        ])
        ->group(['user_id'])
        ->toArray();
        
    $this->set(compact('articles', 'stats'));
}

Views and Templates

Layout Template

<?php
// templates/layout/default.php
?>
<!DOCTYPE html>
<html>
<head>
    <?= $this->Html->charset() ?>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>
        <?= $cakeDescription ?>:
        <?= $this->fetch('title') ?>
    </title>
    <?= $this->Html->meta('icon') ?>
    
    <?= $this->Html->css('bootstrap.min.css') ?>
    <?= $this->fetch('meta') ?>
    <?= $this->fetch('css') ?>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <?= $this->Html->link('My App', ['controller' => 'Pages', 'action' => 'display', 'home'], ['class' => 'navbar-brand']) ?>
            
            <div class="navbar-nav">
                <?= $this->Html->link('Articles', ['controller' => 'Articles', 'action' => 'index'], ['class' => 'nav-link']) ?>
                <?= $this->Html->link('Users', ['controller' => 'Users', 'action' => 'index'], ['class' => 'nav-link']) ?>
            </div>
        </div>
    </nav>
    
    <main class="container mt-4">
        <?= $this->Flash->render() ?>
        <?= $this->fetch('content') ?>
    </main>
    
    <footer class="mt-5 py-3 bg-light">
        <div class="container text-center">
            <p>&copy; 2023 My App. All rights reserved.</p>
        </div>
    </footer>
    
    <?= $this->fetch('script') ?>
</body>
</html>

View Templates

<?php
// templates/Articles/index.php
?>
<h1>Articles</h1>

<?= $this->Html->link('Add Article', ['action' => 'add'], ['class' => 'btn btn-primary mb-3']) ?>

<div class="row">
    <?php foreach ($articles as $article): ?>
        <div class="col-md-6 mb-4">
            <div class="card">
                <div class="card-body">
                    <h5 class="card-title"><?= h($article->title) ?></h5>
                    <p class="card-text"><?= h(substr($article->body, 0, 150)) ?>...</p>
                    <p class="text-muted">
                        By <?= h($article->user->username) ?> on 
                        <?= $article->created->format('M d, Y') ?>
                    </p>
                    <div class="btn-group">
                        <?= $this->Html->link('View', ['action' => 'view', $article->id], ['class' => 'btn btn-sm btn-outline-primary']) ?>
                        <?= $this->Html->link('Edit', ['action' => 'edit', $article->id], ['class' => 'btn btn-sm btn-outline-secondary']) ?>
                        <?= $this->Form->postLink('Delete', ['action' => 'delete', $article->id], [
                            'confirm' => 'Are you sure?',
                            'class' => 'btn btn-sm btn-outline-danger'
                        ]) ?>
                    </div>
                </div>
            </div>
        </div>
    <?php endforeach; ?>
</div>

Form Templates

<?php
// templates/Articles/add.php
?>
<h1>Add Article</h1>

<?= $this->Form->create($article) ?>
<fieldset>
    <legend><?= __('Add Article') ?></legend>
    <?php
        echo $this->Form->control('title', ['class' => 'form-control']);
        echo $this->Form->control('body', [
            'type' => 'textarea',
            'rows' => 10,
            'class' => 'form-control'
        ]);
        echo $this->Form->control('user_id', [
            'type' => 'select',
            'options' => $users,
            'empty' => 'Select User',
            'class' => 'form-control'
        ]);
        echo $this->Form->control('published', ['class' => 'form-check-input']);
    ?>
</fieldset>
<?= $this->Form->button(__('Submit'), ['class' => 'btn btn-primary']) ?>
<?= $this->Form->end() ?>

Routing

Basic Routing

<?php
// config/routes.php
use Cake\Routing\RouteBuilder;
use Cake\Routing\Router;

Router::defaultRouteClass('DashedRoute');

Router::scope('/', function (RouteBuilder $builder) {
    $builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
    $builder->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
    
    // Custom routes
    $builder->connect('/articles/recent', ['controller' => 'Articles', 'action' => 'recent']);
    $builder->connect('/articles/{slug}', ['controller' => 'Articles', 'action' => 'view'])
            ->setPass(['slug'])
            ->setPatterns(['slug' => '[a-z0-9\-]+']);
    
    // RESTful routing
    $builder->resources('Articles');
    $builder->resources('Users', function (RouteBuilder $routes) {
        $routes->resources('Articles');
    });
    
    $builder->fallbacks('DashedRoute');
});

// API routes
Router::prefix('api', function (RouteBuilder $routes) {
    $routes->setExtensions(['json']);
    $routes->resources('Articles');
    $routes->resources('Users');
});

Components and Helpers

Custom Component

<?php
// src/Controller/Component/AuthComponent.php
namespace App\Controller\Component;

use Cake\Controller\Component;
use Cake\Controller\ComponentRegistry;

class AuthComponent extends Component
{
    public function initialize(array $config): void
    {
        parent::initialize($config);
    }
    
    public function login($userData)
    {
        $session = $this->getController()->getRequest()->getSession();
        $session->write('Auth.User', $userData);
        
        return true;
    }
    
    public function logout()
    {
        $session = $this->getController()->getRequest()->getSession();
        $session->delete('Auth.User');
        
        return true;
    }
    
    public function user($key = null)
    {
        $session = $this->getController()->getRequest()->getSession();
        $user = $session->read('Auth.User');
        
        if ($key === null) {
            return $user;
        }
        
        return $user[$key] ?? null;
    }
    
    public function check()
    {
        $session = $this->getController()->getRequest()->getSession();
        return $session->check('Auth.User');
    }
}

Custom Helper

<?php
// src/View/Helper/FormatHelper.php
namespace App\View\Helper;

use Cake\View\Helper;

class FormatHelper extends Helper
{
    public function currency($amount, $currency = 'USD')
    {
        $formatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY);
        return $formatter->formatCurrency($amount, $currency);
    }
    
    public function truncate($text, $length = 100, $ending = '...')
    {
        if (strlen($text) <= $length) {
            return $text;
        }
        
        return substr($text, 0, $length) . $ending;
    }
    
    public function timeAgo($datetime)
    {
        $time = time() - strtotime($datetime);
        
        if ($time < 60) return 'just now';
        if ($time < 3600) return floor($time/60) . ' minutes ago';
        if ($time < 86400) return floor($time/3600) . ' hours ago';
        if ($time < 2629746) return floor($time/86400) . ' days ago';
        
        return date('M j, Y', strtotime($datetime));
    }
}

// Usage in templates
<?= $this->Format->currency(99.99) ?>
<?= $this->Format->truncate($article->body, 150) ?>
<?= $this->Format->timeAgo($article->created) ?>

Middleware and Security

Custom Middleware

<?php
// src/Middleware/CorsMiddleware.php
namespace App\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class CorsMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if ($request->getHeaderLine('Origin')) {
            $response = $handler->handle($request);
            
            return $response
                ->withHeader('Access-Control-Allow-Origin', '*')
                ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
                ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
        }
        
        return $handler->handle($request);
    }
}

Authentication

<?php
// 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());
        } else {
            $this->Flash->error('Invalid username or password');
        }
    }
}

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

// In AppController
public function initialize(): void
{
    parent::initialize();
    
    $this->loadComponent('Auth', [
        'authenticate' => [
            'Form' => [
                'fields' => [
                    'username' => 'email',
                    'password' => 'password'
                ]
            ]
        ],
        'loginAction' => [
            'controller' => 'Users',
            'action' => 'login'
        ],
        'unauthorizedRedirect' => $this->referer()
    ]);
}

Testing

Unit Testing

<?php
// tests/TestCase/Model/Table/ArticlesTableTest.php
namespace App\Test\TestCase\Model\Table;

use App\Model\Table\ArticlesTable;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\TestCase;

class ArticlesTableTest extends TestCase
{
    public $fixtures = [
        'app.Articles',
        'app.Users',
        'app.Comments'
    ];
    
    public function setUp(): void
    {
        parent::setUp();
        $this->Articles = TableRegistry::getTableLocator()->get('Articles');
    }
    
    public function tearDown(): void
    {
        unset($this->Articles);
        parent::tearDown();
    }
    
    public function testFindPublished()
    {
        $result = $this->Articles->find('published');
        $this->assertInstanceOf('Cake\ORM\Query', $result);
        
        $articles = $result->toArray();
        $this->assertNotEmpty($articles);
        
        foreach ($articles as $article) {
            $this->assertTrue($article->published);
        }
    }
    
    public function testValidation()
    {
        $article = $this->Articles->newEntity([
            'title' => 'Te', // Too short
            'body' => '', // Empty
        ]);
        
        $this->assertFalse($this->Articles->save($article));
        $this->assertNotEmpty($article->getErrors());
    }
}

Controller Testing

<?php
// tests/TestCase/Controller/ArticlesControllerTest.php
namespace App\Test\TestCase\Controller;

use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;

class ArticlesControllerTest extends TestCase
{
    use IntegrationTestTrait;
    
    public $fixtures = [
        'app.Articles',
        'app.Users'
    ];
    
    public function testIndex()
    {
        $this->get('/articles');
        $this->assertResponseOk();
        $this->assertResponseContains('Articles');
    }
    
    public function testAdd()
    {
        $data = [
            'title' => 'New Article',
            'body' => 'This is a new article body.',
            'user_id' => 1,
            'published' => true
        ];
        
        $this->post('/articles/add', $data);
        $this->assertRedirect(['action' => 'index']);
        $this->assertSession('Article saved.', 'Flash.flash.0.message');
    }
}

Console Commands

Custom Shell

<?php
// src/Command/ReportCommand.php
namespace App\Command;

use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;

class ReportCommand extends Command
{
    public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
    {
        $parser->addArgument('type', [
            'help' => 'The type of report to generate',
            'required' => true,
            'choices' => ['daily', 'weekly', 'monthly']
        ]);
        
        $parser->addOption('email', [
            'help' => 'Email address to send report to',
            'short' => 'e'
        ]);
        
        return $parser;
    }
    
    public function execute(Arguments $args, ConsoleIo $io): int
    {
        $type = $args->getArgument('type');
        $email = $args->getOption('email');
        
        $io->out("Generating {$type} report...");
        
        // Generate report logic here
        $this->generateReport($type);
        
        if ($email) {
            $this->sendReport($email);
            $io->success("Report sent to {$email}");
        }
        
        $io->success('Report generated successfully!');
        
        return static::CODE_SUCCESS;
    }
    
    private function generateReport($type)
    {
        // Report generation logic
    }
    
    private function sendReport($email)
    {
        // Email sending logic
    }
}

// Usage: bin/cake report daily --email [email protected]

Plugins

Creating a Plugin

# Generate plugin structure
bin/cake bake plugin ContactManager

# Generate plugin controller
bin/cake bake controller --plugin ContactManager Contacts

# Generate plugin model
bin/cake bake model --plugin ContactManager Contacts

Plugin Structure

plugins/ContactManager/
├── config/
├── src/
│   ├── Controller/
│   ├── Model/
│   └── Template/
├── tests/
└── webroot/

Loading and Using Plugins

<?php
// config/bootstrap.php
Plugin::load('ContactManager', ['bootstrap' => true, 'routes' => true]);

// Or in Application.php
public function bootstrap(): void
{
    parent::bootstrap();
    $this->addPlugin('ContactManager');
}

// Using plugin components
$this->loadComponent('ContactManager.Contact');

// Using plugin helpers
$this->loadHelper('ContactManager.Contact');

CakePHP's convention-over-configuration approach makes it one of the fastest frameworks for rapid application development, with powerful ORM capabilities and extensive built-in features.