Skip to content
This is the documentation for pieceofcake2/app-legacy, a CakePHP 2.x plugin for pieceofcake2/cakephp. About this fork.

Application Skeleton Documentation ​

pieceofcake2/app-legacy is the application skeleton for CakePHP 2 applications running on pieceofcake2/cakephp. It uses a directory layout compatible with CakePHP 5 (config/, src/, templates/, tests/, webroot/) and loads CakePHP and all dependencies with Composer. Its application classes are global and its templates are .ctp files; for namespaced classes (App\) and .php templates, use pieceofcake2/app.

WARNING

CakePHP 2.x is for the maintenance of existing applications. For new projects, use the current major version of CakePHP instead.

To move an existing application with the legacy app/ layout to this layout, see Migrating to the Modern Layout.

Requirements ​

  • PHP 8.0, 8.1, 8.2, 8.3, 8.4 or 8.5
  • Composer
  • Database: MySQL 5.6+, PostgreSQL 9.4+, SQLite 3 or SQL Server 2022+
  • PHP extensions: mbstring, intl, openssl and the PDO driver of your database

See the requirements of pieceofcake2/cakephp for details.

Installation ​

Create a new application with Composer's create-project command:

bash
composer create-project pieceofcake2/app-legacy my_app

This creates the my_app directory and installs CakePHP (pieceofcake2/cakephp) into vendor/. The Bake and DebugKit plugins and the development tools (PHPUnit, PHPStan with pieceofcake2/phpstan-cakephp2, and the CakePHP coding standard) are installed as development dependencies, with the Markdown docs of the CakePHP 2 book for AI coding tools (pieceofcake2/docs, in vendor/pieceofcake2/docs/docs/).

Then finish the configuration:

  1. Copy config/database.php.default to config/database.php, and set the connection with DATABASE_URL in config/.env.local (see Environment Variables (.env) and Database and Mailer DSNs) or enter it in the file. Set the test database in config/.env.test.local (see Running Tests).
  2. Change Security.salt and Security.cipherSeed in config/core.php, or set SECURITY_SALT and SECURITY_CIPHER_SEED in config/.env.local. The skeleton ships with well-known example values that must not be used in a real application. See Generating the Security Salts.
  3. If you send email, copy config/email.php.default to config/email.php, and set the transport with MAILER_DSN or configure it in the file.
  4. Make sure that the web server can write to tmp/ and logs/.

Version Control ​

The .gitignore file of the skeleton is meant for your application, so you can run git init right after create-project. It ignores:

  • vendor/ and the plugins that Composer installs into plugins/ (plugins/Bake/ and plugins/DebugKit/). If you install more plugins with Composer, add them to .gitignore; the plugins of your own application in plugins/ are committed.
  • the contents of tmp/ and logs/. The empty files that keep the directories are committed.
  • config/database.php and config/email.php, and the .local files of .env (config/.env.local and config/.env.*.local), which hold the settings and credentials of each machine. config/.env and config/.env.test are committed, as in Symfony; keep real secrets in the .local files or in real environment variables.
  • phpunit.xml (a local copy of phpunit.xml.dist) and .phpunit.result.cache.

Commit composer.lock, so that every environment installs the same versions of the dependencies.

Development Plugins ​

config/bootstrap.php loads the Bake and DebugKit plugins when they are installed:

php
use Cake\Core\CakePlugin;
use Cake\Error\MissingPluginException;

foreach (['Bake', 'DebugKit'] as $plugin) {
    try {
        CakePlugin::load($plugin);
    } catch (MissingPluginException) {
        // The plugin is not installed.
    }
}

They are development dependencies, so they are skipped when the application is installed with composer install --no-dev. Run Bake with:

bash
bin/cake bake

To show the DebugKit toolbar, add the DebugKit.Toolbar component to your AppController (see the DebugKit documentation). Add it only when the plugin is loaded, so that the application still works without the development dependencies:

php
use Cake\Controller\Controller;
use Cake\Core\CakePlugin;

class AppController extends Controller
{
    public function __construct($request = null, $response = null)
    {
        if (CakePlugin::loaded('DebugKit')) {
            $this->components[] = 'DebugKit.Toolbar';
        }
        parent::__construct($request, $response);
    }
}

