CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

Quick start with Migrations plugin

In a previous post I gave an overview of the CakePHP Migrations plugin, what it does and why you should use it in your applications. This article will explain how to use it in a practical way. We are going to bake a simple blog application recipe application and see how migrations are integrated in the development process.

Since we recently moved all our open source projects on http://cakedc.github.com/, this sample application source code is also available there: Sample Migrations Application - Github (it is a CakePHP 1.3 application). Ready?

Bake a new application and add the migrations plugin

First of all, we need to bake a new CakePHP application. Easy enough to do using cake bake, then configure your database (an empty database is sufficient for now) and check that the home page is all green! If you have not set up your environment to use the CakePHP command line yet, take some time to do so... it worth it!

Adding the migrations plugin might also be a straightforward task. You can either download the archive containing the plugin code and unzip it in the "/plugins/migrations" folder of your application, or  add it as a git submodule with the following command:

git submodule add git://github.com/CakeDC/Migrations.git plugins/migrations

Then check that it is correctly installed by executing the following command from your application root:

cake migration help

If you see a list of available commands you can move on next step.

Create initial tables and bake the MVC

We now need something to migrate! Let's create some tables in the database. The application will have Users who can publish Recipes, each one having several Ingredients (of course Ingredients can be used in many Recipes). Here is a SQL dump of this simple database schema:

CREATE TABLE `ingredients` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

CREATE TABLE `ingredients_recipes` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ingredient_id` int(11) NOT NULL,
  `recipe_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

CREATE TABLE `recipes` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `name` varchar(100) NOT NULL,
  `content` text NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `password` varchar(255) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

As our goal here is not to focus on the application code itself, baked MVC from these tables might be sufficient... just run the command cake bake all for User, Recipe and Ingredient to bake'em all!

At this point we must have an application with an initial architecture ready to share. To start from here, one will just have to checkout the related commit... but don't you see a problem with this? How will he create the initial database? Maybe we could send him the SQL dump by email, or better commit it with the application! It is where the Migrations plugin comes in.

Generate the initial migration

"Be kind with your coworkers and include the database schema with your code... along with some sample data."

Let's use the migrations shell to generate an agnostic database schema containing our 4 tables, and an initial admin user account. To do so we just need to run the following command:

cake migration generate

After entering a name for the migration and selected the database dump option, we might have a new "/config/migrations" directory containing two files:

  • map.php representing the different migrations order,
  • name_of_the_migration.php a migration file containing all the necessary information to create your actual database. In the sample application it is named: "001_added_users_recipes_and_ingredients_tables.php". You might have noticed that we added a 001 prefix to the migration name to make it easier to see migrations order, it is a good practice.

We can now open the generated migration file (/config/migrations/001_added_users_recipes_and_ingredients_tables.php) and take a look at it. If you need more information and understand all available migration directives, you can read the plugin documentation.

For now we are just going to focus on the empty "after()" callback. This callback is triggered once the migration has been executed, and allow you to do whatever you want, given the direction of the migration: applied (up) or reverted (down). We are going to use this callback to create an initial admin User. Here is the code of the callback (as you are a CakePHP developer you might understand it quite easily):

function after($direction) {
	if ($direction === 'up') {
		if (!class_exists('Security')) {
			App::import('Core', 'Security');
		}

		$User = $this->generateModel('User');
		$user = array(
			'User' => array(
				'name' => 'admin',
				'password' => Security::hash('unsecurepassword', null, true)));
		$User->save($user);
	}
	return true;
}

Notice the use of the generateModel() method provided by the Migrations plugin. It is a shorthand allowing you to cleanly load a model in the callback to insert new data or update the existing. We could explain the reason of it more deeply but it is not the goal of this article, so just keep in mind that it is the best way to load a Model from callbacks!

Here we are! We can now share the application with anyone. After checked out the application, one will just have to run cake migration all to turn an empty database to a database containing all the needed tables, and an initial admin user to start using the application.

Categorize the recipes!

As the application evolves, we need to sort recipes by categories. This change involves two changes in the current database schema: a new categories table must be created, and a category_id field added to the recipes table.

Note: If you later want to use the migrations diff feature to generate a migration containing a diff between your previous database schema and the current one, you have to generate a Cake Schema of your database at this point. Simply run cake schema generate.

We can now update the recipes table and create a new categories table. Here is a simple SQL script:

CREATE TABLE `categories` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;
ALTER TABLE `recipes` ADD `category_id` INT NOT NULL

Bake the MVC for categories and update recipes view pages to display the category so the application reflect these database changes. Before sharing these code changes, we need to generate a second migration describing the above SQL snippet in an agnostic way... and creating initial categories!

