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
- Convention over Configuration: Minimal setup required
- Built-in ORM: Powerful database abstraction layer
- Rapid Development: Code generation and scaffolding
- Security Features: Built-in protection against common vulnerabilities
- Flexible Templating: Easy view management
- 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>© 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.