The toolbar is only shown when debug is greater than 0.

Load the other plugins in config/bootstrap.php with CakePlugin::load() (or CakePlugin::loadAll()). CakePlugin::load() throws a MissingPluginException when the plugin is not installed, so wrap the plugins that are development dependencies as shown above.

Directory Layout ​

my_app/
├── bin/
│   └── cake            the console
├── config/             configuration (core.php, bootstrap.php, routes.php, database.php, ...)
├── logs/               log files
├── plugins/            plugins installed by Composer
├── resources/
│   └── locales/        translation files
├── src/                PHP code
│   ├── Console/        shells, tasks and bake templates
│   ├── Controller/
│   ├── Lib/
│   ├── Model/
│   └── View/           helpers
├── templates/          view templates, layouts, elements and email templates
├── tests/
│   ├── Fixture/
│   └── TestCase/
├── tmp/                cache, sessions and test files
├── vendor/             Composer packages, including CakePHP
└── webroot/            document root (index.php, test.php, css/, js/, img/)

webroot/index.php, webroot/test.php and bin/cake load config/define.php, which sets the root directory (ROOT), the application directory (APP_DIR, src), the document root (WWW_ROOT) and the Composer vendor directory (VENDORS). CakePHP finds the other directories by itself: config/, tests/, tmp/, logs/, templates/ and resources/locales/ are used when they exist.

The legacy layout with an app/ directory (app/Config/, app/Controller/, app/View/, ...) is supported as well, but the skeleton only ships the modern layout.

Configuration ​

FileContents
config/core.phpDebug level, error and exception handling, caching, session, security salts.
config/bootstrap.phpCache configuration, plugin loading, logging and other application setup.
config/routes.phpURL routing.
config/database.phpDatabase connections (default, and test with APP_ENV=test). Copy it from database.php.default.
config/email.phpEmail transports and profiles. Copy it from email.php.default.
config/acl.php, config/acl.ini.phpConfiguration of IniAcl and PhpAcl.
config/define.phpPath constants of the modern layout.
config/Schema/Schema files of the DbAcl, i18n and session tables.

Environment Variables (.env) ​

Settings such as the debug level, security salt and database credentials are read from environment variables. As in Symfony, config/core.php loads them from .env files in config/ with symfony/dotenv, so they apply to web requests, bin/cake and tests:

php
// config/core.php

use Symfony\Component\Dotenv\Dotenv;

(new Dotenv())->usePutenv()->loadEnv(CONFIG . '.env');

The files are loaded in this order and merged; a later file wins over an earlier one:

FileLoadedIn Git
config/.envalways (required)committed: the defaults of the application
config/.env.localexcept when APP_ENV is testignored: your machine
config/.env.{APP_ENV} (e.g. config/.env.test)alwayscommitted: the defaults of an environment
config/.env.{APP_ENV}.localalwaysignored: your machine, for that environment
  • A variable that is set in the real environment (web server, shell, container, CI) is never overwritten, so real environment variables win over all files. The values are written to $_SERVER, $_ENV and putenv(), so env(), getenv(), $_SERVER and $_ENV return them.
  • APP_ENV comes from the real environment, otherwise from the files: config/.env sets APP_ENV=dev. vendor/bin/phpunit, bin/cake test and webroot/test.php set APP_ENV=test when it is not set (see Running Tests). In production, set APP_ENV=prod as a real environment variable.
  • config/.env must exist; the skeleton ships it. config/.env.local and the other .local files hold only what differs on your machine, such as DATABASE_URL and the salts. Never put production secrets in a committed file.
  • Values do not need quotes unless they contain spaces. Inside double quotes, $NAME and ${NAME} are replaced with the value of a variable; single-quote a value that contains $.