Nothing different than what we did previously: run cake migration generate, give a name to the migration, and choose between generating a diff from the schema.php file (if one was generated), generating a dump of the database (we will remove unnecessary instructions later) or generating an empty migration file. Once generated, it is always important to check the generated directives for the migration and fix them if needed. The migration must look like this:

var $migration = array(
	'up' => array(
		'create_table' => array(
			'categories' => array(
				'id' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'key' => 'primary'),
				'name' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 100),
				'indexes' => array(
					'PRIMARY' => array('column' => 'id', 'unique' => 1),
				),
				'tableParameters' => array('charset' => 'latin1', 'collate' => 'latin1_swedish_ci', 'engine' => 'MyISAM'),
			),
		),
		'create_field' => array(
			'recipes' => array(
				'category_id' => array('type' => 'integer', 'null' => false, 'default' => NULL)
			),
		),
	),
	'down' => array(
		'drop_table' => array(
			'categories'
		),
		'drop_field' => array(
			'recipes' => array(
				'category_id'
			),
		),
	),
);

If you understood what we did in the first migration callback to add an initial user you might be able to implement this one. We would like to add initial categories: Starters, Main Dish and Desserts.

For lazy people, the code is here:

function after($direction) {
	if ($direction === 'up') {
		$Category = $this->generateModel('Category');
		$categories = array(
			array('name' => 'Starters'),
			array('name' => 'Main Dish'),
			array('name' => 'Desserts'));
		$Category->saveAll($categories);
	}
	return true;
}

Here we are again! The changes are ready to commit, and the commit will contains both code and database changes. One could update the database after checking out this commit by running: cake migration all.

The end

I hope this very simple use case and the code we built will help you to start using Migrations. As you could see it is very simple to use and will make your life much more easier: you would not have to worry anymore about the state of your database schema.

The source code of this tutorial is available on Github. If you found any bug or have any suggestion about the Migrations plugin, please create a ticket on Github. Comment this article if you have any question, and do not hesitate to share it if you found it useful!

Latest articles

Our Gift To You - The CakeDC Advent Calendar 2024

So, we are back! It’s been a while, right?    We are aware that the blog has been quiet, but boy do we have a surprise for you all.    First, let’s recap this year… a lot of releases from CakeDC like plugins and contributions to the latest CakePHP versions. We, like every other baker, have been enjoying all of the new features that Cake 5 has presented. We look forward to seeing what the core team has in store next. It is a company goal to be more involved in the CakePHP community in 2025, so you’ll be seeing some familiar faces in the community channels.     Oh, and I would be remiss if I did not also express our gratitude to our clients. We have had a great year with all of you.  We are so thankful to work with each and every one of you and it has been a pleasure baking code for your many projects.    Now… on to the good stuff.  The team has decided to write a series of blogs, in the form of an advent calendar. It is the holiday season after all. So what can you expect? I don’t want to give away too much, but I will say you’ll get to savor some tasty cake related topics like: HTMX, JWT with CakePHP, plugins, security, PHP 8.4 … and so much more.    This is the gift that keeps on giving... the whole month. So pre heat those ovens… and get ready for a new blog each day from the CakeDC team! 
CakeDC Advent Calendar 2024! 

Build a Single Page Application Using CakePHP and InertiaJS

Build a Single Page Application using CakePHP and InertiaJS

  The Inertia Plugin allows a CakePHP application to integrate Vue 3 components in the front end, without the need to write a specific API for data transfer. This is done  by adding a Middleware and view classes that facilitate the conversion of objects and data in JSON almost automatically, as well as the direct load in the components. The plugin is thought of as a base to extend and use your app’s specific controllers and views from. Just because  it works out of the box doesn't mean it is intended to be used exactly as is,  but this will  provide you a good kick start. See the repo here: https://github.com/CakeDC/cakephp-inertia

Requirements

  • CakePHP 4.5
  • PHP >= 8.1
  • NodeJS 18.9 (only for build Vue Components, not required on running site)

 

Step 1: Create a basic CakePHP install

  For this example I will use a basic installation using Docker and Composer.  First you must create project from cakephp/app  
$> composer create-project --prefer-dist cakephp/app:~4.5 inertia_app $> cd inertia_app $> cp config/app_local.example.php config/app_local.php
  Then write an docker-compose.yml file as:
version: '3' services:   psql13:     image: postgres:13     container_name: inertia-app-postgres13     volumes:       - ./tmp/data/inertia-postgres13__db:/var/lib/postgresql:delegated     environment:       - POSTGRES_USER=my_app       - POSTGRES_PASSWORD=secret       - POSTGRES_DB=my_app       - PGUSER=my_app       - PGDATABASE=my_app       - PGPASSWORD=secret     ports:       - '7432:5432'     cakephp:     image: webdevops/php-nginx:8.1     container_name: inertia-app-cakephp     working_dir: /application     volumes:       - ./:/application:cached       - ~/.ssh:/home/application/.ssh:ro     environment:       - WEB_DOCUMENT_ROOT=/application/webroot       - DATABASE_URL=postgres://my_app:secret@inertia-app-postgres13:5432/my_app     ports:       - "9099:80"
  Launch the container and go to http://localhost:9099/  
$> docker-compose up -d
 

Step 2: Add CakePHP Inertia plugin

  Install plugin via command line:
$> composer require cakedc/cakephp-inertia
  Once installed enable it in src/Application.php, adding at the bottom of bootstrap function:
$this->addPlugin('CakeDC/Inertia');
  or by command line:
$> bin/cake plugin load CakeDC/Inertia

 

Step 3: Create Vue App and install it

  To create Vue App type in command line:
$> bin/cake create_vue_app
  This command create in the resources directory the files that use our App, also create in root directory the files:
  • webpack.mix.js
  • package.json
  Then in root directory install with NPM:
$> npm install

 

Step 4: Create simple SPA (Single Page Application)

  Create a single page called dashboard that show values sets in a controller action We need to first add InertiaResponseTrait  
use CakeDC\Inertia\Traits\InertiaResponseTrait;   class PagesController extends AppController {    use InertiaResponseTrait;    ...  ...   }
  Create a new function that would look like this:
public function dashboard() {   //set default php layout of plugin that use vue   $this->viewBuilder()->setTheme('CakeDC/Inertia');     $page = [       'text' => 'hello world 1',       'other' => 'hello world 2',   ];   $this->set(compact('page')); }
  in config/routes.php uncomment lines to catch all routes:
$builder->connect('/{controller}', ['action' => 'index']); $builder->connect('/{controller}/{action}/*', []);
and comment line:
$builder->connect('/pages/*', 'Pages::display');
  Then create file resources/js/Components/Pages/Dashboard.vue that would look like this:
<script setup> import Layout from '../Layout' import { Head } from '@inertiajs/vue3' import {onMounted} from "vue";   defineProps({     csrfToken: String,     flash: Array,     page: Array, })     onMounted(() => {     console.log('Component Dashboard onMounted hook called') }) </script>   <template>     <Layout>         <Head title="Welcome" />         <h1>Welcome</h1>         <p>{{page.text}}</p>         <p>{{page.other}}</p>     </Layout> </template>
  On root directory execute:
$> npm run dev
  IMPORTANT: Whenever you modify the .vue templates, you must run this script. Go to http://localhost:9099/pages/dashboard to see that Dashboard Vue Component prints values assignments on Dashboard CakePHP function.
   

 

Step 5: Bake CRUD system

  For this example, we use sql file on config/sql/example/postgresql.pgsql   That creates a database with the relations     Once the database has been created, bake models and controllers as normal using:
$> bin/cake bake model Pages --theme CakeDC/Inertia $> bin/cake bake controller Pages --theme CakeDC/Inertia $> bin/cake bake model Tags --theme CakeDC/Inertia $> bin/cake bake controller Tags --theme CakeDC/Inertia $> bin/cake bake model Categories --theme CakeDC/Inertia $> bin/cake bake controller Categories --theme CakeDC/Inertia
  and bake templates using vue_template instead of template as:
$> bin/cake bake vue_template Pages --theme CakeDC/Inertia $> bin/cake bake vue_template Tags --theme CakeDC/Inertia $> bin/cake bake vue_template Categories --theme CakeDC/Inertia
  Again run:
$> npm run dev
  You can the results from this example by going to http://localhost:9099/pages/index   In the following recording you can see how to add, edit and delete a record without reloading the page at any time.

 

Step 6: Using prefix and adding a navigation menu

  Add route to prefix Admin on config/routes.php
$builder->prefix('admin', function (RouteBuilder $builder) {    $builder->fallbacks(DashedRoute::class); });
  To generate controllers and template with a prefix use --prefix option of bake command as:
$> bin/cake bake controller Pages --prefix Admin --theme CakeDC/Inertia $> bin/cake bake controller Tags --prefix Admin --theme CakeDC/Inertia $> bin/cake bake controller Categories --prefix Admin --theme CakeDC/Inertia $> bin/cake bake vue_template Pages --prefix Admin --theme CakeDC/Inertia $> bin/cake bake vue_template Tags --prefix Admin --theme CakeDC/Inertia $> bin/cake bake vue_template Categories --prefix Admin --theme CakeDC/Inertia
  You can add a horizontal menu to navigate through controllers   Edit resources/Components/Layout.vue and put inside header tag links as:
<header>    <Link as="button" href="/pages/index" class="button shadow radius right small">Pages</Link>    <Link as="button" href="/tags/index" class="button shadow radius right small">Tags</Link>    <Link as="button" href="/categories/index" class="button shadow radius right small">Categories</Link> </header>
  Again run:
$> npm run dev
  You can see the results from this  example by going to http://localhost:9099/admin/pages/index   In the following recording you can see how to add, edit and delete a record without reloading the page at any time and navigate through pages, tags and categories.

  Hopefully this example will make your experience easier! Let us know: [email protected].

When and why should you upgrade to CakePHP 5?

CakePHP 5.0.0 was released on September 10th. The current version as of today is 5.0.3 (released Nov 28th and compatible with PHP 8.3 https://github.com/cakephp/cakephp/releases/tag/5.0.3). You might be asking yourself some questions related to the upgrade… here's what we've been recommending to our clients to do since version 5 was released. Leaving aside the obvious reasons for an upgrade, today we're going to categorize the decision from 2 different points of view: Your current CakePHP version, and your role in the project.

When should you upgrade? 

  We are going to use current CakePHP version as the main criteria: * If you are in CakePHP <= 2   * We strongly recommend an upgrade as soon as possible. If you are unable to upgrade, try to keep your PHP version and all the underlying dependencies as fresh as you can and isolate the application as much as possible. If your application is internal, consider using a VPN blocking all outside traffic. If your site is open to the public, consider using an isolated environment, hardened. Adding a web application firewall and a strict set of rules could also help to mitigate potential security issues. Even if CakePHP is very secure, the older versions of CakePHP, like  1 and 2  have a very old code base , and other vendors/ libraries could be a serious security risk for your project at this point.   * If you are in CakePHP 3.x   * The effort to upgrade at least to CakePHP 4.x should not be a blocker. We would recommend upgrading at least to the latest CakePHP 4.5.x. You can actually "ignore" the deprecations for now, you don't need to plan for upgrading your authentication/authorization layers just yet, focus on getting your project stable and up to CakePHP 4.5.x in the first round.   * If you are in CakePHP 4.x   * Upgrading to CakePHP 5.x is not an immediate priority for you.   * I would say, 2024 is a good time to start planning for an upgrade. Feature and bugfix releases for 4.x will continue until September 2025. Security fixes will continue for 4.x until September 2026. You have plenty of time to consider an upgrade, and take advantage of newer (and faster!) PHP versions.  

Why should you upgrade? 

  We are going to use your role in the project to provide some good reasons: * If you are a developer   * More strict types, meaning better IDE support and more errors catched at development time.   * New features in CakePHP 5.x will make your code more readable, like Typed finder parameters https://book.cakephp.org/5/en/appendices/5-0-migration-guide.html#typed-finder-parameters      * Quality of life features, reducing development time like https://book.cakephp.org/5/en/appendices/5-0-migration-guide.html#plugin-installer   * Compatibility with PHP 8.3 for extra performance & support   * If you are a manager   * Ensure your development team is forced to drop old auth code and embrace the new authentication/authorization layer https://book.cakephp.org/5/en/appendices/5-0-migration-guide.html#auth   * The new authentication layer will allow you to easily integrate features like single sign on, two factor authentication or hardware keys (like Yubikeys), as there are plugins available handling all these features.   * Get an extended support window. CakePHP is one of the longest maintained frameworks out there, upgrading to CakePHP 5 will keep your core maintained past 2026.   * Upgrade to PHP 8.3 and force legacy vendors to be up to date with the new version, this will also push your team to get familiar with the new PHP core features.   * If you are an investor, not directly related with the project day-to-day operations   * Secure your inversion for a longer period.   * Reduce your exposure to security issues.   * Send a strong message to your partners, keeping your product updated with the latest technology trends.   * Send a strong message to your team, investing in the upgrade of your application will let them know the project is aiming for a long term future.   In conclusion, upgrading to CakePHP 5 is a good move for 2024 whether you're a developer, manager, or investor. The version 5 is stable and ready to go. Staying current becomes not just a best practice but a strategic advantage.   If you are in doubt, feel free to contact us. We'll review your case (for free) and provide an actionable recommendation based on your current situation in the next business day.  

We Bake with CakePHP