VariableSettingDefault
APP_ENVthe environment: dev, test or prod; selects config/.env.{APP_ENV}dev (from config/.env)
APP_DEBUGdebug (0, 1, 2)0
SECURITY_SALTSecurity.saltvalue in config/core.php
SECURITY_CIPHER_SEEDSecurity.cipherSeed (1 to 4294967295, legacy 'cipher' cookies only)value in config/core.php
COOKIE_KEYCookie.key, the CookieComponent key for the 'aes' typenull (Security.salt is used)
DATABASE_URLdefault connection in config/database.php (also test with APP_ENV=test), as a DSNvalues in config/database.php
MAILER_DSNtransport of the default profile in config/email.php, as a Symfony Mailer DSNvalues in config/email.php
  • Unset or empty variables keep the defaults.
  • In production, set APP_ENV=prod and the other settings as real environment variables.
  • DATABASE_URL is read by config/database.php.default, and MAILER_DSN by config/email.php.default. If you created config/database.php or config/email.php from an earlier skeleton, copy the __construct() method into it (and remove the applyEnv() method of the former DB_* variables and the former APP_ENV branch). Copy the APP_ENV default of tests/bootstrap.php, bin/cake and webroot/test.php from the skeleton too.

Migrating from an Earlier Skeleton ​

Applications created from pieceofcake2/app 2.4.x load the .env files with josegonzalez/dotenv. To switch to the loading above:

  1. composer remove josegonzalez/dotenv and composer require symfony/dotenv.
  2. Replace the .env loading at the top of config/core.php with the two lines above, and the Configure::write('debug', ...) line with Configure::write('debug', env('APP_DEBUG') ?? 0);.
  3. Move a root .env to config/: the root .env files are no longer read. Commit config/.env with the defaults and APP_ENV=dev (see the skeleton's file), and move the values of your machine and the secrets to config/.env.local.
  4. Keep only what the tests change in config/.env.test, and only what differs on your machine in config/.env.test.local: the files are merged now.
  5. In .gitignore, ignore /config/.env.local and /config/.env.*.local instead of config/.env and the root .env files.
  6. In production, set APP_ENV=prod (and the other settings) as real environment variables.

Database and Mailer DSNs ​

A DSN sets a whole connection in one string, as in CakePHP 3.x and later and in Symfony. The 'dsn' key of a connection in config/database.php and of a profile in config/email.php takes the DSN, and the skeleton sets it from the environment variables of Symfony (DATABASE_URL, MAILER_DSN):

php
// config/database.php
$this->default['dsn'] = env('DATABASE_URL');
if (env('APP_ENV') === 'test') {
    $this->test = $this->default;
}

// config/email.php
$this->default['dsn'] = env('MAILER_DSN');

There is one variable for the database, as in Symfony: DATABASE_URL is the database of the current environment, and both connections use it. In the tests (APP_ENV=test) it is the test database of config/.env.test.

dotenv
# MySQL
DATABASE_URL="mysql://user:password@localhost:3306/my_app?encoding=utf8mb4"
# PostgreSQL
DATABASE_URL="postgres://user:password@localhost:5432/my_app?schema=public"
# SQLite: three slashes for a path relative to the working directory, four for an absolute path
DATABASE_URL="sqlite:////var/db/my_app.sqlite"
# SQLite in memory (e.g. in config/.env.test.local)
DATABASE_URL="sqlite:///:memory:"
# SQL Server
DATABASE_URL="sqlserver://user:password@localhost:1433/my_app?encoding=utf8"

# SMTP with STARTTLS on port 587
MAILER_DSN="smtp://user:password@smtp.example.com:587"
# SMTP over SSL/TLS (port 465)
MAILER_DSN="smtps://user:password@smtp.example.com"
# Local SMTP server for development (e.g. Mailpit)
MAILER_DSN="smtp://localhost:1025"
# PHP mail()
MAILER_DSN="sendmail://default"
# Do not send (Debug transport)
MAILER_DSN="null://null"
  • The scheme selects the datasource (mysql, postgres, sqlite, sqlserver) or the transport (smtp / smtps for Smtp, sendmail / native for Mail, null for Debug).
  • Query parameters set the other keys: encoding, schema, prefix, persistent=true or flags[1002]=... for a database, timeout, client or tls=1 for SMTP.
  • Encode reserved characters of a password: %40 for @, %3F for ?, %23 for #, %25 for %. For example, p@ss? is p%40ss%3F.
  • The keys in the DSN replace the same keys in config/database.php or config/email.php, and the other keys are kept. The keys of the skeleton files are defaults: a DSN without a password (mysql://root@localhost/my_app) keeps 'password' => 'password' of the file, so write mysql://root:@localhost/my_app for an empty password.
  • An unset or empty variable is ignored, and the keys of the file are used.
  • Plugins can add schemes (e.g. ses+api:// of an Amazon SES plugin) with CakeEmail::setDsnClassMap() and ConnectionManager::setDsnClassMap() in their bootstrap.
  • See Datasources and Email in the book for all the rules.

Docker and CI ​

Pass the DSNs as environment variables. With Docker Compose:

yaml
services:
  app:
    environment:
      DATABASE_URL: "mysql://my_app:secret@db:3306/my_app?encoding=utf8mb4"
      MAILER_DSN: "smtp://mailpit:1025"
  db:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: my_app
      MYSQL_USER: my_app
      MYSQL_PASSWORD: secret
      MYSQL_RANDOM_ROOT_PASSWORD: "yes"
  mailpit:
    image: axllent/mailpit

In GitHub Actions, set DATABASE_URL to the test database for the test step. Real environment variables win over config/.env.test:

yaml
      - name: Run tests
        env:
          DATABASE_URL: mysql://root:root@127.0.0.1:3306/test_app?encoding=utf8mb4
        run: |
          cp config/database.php.default config/database.php
          vendor/bin/phpunit

Generating the Security Salts ​

Generate Security.salt and Security.cipherSeed with a cryptographically secure random generator. These commands work in any POSIX shell on Linux and macOS (the cipher seed is only for the legacy 'cipher' cookie type; see cipherSeed and the 'cipher' cookie type):

bash
# SECURITY_SALT: 64 hexadecimal characters
php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'
openssl rand -hex 32                               # without PHP

# SECURITY_CIPHER_SEED: a number from 1 to 4294967295
php -r 'echo random_int(1, 4294967295), PHP_EOL;'
od -An -N4 -tu4 /dev/urandom | tr -dc 0-9; echo    # without PHP

To append both to config/.env.local, which is ignored by Git (the last line wins when a variable is set twice):

bash
php -r 'printf("SECURITY_SALT=\"%s\"\nSECURITY_CIPHER_SEED=\"%d\"\n", bin2hex(random_bytes(32)), random_int(1, 4294967295));' >> config/.env.local

or without PHP:

bash
printf 'SECURITY_SALT="%s"\nSECURITY_CIPHER_SEED="%s"\n' "$(openssl rand -hex 32)" "$(od -An -N4 -tu4 /dev/urandom | tr -dc 0-9)" >> config/.env.local
  • Keep the salt at least 32 characters long: it is the default key of CookieComponent, and the 'aes' cookie type rejects shorter keys.
  • Do not use a longer cipher seed: only 1 to 4294967295 is meaningful (see below).
  • Use different values in each environment, and do not commit them. In production, set real environment variables.
  • Changing Security.salt later logs users out, invalidates open forms, makes encrypted cookies unreadable, breaks data encrypted with Security::encrypt() without an explicit HMAC salt, and breaks passwords hashed with SimplePasswordHasher (bcrypt hashes are not affected). Changing Security.cipherSeed only breaks values encrypted with Security::cipher(), such as cookies of the default 'cipher' type.

See Generating the Security Salts in the book for details and for how to rotate the values safely.

Security.cipherSeed is used only by Security::cipher(), that is, by the 'cipher' encryption type of CookieComponent, which is the default type and is insecure.

  • Security::cipher() calls mt_srand((int)(float)$seed), so only 1 to 4294967295 is meaningful. Values with 27 or more digits, and about 95% of 26-digit values, end up as seed 0. This includes the 29-digit default of config/core.php in the original CakePHP 2 and in earlier versions of this skeleton, the blog tutorial example, and values generated by the command in earlier versions of config/.env.example. The fallback in config/core.php is now an in-range example value; replace it all the same.
  • An in-range seed does not make 'cipher' secure: there are only about 4.3 billion seeds, and the keystream is a non-cryptographic Mersenne Twister XOR, so it can be brute-forced.
  • Changing the seed makes existing 'cipher' cookies unreadable.

Use the 'aes' type with its own key of 32 random bytes or more instead, and make sure Security.useOpenSsl is true. The skeleton sets it in config/core.php; without it, Security::encrypt()/decrypt() call mcrypt, which does not exist on PHP 8, and 'aes' cookies fail (see Enable OpenSSL).

config/core.php sets Cookie.type and Cookie.key, next to the salts, so no controller changes are needed:

php
Configure::write('Cookie.type', 'aes');
Configure::write('Cookie.key', env('COOKIE_KEY') ?: null);

CookieComponent uses an explicit 'type'/'key' setting (or type() and $this->Cookie->key) first, then Cookie.type/Cookie.key, then 'cipher' and Security.salt.

The 'aes' type needs a key of at least 32 bytes. When COOKIE_KEY is unset, Cookie.key is null and the key is Security.salt; if the salt is shorter than 32 bytes, writing or reading an encrypted cookie throws a CakeException.

Generate the key with php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;' or openssl rand -hex 32, and set it as COOKIE_KEY in config/.env.local (or as a real environment variable in production). Switching an existing application from 'cipher' to 'aes' loses the existing cookie values once (for example, remember-me logins). See cipherSeed and the 'cipher' Cookie Type in the book for the impact, which option fits your application, and the migration steps.

Running the Application ​

For development, start PHP's built-in web server with:

bash
bin/cake server

and open http://localhost:8765/. Use --port (-p) to choose another port.

In production, point the document root of your web server to webroot/, never to the project directory, which contains config/, logs/, tmp/, vendor/ and .env files. Set APP_ENV=prod and the other settings as real environment variables (see Environment Variables (.env)). The webroot/.htaccess file rewrites the requests to index.php for Apache with mod_rewrite. For Apache and nginx examples, see Web Server Configuration in the book.

Pages and Styles ​

The layouts, the home page, the error pages and the flash messages are ported from the CakePHP 5 application skeleton and use its stylesheets in webroot/css/:

FileDescription
normalize.min.cssnormalize.css 8.0.1
milligram.min.cssMilligram 1.4.1
fonts.cssThe Raleway font, served from webroot/font/ (no request to a font service)
cake.cssThe application styles: layout, flash messages, forms, tables, paginator, error pages and debug output
home.cssThe styles of the home page only
cake.generic.cssThe stylesheet of the earlier skeleton. The templates no longer load it; it is kept for views that still use its classes (for example views baked with the legacy bake theme) and is loaded by the fallback layouts of the core

Templates:

  • templates/Layouts/default.ctp: the top navigation bar and the content container. While debug is 2, the SQL log is shown below the content.
  • templates/Layouts/error.ctp: a minimal layout for the error pages in production.
  • templates/Layouts/flash.ctp: the page rendered by Controller::flash().
  • templates/Elements/Flash/: default, success, error, warning and info flash messages ($this->Flash->success('Saved.') and so on). Messages are escaped; pass ['params' => ['escape' => false]] to output HTML. Click a message to hide it.

Views baked with pieceofcake2/bake use the same styles.

Home Page ​

templates/Pages/home.ctp is shown at / while debug is enabled. It checks the environment of the application: the PHP version and extensions (mbstring, intl, openssl, PDO and the driver of the default datasource), the tmp/ and logs/ directories, the cache, the database connection (with the error message when it fails), the security salt and cipher seed, and the DebugKit and Bake plugins. Replace it with your own page before turning debug off: in production it throws a NotFoundException.

Home page

On a phone:

Home page on a phone

Error Pages ​

When debug is 0, templates/Errors/error400.ctp and error500.ctp render a short message in the error layout:

Error 400 in production

Error 500 in production

In debug mode, they use the default layout and show the stack trace, like the other error pages of CakePHP.

Flash Messages ​

Flash messages

Existing Applications ​

Applications created from an earlier version of the skeleton are not affected: they own their templates and webroot/css/. To use the new look, copy templates/Layouts/, templates/Elements/Flash/, templates/Errors/, webroot/css/, webroot/font/ and webroot/img/cake.logo.svg into your application. Views that you baked for cake.generic.css need to be baked again (or restyled), see Baked Views and the Legacy Theme.

Development ​

Running Tests ​

Put your test cases in tests/TestCase/ and your fixtures in tests/Fixture/. vendor/bin/phpunit and bin/cake test run with APP_ENV=test by default: tests/bootstrap.php and bin/cake set it before CakePHP loads the .env files when it is not set (webroot/test.php, the browser test runner, does the same). So the tests load config/.env.test after config/.env, without config/.env.local. config/.env.test is committed and changes only what the tests need: DATABASE_URL is the test database (the default connection, which config/database.php copies to the test connection with APP_ENV=test) and MAILER_DSN is null://null. Set your test database in config/.env.test.local, which is ignored by Git, or as a real environment variable, which wins over the files, as CI does:

dotenv
# config/.env.test.local
DATABASE_URL="mysql://my_app:secret@localhost:3306/test_my_app?encoding=utf8mb4"

Your development DATABASE_URL stays in config/.env.local, which the tests do not load. A real environment variable wins over the files, so do not export your development DATABASE_URL or another APP_ENV in the shell where you run the tests: the fixtures drop and create tables.

Then run the tests with PHPUnit:

bash
vendor/bin/phpunit

or a single test case with the test shell:

bash
bin/cake test app Controller/PostsController

phpunit.xml.dist has two test suites:

SuiteDirectoryRun with
Apptests/TestCase/, without EndToEnd/vendor/bin/phpunit (the default suite)
EndToEndtests/TestCase/EndToEnd/vendor/bin/phpunit --testsuite EndToEnd

vendor/bin/phpunit --testsuite App,EndToEnd runs both. defaultTestSuite="App" in phpunit.xml.dist keeps vendor/bin/phpunit fast and free of a browser. Delete that one attribute to make vendor/bin/phpunit run everything, as a Symfony application does.

End-to-End Tests ​

tests/TestCase/EndToEnd/ is for browser End-to-End tests with pieceofcake2/panther (Markdown), which runs them the way Symfony Panther does: the application starts with PHP's built-in web server and headless Chrome drives it. To use them:

  1. Install the package and a chromedriver that matches your Chrome:

    bash
    composer require --dev "pieceofcake2/panther:^1.0.1" dbrekelmans/bdi
    vendor/bin/bdi detect drivers
  2. Uncomment the ServerExtension in phpunit.xml.dist. It keeps the web server and the browser running for the whole run; nothing starts until an End-to-End test runs.

  3. Write tests that extend PieceOfCake2\Panther\TestSuite\PantherTestCase in tests/TestCase/EndToEnd/, and run them with vendor/bin/phpunit --testsuite EndToEnd.

The web server that Panther starts inherits the environment of PHPUnit, including APP_ENV=test. It loads config/.env.test (and config/.env.test.local) too, where DATABASE_URL (the default connection of the pages) is the test database, so the pages read the fixtures of the tests. No other setting is needed. Panther's PANTHER_APP_ENV is only needed to give the web server a different APP_ENV than the tests.

The tests are configured with Panther's PANTHER_* variables. phpunit.xml.dist sets PANTHER_ERROR_SCREENSHOT_DIR=./tmp/error-screenshots for the screenshots of the failed tests.

Set per-machine values, such as PANTHER_NO_HEADLESS=1 (to watch the browser), in your shell or in a phpunit.xml copy of phpunit.xml.dist. In CI, run the End-to-End tests in a separate job; see the panther documentation for the variables, the test database and a GitHub Actions example.

Code Standards ​

Check your code against the CakePHP coding standard:

bash
vendor/bin/phpcs

Static Analysis ​

Run PHPStan (level 5) on src/ and config/:

bash
vendor/bin/phpstan analyse

The configuration is in phpstan.neon and uses the pieceofcake2/phpstan-cakephp2 extension.

Origin ​

This package is based on the app/ directory of cakephp/cakephp (cakephp/cakephp on Packagist, last upstream version 2.10.24), originally by the CakePHP community (Cake Software Foundation, Inc.). It was extracted into a separate package and is maintained for CakePHP 2 on modern PHP by pieceofcake2. Changes from the original are listed in the commit history.

See LICENSE for copyright and license information. Not affiliated with the Cake Software Foundation.