+
+CDN
+Content Delivery Network. A 3rd party vendor you can pay to help
+distribute your content to data centers around the world. This helps
+put your static assets closer to geographically distributed users.
+
+columns
+Used in the ORM when referring to the table columns in an database
+table.
+
+CSRF
+Cross Site Request Forgery. Prevents replay attacks, double
+submissions and forged requests from other domains.
+
+DI Container
+In `Application::services()` you can configure application services
+and their dependencies. Application services are automatically injected
+into Controller actions, and Command Constructors. See
+[Dependency Injection](../development/dependency-injection).
+
+DSN
+Data Source Name. A connection string format that is formed like a URI.
+CakePHP supports DSNs for Cache, Database, Log and Email connections.
+
+dot notation
+Dot notation defines an array path, by separating nested levels with `.`
+For example:
+
+ Cache.default.engine
+
+Would point to the following value:
+
+``` php
+[
+ 'Cache' => [
+ 'default' => [
+ 'engine' => 'File'
+ ]
+ ]
+]
+```
+
+DRY
+Don't repeat yourself. Is a principle of software development aimed at
+reducing repetition of information of all kinds. In CakePHP DRY is used
+to allow you to code things once and re-use them across your
+application.
+
+fields
+A generic term used to describe both entity properties, or database
+columns. Often used in conjunction with the FormHelper.
+
+HTML attributes
+An array of key =\> values that are composed into HTML attributes. For example:
+
+``` text
+// Given
+['class' => 'my-class', 'target' => '_blank']
+
+// Would generate
+class="my-class" target="_blank"
+```
+
+If an option can be minimized or accepts its name as the value, then `true`
+can be used:
+
+``` text
+// Given
+['checked' => true]
+
+// Would generate
+checked="checked"
+```
+
+PaaS
+Platform as a Service. Platform as a Service providers will provide
+cloud based hosting, database and caching resources. Some popular
+providers include Heroku, EngineYard and PagodaBox
+
+properties
+Used when referencing columns mapped onto an ORM entity.
+
+plugin syntax
+Plugin syntax refers to the dot separated class name indicating classes
+are part of a plugin:
+
+``` text
+// The plugin is "DebugKit", and the class name is "Toolbar".
+'DebugKit.Toolbar'
+
+// The plugin is "AcmeCorp/Tools", and the class name is "Toolbar".
+'AcmeCorp/Tools.Toolbar'
+```
+
+routes.php
+A file in the `config/` directory that contains routing configuration.
+This file is included before each request is processed.
+It should connect all the routes your application needs so
+requests can be routed to the correct controller + action.
+
+routing array
+An array of attributes that are passed to `Router::url()`.
+They typically look like:
+
+``` php
+['controller' => 'Posts', 'action' => 'view', 5]
+```
+
+
diff --git a/docs/en/appendices/migration-guides.md b/docs/en/appendices/migration-guides.md
new file mode 100644
index 0000000000..cf2cc6b4f6
--- /dev/null
+++ b/docs/en/appendices/migration-guides.md
@@ -0,0 +1,34 @@
+# Migration Guides
+
+Migration guides contain information regarding the new features introduced in
+each version and the migration path between 5.x minor releases.
+
+## Upgrade Tool
+
+CakePHP provides an [upgrade tool](https://github.com/cakephp/upgrade) that
+automates many code changes using [Rector](https://getrector.com/). The tool
+has rulesets for each minor version to help automate tedious code changes like
+method renames and signature updates.
+
+To use the upgrade tool:
+
+``` text
+# Install the upgrade tool
+git clone https://github.com/cakephp/upgrade
+cd upgrade
+git checkout 5.x
+composer install --no-dev
+
+# Run rector with the desired ruleset
+bin/cake upgrade rector --rules cakephp51
+```
+
+Run rector before updating your `composer.json` dependencies
+to ensure the tool can resolve class names correctly.
+
+- [5 0 Upgrade Guide](5-0-upgrade-guide)
+- [5 0 Migration Guide](5-0-migration-guide)
+- [5 1 Migration Guide](5-1-migration-guide)
+- [5 2 Migration Guide](5-2-migration-guide)
+- [5 3 Migration Guide](5-3-migration-guide)
+- [Phpunit10](phpunit10)
diff --git a/docs/en/appendices/phpunit10.md b/docs/en/appendices/phpunit10.md
new file mode 100644
index 0000000000..a6606f000e
--- /dev/null
+++ b/docs/en/appendices/phpunit10.md
@@ -0,0 +1,68 @@
+# PHPUnit 10 Upgrade
+
+With CakePHP 5 the minimum PHPUnit version has changed from `^8.5 || ^9.3` to `^10.1`. This introduces a few breaking changes from PHPUnit as well as from CakePHP's side.
+
+## phpunit.xml adjustments
+
+It is recommended to let PHPUnit update its configuration file via the following command:
+
+ vendor/bin/phpunit --migrate-configuration
+
+> [!NOTE]
+> Make sure you are already on PHPUnit 10 via `vendor/bin/phpunit --version` before executing this command!
+
+With this command out of the way your `phpunit.xml` already has most of the recommended changes present.
+
+### New event system
+
+PHPUnit 10 removed the old hook system and introduced a new [Event system](https://docs.phpunit.de/en/10.5/extending-phpunit.html#extending-the-test-runner)
+which requires the following code in your `phpunit.xml` to be adjusted from:
+
+``` php
+
+
+
+```
+
+to:
+
+``` php
+
+
+
+```
+
+## `->withConsecutive()` has been removed
+
+You can convert the removed `->withConsecutive()` method to a
+working interim solution like you can see here:
+
+``` php
+->withConsecutive(['firstCallArg'], ['secondCallArg'])
+```
+
+should be converted to:
+
+``` php
+->with(
+ ...self::withConsecutive(['firstCallArg'], ['secondCallArg'])
+)
+```
+
+the static `self::withConsecutive()` method has been added via the `Cake\TestSuite\PHPUnitConsecutiveTrait`
+to the base `Cake\TestSuite\TestCase` class so you don't have to manually add that trait to your Testcase classes.
+
+## data providers have to be static
+
+If your testcases leverage the data provider feature of PHPUnit then
+you have to adjust your data providers to be static:
+
+``` php
+public function myProvider(): array
+```
+
+should be converted to:
+
+``` text
+public static function myProvider(): array
+```
diff --git a/docs/en/bake.md b/docs/en/bake.md
new file mode 100644
index 0000000000..1104d57fbe
--- /dev/null
+++ b/docs/en/bake.md
@@ -0,0 +1,3 @@
+# Bake Console
+
+This page has [moved](https://book.cakephp.org/bake/2.x/en/index.html).
diff --git a/docs/en/bake/development.md b/docs/en/bake/development.md
new file mode 100644
index 0000000000..ee1fa8222a
--- /dev/null
+++ b/docs/en/bake/development.md
@@ -0,0 +1,3 @@
+# Extending Bake
+
+This page has [moved](https://book.cakephp.org/bake/2.x/en/development.html).
diff --git a/docs/en/bake/usage.md b/docs/en/bake/usage.md
new file mode 100644
index 0000000000..7be48ac6c0
--- /dev/null
+++ b/docs/en/bake/usage.md
@@ -0,0 +1,3 @@
+# Code Generation with Bake
+
+This page has [moved](https://book.cakephp.org/bake/2.x/en/usage.html).
diff --git a/docs/en/chronos.md b/docs/en/chronos.md
new file mode 100644
index 0000000000..f83a14f3d7
--- /dev/null
+++ b/docs/en/chronos.md
@@ -0,0 +1,3 @@
+# Chronos
+
+This page has [moved](https://book.cakephp.org/chronos/2.x/en/).
diff --git a/docs/en/console-commands.md b/docs/en/console-commands.md
new file mode 100644
index 0000000000..18bc6f30ab
--- /dev/null
+++ b/docs/en/console-commands.md
@@ -0,0 +1,164 @@
+# Console Commands
+
+In addition to a web framework, CakePHP also provides a console framework for
+creating command line tools & applications. Console applications are ideal for
+handling a variety of background & maintenance tasks that leverage your existing
+application configuration, models, plugins and domain logic.
+
+CakePHP provides several console tools for interacting with CakePHP features
+like i18n and routing that enable you to introspect your application and
+generate related files.
+
+## The CakePHP Console
+
+The CakePHP Console uses a dispatcher-type system to load commands, parse
+their arguments and invoke the correct command. While the examples below use
+bash the CakePHP console is compatible with any \*nix shell and windows.
+
+A CakePHP application contains **src/Command** directory that contain its commands.
+It also comes with an executable in the **bin** directory:
+
+``` bash
+$ cd /path/to/app
+$ bin/cake
+```
+
+> [!NOTE]
+> For Windows, the command needs to be `bin\cake` (note the backslash).
+
+Running the Console with no arguments will list out available commands. You
+could then run the any of the listed commands by using its name:
+
+``` bash
+# run server command
+bin/cake server
+
+# run migrations command
+bin/cake migrations -h
+
+# run bake (with plugin prefix)
+bin/cake bake.bake -h
+```
+
+Plugin commands can be invoked without a plugin prefix if the commands's name
+does not overlap with an application or framework command. In the case that two
+plugins provide a command with the same name, the first loaded plugin will get
+the short alias. You can always use the `plugin.command` format to
+unambiguously reference a command.
+
+## Console Applications
+
+By default CakePHP will automatically discover all the commands in your
+application and its plugins. You may want to reduce the number of exposed
+commands, when building standalone console applications. You can use your
+`Application`'s `console()` hook to limit which commands are exposed and
+rename commands that are exposed:
+
+``` php
+// in src/Application.php
+namespace App;
+
+use App\Command\UserCommand;
+use App\Command\VersionCommand;
+use Cake\Console\CommandCollection;
+use Cake\Http\BaseApplication;
+
+class Application extends BaseApplication
+{
+ public function console(CommandCollection $commands): CommandCollection
+ {
+ // Add by classname
+ $commands->add('user', UserCommand::class);
+
+ // Add instance
+ $commands->add('version', new VersionCommand());
+
+ return $commands;
+ }
+}
+```
+
+In the above example, the only commands available would be `help`, `version`
+and `user`. See the [Plugin Commands](plugins#plugin-commands) section for how to add commands in
+your plugins.
+
+> [!NOTE]
+> When adding multiple commands that use the same Command class, the `help`
+> command will display the shortest option.
+
+## Renaming Commands
+
+There are cases where you will want to rename commands, to create nested
+commands or subcommands. While the default auto-discovery of commands will not
+do this, you can register your commands to create any desired naming.
+
+You can customize the command names by defining each command in your plugin:
+
+``` php
+public function console(CommandCollection $commands): CommandCollection
+{
+ // Add commands with nested naming
+ $commands->add('user dump', UserDumpCommand::class);
+ $commands->add('user:show', UserShowCommand::class);
+
+ // Rename a command entirely
+ $commands->add('lazer', UserDeleteCommand::class);
+
+ return $commands;
+}
+```
+
+When overriding the `console()` hook in your application, remember to
+call `$commands->autoDiscover()` to add commands from CakePHP, your
+application, and plugins.
+
+If you need to rename/remove any attached commands, you can use the
+`Console.buildCommands` event on your application event manager to modify the
+available commands.
+
+## Commands
+
+See the [Command Objects](console-commands/commands) chapter on how to create your first
+command. Then learn more about commands:
+
+- [Command Objects](console-commands/commands)
+- [Command Input/Output](console-commands/input-output)
+- [Option Parsers](console-commands/option-parsers)
+- [Running Shells as Cron Jobs](console-commands/cron-jobs)
+
+## CakePHP Provided Commands
+
+- [Cache Tool](console-commands/cache)
+- [Completion Tool](console-commands/completion)
+- [CounterCache Tool](console-commands/counter-cache)
+- [I18N Tool](console-commands/i18n)
+- [Plugin Tool](console-commands/plugin)
+- [Schema Cache Tool](console-commands/schema-cache)
+- [Routes Tool](console-commands/routes)
+- [Server Tool](console-commands/server)
+- [Interactive Console (REPL)](console-commands/repl)
+
+## Routing in the Console Environment
+
+In command-line interface (CLI), specifically your console commands,
+`env('HTTP_HOST')` and other webbrowser specific environment variables are not
+set.
+
+If you generate reports or send emails that make use of `Router::url()` those
+will contain the default host `http://localhost/` and thus resulting in
+invalid URLs. In this case you need to specify the domain manually.
+You can do that using the Configure value `App.fullBaseUrl` from your
+bootstrap or config, for example.
+
+For sending emails, you should provide Email class with the host you want to
+send the email with:
+
+``` php
+use Cake\Mailer\Email;
+
+$email = new Email();
+$email->setDomain('www.example.org');
+```
+
+This asserts that the generated message IDs are valid and fit to the domain the
+emails are sent from.
diff --git a/docs/en/console-commands/cache.md b/docs/en/console-commands/cache.md
new file mode 100644
index 0000000000..3367a8cc2f
--- /dev/null
+++ b/docs/en/console-commands/cache.md
@@ -0,0 +1,15 @@
+# Cache Tool
+
+To help you better manage cached data from a CLI environment, a console command
+is available for clearing cached data your application has:
+
+``` text
+// Clear one cache config
+bin/cake cache clear
+
+// Clear all cache configs
+bin/cake cache clear_all
+
+// Clear one cache group
+bin/cake cache clear_group
+```
diff --git a/docs/en/console-commands/commands.md b/docs/en/console-commands/commands.md
new file mode 100644
index 0000000000..b8aa442d6f
--- /dev/null
+++ b/docs/en/console-commands/commands.md
@@ -0,0 +1,660 @@
+# Command Objects
+
+`class` Cake\\Console\\**Command**
+
+CakePHP comes with a number of built-in commands for speeding up your
+development, and automating routine tasks. You can use these same libraries to
+create commands for your application and plugins.
+
+## Creating a Command
+
+Let's create our first Command. For this example, we'll create a
+simple Hello world command. In your application's **src/Command** directory create
+**HelloCommand.php**. Put the following code inside it:
+
+``` php
+out('Hello world.');
+
+ return static::CODE_SUCCESS;
+ }
+}
+```
+
+Command classes must implement an `execute()` method that does the bulk of
+their work. This method is called when a command is invoked. Lets call our first
+command application directory, run:
+
+``` bash
+bin/cake hello
+```
+
+You should see the following output:
+
+ Hello world.
+
+Our `execute()` method isn't very interesting let's read some input from the
+command line:
+
+``` php
+addArgument('name', [
+ 'help' => 'What is your name',
+ ]);
+
+ return $parser;
+ }
+
+ public function execute(Arguments $args, ConsoleIo $io): int
+ {
+ $name = $args->getArgument('name');
+ $io->out("Hello {$name}.");
+
+ return static::CODE_SUCCESS;
+ }
+}
+```
+
+After saving this file, you should be able to run the following command:
+
+``` bash
+bin/cake hello jillian
+
+# Outputs
+Hello jillian
+```
+
+## Changing the Default Command Name
+
+CakePHP will use conventions to generate the name your commands use on the
+command line. If you want to overwrite the generated name implement the
+`defaultName()` method in your command:
+
+``` text
+public static function defaultName(): string
+{
+ return 'oh_hi';
+}
+```
+
+The above would make our `HelloCommand` accessible by `cake oh_hi` instead
+of `cake hello`.
+
+## Defining Arguments and Options
+
+As we saw in the last example, we can use the `buildOptionParser()` hook
+method to define arguments. We can also define options. For example, we could
+add a `yell` option to our `HelloCommand`:
+
+``` php
+// ...
+protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+{
+ $parser
+ ->addArgument('name', [
+ 'help' => 'What is your name',
+ ])
+ ->addOption('yell', [
+ 'help' => 'Shout the name',
+ 'boolean' => true,
+ ]);
+
+ return $parser;
+}
+
+public function execute(Arguments $args, ConsoleIo $io): int
+{
+ $name = $args->getArgument('name');
+ if ($args->getOption('yell')) {
+ $name = mb_strtoupper($name);
+ }
+ $io->out("Hello {$name}.");
+
+ return static::CODE_SUCCESS;
+}
+```
+
+See the [Option Parsers](../console-commands/option-parsers) section for more information.
+
+## Creating Output
+
+Commands are provided a `ConsoleIo` instance when executed. This object allows
+you to interact with `Cake\Console\ConsoleIo::out()` and
+`Cake\Console\ConsoleIo::err()` to emit on `stdout`, and
+`stderr`. Files can be created with overwrite confirmation with
+`Cake\Console\ConsoleIo::createFile()`. :ref:\`command-helpers
+provide 'macros' for output generation. See the
+[Command Input/Output](../console-commands/input-output) section for more information.
+
+## Using Models in Commands
+
+You'll often need access to your application's business logic in console
+commands. You can load models in commands, just as you would in a controller
+using `$this->fetchTable()` since command use the `LocatorAwareTrait`:
+
+``` php
+addArgument('name', [
+ 'help' => 'What is your name'
+ ]);
+
+ return $parser;
+ }
+
+ public function execute(Arguments $args, ConsoleIo $io): int
+ {
+ $name = $args->getArgument('name');
+ $user = $this->fetchTable()->findByUsername($name)->first();
+
+ $io->out(print_r($user, true));
+
+ return static::CODE_SUCCESS;
+ }
+}
+```
+
+The above command, will fetch a user by username and display the information
+stored in the database.
+
+## Exit Codes and Stopping Execution
+
+When your commands hit an unrecoverable error you can use the `abort()` method
+to terminate execution:
+
+``` php
+// ...
+public function execute(Arguments $args, ConsoleIo $io): int
+{
+ $name = $args->getArgument('name');
+ if (strlen($name) < 5) {
+ // Halt execution, output to stderr, and set exit code to 1
+ $io->error('Name must be at least 4 characters long.');
+ $this->abort();
+ }
+
+ return static::CODE_SUCCESS;
+}
+```
+
+You can also use `abort()` on the `$io` object to emit a message and code:
+
+``` php
+public function execute(Arguments $args, ConsoleIo $io): int
+{
+ $name = $args->getArgument('name');
+ if (strlen($name) < 5) {
+ // Halt execution, output to stderr, and set exit code to 99
+ $io->abort('Name must be at least 4 characters long.', 99);
+ }
+
+ return static::CODE_SUCCESS;
+}
+```
+
+You can pass any desired exit code into `abort()`.
+
+> [!TIP]
+> Avoid exit codes 64 - 78, as they have specific meanings described by
+> `sysexits.h`. Avoid exit codes above 127, as these are used to indicate
+> process exit by signal, such as SIGKILL or SIGSEGV.
+>
+> You can read more about conventional exit codes in the sysexit manual page
+> on most Unix systems (`man sysexits`), or the `System Error Codes` help
+> page in Windows.
+
+## Calling other Commands
+
+You may need to call other commands from your command. You can use
+`executeCommand` to do that:
+
+``` php
+// You can pass an array of CLI options and arguments.
+$this->executeCommand(OtherCommand::class, ['--verbose', 'deploy']);
+
+// Can pass an instance of the command if it has constructor args
+$command = new OtherCommand($otherArgs);
+$this->executeCommand($command, ['--verbose', 'deploy']);
+```
+
+> [!NOTE]
+> When calling `executeCommand()` in a loop, it is recommended to pass in the
+> parent command's `ConsoleIo` instance as the optional 3rd argument to
+> avoid a potential "open files" limit that could occur in some environments.
+
+## Setting Command Description
+
+You may want to set a command description via:
+
+``` php
+class UserCommand extends Command
+{
+ public static function getDescription(): string
+ {
+ return 'My custom description';
+ }
+}
+```
+
+This will show your description in the Cake CLI:
+
+``` bash
+bin/cake
+
+App:
+ - user
+ └─── My custom description
+```
+
+As well as in the help section of your command:
+
+``` bash
+cake user --help
+My custom description
+
+Usage:
+cake user [-h] [-q] [-v]
+```
+
+## Grouping Commands
+
+By default in the help output CakePHP will group commands into core, app, and
+plugin groups. You can customize the grouping of commands by implementing
+`getGroup()`:
+
+``` php
+class CleanupCommand extends Command
+{
+ public static function getGroup(): string
+ {
+ return 'maintenance';
+ }
+}
+```
+
+::: info Added in version 5.3.0
+Custom grouping support was added.
+:::
+
+
+
+## Testing Commands
+
+To make testing console applications easier, CakePHP comes with a
+`ConsoleIntegrationTestTrait` trait that can be used to test console applications
+and assert against their results.
+
+To get started testing your console application, create a test case that uses the
+`Cake\TestSuite\ConsoleIntegrationTestTrait` trait. This trait contains a method
+`exec()` that is used to execute your command. You can pass the same string
+you would use in the CLI to this method.
+
+Let's start with a very simple command, located in
+**src/Command/UpdateTableCommand.php**:
+
+``` php
+namespace App\Command;
+
+use Cake\Command\Command;
+use Cake\Console\Arguments;
+use Cake\Console\ConsoleIo;
+use Cake\Console\ConsoleOptionParser;
+
+class UpdateTableCommand extends Command
+{
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ {
+ $parser->setDescription('My cool console app');
+
+ return $parser;
+ }
+}
+```
+
+To write an integration test for this command, we would create a test case in
+**tests/TestCase/Command/UpdateTableTest.php** that uses the
+`Cake\TestSuite\ConsoleIntegrationTestTrait` trait. This command doesn't do much at the
+moment, but let's just test that our command's description is displayed in `stdout`:
+
+``` php
+namespace App\Test\TestCase\Command;
+
+use Cake\TestSuite\ConsoleIntegrationTestTrait;
+use Cake\TestSuite\TestCase;
+
+class UpdateTableCommandTest extends TestCase
+{
+ use ConsoleIntegrationTestTrait;
+
+ public function testDescriptionOutput()
+ {
+ $this->exec('update_table --help');
+ $this->assertOutputContains('My cool console app');
+ }
+}
+```
+
+Our test passes! While this is very trivial example, it shows that creating an
+integration test case for console applications can follow command line
+conventions. Let's continue by adding more logic to our command:
+
+``` php
+namespace App\Command;
+
+use Cake\Command\Command;
+use Cake\Console\Arguments;
+use Cake\Console\ConsoleIo;
+use Cake\Console\ConsoleOptionParser;
+use Cake\I18n\DateTime;
+
+class UpdateTableCommand extends Command
+{
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ {
+ $parser
+ ->setDescription('My cool console app')
+ ->addArgument('table', [
+ 'help' => 'Table to update',
+ 'required' => true
+ ]);
+
+ return $parser;
+ }
+
+ public function execute(Arguments $args, ConsoleIo $io): int
+ {
+ $table = $args->getArgument('table');
+ $this->fetchTable($table)->updateQuery()
+ ->set([
+ 'modified' => new DateTime()
+ ])
+ ->execute();
+
+ return static::CODE_SUCCESS;
+ }
+}
+```
+
+This is a more complete command that has required options and relevant logic.
+Modify your test case to the following snippet of code:
+
+``` php
+namespace Cake\Test\TestCase\Command;
+
+use Cake\Command\Command;
+use Cake\I18n\DateTime;
+use Cake\TestSuite\ConsoleIntegrationTestTrait;
+use Cake\TestSuite\TestCase;
+
+class UpdateTableCommandTest extends TestCase
+{
+ use ConsoleIntegrationTestTrait;
+
+ protected $fixtures = [
+ // assumes you have a UsersFixture
+ 'app.Users',
+ ];
+
+ public function testDescriptionOutput()
+ {
+ $this->exec('update_table --help');
+ $this->assertOutputContains('My cool console app');
+ }
+
+ public function testUpdateModified()
+ {
+ $now = new DateTime('2017-01-01 00:00:00');
+ DateTime::setTestNow($now);
+
+ $this->loadFixtures('Users');
+
+ $this->exec('update_table Users');
+ $this->assertExitCode(Command::CODE_SUCCESS);
+
+ $user = $this->getTableLocator()->get('Users')->get(1);
+ $this->assertSame($user->modified->timestamp, $now->timestamp);
+
+ DateTime::setTestNow(null);
+ }
+}
+```
+
+As you can see from the `testUpdateModified` method, we are testing that our
+command updates the table that we are passing as the first argument. First, we
+assert that the command exited with the proper status code, `0`. Then we check
+that our command did its work, that is, updated the table we provided and set
+the `modified` column to the current time.
+
+Remember, `exec()` will take the same string you type into your CLI, so you
+can include options and arguments in your command string.
+
+### Testing Interactive Commands
+
+Consoles are often interactive. Testing interactive commands with the
+`Cake\TestSuite\ConsoleIntegrationTestTrait` trait only requires passing the
+inputs you expect as the second parameter of `exec()`. They should be
+included as an array in the order that you expect them.
+
+Continuing with our example command, let's add an interactive confirmation.
+Update the command class to the following:
+
+``` php
+namespace App\Command;
+
+use Cake\Command\Command;
+use Cake\Console\Arguments;
+use Cake\Console\ConsoleIo;
+use Cake\Console\ConsoleOptionParser;
+use Cake\I18n\DateTime;
+
+class UpdateTableCommand extends Command
+{
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ {
+ $parser
+ ->setDescription('My cool console app')
+ ->addArgument('table', [
+ 'help' => 'Table to update',
+ 'required' => true
+ ]);
+
+ return $parser;
+ }
+
+ public function execute(Arguments $args, ConsoleIo $io): int
+ {
+ $table = $args->getArgument('table');
+ if ($io->ask('Are you sure?', 'n', ['y', 'n']) !== 'y') {
+ $io->error('You need to be sure.');
+ $this->abort();
+ }
+ $this->fetchTable($table)->updateQuery()
+ ->set([
+ 'modified' => new DateTime()
+ ])
+ ->execute();
+
+ return static::CODE_SUCCESS;
+ }
+}
+```
+
+Now that we have an interactive command, we can add a test case that tests
+that we receive the proper response, and one that tests that we receive an
+incorrect response. Remove the `testUpdateModified` method and, add the following methods to
+**tests/TestCase/Command/UpdateTableCommandTest.php**:
+
+``` php
+public function testUpdateModifiedSure()
+{
+ $now = new DateTime('2017-01-01 00:00:00');
+ DateTime::setTestNow($now);
+
+ $this->loadFixtures('Users');
+
+ $this->exec('update_table Users', ['y']);
+ $this->assertExitCode(Command::CODE_SUCCESS);
+
+ $user = $this->getTableLocator()->get('Users')->get(1);
+ $this->assertSame($user->modified->timestamp, $now->timestamp);
+
+ DateTime::setTestNow(null);
+}
+
+public function testUpdateModifiedUnsure()
+{
+ $user = $this->getTableLocator()->get('Users')->get(1);
+ $original = $user->modified->timestamp;
+
+ $this->exec('my_console best_framework', ['n']);
+ $this->assertExitCode(Command::CODE_ERROR);
+ $this->assertErrorContains('You need to be sure.');
+
+ $user = $this->getTableLocator()->get('Users')->get(1);
+ $this->assertSame($original, $user->timestamp);
+}
+```
+
+In the first test case, we confirm the question, and records are updated. In the
+second test we don't confirm and records are not updated, and we can check that
+our error message was written to `stderr`.
+
+### Assertion methods
+
+The `Cake\TestSuite\ConsoleIntegrationTestTrait` trait provides a number of
+assertion methods that make help assert against console output:
+
+``` php
+// assert that the command exited as success
+$this->assertExitSuccess();
+
+// assert that the command exited as an error
+$this->assertExitError();
+
+// assert that the command exited with the expected code
+$this->assertExitCode($expected);
+
+// assert that stdout contains a string
+$this->assertOutputContains($expected);
+
+// assert that stderr contains a string
+$this->assertErrorContains($expected);
+
+// assert that stdout matches a regular expression
+$this->assertOutputRegExp($expected);
+
+// assert that stderr matches a regular expression
+$this->assertErrorRegExp($expected);
+```
+
+### Debug Helpers
+
+You can use `debugOutput()` to output the exit code, stdout and stderr of the
+last run command:
+
+``` php
+$this->exec('update_table Users');
+$this->assertExitCode(Command::CODE_SUCCESS);
+$this->debugOutput();
+```
+
+::: info Added in version 4.2.0
+The `debugOutput()` method was added.
+:::
+
+## Lifecycle Callbacks
+
+Like Controllers, Commands offer lifecycle events that allow you to observe
+the framework calling your application code. Commands have:
+
+- `Command.beforeExecute` is called before a command's `execute()` method.
+ The event is passed the `Arguments` parameter as `args` and the
+ `ConsoleIo` parameter as `io`. This event cannot be stopped or have its
+ result replaced.
+- `Command.afterExecute` is called after a command's `execute()` method is
+ complete. The event contains `Arguments` as `args`, `ConsoleIo` as
+ `io` and the command result as `result`. This event cannot be stopped or
+ have its result replaced.
+
+::: info Added in version 5.3.0
+The `beforeExecute()` and `afterExecute()` hook methods were added.
+:::
+
+### beforeExecute()
+
+`method` Cake\\Console\\Command::**beforeExecute**(EventInterface $event, Arguments $args, ConsoleIo $io): void
+
+Called before the `execute()` method runs. Useful for initialization and
+validation:
+
+``` php
+use Cake\Event\EventInterface;
+
+class MyCommand extends Command
+{
+ public function beforeExecute(EventInterface $event, Arguments $args, ConsoleIo $io): void
+ {
+ parent::beforeExecute($event);
+
+ $io->out('Starting command execution');
+
+ if (!$this->checkPrerequisites()) {
+ $io->abort('Prerequisites not met');
+ }
+ }
+}
+```
+
+### afterExecute()
+
+`method` Cake\\Console\\Command::**afterExecute**(EventInterface $event, Arguments $args, ConsoleIo $io): void
+
+Called after the `execute()` method completes. Useful for cleanup and
+logging:
+
+``` php
+public function afterExecute(EventInterface $event, Arguments $args, ConsoleIo $io, mixed $result): void
+{
+ parent::afterExecute($event);
+
+ $this->cleanup();
+ $io->out('Command execution completed');
+}
+```
diff --git a/docs/en/console-commands/completion.md b/docs/en/console-commands/completion.md
new file mode 100644
index 0000000000..a4605c36ed
--- /dev/null
+++ b/docs/en/console-commands/completion.md
@@ -0,0 +1,174 @@
+# Completion Tool
+
+Working with the console gives the developer a lot of possibilities but having
+to completely know and write those commands can be tedious. Especially when
+developing new shells where the commands differ per minute iteration. The
+Completion Shells aids in this matter by providing an API to write completion
+scripts for shells like bash, zsh, fish etc.
+
+## Sub Commands
+
+The Completion Shell consists of a number of sub commands to assist the
+developer creating its completion script. Each for a different step in the
+autocompletion process.
+
+### Commands
+
+For the first step commands outputs the available Shell Commands, including
+plugin name when applicable. (All returned possibilities, for this and the other
+sub commands, are separated by a space.) For example:
+
+ bin/cake Completion commands
+
+Returns:
+
+ acl api bake command_list completion console i18n schema server test testsuite upgrade
+
+Your completion script can select the relevant commands from that list to
+continue with. (For this and the following sub commands.)
+
+### subCommands
+
+Once the preferred command has been chosen subCommands comes in as the second
+step and outputs the possible sub command for the given shell command. For
+example:
+
+ bin/cake Completion subcommands bake
+
+Returns:
+
+ controller db_config fixture model plugin project test view
+
+### options
+
+As the third and final options outputs options for the given (sub) command as
+set in getOptionParser. (Including the default options inherited from Shell.)
+For example:
+
+ bin/cake Completion options bake
+
+Returns:
+
+ --help -h --verbose -v --quiet -q --everything --connection -c --force -f --plugin -p --prefix --theme -t
+
+You can also pass an additional argument being the shell sub-command : it will
+output the specific options of this sub-command.
+
+## How to enable Bash autocompletion for the CakePHP Console
+
+First, make sure the **bash-completion** library is installed. If not, you do it
+with the following command:
+
+ apt-get install bash-completion
+
+Create a file named **cake** in **/etc/bash_completion.d/** and put the
+[Bash Completion File Content](#bash-completion-file-content) inside it.
+
+Save the file, then restart your console.
+
+> [!NOTE]
+> If you are using MacOS X, you can install the **bash-completion** library
+> using **homebrew** with the command `brew install bash-completion`.
+> The target directory for the **cake** file will be
+> **/usr/local/etc/bash_completion.d/**.
+
+
+
+### Bash Completion file content
+
+This is the code you need to put inside the **cake** file in the correct location
+in order to get autocompletion when using the CakePHP console:
+
+``` bash
+#
+# Bash completion file for CakePHP console
+#
+
+_cake()
+{
+ local cur prev opts cake
+ COMPREPLY=()
+ cake="${COMP_WORDS[0]}"
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
+
+ if [[ "$cur" == -* ]] ; then
+ if [[ ${COMP_CWORD} = 1 ]] ; then
+ opts=$(${cake} Completion options)
+ elif [[ ${COMP_CWORD} = 2 ]] ; then
+ opts=$(${cake} Completion options "${COMP_WORDS[1]}")
+ else
+ opts=$(${cake} Completion options "${COMP_WORDS[1]}" "${COMP_WORDS[2]}")
+ fi
+
+ COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+ return 0
+ fi
+
+ if [[ ${COMP_CWORD} = 1 ]] ; then
+ opts=$(${cake} Completion commands)
+ COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+ return 0
+ fi
+
+ if [[ ${COMP_CWORD} = 2 ]] ; then
+ opts=$(${cake} Completion subcommands $prev)
+ COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+ if [[ $COMPREPLY = "" ]] ; then
+ _filedir
+ return 0
+ fi
+ return 0
+ fi
+
+ opts=$(${cake} Completion fuzzy "${COMP_WORDS[@]:1}")
+ COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+ if [[ $COMPREPLY = "" ]] ; then
+ _filedir
+ return 0
+ fi
+ return 0;
+}
+
+complete -F _cake cake bin/cake
+```
+
+## Using autocompletion
+
+Once enabled, the autocompletion can be used the same way than for other
+built-in commands, using the **TAB** key.
+Three type of autocompletion are provided. The following output are from a fresh CakePHP install.
+
+### Commands
+
+Sample output for commands autocompletion:
+
+``` bash
+$ bin/cake
+bake i18n schema_cache routes
+console migrations plugin server
+```
+
+### Subcommands
+
+Sample output for subcommands autocompletion:
+
+``` bash
+$ bin/cake bake
+behavior helper command
+cell mailer command_helper
+component migration template
+controller migration_snapshot test
+fixture model
+form plugin
+```
+
+### Options
+
+Sample output for subcommands options autocompletion:
+
+``` bash
+$ bin/cake bake -
+-c --everything --force --help --plugin -q -t -v
+--connection -f -h -p --prefix --quiet --theme --verbose
+```
diff --git a/docs/en/console-commands/counter-cache.md b/docs/en/console-commands/counter-cache.md
new file mode 100644
index 0000000000..ad3eab7f80
--- /dev/null
+++ b/docs/en/console-commands/counter-cache.md
@@ -0,0 +1,24 @@
+# CounterCache Tool
+
+The CounterCacheCommand provides a CLI tool for rebuilding the counter caches
+in your application and plugin models. It can be used in maintenance and
+recovery operations, or to populate new counter caches added to your
+application.
+
+``` bash
+bin/cake counter_cache --assoc Comments Articles
+```
+
+This would rebuild the `Comments` related counters on the `Articles` table.
+For very large tables you may need to rebuild counters in batches. You can use
+the `--limit` and `--page` options to incrementally rebuild counter state.
+
+``` bash
+bin/cake counter_cache --assoc Comments --limit 100 --page 2 Articles
+```
+
+When `limit` and `page` are used, records will be ordered by the table's
+primary key.
+
+::: info Added in version 5.2.0
+:::
diff --git a/docs/en/console-commands/cron-jobs.md b/docs/en/console-commands/cron-jobs.md
new file mode 100644
index 0000000000..3d4ed76dc2
--- /dev/null
+++ b/docs/en/console-commands/cron-jobs.md
@@ -0,0 +1,34 @@
+# Running Shells as Cron Jobs
+
+A common thing to do with a shell is making it run as a cronjob to
+clean up the database once in a while or send newsletters. This is
+trivial to setup, for example:
+
+``` text
+*/5 * * * * cd /full/path/to/root && bin/cake myshell myparam
+# * * * * * command to execute
+# │ │ │ │ │
+# │ │ │ │ │
+# │ │ │ │ \───── day of week (0 - 6) (0 to 6 are Sunday to Saturday,
+# | | | | or use names)
+# │ │ │ \────────── month (1 - 12)
+# │ │ \─────────────── day of month (1 - 31)
+# │ \──────────────────── hour (0 - 23)
+# \───────────────────────── min (0 - 59)
+```
+
+You can see more info here:
+
+> [!TIP]
+> Use `-q` (or --quiet) to silence any output for cronjobs.
+
+## Cron Jobs on Shared Hosting
+
+On some shared hostings `cd /full/path/to/root && bin/cake mycommand myparam`
+might not work. Instead you can use
+`php /full/path/to/root/bin/cake.php mycommand myparam`.
+
+> [!NOTE]
+> register_argc_argv has to be turned on by including `register_argc_argv = 1` in your php.ini. If you cannot change register_argc_argv globally,
+> you can tell the cron job to use your own configuration by
+> specifying it with `-d register_argc_argv=1` parameter. Example: `php -d register_argc_argv=1 /full/path/to/root/bin/cake.php myshell myparam`
diff --git a/docs/en/console-commands/i18n.md b/docs/en/console-commands/i18n.md
new file mode 100644
index 0000000000..d91c4990cf
--- /dev/null
+++ b/docs/en/console-commands/i18n.md
@@ -0,0 +1,81 @@
+# I18N Tool
+
+The i18n features of CakePHP use [po files](https://en.wikipedia.org/wiki/GNU_gettext)
+as their translation source. PO files integrate with commonly used translation tools
+like [Poedit](https://www.poedit.net/).
+
+The i18n commands provides a quick way to generate po template files.
+These templates files can then be given to translators so they can translate the
+strings in your application. Once you have translations done, pot files can be
+merged with existing translations to help update your translations.
+
+## Generating POT Files
+
+POT files can be generated for an existing application using the `extract`
+command. This command will scan your entire application for `__()` style
+function calls, and extract the message string. Each unique string in your
+application will be combined into a single POT file:
+
+``` bash
+bin/cake i18n extract
+```
+
+The above will run the extraction command. The result of this command will be the
+file **resources/locales/default.pot**. You use the pot file as a template for creating
+po files. If you are manually creating po files from the pot file, be sure to
+correctly set the `Plural-Forms` header line.
+
+### Generating POT Files for Plugins
+
+You can generate a POT file for a specific plugin using:
+
+``` bash
+bin/cake i18n extract --plugin
+```
+
+This will generate the required POT files used in the plugins.
+
+### Extracting from multiple folders at once
+
+Sometimes, you might need to extract strings from more than one directory of
+your application. For instance, if you are defining some strings in the
+`config/` directory of your application, you probably want to extract strings
+from this directory as well as from the `src/` directory. You can do it by
+using the `--paths` option. It takes a comma-separated list of absolute paths
+to extract:
+
+``` bash
+bin/cake i18n extract --paths /var/www/app/config,/var/www/app/src
+```
+
+### Excluding Folders
+
+You can pass a comma separated list of folders that you wish to be excluded.
+Any path containing a path segment with the provided values will be ignored:
+
+``` bash
+bin/cake i18n extract --exclude vendor,tests
+```
+
+### Skipping Overwrite Warnings for Existing POT Files
+
+By adding `--overwrite`, the shell script will no longer warn you if a POT
+file already exists and will overwrite by default:
+
+``` bash
+bin/cake i18n extract --overwrite
+```
+
+### Extracting Messages from the CakePHP Core Libraries
+
+By default, the extract shell script will ask you if you like to extract
+the messages used in the CakePHP core libraries. Set `--extract-core` to yes
+or no to set the default behavior:
+
+``` bash
+bin/cake i18n extract --extract-core yes
+
+// or
+
+bin/cake i18n extract --extract-core no
+```
diff --git a/docs/en/console-commands/input-output.md b/docs/en/console-commands/input-output.md
new file mode 100644
index 0000000000..0a7a77cb54
--- /dev/null
+++ b/docs/en/console-commands/input-output.md
@@ -0,0 +1,412 @@
+# Command Input/Output
+
+`class` Cake\\Console\\**ConsoleIo**
+
+CakePHP provides the `ConsoleIo` object to commands so that they can
+interactively read user input and output information to the user.
+
+
+
+## Command Helpers
+
+Formatting console output can be tedious and lead to maintenance issues. To
+enable better re-use and testability of console formatting code, CakePHP command
+helpers provide 'macros' for console formatting logic. Command Helpers can be
+accessed and used from any command:
+
+``` php
+// Output some data as a table.
+$io->helper('Table')->output($data);
+
+// Get a helper from a plugin.
+$io->helper('Plugin.HelperName')->output($data);
+```
+
+You can also get instances of helpers and call any public methods, to manipulate
+state and generate updated output:
+
+``` php
+// Get and use the Progress Helper.
+$progress = $io->helper('Progress');
+$progress->increment(10);
+$progress->draw();
+```
+
+## Creating Helpers
+
+While CakePHP comes with a few command helpers you can create more in your
+application or plugins. As an example, we'll create a simple helper to generate
+fancy headings. First create the **src/Command/Helper/HeadingHelper.php** and put
+the following in it:
+
+``` php
+_io->out($marker . ' ' . $args[0] . ' ' . $marker);
+ }
+}
+```
+
+We can then use this new helper in one of our shell commands by calling it:
+
+``` php
+// With ### on either side
+$this->helper('Heading')->output(['It works!']);
+
+// With ~~~~ on either side
+$this->helper('Heading')->output(['It works!', '~', 4]);
+```
+
+Helpers generally implement the `output()` method which takes an array of
+parameters. However, because Console Helpers are vanilla classes they can
+implement additional methods that take any form of arguments.
+
+> [!NOTE]
+> Helpers can also live in `src/Shell/Helper` for backwards compatibility.
+
+## Built-In Helpers
+
+### Table Helper
+
+The TableHelper assists in making well formatted ASCII art tables. Using it is
+pretty simple:
+
+``` php
+$data = [
+ ['Header 1', 'Header', 'Long Header'],
+ ['short', 'Longish thing', 'short'],
+ ['Longer thing', 'short', 'Longest Value'],
+];
+$io->helper('Table')->output($data);
+
+// Outputs
++--------------+---------------+---------------+
+| Header 1 | Header | Long Header |
++--------------+---------------+---------------+
+| short | Longish thing | short |
+| Longer thing | short | Longest Value |
++--------------+---------------+---------------+
+```
+
+You can use the `` formatting tag in tables to right align
+content:
+
+``` php
+$data = [
+ ['Name', 'Total Price'],
+ ['Cake Mix', '1.50'],
+];
+$io->helper('Table')->output($data);
+
+// Outputs
++----------+-------------+
+| Name 1 | Total Price |
++----------+-------------+
+| Cake Mix | 1.50 |
++----------+-------------+
+```
+
+### Progress Helper
+
+The ProgressHelper can be used in two different ways. The simple mode lets you
+provide a callback that is invoked until the progress is complete:
+
+``` php
+$io->helper('Progress')->output(['callback' => function ($progress) {
+ // Do work here.
+ $progress->increment(20);
+ $progress->draw();
+}]);
+```
+
+You can control the progress bar more by providing additional options:
+
+- `total` The total number of items in the progress bar. Defaults
+ to 100.
+- `width` The width of the progress bar. Defaults to 80.
+- `callback` The callback that will be called in a loop to advance the
+ progress bar.
+
+An example of all the options in use would be:
+
+``` php
+$io->helper('Progress')->output([
+ 'total' => 10,
+ 'width' => 20,
+ 'callback' => function ($progress) {
+ $progress->increment(2);
+ $progress->draw();
+ }
+]);
+```
+
+The progress helper can also be used manually to increment and re-render the
+progress bar as necessary:
+
+``` php
+$progress = $io->helper('Progress');
+$progress->init([
+ 'total' => 10,
+ 'width' => 20,
+]);
+
+$progress->increment(4);
+$progress->draw();
+```
+
+### Banner Helper
+
+The `BannerHelper` can be used to format one or more lines of text into
+a banner with a background and horizontal padding:
+
+``` php
+$io->helper('Banner')
+ ->withPadding(5)
+ ->withStyle('success.bg')
+ ->output(['Work complete']);
+```
+
+::: info Added in version 5.1.0
+The `BannerHelper` was added in 5.1
+:::
+
+## Getting User Input
+
+`method` Cake\\Console\\ConsoleIo::**ask**($question, $choices = null, $default = null): string
+
+When building interactive console applications you'll need to get user input.
+CakePHP provides a way to do this:
+
+``` php
+// Get arbitrary text from the user.
+$color = $io->ask('What color do you like?');
+
+// Get a choice from the user.
+$selection = $io->askChoice('Red or Green?', ['R', 'G'], 'R');
+```
+
+Selection validation is case-insensitive.
+
+## Creating Files
+
+`method` Cake\\Console\\ConsoleIo::**createFile**($path, $contents): bool
+
+Creating files is often important part of many console commands that help
+automate development and deployment. The `createFile()` method gives you
+a simple interface for creating files with interactive confirmation:
+
+``` php
+// Create a file with confirmation on overwrite
+$io->createFile('bower.json', $stuff);
+
+// Force overwriting without asking
+$io->createFile('bower.json', $stuff, true);
+```
+
+## Creating Output
+
+### ConsoleIo::out()
+
+Writing to `stdout` is done using the `out()` method:
+
+``` php
+// Write to stdout
+$io->out('Normal message');
+```
+
+### ConsoleIo::err()
+
+Writing to `stderr` is done using the `err()` method:
+
+``` php
+// Write to stderr
+$io->err('Error message');
+```
+
+In addition to vanilla output methods, CakePHP provides wrapper methods that
+style output with appropriate ANSI colors:
+
+``` php
+// Green text on stdout
+$io->success('Success message');
+
+// Cyan text on stdout
+$io->info('Informational text');
+
+// Blue text on stdout
+$io->comment('Additional context');
+
+// Red text on stderr
+$io->error('Error text');
+
+// Yellow text on stderr
+$io->warning('Warning text');
+```
+
+Color formatting will automatically be disabled if `posix_isatty` returns
+true, or if the `NO_COLOR` environment variable is set.
+
+`ConsoleIo` provides two convenience methods regarding the output level:
+
+``` php
+// Would only appear when verbose output is enabled (-v)
+$io->verbose('Verbose message');
+
+// Would appear at all levels.
+$io->quiet('Quiet message');
+```
+
+You can also create blank lines or draw lines of dashes:
+
+``` php
+// Output 2 newlines
+$io->out($io->nl(2));
+
+// Draw a horizontal line
+$io->hr();
+```
+
+Lastly, you can update the current line of text on the screen:
+
+``` php
+$io->out('Counting down');
+$io->out('10', 0);
+for ($i = 9; $i > 0; $i--) {
+ sleep(1);
+ $io->overwrite($i, 0, 2);
+}
+```
+
+> [!NOTE]
+> It is important to remember, that you cannot overwrite text
+> once a new line has been output.
+
+
+
+## Output Levels
+
+Console applications often need different levels of verbosity. For example, when
+running as a cron job, most output is un-necessary. You can use output levels to
+flag output appropriately. The user of the shell, can then decide what level of
+detail they are interested in by setting the correct flag when calling the
+command. There are 3 levels:
+
+- `QUIET` - Only absolutely important information should be marked for quiet
+ output.
+- `NORMAL` - The default level, and normal usage.
+- `VERBOSE` - Mark messages that may be too noisy for everyday use, but
+ helpful for debugging as `VERBOSE`.
+
+You can mark output as follows:
+
+``` php
+// Would appear at all levels.
+$io->out('Quiet message', 1, ConsoleIo::QUIET);
+$io->quiet('Quiet message');
+
+// Would not appear when quiet output is toggled.
+$io->out('normal message', 1, ConsoleIo::NORMAL);
+$io->out('loud message', 1, ConsoleIo::VERBOSE);
+$io->verbose('Verbose output');
+
+// Would only appear when verbose output is enabled.
+$io->out('extra message', 1, ConsoleIo::VERBOSE);
+$io->verbose('Verbose output');
+```
+
+You can control the output level of commands, by using the `--quiet` and
+`--verbose` options. These options are added by default, and allow you to
+consistently control output levels inside your CakePHP comands.
+
+The `--quiet` and `--verbose` options also control how logging data is
+output to stdout/stderr. Normally info and higher log messages are output to
+stdout/stderr. When `--verbose` is used, debug logs will be output to stdout.
+When `--quiet` is used, only warning and higher log messages will be output to
+stderr.
+
+## Styling Output
+
+Styling output is done by including tags - just like HTML - in your output.
+These tags will be replaced with the correct ansi code sequence, or
+stripped if you are on a console that doesn't support ansi codes. There
+are several built-in styles, and you can create more. The built-in ones are
+
+- `success` Success messages. Green text.
+- `error` Error messages. Red text.
+- `warning` Warning messages. Yellow text.
+- `info` Informational messages. Cyan text.
+- `comment` Additional text. Blue text.
+- `question` Text that is a question, added automatically by shell.
+- `info.bg` White background with cyan text.
+- `warning.bg` Yellow background with black text.
+- `error.bg` Red background with black text.
+- `success.bg` Green background with black text.
+
+You can create additional styles using `$io->setStyle()`. To declare a
+new output style you could do:
+
+``` php
+$io->setStyle('flashy', ['text' => 'magenta', 'blink' => true]);
+```
+
+This would then allow you to use a `` tag in your shell output, and if
+ansi colors are enabled, the following would be rendered as blinking magenta
+text `$this->out('Whoooa Something went wrong');`. When
+defining styles you can use the following colors for the `text` and
+`background` attributes:
+
+- black
+- blue
+- cyan
+- green
+- magenta
+- red
+- white
+- yellow
+
+You can also use the following options as boolean switches, setting them to a
+truthy value enables them.
+
+- blink
+- bold
+- reverse
+- underline
+
+Adding a style makes it available on all instances of ConsoleOutput as well,
+so you don't have to redeclare styles for both stdout and stderr objects.
+
+::: info Changed in version 5.1.0
+The `info.bg`, `warning.bg`, `error.bg`, and `success.bg` were added.
+:::
+
+## Turning Off Coloring
+
+Although coloring is pretty, there may be times when you want to turn it off,
+or force it on:
+
+``` php
+$io->outputAs(ConsoleOutput::RAW);
+```
+
+The above will put the output object into raw output mode. In raw output mode,
+no styling is done at all. There are three modes you can use.
+
+- `ConsoleOutput::COLOR` - Output with color escape codes in place.
+- `ConsoleOutput::PLAIN` - Plain text output, known style tags will be
+ stripped from the output.
+- `ConsoleOutput::RAW` - Raw output, no styling or formatting will be done.
+ This is a good mode to use if you are outputting XML or, want to debug why
+ your styling isn't working.
+
+By default on \*nix systems ConsoleOutput objects default to color output.
+On Windows systems, plain output is the default unless the `ANSICON`
+environment variable is present.
diff --git a/docs/en/console-commands/option-parsers.md b/docs/en/console-commands/option-parsers.md
new file mode 100644
index 0000000000..4ae45e25e3
--- /dev/null
+++ b/docs/en/console-commands/option-parsers.md
@@ -0,0 +1,378 @@
+# Option Parsers
+
+`class` Cake\\Console\\**ConsoleOptionParser**
+
+Console applications typically take options and arguments as the primary way to
+get information from the terminal into your commands.
+
+## Defining an OptionParser
+
+Commands and Shells provide a `buildOptionParser($parser)` hook method that
+you can use to define the options and arguments for your commands:
+
+``` php
+protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+{
+ // Define your options and arguments.
+
+ // Return the completed parser
+ return $parser;
+}
+```
+
+Shell classes use the `getOptionParser()` hook method to define their option
+parser:
+
+``` php
+public function getOptionParser()
+{
+ // Get an empty parser from the framework.
+ $parser = parent::getOptionParser();
+
+ // Define your options and arguments.
+
+ // Return the completed parser
+ return $parser;
+}
+```
+
+## Using Arguments
+
+`method` Cake\\Console\\ConsoleOptionParser::**addArgument**($name, $params = [])
+
+Positional arguments are frequently used in command line tools,
+and `ConsoleOptionParser` allows you to define positional
+arguments as well as make them required. You can add arguments
+one at a time with `$parser->addArgument();` or multiple at once
+with `$parser->addArguments();`:
+
+``` php
+$parser->addArgument('model', ['help' => 'The model to bake']);
+```
+
+You can use the following options when creating an argument:
+
+- `help` The help text to display for this argument.
+- `required` Whether this parameter is required.
+- `index` The index for the arg, if left undefined the argument will be put
+ onto the end of the arguments. If you define the same index twice the
+ first option will be overwritten.
+- `choices` An array of valid choices for this argument. If left empty all
+ values are valid. An exception will be raised when parse() encounters an
+ invalid value.
+- `separator` A character sequence that separates arguments that should be
+ parsed into an array.
+
+Arguments that have been marked as required will throw an exception when
+parsing the command if they have been omitted. So you don't have to
+handle that in your shell.
+
+::: info Added in version 5.2.0
+The `separator` option was added.
+:::
+
+### Adding Multiple Arguments
+
+`method` Cake\\Console\\ConsoleOptionParser::**addArguments**(array $args)
+
+If you have an array with multiple arguments you can use
+`$parser->addArguments()` to add multiple arguments at once. :
+
+``` php
+$parser->addArguments([
+ 'node' => ['help' => 'The node to create', 'required' => true],
+ 'parent' => ['help' => 'The parent node', 'required' => true],
+]);
+```
+
+As with all the builder methods on ConsoleOptionParser, addArguments
+can be used as part of a fluent method chain.
+
+### Validating Arguments
+
+When creating positional arguments, you can use the `required` flag, to
+indicate that an argument must be present when a shell is called.
+Additionally you can use `choices` to force an argument to be from a list of
+valid choices:
+
+``` php
+$parser->addArgument('type', [
+ 'help' => 'The type of node to interact with.',
+ 'required' => true,
+ 'choices' => ['aro', 'aco'],
+]);
+```
+
+The above will create an argument that is required and has validation on the
+input. If the argument is either missing, or has an incorrect value an exception
+will be raised and the shell will be stopped.
+
+## Using Options
+
+`method` Cake\\Console\\ConsoleOptionParser::**addOption**($name, array $options = [])
+
+Options or flags are used in command line tools to provide unordered key/value
+arguments for your commands. Options can define both verbose and short aliases.
+They can accept a value (e.g `--connection=default`) or be boolean options
+(e.g `--verbose`). Options are defined with the `addOption()` method:
+
+``` php
+$parser->addOption('connection', [
+ 'short' => 'c',
+ 'help' => 'connection',
+ 'default' => 'default',
+]);
+```
+
+The above would allow you to use either `cake myshell --connection=other`,
+`cake myshell --connection other`, or `cake myshell -c other`
+when invoking the shell.
+
+Boolean switches do not accept or consume values, and their presence just
+enables them in the parsed parameters:
+
+``` php
+$parser->addOption('no-commit', ['boolean' => true]);
+```
+
+This option when used like `cake mycommand --no-commit something` would have
+a value of `true`, and 'something' would be a treated as a positional
+argument.
+
+When creating options you can use the following options to define the behavior
+of the option:
+
+- `short` - The single letter variant for this option, leave undefined for
+ none.
+- `help` - Help text for this option. Used when generating help for the
+ option.
+- `default` - The default value for this option. If not defined the default
+ will be `true`.
+- `boolean` - The option uses no value, it's just a boolean switch.
+ Defaults to `false`.
+- `multiple` - The option can be provided multiple times. The parsed option
+ will be an array of values when this option is enabled.
+- `separator` - A character sequence that the option value is split into an
+ array with.
+- `choices` - An array of valid choices for this option. If left empty all
+ values are valid. An exception will be raised when parse() encounters an
+ invalid value.
+
+::: info Added in version 5.2.0
+The `separator` option was added.
+:::
+
+### Adding Multiple Options
+
+`method` Cake\\Console\\ConsoleOptionParser::**addOptions**(array $options)
+
+If you have an array with multiple options you can use `$parser->addOptions()`
+to add multiple options at once. :
+
+``` php
+$parser->addOptions([
+ 'node' => ['short' => 'n', 'help' => 'The node to create'],
+ 'parent' => ['short' => 'p', 'help' => 'The parent node'],
+]);
+```
+
+As with all the builder methods on ConsoleOptionParser, addOptions can be used
+as part of a fluent method chain.
+
+### Validating Options
+
+Options can be provided with a set of choices much like positional arguments
+can be. When an option has defined choices, those are the only valid choices
+for an option. All other values will raise an `InvalidArgumentException`:
+
+``` php
+$parser->addOption('accept', [
+ 'help' => 'What version to accept.',
+ 'choices' => ['working', 'theirs', 'mine'],
+]);
+```
+
+### Using Boolean Options
+
+Options can be defined as boolean options, which are useful when you need to
+create some flag options. Like options with defaults, boolean options always
+include themselves into the parsed parameters. When the flags are present they
+are set to `true`, when they are absent they are set to `false`:
+
+``` php
+$parser->addOption('verbose', [
+ 'help' => 'Enable verbose output.',
+ 'boolean' => true
+]);
+```
+
+The following option would always have a value in the parsed parameter. When not
+included its default value would be `false`, and when defined it will be
+`true`.
+
+### Building a ConsoleOptionParser from an Array
+
+`method` Cake\\Console\\ConsoleOptionParser::**buildFromArray**($spec): static
+
+Option parsers can also be defined as arrays. Within the array, you can define
+keys for `arguments`, `options`, `description` and `epilog`. The values
+for arguments, and options, should follow the format that
+`Cake\Console\ConsoleOptionParser::addArguments()` and
+`Cake\Console\ConsoleOptionParser::addOptions()` use. You can also
+use `buildFromArray` on its own, to build an option parser:
+
+``` php
+public function getOptionParser()
+{
+ return ConsoleOptionParser::buildFromArray([
+ 'description' => [
+ __("Use this command to grant ACL permissions. Once executed, the "),
+ __("ARO specified (and its children, if any) will have ALLOW access "),
+ __("to the specified ACO action (and the ACO's children, if any).")
+ ],
+ 'arguments' => [
+ 'aro' => ['help' => __('ARO to check.'), 'required' => true],
+ 'aco' => ['help' => __('ACO to check.'), 'required' => true],
+ 'action' => ['help' => __('Action to check')],
+ ],
+ ]);
+}
+```
+
+### Merging Option Parsers
+
+`method` Cake\\Console\\ConsoleOptionParser::**merge**($spec)
+
+When building a group command, you maybe want to combine several parsers for
+this:
+
+``` php
+$parser->merge($anotherParser);
+```
+
+Note that the order of arguments for each parser must be the same, and that
+options must also be compatible for it work. So do not use keys for different
+things.
+
+## Getting Help from Shells
+
+By defining your options and arguments with the option parser CakePHP can
+automatically generate rudimentary help information and add a `--help` and
+`-h` to each of your commands. Using one of these options will allow you to
+see the generated help content:
+
+``` bash
+bin/cake bake --help
+bin/cake bake -h
+```
+
+Would both generate the help for bake. You can also get help for nested
+commands:
+
+``` bash
+bin/cake bake model --help
+bin/cake bake model -h
+```
+
+The above would get you the help specific to bake's model command.
+
+### Getting Help as XML
+
+When building automated tools or development tools that need to interact with
+CakePHP shell commands, it's nice to have help available in a machine parse-able format.
+By providing the `xml` option when requesting help you can have help content
+returned as XML:
+
+``` bash
+cake bake --help xml
+cake bake -h xml
+```
+
+The above would return an XML document with the generated help, options, and
+arguments for the selected shell. A sample XML document would
+look like:
+
+``` xml
+
+
+ bake fixture
+ Generate fixtures for use with the test suite. You can use
+ `bake fixture all` to bake all fixtures.
+
+ Omitting all arguments and options will enter into an interactive
+ mode.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Customizing Help Output
+
+You can further enrich the generated help content by adding a description, and
+epilog.
+
+### Set the Description
+
+`method` Cake\\Console\\ConsoleOptionParser::**setDescription**($text)
+
+The description displays above the argument and option information. By passing
+in either an array or a string, you can set the value of the description:
+
+``` php
+// Set multiple lines at once
+$parser->setDescription(['line one', 'line two']);
+
+// Read the current value
+$parser->getDescription();
+```
+
+### Set the Epilog
+
+`method` Cake\\Console\\ConsoleOptionParser::**setEpilog**($text)
+
+Gets or sets the epilog for the option parser. The epilog is displayed after the
+argument and option information. By passing in either an array or a string, you
+can set the value of the epilog:
+
+``` php
+// Set multiple lines at once
+$parser->setEpilog(['line one', 'line two']);
+
+// Read the current value
+$parser->getEpilog();
+```
diff --git a/docs/en/console-commands/plugin.md b/docs/en/console-commands/plugin.md
new file mode 100644
index 0000000000..0848f03db6
--- /dev/null
+++ b/docs/en/console-commands/plugin.md
@@ -0,0 +1,58 @@
+
+
+# Plugin Tool
+
+The plugin tool allows you to load and unload plugins via the command prompt.
+If you need help, run:
+
+``` bash
+bin/cake plugin --help
+```
+
+## Loading Plugins
+
+Via the `Load` task you are able to load plugins in your
+**config/bootstrap.php**. You can do this by running:
+
+``` bash
+bin/cake plugin load MyPlugin
+```
+
+This will add the following to your **src/Application.php**:
+
+``` php
+// In the bootstrap method add:
+$this->addPlugin('MyPlugin');
+```
+
+## Unloading Plugins
+
+You can unload a plugin by specifying its name:
+
+``` bash
+bin/cake plugin unload MyPlugin
+```
+
+This will remove the line `$this->addPlugin('MyPlugin',...)` from
+**src/Application.php**.
+
+## Plugin Assets
+
+CakePHP by default serves plugins assets using the `AssetMiddleware` middleware.
+While this is a good convenience, it is recommended to symlink / copy
+the plugin assets under app's webroot so that they can be directly served by the
+web server without invoking PHP. You can do this by running:
+
+``` bash
+bin/cake plugin assets symlink
+```
+
+Running the above command will symlink all plugins assets under app's webroot.
+On Windows, which doesn't support symlinks, the assets will be copied in
+respective folders instead of being symlinked.
+
+You can symlink assets of one particular plugin by specifying its name:
+
+``` bash
+bin/cake plugin assets symlink MyPlugin
+```
diff --git a/docs/en/console-commands/repl.md b/docs/en/console-commands/repl.md
new file mode 100644
index 0000000000..bc0cabc9a2
--- /dev/null
+++ b/docs/en/console-commands/repl.md
@@ -0,0 +1,52 @@
+# Interactive Console (REPL)
+
+CakePHP offers
+[REPL(Read Eval Print Loop) plugin](https://github.com/cakephp/repl) to let
+you explore some CakePHP and your application in an interactive console.
+
+> [!NOTE]
+> The plugin was shipped with the CakePHP app skeleton before 4.3.
+
+You can start the interactive console using:
+
+``` bash
+bin/cake console
+```
+
+This will bootstrap your application and start an interactive console. At this
+point you can interact with your application code and execute queries using your
+application's models:
+
+``` bash
+bin/cake console
+
+>>> $articles = Cake\Datasource\FactoryLocator::get('Table')->get('Articles');
+// object(Cake\ORM\Table)(
+//
+// )
+>>> $articles->find()->all();
+```
+
+Since your application has been bootstrapped you can also test routing using the
+REPL:
+
+``` php
+>>> Cake\Routing\Router::parse('/articles/view/1');
+// [
+// 'controller' => 'Articles',
+// 'action' => 'view',
+// 'pass' => [
+// 0 => '1'
+// ],
+// 'plugin' => NULL
+// ]
+```
+
+You can also test generating URLs:
+
+``` php
+>>> Cake\Routing\Router::url(['controller' => 'Articles', 'action' => 'edit', 99]);
+// '/articles/edit/99'
+```
+
+To quit the REPL you can use `CTRL-C` or by typing `exit`.
diff --git a/docs/en/console-commands/routes.md b/docs/en/console-commands/routes.md
new file mode 100644
index 0000000000..c0f3607c3e
--- /dev/null
+++ b/docs/en/console-commands/routes.md
@@ -0,0 +1,35 @@
+# Routes Tool
+
+The routes tool provides a simple to use CLI interface for testing and debugging
+routes. You can use it to test how routes are parsed, and what URLs routing
+parameters will generate.
+
+## Getting a List of all Routes
+
+``` bash
+bin/cake routes
+```
+
+## Testing URL parsing
+
+You can quickly see how a URL will be parsed using the `check` method:
+
+``` bash
+bin/cake routes check /articles/edit/1
+```
+
+If your route contains any query string parameters remember to surround the URL
+in quotes:
+
+``` bash
+bin/cake routes check "/articles/?page=1&sort=title&direction=desc"
+```
+
+## Testing URL Generation
+
+You can see the URL a `routing array` will generate using the
+`generate` method:
+
+``` bash
+bin/cake routes generate controller:Articles action:edit 1
+```
diff --git a/docs/en/console-commands/schema-cache.md b/docs/en/console-commands/schema-cache.md
new file mode 100644
index 0000000000..5fe13e4172
--- /dev/null
+++ b/docs/en/console-commands/schema-cache.md
@@ -0,0 +1,29 @@
+# Schema Cache Tool
+
+The SchemaCacheCommand provides a simple CLI tool for managing your application's
+metadata caches. In deployment situations it is helpful to rebuild the metadata
+cache in-place without clearing the existing cache data. You can do this by
+running:
+
+``` bash
+bin/cake schema_cache build --connection default
+```
+
+This will rebuild the metadata cache for all tables on the `default`
+connection. If you only need to rebuild a single table you can do that by
+providing its name:
+
+``` bash
+bin/cake schema_cache build --connection default articles
+```
+
+In addition to building cached data, you can use the SchemaCacheShell to remove
+cached metadata as well:
+
+``` bash
+# Clear all metadata
+bin/cake schema_cache clear
+
+# Clear a single table
+bin/cake schema_cache clear articles
+```
diff --git a/docs/en/console-commands/server.md b/docs/en/console-commands/server.md
new file mode 100644
index 0000000000..a92629ab82
--- /dev/null
+++ b/docs/en/console-commands/server.md
@@ -0,0 +1,26 @@
+# Server Tool
+
+The `ServerCommand` lets you stand up a simple webserver using the built in PHP
+webserver. While this server is *not* intended for production use it can
+be handy in development when you want to quickly try an idea out and don't want
+to spend time configuring Apache or Nginx. You can start the server command with:
+
+``` bash
+bin/cake server
+```
+
+You should see the server boot up and attach to port 8765. You can visit the
+CLI server by visiting `http://localhost:8765`
+in your web-browser. You can close the server by pressing `CTRL-C` in your
+terminal.
+
+> [!NOTE]
+> Try `bin/cake server -H 0.0.0.0` if the server is unreachable from other hosts.
+
+## Changing the Port and Document Root
+
+You can customize the port and document root using options:
+
+``` bash
+bin/cake server --port 8080 --document_root path/to/app
+```
diff --git a/docs/en/contents.md b/docs/en/contents.md
new file mode 100644
index 0000000000..f825b6d1c0
--- /dev/null
+++ b/docs/en/contents.md
@@ -0,0 +1,79 @@
+# Contents
+
+### Preface
+
+- [CakePHP at a Glance](intro)
+- [Quick Start Guide](quickstart)
+- [Migration Guides](appendices/migration-guides)
+- [Tutorials & Examples](tutorials-and-examples)
+- [Contributing](contributing)
+- [Release Policy](release-policy)
+
+### Getting Started
+
+- [Installation](installation)
+- [Configuration](development/configuration)
+- [Application](development/application)
+- [Dependency Injection](development/dependency-injection)
+- [Routing](development/routing)
+- [Request & Response Objects](controllers/request-response)
+- [Middleware](controllers/middleware)
+- [Controllers](controllers)
+- [Views](views)
+- [Database Access & ORM](orm)
+
+### Using CakePHP
+
+- [Caching](core-libraries/caching)
+- [Console Commands](console-commands)
+- [Debugging](development/debugging)
+- [Deployment](deployment)
+- [Mailer](core-libraries/email)
+- [Error & Exception Handling](development/errors)
+- [Events System](core-libraries/events)
+- [Internationalization & Localization](core-libraries/internationalization-and-localization)
+- [Logging](core-libraries/logging)
+- [Modelless Forms](core-libraries/form)
+- [Pagination](controllers/pagination)
+- [Plugins](plugins)
+- [REST](development/rest)
+- [Security](security)
+- [Sessions](development/sessions)
+- [Testing](development/testing)
+- [Validation](core-libraries/validation)
+
+### Utility Classes
+
+- [App Class](core-libraries/app)
+- [Collections](core-libraries/collections)
+- [Hash](core-libraries/hash)
+- [Http Client](core-libraries/httpclient)
+- [Inflector](core-libraries/inflector)
+- [Number](core-libraries/number)
+- [Plugin Class](core-libraries/plugin)
+- [Registry Objects](core-libraries/registry-objects)
+- [Text](core-libraries/text)
+- [Date & Time](core-libraries/time)
+- [Xml](core-libraries/xml)
+
+### Plugins & Packages
+
+- [Standalone Packages](standalone-packages)
+- [Authentication](https://book.cakephp.org/authentication/3/)
+- [Authorization](https://book.cakephp.org/authorization/3/)
+- [Bake](https://book.cakephp.org/bake/3/)
+- [Debug Kit](https://book.cakephp.org/debugkit/5/)
+- [Migrations](https://book.cakephp.org/migrations/4/)
+- [Elasticsearch](https://book.cakephp.org/elasticsearch/4/)
+- [Phinx](https://book.cakephp.org/phinx/0/en/)
+- [Chronos](https://book.cakephp.org/chronos/3/)
+- [Queue](https://book.cakephp.org/queue/2/)
+
+### Other
+
+- [Constants & Functions](core-libraries/global-constants-and-functions)
+- [Appendices](appendices)
+
+
+
+
diff --git a/docs/en/contributing.md b/docs/en/contributing.md
new file mode 100644
index 0000000000..d6c4fb4aaf
--- /dev/null
+++ b/docs/en/contributing.md
@@ -0,0 +1,10 @@
+# Contributing
+
+There are a number of ways you can contribute to CakePHP. The following sections
+cover the various ways you can contribute to CakePHP:
+
+- [Documentation](contributing/documentation)
+- [Tickets](contributing/tickets)
+- [Code](contributing/code)
+- [Coding Standards](contributing/cakephp-coding-conventions)
+- [Backwards Compatibility Guide](contributing/backwards-compatibility)
diff --git a/docs/en/contributing/backwards-compatibility.md b/docs/en/contributing/backwards-compatibility.md
new file mode 100644
index 0000000000..58b01c7b7a
--- /dev/null
+++ b/docs/en/contributing/backwards-compatibility.md
@@ -0,0 +1,298 @@
+# Backwards Compatibility Guide
+
+Ensuring that you can upgrade your applications easily and smoothly is important
+to us. That's why we only break compatibility at major release milestones.
+You might be familiar with [semantic versioning](https://semver.org/), which is
+the general guideline we use on all CakePHP projects. In short, semantic
+versioning means that only major releases (such as 2.0, 3.0, 4.0) can break
+backwards compatibility. Minor releases (such as 2.1, 3.1, 3.2) may introduce new
+features, but are not allowed to break compatibility. Bug fix releases (such as 2.1.2,
+3.0.1) do not add new features, but fix bugs or enhance performance only.
+
+> [!NOTE]
+> Deprecations are removed with the next major version of the framework.
+> It is advised that you adapt to deprecations as they are introduced to
+> ensure future upgrades are easier.
+
+To clarify what changes you can expect in each release tier we have more
+detailed information for developers using CakePHP, and for developers working on
+CakePHP that helps set expectations of what can be done in minor releases. Major
+releases can have as many breaking changes as required.
+
+## Migration Guides
+
+For each major and minor release, the CakePHP team will provide a migration
+guide. These guides explain the new features and any breaking changes that are
+in each release. They can be found in the [Appendices](../appendices) section of the
+cookbook.
+
+## Using CakePHP
+
+If you are building your application with CakePHP, the following guidelines
+explain the stability you can expect.
+
+### Interfaces
+
+Outside of major releases, interfaces provided by CakePHP will **not** have any
+existing methods changed. New methods may be added, but no existing methods will
+be changed.
+
+### Classes
+
+Classes provided by CakePHP can be constructed and have their public methods and
+properties used by application code and outside of major releases backwards
+compatibility is ensured.
+
+> [!NOTE]
+> Some classes in CakePHP are marked with the `@internal` API doc tag. These
+> classes are **not** stable and do not have any backwards compatibility
+> promises.
+
+In minor releases, new methods may be added to classes, and existing methods may
+have new arguments added. Any new arguments will have default values, but if
+you've overridden methods with a differing signature you may see fatal errors.
+Methods that have new arguments added will be documented in the migration guide
+for that release.
+
+The following table outlines several use cases and what compatibility you can
+expect from CakePHP:
+
+
Add a default argument value to an existing method argument
+
Yes
+
+
+
+
+
+
+
Your code may be broken by minor releases. Check the migration guide for details.↩︎
+
Your code may be broken by minor releases. Check the migration guide for details.↩︎
+
Your code may be broken by minor releases. Check the migration guide for details.↩︎
+
Your code may be broken by minor releases. Check the migration guide for details.↩︎
+
Your code may be broken by minor releases. Check the migration guide for details.↩︎
+
+
+
+## Working on CakePHP
+
+If you are helping make CakePHP even better please keep the following guidelines
+in mind when adding/changing functionality:
+
+In a minor release you can:
+
+
Add a new required argument to an existing method.
+
No
+
+
+
Remove a default value from an existing argument
+
No
+
+
+
Change method type void
+
Yes
+
+
+
+
+
+
+
You can change a class/method name as long as the old name remains available. This is generally avoided unless renaming has significant benefit.↩︎
+
Avoid whenever possible. Any removals need to be documented in the migration guide.↩︎
+
Avoid whenever possible. Any removals need to be documented in the migration guide.↩︎
+
You can change a class/method name as long as the old name remains available. This is generally avoided unless renaming has significant benefit.↩︎
+
+
+
+## Deprecations
+
+In each minor release, features may be deprecated. If features are deprecated,
+API documentation and runtime warnings will be added. Runtime errors help you
+locate code that needs to be updated before it breaks. If you wish to disable
+runtime warnings you can do so using the `Error.errorLevel` configuration
+value:
+
+``` text
+// in config/app.php
+// ...
+'Error' => [
+ 'errorLevel' => E_ALL ^ E_USER_DEPRECATED,
+]
+// ...
+```
+
+Will disable runtime deprecation warnings.
+
+
+
+## Experimental Features
+
+Experimental features are **not included** in the above backwards compatibility
+promises. Experimental features can have breaking changes made in minor releases
+as long as they remain experimental. Experimental features can be identified by
+the warning in the book and the usage of `@experimental` in the API
+documentation.
+
+Experimental features are intended to help gather feedback on how a feature
+works before it becomes stable. Once the interfaces and behavior has been vetted
+with the community the experimental flags will be removed.
diff --git a/docs/en/contributing/cakephp-coding-conventions.md b/docs/en/contributing/cakephp-coding-conventions.md
new file mode 100644
index 0000000000..3e923a4134
--- /dev/null
+++ b/docs/en/contributing/cakephp-coding-conventions.md
@@ -0,0 +1,690 @@
+# Coding Standards
+
+CakePHP developers will use the [PSR-12 coding style guide](https://www.php-fig.org/psr/psr-12/) in addition to the following rules as
+coding standards.
+
+It is recommended that others developing CakeIngredients follow the same
+standards.
+
+You can use the [CakePHP Code Sniffer](https://github.com/cakephp/cakephp-codesniffer) to check that your code
+follows required standards.
+
+## Adding New Features
+
+No new features should be added, without having their own tests – which
+should be passed before committing them to the repository.
+
+## IDE Setup
+
+Please make sure your IDE is set up to "trim right" on whitespaces.
+There should be no trailing spaces per line.
+
+Most modern IDEs also support an `.editorconfig` file. The CakePHP app
+skeleton ships with it by default. It already contains best practise defaults.
+
+We recommend to use the [IdeHelper](https://github.com/dereuromark/cakephp-ide-helper) plugin if you
+want to maximize IDE compatibility. It will assist to keep the annotations up-to-date which will make
+the IDE fully understand how all classes work together and provides better type-hinting and auto-completion.
+
+## Indentation
+
+Four spaces will be used for indentation.
+
+So, indentation should look like this:
+
+``` text
+// base level
+ // level 1
+ // level 2
+ // level 1
+// base level
+```
+
+Or:
+
+``` php
+$booleanVariable = true;
+$stringVariable = 'moose';
+if ($booleanVariable) {
+ echo 'Boolean value is true';
+ if ($stringVariable === 'moose') {
+ echo 'We have encountered a moose';
+ }
+}
+```
+
+In cases where you're using a multi-line function call use the following
+guidelines:
+
+- Opening parenthesis of a multi-line function call must be the last content on
+ the line.
+- Only one argument is allowed per line in a multi-line function call.
+- Closing parenthesis of a multi-line function call must be on a line by itself.
+
+As an example, instead of using the following formatting:
+
+``` php
+$matches = array_intersect_key($this->_listeners,
+ array_flip(preg_grep($matchPattern,
+ array_keys($this->_listeners), 0)));
+```
+
+Use this instead:
+
+``` php
+$matches = array_intersect_key(
+ $this->_listeners,
+ array_flip(
+ preg_grep($matchPattern, array_keys($this->_listeners), 0)
+ )
+);
+```
+
+## Line Length
+
+It is recommended to keep lines at approximately 100 characters long for better
+code readability. A limit of 80 or 120 characters makes it necessary to
+distribute complex logic or expressions by function, as well as give functions
+and objects shorter, more expressive names. Lines must not be
+longer than 120 characters.
+
+In short:
+
+- 100 characters is the soft limit.
+- 120 characters is the hard limit.
+
+## Control Structures
+
+Control structures are for example "`if`", "`for`", "`foreach`",
+"`while`", "`switch`" etc. Below, an example with "`if`":
+
+``` text
+if ((expr_1) || (expr_2)) {
+ // action_1;
+} elseif (!(expr_3) && (expr_4)) {
+ // action_2;
+} else {
+ // default_action;
+}
+```
+
+- In the control structures there should be 1 (one) space before the first
+ parenthesis and 1 (one) space between the last parenthesis and the opening
+ bracket.
+- Always use curly brackets in control structures, even if they are not needed.
+ They increase the readability of the code, and they give you fewer logical
+ errors.
+- Opening curly brackets should be placed on the same line as the control
+ structure. Closing curly brackets should be placed on new lines, and they
+ should have same indentation level as the control structure. The statement
+ included in curly brackets should begin on a new line, and code contained
+ within it should gain a new level of indentation.
+- Inline assignments should not be used inside of the control structures.
+
+``` php
+// wrong = no brackets, badly placed statement
+if (expr) statement;
+
+// wrong = no brackets
+if (expr)
+ statement;
+
+// good
+if (expr) {
+ statement;
+}
+
+// wrong = inline assignment
+if ($variable = Class::function()) {
+ statement;
+}
+
+// good
+$variable = Class::function();
+if ($variable) {
+ statement;
+}
+```
+
+### Ternary Operator
+
+Ternary operators are permissible when the entire ternary operation fits on one
+line. Longer ternaries should be split into `if else` statements. Ternary
+operators should not ever be nested. Optionally parentheses can be used around
+the condition check of the ternary for clarity:
+
+``` php
+// Good, simple and readable
+$variable = isset($options['variable']) ? $options['variable'] : true;
+
+// Nested ternaries are bad
+$variable = isset($options['variable']) ? isset($options['othervar']) ? true : false : false;
+```
+
+### Template Files
+
+In template files developers should use keyword control structures.
+Keyword control structures are easier to read in complex template files. Control
+structures can either be contained in a larger PHP block, or in separate PHP
+tags:
+
+``` php
+You are the admin user.
';
+endif;
+?>
+
The following is also acceptable:
+
+
You are the admin user.
+
+```
+
+## Comparison
+
+Always try to be as strict as possible. If a non-strict test is deliberate it
+might be wise to comment it as such to avoid confusing it for a mistake.
+
+For testing if a variable is null, it is recommended to use a strict check:
+
+``` php
+if ($value === null) {
+ // ...
+}
+```
+
+The value to check against should be placed on the right side:
+
+``` php
+// not recommended
+if (null === $this->foo()) {
+ // ...
+}
+
+// recommended
+if ($this->foo() === null) {
+ // ...
+}
+```
+
+## Function Calls
+
+Functions should be called without space between function's name and starting
+parenthesis. There should be one space between every parameter of a function
+call:
+
+``` php
+$var = foo($bar, $bar2, $bar3);
+```
+
+As you can see above there should be one space on both sides of equals sign (=).
+
+## Method Definition
+
+Example of a method definition:
+
+``` php
+public function someFunction($arg1, $arg2 = '')
+{
+ if (expr) {
+ statement;
+ }
+
+ return $var;
+}
+```
+
+Parameters with a default value, should be placed last in function definition.
+Try to make your functions return something, at least `true` or `false`, so
+it can be determined whether the function call was successful:
+
+``` php
+public function connection($dns, $persistent = false)
+{
+ if (is_array($dns)) {
+ $dnsInfo = $dns;
+ } else {
+ $dnsInfo = BD::parseDNS($dns);
+ }
+
+ if (!($dnsInfo) || !($dnsInfo['phpType'])) {
+ return $this->addError();
+ }
+
+ return true;
+}
+```
+
+There are spaces on both side of the equals sign.
+
+## Bail Early
+
+Try to avoid unnecessary nesting by bailing early:
+
+``` php
+public function run(array $data)
+{
+ ...
+ if (!$success) {
+ return false;
+ }
+
+ ...
+}
+
+public function check(array $data)
+{
+ ...
+ if (!$success) {
+ throw new RuntimeException(/* ... */);
+ }
+
+ ...
+}
+```
+
+This helps to keep the logic sequential which improves readability.
+
+### Typehinting
+
+Arguments that expect objects, arrays or callbacks (callable) can be typehinted.
+We only typehint public methods, though, as typehinting is not cost-free:
+
+``` php
+/**
+ * Some method description.
+ *
+ * @param \Cake\ORM\Table $table The table class to use.
+ * @param array $array Some array value.
+ * @param callable $callback Some callback.
+ * @param bool $boolean Some boolean value.
+ */
+public function foo(Table $table, array $array, callable $callback, $boolean)
+{
+}
+```
+
+Here `$table` must be an instance of `\Cake\ORM\Table`, `$array` must be
+an `array` and `$callback` must be of type `callable` (a valid callback).
+
+Note that if you want to allow `$array` to be also an instance of
+`\ArrayObject` you should not typehint as `array` accepts only the primitive
+type:
+
+``` php
+/**
+ * Some method description.
+ *
+ * @param array|\ArrayObject $array Some array value.
+ */
+public function foo($array)
+{
+}
+```
+
+### Anonymous Functions (Closures)
+
+Defining anonymous functions follows the [PSR-12](https://www.php-fig.org/psr/psr-12/) coding style guide, where they are
+declared with a space after the function keyword, and a space before and after
+the use keyword:
+
+``` php
+$closure = function ($arg1, $arg2) use ($var1, $var2) {
+ // code
+};
+```
+
+## Method Chaining
+
+Method chaining should have multiple methods spread across separate lines, and
+indented with four spaces:
+
+``` php
+$email->from('foo@example.com')
+ ->to('bar@example.com')
+ ->subject('A great message')
+ ->send();
+```
+
+## Commenting Code
+
+All comments should be written in English, and should in a clear way describe
+the commented block of code.
+
+Comments can include the following [phpDocumentor](https://phpdoc.org)
+tags:
+
+- [@deprecated](https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/deprecated.html)
+ Using the `@version ` format, where `version`
+ and `description` are mandatory. Version refers to the one it got deprecated in.
+- [@example](https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/example.html)
+- [@ignore](https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/ignore.html)
+- [@internal](https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/internal.html)
+- [@link](https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/link.html)
+- [@see](https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/see.html)
+- [@since](https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/since.html)
+- [@version](https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/version.html)
+
+PhpDoc tags are very much like JavaDoc tags in Java. Tags are only processed if
+they are the first thing in a DocBlock line, for example:
+
+``` text
+/**
+ * Tag example.
+ *
+ * @author this tag is parsed, but this @version is ignored
+ * @version 1.0 this tag is also parsed
+ */
+```
+
+``` text
+/**
+ * Example of inline phpDoc tags.
+ *
+ * This function works hard with foo() to rule the world.
+ *
+ * @return void
+ */
+function bar()
+{
+}
+
+/**
+ * Foo function.
+ *
+ * @return void
+ */
+function foo()
+{
+}
+```
+
+Comment blocks, with the exception of the first block in a file, should always
+be preceded by a newline.
+
+### Variable Types
+
+Variable types for use in DocBlocks:
+
+Type
+Description
+
+mixed
+A variable with undefined (or multiple) type.
+
+int
+Integer type variable (whole number).
+
+float
+Float type (point number).
+
+bool
+Logical type (true or false).
+
+string
+String type (any value in " " or ' ').
+
+null
+Null type. Usually used in conjunction with another type.
+
+array
+Array type.
+
+object
+Object type. A specific class name should be used if possible.
+
+resource
+Resource type (returned by for example mysql_connect()).
+Remember that when you specify the type as mixed, you should indicate
+whether it is unknown, or what the possible types are.
+
+callable
+Callable function.
+
+You can also combine types using the pipe char:
+
+ int|bool
+
+For more than two types it is usually best to just use `mixed`.
+
+When returning the object itself (for example, for chaining), one should use `$this`
+instead:
+
+``` php
+/**
+ * Foo function.
+ *
+ * @return $this
+ */
+public function foo()
+{
+ return $this;
+}
+```
+
+## Including Files
+
+`include`, `require`, `include_once` and `require_once` do not have
+parentheses:
+
+``` text
+// wrong = parentheses
+require_once('ClassFileName.php');
+require_once ($class);
+
+// good = no parentheses
+require_once 'ClassFileName.php';
+require_once $class;
+```
+
+When including files with classes or libraries, use only and always the
+[require_once](https://php.net/require_once) function.
+
+## PHP Tags
+
+Always use long tags (``) instead of short tags (` ?>`). The short
+echo should be used in template files where appropriate.
+
+### Short Echo
+
+The short echo should be used in template files in place of `=$name;?>
+
+// good = spaces, no semicolon
+
= $name ?>
+```
+
+As of PHP 5.4 the short echo tag (`=`) is no longer to be consider a 'short
+tag' is always available regardless of the `short_open_tag` ini directive.
+
+## Naming Convention
+
+### Functions
+
+Write all functions in camelBack:
+
+``` javascript
+function longFunctionName()
+{
+}
+```
+
+### Classes
+
+Class names should be written in CamelCase, for example:
+
+``` php
+class ExampleClass
+{
+}
+```
+
+### Variables
+
+Variable names should be as descriptive as possible, but also as short as
+possible. All variables should start with a lowercase letter, and should be
+written in camelBack in case of multiple words. Variables referencing objects
+should in some way associate to the class the variable is an object of.
+Example:
+
+``` php
+$user = 'John';
+$users = ['John', 'Hans', 'Arne'];
+
+$dispatcher = new Dispatcher();
+```
+
+### Member Visibility
+
+Use PHP's `public`, `protected` and `private` keywords for methods and variables.
+
+### Example Addresses
+
+For all example URL and mail addresses use "example.com", "example.org" and
+"example.net", for example:
+
+- Email:
+- WWW:
+- FTP:
+
+The "example.com" domain name has been reserved for this (see `2606`) and
+is recommended for use in documentation or as examples.
+
+### Files
+
+File names which do not contain classes should be lowercased and underscored,
+for example:
+
+ long_file_name.php
+
+### Casting
+
+For casting we use:
+
+Type
+Description
+
+(bool)
+Cast to boolean.
+
+(int)
+Cast to integer.
+
+(float)
+Cast to float.
+
+(string)
+Cast to string.
+
+(array)
+Cast to array.
+
+(object)
+Cast to object.
+
+Please use `(int)$var` instead of `intval($var)` and `(float)$var` instead
+of `floatval($var)` when applicable.
+
+### Constants
+
+Constants should be defined in capital letters:
+
+``` text
+define('CONSTANT', 1);
+```
+
+If a constant name consists of multiple words, they should be separated by an
+underscore character, for example:
+
+``` text
+define('LONG_NAMED_CONSTANT', 2);
+```
+
+### Enums
+
+Enum cases are defined in `CamelCase` style:
+
+``` text
+enum ArticleStatus: string
+{
+ case Published = 'Y';
+ case NotPublishedYet = 'N';
+}
+```
+
+## Careful when using empty()/isset()
+
+While `empty()` often seems correct to use, it can mask errors
+and cause unintended effects when `'0'` and `0` are given. When variables or
+properties are already defined, the usage of `empty()` is not recommended.
+When working with variables, it is better to rely on type-coercion to boolean
+instead of `empty()`:
+
+``` php
+function manipulate($var)
+{
+ // Not recommended, $var is already defined in the scope
+ if (empty($var)) {
+ // ...
+ }
+
+ // Use boolean type coercion
+ if (!$var) {
+ // ...
+ }
+ if ($var) {
+ // ...
+ }
+}
+```
+
+When dealing with defined properties you should favour `null` checks over
+`empty()`/`isset()` checks:
+
+``` php
+class Thing
+{
+ private $property; // Defined
+
+ public function readProperty()
+ {
+ // Not recommended as the property is defined in the class
+ if (!isset($this->property)) {
+ // ...
+ }
+ // Recommended
+ if ($this->property === null) {
+
+ }
+ }
+}
+```
+
+When working with arrays, it is better to merge in defaults over using
+`empty()` checks. By merging in defaults, you can ensure that required keys
+are defined:
+
+``` php
+function doWork(array $array)
+{
+ // Merge defaults to remove need for empty checks.
+ $array += [
+ 'key' => null,
+ ];
+
+ // Not recommended, the key is already set
+ if (isset($array['key'])) {
+ // ...
+ }
+
+ // Recommended
+ if ($array['key'] !== null) {
+ // ...
+ }
+}
+```
diff --git a/docs/en/contributing/code.md b/docs/en/contributing/code.md
new file mode 100644
index 0000000000..0c9d7c119d
--- /dev/null
+++ b/docs/en/contributing/code.md
@@ -0,0 +1,141 @@
+# Code
+
+Patches and pull requests are a great way to contribute code back to CakePHP.
+Pull requests can be created in GitHub, and are preferred over patch files in
+ticket comments.
+
+## Initial Setup
+
+Before working on patches for CakePHP, it's a good idea to get your environment
+setup. You'll need the following software:
+
+- Git
+- PHP |minphpversion| or greater - PHPUnit 5.7.0 or greater
+
+Set up your user information with your name/handle and working email address:
+
+ git config --global user.name 'Bob Barker'
+ git config --global user.email 'bob.barker@example.com'
+
+> [!NOTE]
+> If you are new to Git, we highly recommend you to read the excellent and
+> free [ProGit](https://git-scm.com/book/) book.
+
+Get a clone of the CakePHP source code from GitHub:
+
+- If you don't have a [GitHub](https://github.com) account, create one.
+- Fork the [CakePHP repository](https://github.com/cakephp/cakephp) by clicking
+ the **Fork** button.
+
+After your fork is made, clone your fork to your local machine:
+
+ git clone git@github.com:YOURNAME/cakephp.git
+
+Add the original CakePHP repository as a remote repository. You'll use this
+later to fetch changes from the CakePHP repository. This will let you stay up
+to date with CakePHP:
+
+ cd cakephp
+ git remote add upstream git://github.com/cakephp/cakephp.git
+
+Now that you have CakePHP setup you should be able to define a `$test`
+[database connection](../orm/database-basics#database-configuration), and
+[run all the tests](../development/testing#running-tests).
+
+## Working on a Patch
+
+Each time you want to work on a bug, feature or enhancement create a topic
+branch.
+
+The branch you create should be based on the version that your fix/enhancement
+is for. For example if you are fixing a bug in `3.x` you would want to use the
+`master` branch as the base for your branch. If your change is a bug fix for
+the 2.x release series, you should use the `2.x` branch:
+
+``` text
+# fixing a bug on 3.x
+git fetch upstream
+git checkout -b ticket-1234 upstream/master
+
+# fixing a bug on 2.x
+git fetch upstream
+git checkout -b ticket-1234 upstream/2.x
+```
+
+> [!TIP]
+> Use a descriptive name for your branch. Referencing the ticket or feature
+> name is a good convention. Examples include `ticket-1234` and `feature-awesome`.
+
+The above will create a local branch based on the upstream (CakePHP) 2.x branch.
+Work on your fix, and make as many commits as you need; but keep in mind the
+following:
+
+- Follow the [Coding Standards](../contributing/cakephp-coding-conventions).
+- Add a test case to show the bug is fixed, or that the new feature works.
+- Keep your commits logical, and write clear commit messages that provide
+ context on what you changed and why.
+
+## Submitting a Pull Request
+
+Once your changes are done and you're ready for them to be merged into CakePHP,
+you'll want to update your branch:
+
+``` text
+# Rebase fix on top of master
+git checkout master
+git fetch upstream
+git merge upstream/master
+git checkout
+git rebase master
+```
+
+This will fetch + merge in any changes that have happened in CakePHP since you
+started. It will then rebase - or replay your changes on top of the current
+code. You might encounter a conflict during the `rebase`. If the rebase quits
+early you can see which files are conflicted/un-merged with `git status`.
+Resolve each conflict, and then continue the rebase:
+
+``` text
+git add # do this for each conflicted file.
+git rebase --continue
+```
+
+Check that all your tests continue to pass. Then push your branch to your fork:
+
+``` text
+git push origin
+```
+
+If you've rebased after pushing your branch, you'll need to use force push:
+
+``` text
+git push --force origin
+```
+
+Once your branch is on GitHub, you can submit a pull request on GitHub.
+
+### Choosing Where Your Changes will be Merged Into
+
+When making pull requests you should make sure you select the correct base
+branch, as you cannot edit it once the pull request is created.
+
+- If your change is a **bugfix** and doesn't introduce new functionality and
+ only corrects existing behavior that is present in the current release. Then
+ choose **master** as your merge target.
+- If your change is a **new feature** or an addition to the framework, then you
+ should choose the branch with the next version number. For example if the
+ current stable release is `4.0.0`, the branch accepting new features will
+ be `4.next`.
+- If your change is a breaks existing functionality, or APIs then you'll have
+ to choose then next major release. For example, if the current release is
+ `4.0.0` then the next time existing behavior can be broken will be in
+ `5.x` so you should target that branch.
+
+> [!NOTE]
+> Remember that all code you contribute to CakePHP will be licensed under the
+> MIT License, and the [Cake Software Foundation](https://cakefoundation.org/old) will become the owner of any
+> contributed code. Contributors should follow the [CakePHP Community
+> Guidelines](https://cakephp.org/get-involved).
+
+All bug fixes merged into a maintenance branch will also be merged into upcoming
+releases periodically by the core team.
diff --git a/docs/en/contributing/documentation.md b/docs/en/contributing/documentation.md
new file mode 100644
index 0000000000..70378deef9
--- /dev/null
+++ b/docs/en/contributing/documentation.md
@@ -0,0 +1,599 @@
+# Documentation
+
+Contributing to the documentation is simple. The files are hosted on
+. Feel free to fork the repo, add your
+changes/improvements/translations and give back by issuing a pull request.
+You can even edit the docs online with GitHub, without ever downloading the
+files -- the "Edit this page" link on any given page will direct you to
+GitHub's online editor for that page.
+
+The CakePHP documentation is built with [VitePress](https://vitepress.dev), a static site
+generator powered by Vue and Vite. The documentation is
+[continuously integrated](https://en.wikipedia.org/wiki/Continuous_integration)
+and deployed after each pull request is merged.
+
+## Translations
+
+Email the docs team (docs at cakephp dot org) or hop on IRC
+(#cakephp on freenode) to discuss any translation efforts you would
+like to participate in.
+
+### New Translation Language
+
+We want to provide translations that are as complete as possible. However, there
+may be times where a translation file is not up-to-date. You should always
+consider the English version as the authoritative version.
+
+If your language is not in the current languages, please contact us through
+Github and we will consider creating a skeleton folder for it. The following
+sections are the first one you should consider translating as these
+files don't change often:
+
+```
+- index.md
+- intro.md
+- quickstart.md
+- installation.md
+- /intro folder
+- /tutorials-and-examples folder
+```
+
+### Reminder for Docs Administrators
+
+The structure of all language folders should mirror the English folder
+structure. If the structure changes for the English version, we should apply
+those changes in the other languages.
+
+For example, if a new English file is created in **en/file.md**, we should:
+
+- Add the file in all other languages : **fr/file.md**, **zh/file.md**, ...
+
+- Delete the content, but keep the frontmatter and add a translation notice.
+ The following note will be added while nobody has translated the file:
+
+ ```markdown
+ ---
+ title: File Title
+ description: Brief description of the page
+ ---
+
+ # File Title
+
+ ::: warning Translation Needed
+ The documentation is not currently supported in XX language for this page.
+
+ Please feel free to send us a pull request on
+ [GitHub](https://github.com/cakephp/docs) or use the **Edit this page**
+ link to directly propose your changes.
+
+ You can refer to the English version in the language selector to have
+ information about this page's topic.
+ :::
+ ```
+
+### Translator tips
+
+- Browse and edit in the language you want the content to be
+ translated to - otherwise you won't see what has already been
+ translated.
+- Feel free to dive right in if your chosen language already
+ exists on the book.
+- Use [Informal Form](https://en.wikipedia.org/wiki/Register#Linguistics).
+- Translate both the content and the title at the same time.
+- Do compare to the English content before submitting a correction
+ (if you correct something, but don't integrate an 'upstream' change
+ your submission won't be accepted).
+- If you need to write an English term, wrap it in `*asterisks*` for emphasis.
+ For example, "asdf asdf *Controller* asdf" or "asdf asdf Kontroller
+ (*Controller*) asfd".
+- Do not submit partial translations.
+- Do not edit a section with a pending change.
+- Do not use
+ [HTML entities](https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references)
+ for accented characters, the documentation uses UTF-8.
+- Do not significantly change the markup or add new content.
+- If the original content is missing some info, submit an edit for
+ that first.
+
+## Documentation Formatting Guide
+
+The CakePHP documentation is written in [Markdown](https://en.wikipedia.org/wiki/Markdown)
+and built with [VitePress](https://vitepress.dev). Markdown is a lightweight
+markup language that's easy to read and write. To maintain consistency, please
+follow these guidelines when contributing to the CakePHP documentation.
+
+For comprehensive information about VitePress features and configuration,
+refer to the [VitePress Guide](https://vitepress.dev/guide). For detailed
+information about Markdown features and VitePress-specific extensions, see the
+[VitePress Markdown Guide](https://vitepress.dev/guide/markdown).
+
+### Line Length
+
+Lines of text should be wrapped at 80 columns. The only exception should be
+long URLs, and code snippets.
+
+### Headings and Sections
+
+Headings are created using hash symbols (`#`) at the start of a line:
+
+- `#` Page title (H1) - Use only once per page
+- `##` Major sections (H2)
+- `###` Subsections (H3)
+- `####` Sub-subsections (H4)
+- `#####` Sub-sub-subsections (H5)
+- `######` Lowest level heading (H6)
+
+Headings should not be nested more than 4 levels deep for readability.
+Always add a blank line before and after headings.
+
+Example:
+```markdown
+## Major Section
+
+Paragraph text here.
+
+### Subsection
+
+More content.
+```
+
+### Paragraphs
+
+Paragraphs are simply blocks of text, with all the lines at the same level of
+indentation. Paragraphs should be separated by one blank line.
+
+### Inline Markup
+
+- *Italics*: Use single asterisks or underscores: `*text*` or `_text_`
+ - Use for general emphasis and highlighting
+ - Example: *Controller*, *important note*
+
+- **Bold**: Use double asterisks or underscores: `**text**` or `__text__`
+ - Use for strong emphasis, directory paths, and key terms
+ - Example: **config/Migrations**, **articles table**
+
+- `Inline Code`: Use single backticks: `` `text` ``
+ - Use for method names, variable names, class names, and code snippets
+ - Example: `cascadeCallbacks`, `PagesController`, `config()`, `true`
+
+**Tips:**
+- Ensure proper spacing around inline markup for it to render correctly
+- Inline code doesn't require escaping special characters
+- You can escape special characters with backslash if needed: `\*not italic\*`
+
+### Lists
+
+**Unordered Lists**: Start lines with `-`, `*`, or `+` followed by a space:
+
+```markdown
+- First item
+- Second item
+- Third item with
+ multiple lines
+```
+
+**Ordered Lists**: Use numbers followed by a period:
+
+```markdown
+1. First item
+2. Second item
+3. Third item
+```
+
+**Nested Lists**: Indent with 2 or 4 spaces:
+
+```markdown
+- First level
+ - Second level
+ - Third level
+ - Back to second level
+- Back to first level
+```
+
+**Mixed Lists**:
+
+```markdown
+1. Ordered item
+ - Unordered sub-item
+ - Another sub-item
+2. Another ordered item
+```
+
+### Links
+
+#### External Links
+
+Use standard Markdown link syntax:
+
+```markdown
+[Link Text](https://example.com)
+```
+
+Example: [CakePHP Website](https://cakephp.org)
+
+#### Internal Links
+
+Link to other pages in the documentation using relative paths:
+
+```markdown
+[Link Text](../path/to/page.md)
+```
+
+Examples:
+- Same directory: `[Installation](./installation.md)`
+- Parent directory: `[Controllers](../controllers.md)`
+- Subdirectory: `[Components](./controllers/components.md)`
+
+**Note**: VitePress automatically converts `.md` extensions in links to appropriate
+URL paths in the built site.
+
+#### Anchor Links
+
+Link to specific sections within a page using heading anchors:
+
+```markdown
+[Link to section](#heading-text)
+```
+
+VitePress automatically generates anchor IDs from headings in kebab-case.
+For example, `## My Section Title` becomes `#my-section-title`.
+
+Link to a section in another page:
+```markdown
+[Link text](./other-page.md#section-anchor)
+```
+
+### Frontmatter
+
+Each documentation page should start with YAML frontmatter containing metadata:
+
+```markdown
+---
+title: Page Title
+description: A brief description of the page content
+---
+```
+
+Optional frontmatter fields:
+- `title`: Page title (used in browser tab and navigation)
+- `description`: Page description for SEO
+- `head`: Additional head tags
+- `layout`: Custom layout (default is 'doc')
+
+Example:
+```markdown
+---
+title: Controllers
+description: Learn about CakePHP controllers and request handling
+---
+
+# Controllers
+
+Your content here...
+```
+
+### Code Blocks
+
+Code blocks are created using triple backticks with an optional language identifier:
+
+````markdown
+```php
+class MyController extends Controller
+{
+ public function index()
+ {
+ // Your code here
+ }
+}
+```
+````
+
+Common language identifiers:
+- `php` - PHP code
+- `bash` or `shell` - Shell commands
+- `javascript` or `js` - JavaScript
+- `json` - JSON data
+- `yaml` - YAML configuration
+- `html` - HTML markup
+- `css` - CSS styles
+
+**Line Highlighting**: Highlight specific lines in code blocks:
+
+````markdown
+```php{3-6}
+class Example
+{
+ public function highlighted() // This line is highlighted
+ {
+ // These lines are
+ // also highlighted
+ }
+}
+```
+````
+
+**Line Numbers**: Add line numbers to code blocks:
+
+````markdown
+```php:line-numbers
+class Example
+{
+ public function withLineNumbers()
+ {
+ return true;
+ }
+}
+```
+````
+
+**Code Groups**: Display multiple code examples with tabs:
+
+````markdown
+::: code-group
+
+```php [Controller]
+class ArticlesController extends Controller
+{
+ public function index()
+ {
+ $articles = $this->Articles->find('all');
+ }
+}
+```
+
+```php [Model]
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Timestamp');
+ }
+}
+```
+
+```php [Template]
+
+
= h($article->title) ?>
+
+```
+
+:::
+````
+
+### Admonitions
+
+VitePress provides custom containers for tips, warnings, and other callouts.
+
+**Available Container Types:**
+
+```markdown
+::: tip
+This is a tip. Use for helpful information or best practices.
+:::
+
+::: info
+This is an info box. Use for additional context or explanations.
+:::
+
+::: warning
+This is a warning. Use for important caveats or potential issues.
+:::
+
+::: danger
+This is a danger notice. Use for critical information or security concerns.
+:::
+
+::: details Click to expand
+This is a collapsible details block. Content is hidden by default.
+:::
+```
+
+**Custom Titles:**
+
+```markdown
+::: tip Custom Tip Title
+You can customize the title of any container.
+:::
+
+::: warning Important Security Note
+Always validate user input before processing.
+:::
+```
+
+**Version Notices:**
+
+For version-specific information:
+
+```markdown
+::: info New in 5.0.0
+This feature was added in CakePHP 5.0.0.
+:::
+
+::: warning Deprecated in 5.1.0
+This method is deprecated as of CakePHP 5.1.0. Use `newMethod()` instead.
+:::
+```
+
+**Examples:**
+
+::: tip Performance Tip
+Caching query results can significantly improve performance.
+:::
+
+::: warning Security Warning
+Never store passwords in plain text. Always use proper hashing.
+:::
+
+::: info New in 5.0.0
+The new ORM improvements provide better type safety.
+:::
+
+## Validating Your Changes
+
+Before submitting a pull request, you should validate your documentation changes to ensure they meet quality standards.
+
+### Installing Validation Tools
+
+You can use `npx` to run the tools without installing them globally, or install them for faster repeated use:
+
+::: code-group
+
+```bash [Using npx (Recommended)]
+# No installation needed - npx downloads and runs the tools
+npx cspell --config .github/cspell.json "docs/en/**/*.md"
+npx markdownlint-cli --config .github/markdownlint.json "docs/en/**/*.md"
+```
+
+```bash [Global Installation]
+# Install globally for faster execution
+npm install -g cspell markdownlint-cli
+
+# Then run without npx
+cspell --config .github/cspell.json "docs/en/**/*.md"
+markdownlint-cli --config .github/markdownlint.json "docs/en/**/*.md"
+```
+
+:::
+
+### Running Spell Check
+
+We use [cspell](https://cspell.org/) to check spelling in documentation. To check your changes:
+
+::: code-group
+
+```bash [Single File]
+# Check a single file
+npx cspell --config .github/cspell.json docs/en/your-file.md
+```
+
+```bash [Directory]
+# Check all files in a directory
+npx cspell --config .github/cspell.json "docs/en/controllers/*.md"
+```
+
+```bash [All Files]
+# Check all documentation recursively
+npx cspell --config .github/cspell.json "docs/**/*.md"
+```
+
+:::
+
+::: tip Adding Technical Terms
+If cspell flags legitimate technical terms (class names, CakePHP-specific terms), add them to the `words` array in [.github/cspell.json](https://github.com/cakephp/docs/blob/5.x/.github/cspell.json).
+:::
+
+### Running Markdown Lint
+
+We use [markdownlint](https://github.com/DavidAnson/markdownlint) to maintain consistent markdown formatting:
+
+::: code-group
+
+```bash [Single File]
+# Check a single file
+npx markdownlint-cli --config .github/markdownlint.json docs/en/your-file.md
+```
+
+```bash [Directory]
+# Check all files in a directory
+npx markdownlint-cli --config .github/markdownlint.json "docs/en/controllers/*.md"
+```
+
+```bash [All Docs]
+# Check all English documentation
+npx markdownlint-cli --config .github/markdownlint.json "docs/en/**/*.md"
+```
+
+```bash [Auto-fix]
+# Automatically fix formatting issues
+npx markdownlint-cli --config .github/markdownlint.json --fix cs/en/**/*.md"
+```
+
+:::
+
+::: warning Auto-fix Limitations
+The `--fix` flag can automatically correct many formatting issues, but not all. Review changes before committing.
+:::
+
+### Running Link Checker
+
+We use a custom link checker to validate internal markdown links in the documentation:
+
+::: code-group
+
+```bash [Single File]
+# Check a single file
+node bin/check-links.js docs/en/your-file.md
+```
+
+```bash [Directory]
+# Check all files in a directory
+node bin/check-links.js "docs/en/controllers/*.md"
+```
+
+```bash [All Docs]
+# Check all documentation recursively
+node bin/check-links.js "docs/**/*.md"
+```
+
+```bash [With Baseline]
+# Check while ignoring known issues in baseline
+node bin/check-links.js --baseline .github/linkchecker-baseline.json "docs/**/*.md"
+```
+
+:::
+
+The link checker validates:
+- Internal file references (relative links)
+- Anchor links to headings within pages
+- Directory index links
+
+::: info External Links
+The link checker only validates internal markdown links. External URLs (http://, https://) are not checked.
+:::
+
+::: tip Known Issues Baseline
+The repository maintains a baseline of known broken links in `.github/linkchecker-baseline.json`. When you run the checker with `--baseline`, it filters out these known issues and only reports new problems. This is useful when working on specific changes without being overwhelmed by pre-existing issues.
+:::
+
+### GitHub Actions Validation
+
+When you submit a pull request, our CI pipeline automatically runs:
+
+1. **JavaScript syntax validation** - Validates `config.js`
+2. **JSON validation** - Validates `toc_*.json` files
+3. **Markdown linting** - Checks all markdown files
+4. **Spell checking** - Scans documentation for typos
+5. **Link checking** - Validates internal markdown links
+
+::: tip Pre-flight Check
+::: code-group
+
+```bash [Quick Check]
+# Validate markdown and spelling
+npx markdownlint-cli --config .github/markdownlint.json "docs/**/*.md"
+npx cspell --config .github/cspell.json "docs/**/*.md"
+node bin/check-links.js "docs/**/*.md"
+```
+
+```bash [Full Validation]
+# Run all CI checks locally
+npx markdownlint-cli --config .github/markdownlint.json "docs/**/*.md"
+npx cspell --config .github/cspell.json "docs/**/*.md"
+node bin/check-links.js "docs/**/*.md"
+node --check config.js
+jq empty toc_en.json
+```
+
+```bash [Single File]
+# Check your current file before committing
+npx markdownlint-cli --config .github/markdownlint.json docs/en/your-file.md
+npx cspell --config .github/cspell.json docs/en/your-file.md
+node bin/check-links.js docs/en/your-file.md
+```
+
+:::e --check config.js
+jq empty toc_en.json
+```
+:::
+
+If the CI checks fail, review the error messages and fix the issues before requesting a review.
diff --git a/docs/en/contributing/tickets.md b/docs/en/contributing/tickets.md
new file mode 100644
index 0000000000..0bcafd44eb
--- /dev/null
+++ b/docs/en/contributing/tickets.md
@@ -0,0 +1,43 @@
+# Tickets
+
+Getting feedback and help from the community in the form of tickets is an
+extremely important part of the CakePHP development process. All of CakePHP's
+tickets are hosted on [GitHub](https://github.com/cakephp/cakephp/issues).
+
+## Reporting Bugs
+
+Well written bug reports are very helpful. There are a few steps to help create
+the best bug report possible:
+
+- **Do**: Please [search](https://github.com/cakephp/cakephp/search?q=it+is+broken&ref=cmdform&type=Issues)
+ for a similar existing ticket, and ensure someone hasn't already reported your
+ issue, or that it hasn't already been fixed in the repository.
+- **Do**: Please include detailed instructions on **how to reproduce the bug**.
+ This could be in the form of a test-case or a snippet of code that
+ demonstrates the issue. Not having a way to reproduce an issue means it's less
+ likely to get fixed.
+- **Do**: Please give as many details as possible about your environment: (OS,
+ PHP version, CakePHP version).
+- **Don't**: Please don't use the ticket system to ask support questions. Both the support channel on the
+ [CakePHP Slack workspace](https://cakesf.herokuapp.com) and the \#cakephp IRC channel on [Freenode](https://webchat.freenode.net) have many
+ developers available to help answer your questions. Also have a look at
+ [Stack Overflow](https://stackoverflow.com/questions/tagged/cakephp) or the [official CakePHP forum](https://discourse.cakephp.org).
+
+## Reporting Security Issues
+
+If you've found a security issue in CakePHP, please use the following procedure
+instead of the normal bug reporting system. Instead of using the bug tracker,
+mailing list or IRC please send an email to **security \[at\] cakephp.org**.
+Emails sent to this address go to the CakePHP core team on a private mailing
+list.
+
+For each report, we try to first confirm the vulnerability. Once confirmed, the
+CakePHP team will take the following actions:
+
+- Acknowledge to the reporter that we've received the issue, and are working on
+ a fix. We ask that the reporter keep the issue confidential until we announce
+ it.
+- Get a fix/patch prepared.
+- Prepare a post describing the vulnerability, and the possible exploits.
+- Release new versions of all affected versions.
+- Prominently feature the problem in the release announcement.
diff --git a/docs/en/controllers.md b/docs/en/controllers.md
new file mode 100644
index 0000000000..1c66ba908b
--- /dev/null
+++ b/docs/en/controllers.md
@@ -0,0 +1,642 @@
+# Controllers
+
+`class` Cake\\Controller\\**Controller**
+
+Controllers are the 'C' in MVC. After routing has been applied and the correct
+controller has been found, your controller's action is called. Your controller
+should handle interpreting the request data, making sure the correct models
+are called, and the right response or view is rendered. Controllers can be
+thought of as middle layer between the Model and View. You want to keep your
+controllers thin, and your models fat. This will help you reuse
+your code and makes your code easier to test.
+
+Commonly, a controller is used to manage the logic around a single model. For
+example, if you were building a site for an online bakery, you might have a
+RecipesController managing your recipes and an IngredientsController managing your
+ingredients. However, it's also possible to have controllers work with more than
+one model. In CakePHP, a controller is named after the primary model it
+handles.
+
+Your application's controllers extend the `AppController` class, which in turn
+extends the core `Controller` class. The `AppController`
+class can be defined in **src/Controller/AppController.php** and it should
+contain methods that are shared between all of your application's controllers.
+
+Controllers provide a number of methods that handle requests. These are called
+*actions*. By default, each public method in
+a controller is an action, and is accessible from a URL. An action is responsible
+for interpreting the request and creating the response. Usually responses are
+in the form of a rendered view, but there are other ways to create responses as
+well.
+
+
+
+## The App Controller
+
+As stated in the introduction, the `AppController` class is the parent class
+to all of your application's controllers. `AppController` itself extends the
+`Cake\Controller\Controller` class included in CakePHP.
+`AppController` is defined in **src/Controller/AppController.php** as
+follows:
+
+``` php
+namespace App\Controller;
+
+use Cake\Controller\Controller;
+
+class AppController extends Controller
+{
+}
+```
+
+Controller attributes and methods created in your `AppController` will be
+available in all controllers that extend it. Components (which you'll
+learn about later) are best used for code that is used in many (but not
+necessarily all) controllers.
+
+You can use your `AppController` to load components that will be used in every
+controller in your application. CakePHP provides a `initialize()` method that
+is invoked at the end of a Controller's constructor for this kind of use:
+
+``` php
+namespace App\Controller;
+
+use Cake\Controller\Controller;
+
+class AppController extends Controller
+{
+ public function initialize(): void
+ {
+ // Always enable the FormProtection component.
+ $this->loadComponent('FormProtection');
+ }
+}
+```
+
+## Request Flow
+
+When a request is made to a CakePHP application, CakePHP's
+`Cake\Routing\Router` and `Cake\Routing\Dispatcher`
+classes use [Routes Configuration](development/routing#routes-configuration) to find and create the correct
+controller instance. The request data is encapsulated in a request object.
+CakePHP puts all of the important request information into the `$this->request`
+property. See the section on [Cake Request](controllers/request-response#cake-request) for more information on the
+CakePHP request object.
+
+## Controller Actions
+
+Controller actions are responsible for converting the request parameters into a
+response for the browser/user making the request. CakePHP uses conventions to
+automate this process and remove some boilerplate code you would otherwise need
+to write.
+
+By convention, CakePHP renders a view with an inflected version of the action
+name. Returning to our online bakery example, our RecipesController might contain the
+`view()`, `share()`, and `search()` actions. The controller would be found
+in **src/Controller/RecipesController.php** and contain:
+
+``` php
+// src/Controller/RecipesController.php
+
+class RecipesController extends AppController
+{
+ public function view($id)
+ {
+ // Action logic goes here.
+ }
+
+ public function share($customerId, $recipeId)
+ {
+ // Action logic goes here.
+ }
+
+ public function search($query)
+ {
+ // Action logic goes here.
+ }
+}
+```
+
+The template files for these actions would be **templates/Recipes/view.php**,
+**templates/Recipes/share.php**, and **templates/Recipes/search.php**. The
+conventional view file name is the lowercased and underscored version of the
+action name.
+
+Controller actions generally use
+`Controller::set()` to create a context that
+`View` uses to render the view layer. Because of the conventions that
+CakePHP uses, you don't need to create and render the view manually. Instead,
+once a controller action has completed, CakePHP will handle rendering and
+delivering the View.
+
+If for some reason you'd like to skip the default behavior, you can return a
+`Cake\Http\Response` object from the action with the fully
+created response.
+
+In order for you to use a controller effectively in your own application, we'll
+cover some of the core attributes and methods provided by CakePHP's controllers.
+
+## Interacting with Views
+
+Controllers interact with views in a number of ways. First, they
+are able to pass data to the views, using `Controller::set()`. You can also
+decide which view class to use, and which view file should be
+rendered from the controller.
+
+
+
+### Setting View Variables
+
+`method` Cake\\Controller\\Controller::**set**(string $var, mixed $value)
+
+The `Controller::set()` method is the main way to send data from your
+controller to your view. Once you've used `Controller::set()`, the variable
+can be accessed in your view:
+
+``` php
+// First you pass data from the controller:
+
+$this->set('color', 'pink');
+
+// Then, in the view, you can utilize the data:
+?>
+
+You have selected = h($color) ?> icing for the cake.
+```
+
+The `Controller::set()` method also takes an
+associative array as its first parameter. This can often be a quick way to
+assign a set of information to the view:
+
+``` php
+$data = [
+ 'color' => 'pink',
+ 'type' => 'sugar',
+ 'base_price' => 23.95,
+];
+
+// Make $color, $type, and $base_price
+// available to the view:
+
+$this->set($data);
+```
+
+Keep in mind that view vars are shared among all parts rendered by your view.
+They will be available in all parts of the view: the template, the layout and
+all elements inside the former two.
+
+### Setting View Options
+
+If you want to customize the view class, layout/template paths, helpers or the
+theme that will be used when rendering the view, you can use the
+`viewBuilder()` method to get a builder. This builder can be used to define
+properties of the view before it is created:
+
+``` php
+$this->viewBuilder()
+ ->addHelper('MyCustom')
+ ->setTheme('Modern')
+ ->setClassName('Modern.Admin');
+```
+
+The above shows how you can load custom helpers, set the theme and use a custom
+view class.
+
+### Rendering a View
+
+`method` Cake\\Controller\\Controller::**render**(string $view, string $layout): Response
+
+The `Controller::render()` method is automatically called at the end of each requested
+controller action. This method performs all the view logic (using the data
+you've submitted using the `Controller::set()` method), places the view inside its
+`View::$layout`, and serves it back to the end user.
+
+The default view file used by render is determined by convention.
+If the `search()` action of the RecipesController is requested,
+the view file in **templates/Recipes/search.php** will be rendered:
+
+``` php
+namespace App\Controller;
+
+class RecipesController extends AppController
+{
+// ...
+ public function search()
+ {
+ // Render the view in templates/Recipes/search.php
+ return $this->render();
+ }
+// ...
+}
+```
+
+Although CakePHP will automatically call it after every action's logic
+(unless you've called `$this->disableAutoRender()`), you can use it to specify
+an alternate view file by specifying a view file name as first argument of
+`Controller::render()` method.
+
+If `$view` starts with '/', it is assumed to be a view or
+element file relative to the **templates** folder. This allows
+direct rendering of elements, very useful in AJAX calls:
+
+``` php
+// Render the element in templates/element/ajaxreturn.php
+$this->render('/element/ajaxreturn');
+```
+
+The second parameter `$layout` of `Controller::render()` allows you to specify the layout
+with which the view is rendered.
+
+#### Rendering a Specific Template
+
+In your controller, you may want to render a different view than the
+conventional one. You can do this by calling `Controller::render()` directly. Once you
+have called `Controller::render()`, CakePHP will not try to re-render the view:
+
+``` php
+namespace App\Controller;
+
+class PostsController extends AppController
+{
+ public function my_action()
+ {
+ return $this->render('custom_file');
+ }
+}
+```
+
+This would render **templates/Posts/custom_file.php** instead of
+**templates/Posts/my_action.php**.
+
+You can also render views inside plugins using the following syntax:
+`$this->render('PluginName.PluginController/custom_file')`.
+For example:
+
+``` php
+namespace App\Controller;
+
+class PostsController extends AppController
+{
+ public function myAction()
+ {
+ return $this->render('Users.UserDetails/custom_file');
+ }
+}
+```
+
+This would render **plugins/Users/templates/UserDetails/custom_file.php**
+
+
+
+## Content Type Negotiation
+
+`method` Cake\\Controller\\Controller::**addViewClasses**(array $viewClasses)
+
+Controllers can define a list of view classes they support. After the
+controller's action is complete CakePHP will use the view list to perform
+content-type negotiation with either [File Extensions](development/routing#file-extensions) or `Accept`
+headers. This enables your application to re-use the same controller action to
+render an HTML view or render a JSON or XML response. To define the list of
+supported view classes for a controller is done with the `addViewClasses()`
+method:
+
+``` php
+namespace App\Controller;
+
+use Cake\View\JsonView;
+use Cake\View\XmlView;
+
+class PostsController extends AppController
+{
+ public function initialize(): void
+ {
+ parent::initialize();
+
+ $this->addViewClasses([JsonView::class, XmlView::class]);
+ }
+}
+```
+
+The application's `View` class is automatically used as a fallback when no
+other view can be selected based on the request's `Accept` header or routing
+extension. If your application only supports content types for a specific
+actions, you can call `addClasses()` within your action too:
+
+``` php
+public function export(): void
+{
+ // Use a custom CSV view for data exports.
+ $this->addViewClasses([CsvView::class]);
+
+ // Rest of the action code
+}
+```
+
+If within your controller actions you need to process the request or load data
+differently based on the content type you can use
+[Check The Request](controllers/request-response#check-the-request):
+
+``` php
+// In a controller action
+
+// Load additional data when preparing JSON responses
+if ($this->request->is('json')) {
+ $query->contain('Authors');
+}
+```
+
+In case your app need more complex logic to decide which view classes to use
+then you can override the `Controller::viewClasses()` method and return
+an array of view classes as required.
+
+> [!NOTE]
+> View classes must implement the static `contentType()` hook method to
+> participate in content-type negotiation.
+
+## Content Type Negotiation Fallbacks
+
+If no View can be matched with the request's content type preferences, CakePHP
+will use the base `View` class. If you want to require content-type
+negotiation, you can use the `NegotiationRequiredView` which sets a `406` status
+code:
+
+``` php
+public function initialize(): void
+{
+ parent::initialize();
+
+ // Require Accept header negotiation or return a 406 response.
+ $this->addViewClasses([JsonView::class, NegotiationRequiredView::class]);
+}
+```
+
+You can use the `TYPE_MATCH_ALL` content type value to build your own fallback
+view logic:
+
+``` php
+namespace App\View;
+
+use Cake\View\View;
+
+class CustomFallbackView extends View
+{
+ public static function contentType(): string
+ {
+ return static::TYPE_MATCH_ALL;
+ }
+
+}
+```
+
+It is important to remember that match-all views are applied only *after*
+content-type negotiation is attempted.
+
+## Using AjaxView
+
+In applications that use hypermedia or AJAX clients, you often need to render
+view contents without the wrapping layout. You can use the `AjaxView` that
+is bundled with the application skeleton:
+
+``` php
+// In a controller action, or in beforeRender.
+if ($this->request->is('ajax')) {
+ $this->viewBuilder()->setClassName('Ajax');
+}
+```
+
+`AjaxView` will respond as `text/html` and use the `ajax` layout.
+Generally this layout is minimal or contains client specific markup. This
+replaces usage of `RequestHandlerComponent` automatically using the
+`AjaxView` in 4.x.
+
+## Redirecting to Other Pages
+
+`method` Cake\\Controller\\Controller::**redirect**(string|array $url, integer $status): Response|null
+
+The `redirect()` method adds a `Location` header and sets the status code of
+a response and returns it. You should return the response created by
+`redirect()` to have CakePHP send the redirect instead of completing the
+controller action and rendering a view.
+
+You can redirect using `routing array` values:
+
+``` php
+return $this->redirect([
+ 'controller' => 'Orders',
+ 'action' => 'confirm',
+ $order->id,
+ '?' => [
+ 'product' => 'pizza',
+ 'quantity' => 5
+ ],
+ '#' => 'top'
+]);
+```
+
+Or using a relative or absolute URL:
+
+``` php
+return $this->redirect('/orders/confirm');
+
+return $this->redirect('http://www.example.com');
+```
+
+Or to the referer page:
+
+``` php
+return $this->redirect($this->referer());
+```
+
+By using the second parameter you can define a status code for your redirect:
+
+``` php
+// Do a 301 (moved permanently)
+return $this->redirect('/order/confirm', 301);
+
+// Do a 303 (see other)
+return $this->redirect('/order/confirm', 303);
+```
+
+See the [Redirect Component Events](controllers/components#redirect-component-events) section for how to redirect out of
+a life-cycle handler.
+
+## Loading Additional Tables/Models
+
+`method` Cake\\Controller\\Controller::**fetchTable**(string $alias, array $config = [])
+
+The `fetchTable()` method comes handy when you need to use an ORM table that is not
+the controller's default one:
+
+``` php
+// In a controller method.
+$recentArticles = $this->fetchTable('Articles')->find('all',
+ limit: 5,
+ order: 'Articles.created DESC'
+ )
+ ->all();
+```
+
+### fetchModel()
+
+`method` Cake\\Controller\\Controller::**fetchModel**(string|null $modelClass = null, string|null $modelType = null)
+
+The `fetchModel()` method is useful to load non ORM models or ORM tables that
+are not the controller's default:
+
+``` php
+// ModelAwareTrait need to be explicity added to your controler first for fetchModel() to work.
+use ModelAwareTrait;
+
+// Get an ElasticSearch model
+$articles = $this->fetchModel('Articles', 'Elastic');
+
+// Get a webservices model
+$github = $this->fetchModel('GitHub', 'Webservice');
+
+// If you skip the 2nd argument it will by default try to load a ORM table.
+$authors = $this->fetchModel('Authors');
+```
+
+::: info Added in version 4.5.0
+:::
+
+## Paginating a Model
+
+`method` Cake\\Controller\\Controller::**paginate**(): PaginatedInterface
+
+This method is used for paginating results fetched by your models.
+You can specify page sizes, model find conditions and more. See the
+[pagination](controllers/pagination) section for more details on
+how to use `paginate()`.
+
+The `$paginate` attribute gives you a way to customize how `paginate()`
+behaves:
+
+``` php
+class ArticlesController extends AppController
+{
+ protected array $paginate = [
+ 'Articles' => [
+ 'conditions' => ['published' => 1],
+ ],
+ ];
+}
+```
+
+## Configuring Components to Load
+
+`method` Cake\\Controller\\Controller::**loadComponent**($name, $config = []): Component
+
+In your Controller's `initialize()` method you can define any components you
+want loaded, and any configuration data for them:
+
+``` php
+public function initialize(): void
+{
+ parent::initialize();
+ $this->loadComponent('Flash');
+ $this->loadComponent('Comments', Configure::read('Comments'));
+}
+```
+
+
+
+## Request Life-cycle Callbacks
+
+CakePHP controllers trigger several events/callbacks that you can use to insert
+logic around the request life-cycle:
+
+### Event List
+
+- `Controller.initialize`
+- `Controller.startup`
+- `Controller.beforeRedirect`
+- `Controller.beforeRender`
+- `Controller.shutdown`
+
+### Controller Callback Methods
+
+By default the following callback methods are connected to related events if the
+methods are implemented by your controllers
+
+#### beforeFilter()
+
+`method` Cake\\Controller\\Controller::**beforeFilter**(EventInterface $event): void
+
+#### beforeRender()
+
+`method` Cake\\Controller\\Controller::**beforeRender**(EventInterface $event): void
+
+#### afterFilter()
+
+`method` Cake\\Controller\\Controller::**afterFilter**(EventInterface $event): void
+
+In addition to controller life-cycle callbacks, [Components](controllers/components)
+also provide a similar set of callbacks.
+
+Remember to call `AppController`'s callbacks within child controller callbacks
+for best results:
+
+``` php
+//use Cake\Event\EventInterface;
+public function beforeFilter(EventInterface $event): void
+{
+ parent::beforeFilter($event);
+}
+```
+
+
+
+## Using Redirects in Controller Events
+
+To redirect from within a controller callback method you can use the following:
+
+``` php
+public function beforeFilter(EventInterface $event): void
+{
+ if (...) {
+ $event->setResult($this->redirect('/'));
+
+ return;
+ }
+
+ ...
+}
+```
+
+By setting a redirect as event result you let CakePHP know that you don't want any other
+component callbacks to run, and that the controller should not handle the action
+any further.
+
+As of 4.1.0 you can also raise a `RedirectException` to signal a redirect.
+
+## Controller Middleware
+
+`method` Cake\\Controller\\Controller::**middleware**($middleware, array $options = []): void
+
+[Middleware](controllers/middleware) can be defined globally, in
+a routing scope or within a controller. To define middleware for a specific
+controller use the `middleware()` method from your controller's
+`initialize()` method:
+
+``` php
+public function initialize(): void
+{
+ parent::initialize();
+
+ $this->middleware(function ($request, $handler) {
+ // Do middleware logic.
+
+ // Make sure you return a response or call handle()
+ return $handler->handle($request);
+ });
+}
+```
+
+Middleware defined by a controller will be called **before** `beforeFilter()` and action methods are called.
+
+## More on Controllers
+
+- [The Pages Controller](controllers/pages-controller)
+- [Components](controllers/components)
+- [Rate Limiting Middleware](controllers/middleware/rate-limit)
diff --git a/docs/en/controllers/components.md b/docs/en/controllers/components.md
new file mode 100644
index 0000000000..8baeb532c4
--- /dev/null
+++ b/docs/en/controllers/components.md
@@ -0,0 +1,361 @@
+# Components
+
+Components are packages of logic that are shared between controllers.
+CakePHP comes with a fantastic set of core components you can use to aid in
+various common tasks. You can also create your own components. If you find
+yourself wanting to copy and paste things between controllers, you should
+consider creating your own component to contain the functionality. Creating
+components keeps controller code clean and allows you to reuse code between
+different controllers.
+
+For more information on the components included in CakePHP, check out the
+chapter for each component:
+
+- [Flash](../controllers/components/flash)
+- [Form Protection Component](../controllers/components/form-protection)
+- [Checking HTTP Cache](../controllers/components/check-http-cache)
+
+
+
+## Configuring Components
+
+Many of the core components require configuration. One example would be
+the [Form Protection Component](../controllers/components/form-protection). Configuration for these components,
+and for components in general, is usually done via `loadComponent()` in your
+Controller's `initialize()` method or via the `$components` array:
+
+``` php
+class PostsController extends AppController
+{
+ public function initialize(): void
+ {
+ parent::initialize();
+ $this->loadComponent('FormProtection', [
+ 'unlockedActions' => ['index'],
+ ]);
+ $this->loadComponent('Flash');
+ }
+}
+```
+
+You can configure components at runtime using the `setConfig()` method. Often,
+this is done in your controller's `beforeFilter()` method. The above could
+also be expressed as:
+
+``` php
+public function beforeFilter(EventInterface $event): void
+{
+ $this->FormProtection->setConfig('unlockedActions', ['index']);
+}
+```
+
+Like helpers, components implement `getConfig()` and `setConfig()` methods
+to read and write configuration data:
+
+``` php
+// Read config data.
+$this->FormProtection->getConfig('unlockedActions');
+
+// Set config
+$this->Flash->setConfig('key', 'myFlash');
+```
+
+As with helpers, components will automatically merge their `$_defaultConfig`
+property with constructor configuration to create the `$_config` property
+which is accessible with `getConfig()` and `setConfig()`.
+
+### Aliasing Components
+
+One common setting to use is the `className` option, which allows you to
+alias components. This feature is useful when you want to
+replace `$this->Flash` or another common Component reference with a custom
+implementation:
+
+``` php
+// src/Controller/PostsController.php
+class PostsController extends AppController
+{
+ public function initialize(): void
+ {
+ $this->loadComponent('Flash', [
+ 'className' => 'MyFlash',
+ ]);
+ }
+}
+
+// src/Controller/Component/MyFlashComponent.php
+use Cake\Controller\Component\FlashComponent;
+
+class MyFlashComponent extends FlashComponent
+{
+ // Add your code to override the core FlashComponent
+}
+```
+
+The above would *alias* `MyFlashComponent` to `$this->Flash` in your
+controllers.
+
+> [!NOTE]
+> Aliasing a component replaces that instance anywhere that component is used,
+> including inside other Components.
+
+### Loading Components on the Fly
+
+You might not need all of your components available on every controller
+action. In situations like this you can load a component at runtime using the
+`loadComponent()` method in your controller:
+
+``` php
+// In a controller action
+$this->loadComponent('OneTimer');
+$time = $this->OneTimer->getTime();
+```
+
+> [!NOTE]
+> Keep in mind that components loaded on the fly will not have missed
+> callbacks called. If you rely on the `beforeFilter` or `startup`
+> callbacks being called, you may need to call them manually depending on when
+> you load your component.
+
+## Using Components
+
+Once you've included some components in your controller, using them is pretty
+simple. Each component you use is exposed as a property on your controller. If
+you had loaded up the `Cake\Controller\Component\FlashComponent`
+in your controller, you could access it like so:
+
+``` php
+class PostsController extends AppController
+{
+ public function initialize(): void
+ {
+ parent::initialize();
+ $this->loadComponent('Flash');
+ }
+
+ public function delete()
+ {
+ if ($this->Post->delete($this->request->getData('Post.id')) {
+ $this->Flash->success('Post deleted.');
+
+ return $this->redirect(['action' => 'index']);
+ }
+ }
+```
+
+> [!NOTE]
+> Since both Models and Components are added to Controllers as
+> properties they share the same 'namespace'. Be sure to not give a
+> component and a model the same name.
+
+::: info Changed in version 5.1.0
+Components are able to use [Dependency Injection](../development/dependency-injection) to receive services.
+:::
+
+
+
+## Creating a Component
+
+Suppose our application needs to perform a complex mathematical operation in
+many different parts of the application. We could create a component to house
+this shared logic for use in many different controllers.
+
+The first step is to create a new component file and class. Create the file in
+**src/Controller/Component/MathComponent.php**. The basic structure for the
+component would look something like this:
+
+``` php
+namespace App\Controller\Component;
+
+use Cake\Controller\Component;
+
+class MathComponent extends Component
+{
+ public function doComplexOperation($amount1, $amount2)
+ {
+ return $amount1 + $amount2;
+ }
+}
+```
+
+> [!NOTE]
+> All components must extend `Cake\Controller\Component`. Failing
+> to do this will trigger an exception.
+
+Components can use [Dependency Injection](../development/dependency-injection) to receive services
+as constructor parameters:
+
+``` php
+namespace App\Controller\Component;
+
+use Cake\Controller\Component;
+use App\Service\UserService;
+
+class SsoComponent extends Component
+{
+ private Users $users;
+
+ public function __construct(
+ ComponentRegistry $registry,
+ array $config = [],
+ UserService $users
+ ) {
+ parent::__construct($registry, $config);
+ $this->users = $users;
+ }
+}
+```
+
+### Including your Component in your Controllers
+
+Once our component is finished, we can use it in the application's
+controllers by loading it during the controller's `initialize()` method.
+Once loaded, the controller will be given a new attribute named after the
+component, through which we can access an instance of it:
+
+``` php
+// In a controller
+// Make the new component available at $this->Math,
+// as well as the standard $this->Flash
+public function initialize(): void
+{
+ parent::initialize();
+ $this->loadComponent('Math');
+ $this->loadComponent('Flash');
+}
+```
+
+When including Components in a Controller you can also declare a
+set of parameters that will be passed on to the Component's
+constructor. These parameters can then be handled by
+the Component:
+
+``` php
+// In your controller.
+public function initialize(): void
+{
+ parent::initialize();
+ $this->loadComponent('Math', [
+ 'precision' => 2,
+ 'randomGenerator' => 'srand',
+ ]);
+ $this->loadComponent('Flash');
+}
+```
+
+The above would pass the array containing precision and randomGenerator to
+`MathComponent::initialize()` in the `$config` parameter.
+
+### Using Other Components in your Component
+
+Sometimes one of your components may need to use another component.
+You can load other components by adding them to the \$components property:
+
+``` php
+// src/Controller/Component/CustomComponent.php
+namespace App\Controller\Component;
+
+use Cake\Controller\Component;
+
+class CustomComponent extends Component
+{
+ // The other component your component uses
+ protected array $components = ['Existing'];
+
+ // Execute any other additional setup for your component.
+ public function initialize(array $config): void
+ {
+ $this->Existing->foo();
+ }
+
+ public function bar()
+ {
+ // ...
+ }
+}
+
+// src/Controller/Component/ExistingComponent.php
+namespace App\Controller\Component;
+
+use Cake\Controller\Component;
+
+class ExistingComponent extends Component
+{
+ public function foo()
+ {
+ // ...
+ }
+}
+```
+
+> [!NOTE]
+> In contrast to a component included in a controller
+> no callbacks will be triggered on a component's component.
+
+### Accessing a Component's Controller
+
+From within a Component you can access the current controller through the
+registry:
+
+``` php
+$controller = $this->getController();
+```
+
+## Component Callbacks
+
+Components also offer a few request life-cycle callbacks that allow them to
+augment the request cycle.
+
+`method` Class::**beforeFilter**(EventInterface $event): void
+
+`method` Class::**startup**(EventInterface $event): void
+
+`method` Class::**beforeRender**(EventInterface $event): void
+
+`method` Class::**afterFilter**(EventInterface $event): void
+
+`method` Class::**beforeRedirect**(EventInterface $event, $url, Response $response): void
+
+
+
+## Using Redirects in Component Events
+
+To redirect from within a component callback method you can use the following:
+
+``` php
+public function beforeFilter(EventInterface $event): void
+{
+ if (...) {
+ $event->setResult($this->getController()->redirect('/'));
+
+ return;
+ }
+
+ ...
+}
+```
+
+By setting a redirect as event result you let CakePHP know that you don't want any other
+component callbacks to run, and that the controller should not handle the action
+any further. As of 4.1.0 you can raise a `RedirectException` to signal
+a redirect:
+
+``` php
+use Cake\Http\Exception\RedirectException;
+use Cake\Routing\Router;
+
+public function beforeFilter(EventInterface $event): void
+{
+ throw new RedirectException(Router::url('/'))
+}
+```
+
+Raising an exception will halt all other event listeners and create a new
+response that doesn't retain or inherit any of the current response's headers.
+When raising a `RedirectException` you can include additional headers:
+
+``` php
+throw new RedirectException(Router::url('/'), 302, [
+ 'Header-Key' => 'value',
+]);
+```
diff --git a/docs/en/controllers/components/check-http-cache.md b/docs/en/controllers/components/check-http-cache.md
new file mode 100644
index 0000000000..eab42313a9
--- /dev/null
+++ b/docs/en/controllers/components/check-http-cache.md
@@ -0,0 +1,33 @@
+# Checking HTTP Cache
+
+`class` **CheckHttpCacheComponent**(ComponentCollection $collection, array $config = [])
+
+The HTTP cache validation model is one of the processes used for cache gateways,
+also known as reverse proxies, to determine if they can serve a stored copy of
+a response to the client. Under this model, you mostly save bandwidth, but when
+used correctly you can also save some CPU processing, reducing response
+times:
+
+``` php
+// in a Controller
+public function initialize(): void
+{
+ parent::initialize();
+
+ $this->addComponent('CheckHttpCache');
+}
+```
+
+Enabling the `CheckHttpCacheComponent` in your controller automatically
+activates a `beforeRender` check. This check compares caching headers set in
+the response object to the caching headers sent in the request to determine
+whether the response was not modified since the last time the client asked for
+it. The following request headers are used:
+
+- `If-None-Match` is compared with the response's `Etag` header.
+- `If-Modified-Since` is compared with the response's `Last-Modified`
+ header.
+
+If response headers match the request header criteria, then view rendering is
+skipped. This saves your application generating a view, saving bandwidth and
+time. When response headers match, an empty response is returned with a `304 Not Modified` status code.
diff --git a/docs/en/controllers/components/flash.md b/docs/en/controllers/components/flash.md
new file mode 100644
index 0000000000..47f98bd2df
--- /dev/null
+++ b/docs/en/controllers/components/flash.md
@@ -0,0 +1,107 @@
+# Flash
+
+`class` Cake\\Controller\\Component\\**FlashComponent**(ComponentCollection $collection, array $config = [])
+
+FlashComponent provides a way to set one-time notification messages to be
+displayed after processing a form or acknowledging data. CakePHP refers to these
+messages as "flash messages". FlashComponent writes flash messages to
+`$_SESSION`, to be rendered in a View using
+[FlashHelper](../../views/helpers/flash).
+
+## Setting Flash Messages
+
+FlashComponent provides two ways to set flash messages: its `__call()` magic
+method and its `set()` method. To furnish your application with verbosity,
+FlashComponent's `__call()` magic method allows you use a method name that
+maps to an element located under the **templates/element/flash** directory.
+By convention, camelcased methods will map to the lowercased and underscored
+element name:
+
+``` php
+// Uses templates/element/flash/success.php
+$this->Flash->success('This was successful');
+
+// Uses templates/element/flash/great_success.php
+$this->Flash->greatSuccess('This was greatly successful');
+```
+
+Alternatively, to set a plain-text message without rendering an element, you can
+use the `set()` method:
+
+``` php
+$this->Flash->set('This is a message');
+```
+
+Flash messages are appended to an array internally. Successive calls to
+`set()` or `__call()` with the same key will append the messages in the
+`$_SESSION`. If you want to overwrite existing messages when setting a flash
+message, set the `clear` option to `true` when configuring the Component.
+
+FlashComponent's `__call()` and `set()` methods optionally take a second
+parameter, an array of options:
+
+- `key` Defaults to 'flash'. The array key found under the `Flash` key in
+ the session.
+- `element` Defaults to `null`, but will automatically be set when using the
+ `__call()` magic method. The element name to use for rendering.
+- `params` An optional array of keys/values to make available as variables
+ within an element.
+- `clear` expects a `bool` and allows you to delete all messages in the
+ current stack and start a new one.
+
+An example of using these options:
+
+``` php
+// In your Controller
+$this->Flash->success('The user has been saved', [
+ 'key' => 'positive',
+ 'clear' => true,
+ 'params' => [
+ 'name' => $user->name,
+ 'email' => $user->email,
+ ],
+]);
+
+// In your View
+= $this->Flash->render('positive') ?>
+
+
+
+```
+
+Note that the parameter `element` will be always overridden while using
+`__call()`. In order to retrieve a specific element from a plugin, you should
+set the `plugin` parameter. For example:
+
+``` php
+// In your Controller
+$this->Flash->warning('My message', ['plugin' => 'PluginName']);
+```
+
+The code above will use the **warning.php** element under
+**plugins/PluginName/templates/element/flash** for rendering the flash
+message.
+
+> [!NOTE]
+> By default, CakePHP escapes the content in flash messages to prevent cross
+> site scripting. User data in your flash messages will be HTML encoded and
+> safe to be printed. If you want to include HTML in your flash messages, you
+> need to pass the `escape` option and adjust your flash message templates
+> to allow disabling escaping when the escape option is passed.
+
+## HTML in Flash Messages
+
+It is possible to output HTML in flash messages by using the `'escape'` option
+key:
+
+``` php
+$this->Flash->info(sprintf('%s %s', h($highlight), h($message)), ['escape' => false]);
+```
+
+Make sure that you escape the input manually, then. In the above example
+`$highlight` and `$message` are non-HTML input and therefore escaped.
+
+For more information about rendering your flash messages, please refer to the
+[FlashHelper](../../views/helpers/flash) section.
diff --git a/docs/en/controllers/components/form-protection.md b/docs/en/controllers/components/form-protection.md
new file mode 100644
index 0000000000..b81bab9d14
--- /dev/null
+++ b/docs/en/controllers/components/form-protection.md
@@ -0,0 +1,159 @@
+# Form Protection Component
+
+`class` **FormProtection**(ComponentCollection $collection, array $config = [])
+
+The FormProtection Component provides protection against form data tampering.
+
+Like all components it is configured through several configurable parameters.
+All of these properties can be set directly or through setter methods of the
+same name in your controller's `initialize()` or `beforeFilter()` methods.
+
+If you are using other components that process form data in their `startup()`
+callbacks, be sure to place FormProtection Component before those components
+in your `initialize()` method.
+
+> [!NOTE]
+> When using the FormProtection Component you **must** use the FormHelper to create
+> your forms. In addition, you must **not** override any of the fields' "name"
+> attributes. The FormProtection Component looks for certain indicators that are
+> created and managed by the FormHelper (especially those created in
+> `Cake\View\Helper\FormHelper::create()` and
+> `Cake\View\Helper\FormHelper::end()`). Dynamically altering
+> the fields that are submitted in a POST request, such as disabling, deleting
+> or creating new fields via JavaScript, is likely to cause the form token
+> validation to fail.
+
+## Form tampering prevention
+
+By default the `FormProtectionComponent` prevents users from tampering with
+forms in specific ways. It will prevent the following things:
+
+- Form's action (URL) cannot be modified.
+- Unknown fields cannot be added to the form.
+- Fields cannot be removed from the form.
+- Values in hidden inputs cannot be modified.
+
+Preventing these types of tampering is accomplished by working with the `FormHelper`
+and tracking which fields are in a form. The values for hidden fields are
+tracked as well. All of this data is combined and turned into a hash and hidden
+token fields are automatically be inserted into forms. When a form is submitted,
+the `FormProtectionComponent` will use the POST data to build the same structure
+and compare the hash.
+
+> [!NOTE]
+> The FormProtectionComponent will **not** prevent select options from being
+> added/changed. Nor will it prevent radio options from being added/changed.
+
+## Usage
+
+Configuring the form protection component is generally done in the controller's
+`initialize()` or `beforeFilter()` callbacks
+
+Available options are:
+
+validate
+Set to `false` to completely skip the validation of POST
+requests, essentially turning off form validation.
+
+unlockedFields
+Set to a list of form fields to exclude from POST validation. Fields can be
+unlocked either in the Component, or with
+`FormHelper::unlockField()`. Fields that have been unlocked are
+not required to be part of the POST and hidden unlocked fields do not have
+their values checked.
+
+unlockedActions
+Actions to exclude from POST validation checks.
+
+validationFailureCallback
+Callback to call in case of validation failure. Must be a valid Closure.
+Unset by default in which case exception is thrown on validation failure.
+
+## Disabling form tampering checks
+
+``` php
+namespace App\Controller;
+
+use App\Controller\AppController;
+use Cake\Event\EventInterface;
+
+class WidgetsController extends AppController
+{
+ public function initialize(): void
+ {
+ parent::initialize();
+
+ $this->loadComponent('FormProtection');
+ }
+
+ public function beforeFilter(EventInterface $event): void
+ {
+ parent::beforeFilter($event);
+
+ if ($this->request->getParam('prefix') === 'Admin') {
+ $this->FormProtection->setConfig('validate', false);
+ }
+ }
+}
+```
+
+The above example would disable form tampering prevention for admin prefixed
+routes.
+
+## Disabling form tampering for specific actions
+
+There may be cases where you want to disable form tampering prevention for an
+action (ex. AJAX requests). You may "unlock" these actions by listing them in
+`$this->FormProtection->setConfig('unlockedActions', ['edit']);` in your `beforeFilter()`:
+
+``` php
+namespace App\Controller;
+
+use App\Controller\AppController;
+use Cake\Event\EventInterface;
+
+class WidgetController extends AppController
+{
+ public function initialize(): void
+ {
+ parent::initialize();
+ $this->loadComponent('FormProtection');
+ }
+
+ public function beforeFilter(EventInterface $event): void
+ {
+ parent::beforeFilter($event);
+
+ $this->FormProtection->setConfig('unlockedActions', ['edit']);
+ }
+}
+```
+
+This example would disable all security checks for the edit action.
+
+## Handling validation failure through callbacks
+
+If form protection validation fails it will result in a 400 error by default.
+You can configure this behavior by setting the `validationFailureCallback`
+configuration option to a callback function in the controller.
+
+By configuring a callback method you can customize how the failure handling process
+works:
+
+``` php
+use Cake\Controller\Exception\FormProtectionException;
+
+public function beforeFilter(EventInterface $event): void
+{
+ parent::beforeFilter($event);
+
+ $this->FormProtection->setConfig(
+ 'validationFailureCallback',
+ // Prior to 5.2 use Cake\Http\Exception\BadRequestException.
+ function (FormProtectionException $exception) {
+ // You can either return a response instance or throw the exception
+ // received as argument.
+ }
+ );
+}
+```
diff --git a/docs/en/controllers/middleware.md b/docs/en/controllers/middleware.md
new file mode 100644
index 0000000000..939022c328
--- /dev/null
+++ b/docs/en/controllers/middleware.md
@@ -0,0 +1,310 @@
+# Middleware
+
+Middleware objects give you the ability to 'wrap' your application in re-usable,
+composable layers of Request handling, or response building logic. Visually,
+your application ends up at the center, and middleware is wrapped around the app
+like an onion. Here we can see an application wrapped with Routes, Assets,
+Exception Handling and CORS header middleware.
+
+
+
+When a request is handled by your application it enters from the outermost
+middleware. Each middleware can either delegate the request/response to the next
+layer, or return a response. Returning a response prevents lower layers from
+ever seeing the request. An example of that is the AssetMiddleware handling
+a request for a plugin image during development.
+
+
+
+If no middleware take action to handle the request, a controller will be located
+and have its action invoked, or an exception will be raised generating an error
+page.
+
+Middleware are part of the new HTTP stack in CakePHP that leverages the PSR-7
+request and response interfaces. CakePHP also supports the PSR-15 standard for
+server request handlers so you can use any PSR-15 compatible middleware available
+on [The Packagist](https://packagist.org).
+
+## Middleware in CakePHP
+
+CakePHP provides several middleware to handle common tasks in web applications:
+
+- `Cake\Error\Middleware\ErrorHandlerMiddleware` traps exceptions from the
+ wrapped middleware and renders an error page using the
+ [Error & Exception Handling](../development/errors) Exception handler.
+- `Cake\Routing\AssetMiddleware` checks whether the request is referring to a
+ theme or plugin asset file, such as a CSS, JavaScript or image file stored in
+ either a plugin's webroot folder or the corresponding one for a Theme.
+- `Cake\Routing\Middleware\RoutingMiddleware` uses the `Router` to parse the
+ incoming URL and assign routing parameters to the request.
+- `Cake\I18n\Middleware\LocaleSelectorMiddleware` enables automatic language
+ switching from the `Accept-Language` header sent by the browser.
+- `Cake\Http\Middleware\EncryptedCookieMiddleware` gives you the ability to
+ manipulate encrypted cookies in case you need to manipulate cookie with
+ obfuscated data.
+- `Cake\Http\Middleware\BodyParserMiddleware` allows you to decode JSON, XML
+ and other encoded request bodies based on `Content-Type` header.
+- [Cake\Http\Middleware\HttpsEnforcerMiddleware](../security/https-enforcer)
+ requires HTTPS to be used.
+- [Cake\Http\Middleware\CsrfProtectionMiddleware](../security/csrf) adds
+ double-submit cookie based CSRF protection to your application.
+- [Cake\Http\Middleware\SessionCsrfProtectionMiddleware](../security/csrf)
+ adds session based CSRF protection to your application.
+- [Cake\Http\Middleware\CspMiddleware](../security/content-security-policy)
+ makes it simpler to add Content-Security-Policy headers to your application.
+- [Cake\Http\Middleware\SecurityHeadersMiddleware](../security/security-headers)
+ makes it possible to add security related headers like `X-Frame-Options` to
+ responses.
+- [Cake\Http\Middleware\RateLimitMiddleware](../controllers/middleware/rate-limit)
+ provides configurable rate limiting to protect against abuse and ensure fair
+ usage of resources.
+
+
+
+## Using Middleware
+
+Middleware can be applied to your application globally, to individual
+routing scopes, or to specific controllers.
+
+To apply middleware to all requests, use the `middleware` method of your
+`App\Application` class. Your application's `middleware` hook method will be
+called at the beginning of the request process, you can use the
+`MiddlewareQueue` object to attach middleware:
+
+``` php
+namespace App;
+
+use Cake\Core\Configure;
+use Cake\Error\Middleware\ErrorHandlerMiddleware;
+use Cake\Http\BaseApplication;
+use Cake\Http\MiddlewareQueue;
+
+class Application extends BaseApplication
+{
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ // Bind the error handler into the middleware queue.
+ $middlewareQueue->add(new ErrorHandlerMiddleware(Configure::read('Error'), $this));
+
+ // Add middleware by classname.
+ // As of 4.5.0 classname middleware are optionally resolved
+ // using the DI container. If the class is not found in the
+ // container then an instance is created by the middleware queue.
+ $middlewareQueue->add(UserRateLimiting::class);
+
+ return $middlewareQueue;
+ }
+}
+```
+
+In addition to adding to the end of the `MiddlewareQueue` you can do
+a variety of operations:
+
+``` php
+$layer = new \App\Middleware\CustomMiddleware;
+
+// Added middleware will be last in line.
+$middlewareQueue->add($layer);
+
+// Prepended middleware will be first in line.
+$middlewareQueue->prepend($layer);
+
+// Insert in a specific slot. If the slot is out of
+// bounds, it will be added to the end.
+$middlewareQueue->insertAt(2, $layer);
+
+// Insert before another middleware.
+// If the named class cannot be found,
+// an exception will be raised.
+$middlewareQueue->insertBefore(
+ 'Cake\Error\Middleware\ErrorHandlerMiddleware',
+ $layer
+);
+
+// Insert after another middleware.
+// If the named class cannot be found, the
+// middleware will added to the end.
+$middlewareQueue->insertAfter(
+ 'Cake\Error\Middleware\ErrorHandlerMiddleware',
+ $layer
+);
+```
+
+If your middleware is only applicable to a subset of routes or individual
+controllers you can use [Route scoped middleware](../development/routing#route-scoped-middleware),
+or [Controller middleware](../controllers#controller-middleware).
+
+### Adding Middleware from Plugins
+
+Plugins can use their `middleware` hook method to apply any middleware they
+have to the application's middleware queue:
+
+``` php
+// in plugins/ContactManager/src/Plugin.php
+namespace ContactManager;
+
+use Cake\Core\BasePlugin;
+use Cake\Http\MiddlewareQueue;
+use ContactManager\Middleware\ContactManagerContextMiddleware;
+
+class Plugin extends BasePlugin
+{
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ $middlewareQueue->add(new ContactManagerContextMiddleware());
+
+ return $middlewareQueue;
+ }
+}
+```
+
+## Creating Middleware
+
+Middleware can either be implemented as anonymous functions (Closures), or classes
+which extend `Psr\Http\Server\MiddlewareInterface`. While Closures are suitable
+for smaller tasks they make testing harder, and can create a complicated
+`Application` class. Middleware classes in CakePHP have a few conventions:
+
+- Middleware class files should be put in **src/Middleware**. For example:
+ **src/Middleware/CorsMiddleware.php**
+- Middleware classes should be suffixed with `Middleware`. For example:
+ `LinkMiddleware`.
+- Middleware must implement `Psr\Http\Server\MiddlewareInterface`.
+
+Middleware can return a response either by calling `$handler->handle()` or by
+creating their own response. We can see both options in our simple middleware:
+
+``` php
+// In src/Middleware/TrackingCookieMiddleware.php
+namespace App\Middleware;
+
+use Cake\Http\Cookie\Cookie;
+use Cake\I18n\Time;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Psr\Http\Server\MiddlewareInterface;
+
+class TrackingCookieMiddleware implements MiddlewareInterface
+{
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface
+ {
+ // Calling $handler->handle() delegates control to the *next* middleware
+ // In your application's queue.
+ $response = $handler->handle($request);
+
+ if (!$request->getCookie('landing_page')) {
+ $expiry = new Time('+ 1 year');
+ $response = $response->withCookie(new Cookie(
+ 'landing_page',
+ $request->getRequestTarget(),
+ $expiry
+ ));
+ }
+
+ return $response;
+ }
+}
+```
+
+Now that we've made a very simple middleware, let's attach it to our
+application:
+
+``` php
+// In src/Application.php
+namespace App;
+
+use App\Middleware\TrackingCookieMiddleware;
+use Cake\Http\MiddlewareQueue;
+
+class Application
+{
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ // Add your simple middleware onto the queue
+ $middlewareQueue->add(new TrackingCookieMiddleware());
+
+ // Add some more middleware onto the queue
+
+ return $middlewareQueue;
+ }
+}
+```
+
+
+
+## Routing Middleware
+
+Routing middleware is responsible for applying your application's routes and
+resolving the plugin, controller, and action a request is going to:
+
+``` php
+// In Application.php
+public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+{
+ // ...
+ $middlewareQueue->add(new RoutingMiddleware($this));
+}
+```
+
+
+
+## Encrypted Cookie Middleware
+
+If your application has cookies that contain data you want to obfuscate and
+protect against user tampering, you can use CakePHP's encrypted cookie
+middleware to transparently encrypt and decrypt cookie data via middleware.
+Cookie data is encrypted with via OpenSSL using AES:
+
+``` php
+use Cake\Http\Middleware\EncryptedCookieMiddleware;
+
+$cookies = new EncryptedCookieMiddleware(
+ // Names of cookies to protect
+ ['secrets', 'protected'],
+ Configure::read('Security.cookieKey')
+);
+
+$middlewareQueue->add($cookies);
+```
+
+> [!NOTE]
+> It is recommended that the encryption key you use for cookie data, is used
+> *exclusively* for cookie data.
+
+The encryption algorithms and padding style used by the cookie middleware are
+backwards compatible with `CookieComponent` from earlier versions of CakePHP.
+
+
+
+## Body Parser Middleware
+
+If your application accepts JSON, XML or other encoded request bodies, the
+`BodyParserMiddleware` will let you decode those requests into an array that
+is available via `$request->getParsedData()` and `$request->getData()`. By
+default only `json` bodies will be parsed, but XML parsing can be enabled with
+an option. You can also define your own parsers:
+
+``` php
+use Cake\Http\Middleware\BodyParserMiddleware;
+
+// only JSON will be parsed.
+$bodies = new BodyParserMiddleware();
+
+// Enable XML parsing
+$bodies = new BodyParserMiddleware(['xml' => true]);
+
+// Disable JSON parsing
+$bodies = new BodyParserMiddleware(['json' => false]);
+
+// Add your own parser matching content-type header values
+// to the callable that can parse them.
+$bodies = new BodyParserMiddleware();
+$bodies->addParser(['text/csv'], function ($body, $request) {
+ // Use a CSV parsing library.
+ return Csv::parse($body);
+});
+```
diff --git a/docs/en/controllers/middleware/rate-limit.md b/docs/en/controllers/middleware/rate-limit.md
new file mode 100644
index 0000000000..a53e7d0657
--- /dev/null
+++ b/docs/en/controllers/middleware/rate-limit.md
@@ -0,0 +1,391 @@
+# Rate Limiting Middleware
+
+::: info Added in version 5.3
+:::
+
+The `RateLimitMiddleware` provides configurable rate limiting for your
+application to protect against abuse and ensure fair usage of resources.
+
+## Basic Usage
+
+To use rate limiting in your application, add the middleware to your
+middleware queue:
+
+``` php
+// In src/Application.php
+use Cake\Http\Middleware\RateLimitMiddleware;
+
+public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+{
+ $middlewareQueue
+ // ... other middleware
+ ->add(new RateLimitMiddleware([
+ 'limit' => 60, // 60 requests
+ 'window' => 60, // per 60 seconds
+ 'identifier' => RateLimitMiddleware::IDENTIFIER_IP,
+ ]));
+
+ return $middlewareQueue;
+}
+```
+
+When a client exceeds the rate limit, they will receive a
+`429 Too Many Requests` response.
+
+## Constants
+
+The middleware provides constants for common identifier and strategy values:
+
+### Identifier Constants
+
+- `RateLimitMiddleware::IDENTIFIER_IP` - Client IP address (default)
+- `RateLimitMiddleware::IDENTIFIER_USER` - Authenticated user
+- `RateLimitMiddleware::IDENTIFIER_ROUTE` - Route (controller/action combination)
+- `RateLimitMiddleware::IDENTIFIER_API_KEY` - API key from token headers
+- `RateLimitMiddleware::IDENTIFIER_TOKEN` - Alias for API key
+
+### Strategy Constants
+
+- `RateLimitMiddleware::STRATEGY_SLIDING_WINDOW` - Sliding window algorithm (default)
+- `RateLimitMiddleware::STRATEGY_FIXED_WINDOW` - Fixed window algorithm
+- `RateLimitMiddleware::STRATEGY_TOKEN_BUCKET` - Token bucket algorithm
+
+## Configuration Options
+
+The middleware accepts the following configuration options:
+
+- **limit** - Maximum number of requests allowed (default: 60)
+- **window** - Time window in seconds (default: 60)
+- **identifier** - How to identify clients. Use identifier constants (default: `IDENTIFIER_IP`)
+- **strategy** - Rate limiting algorithm. Use strategy constants (default: `STRATEGY_SLIDING_WINDOW`)
+- **strategyClass** - Fully qualified class name of a custom rate limiter strategy. Takes precedence over `strategy` option
+- **cache** - Cache configuration to use (default: 'default')
+- **headers** - Whether to include rate limit headers in responses (default: true)
+- **includeRetryAfter** - Whether to include Retry-After header in 429 responses (default: true)
+- **message** - Custom error message for rate limit exceeded (default: 'Rate limit exceeded. Please try again later.')
+- **ipHeader** - Header name(s) to check for client IP when behind a proxy (default: 'x-forwarded-for')
+- **tokenHeaders** - Array of headers to check for API tokens (default: `['Authorization', 'X-API-Key']`)
+- **skipCheck** - Callback to determine if a request should skip rate limiting
+- **costCallback** - Callback to determine the cost of a request
+- **identifierCallback** - Callback to determine the identifier for a request
+- **limitCallback** - Callback to determine the limit for a specific identifier
+- **keyGenerator** - Callback for custom cache key generation
+- **limiters** - Array of named limiter configurations for different rate limit profiles
+- **limiterResolver** - Callback to resolve which named limiter applies to a request
+
+## Identifier Types
+
+### IP Address
+
+The default identifier type tracks requests by IP address:
+
+``` php
+use Cake\Http\Middleware\RateLimitMiddleware;
+
+new RateLimitMiddleware([
+ 'identifier' => RateLimitMiddleware::IDENTIFIER_IP,
+ 'limit' => 100,
+ 'window' => 60,
+])
+```
+
+The middleware automatically handles proxy headers. You can configure
+which headers to check using the `ipHeader` option:
+
+``` text
+new RateLimitMiddleware([
+ 'identifier' => RateLimitMiddleware::IDENTIFIER_IP,
+ 'ipHeader' => ['CF-Connecting-IP', 'X-Forwarded-For'],
+])
+```
+
+### User-based
+
+Track requests per authenticated user:
+
+``` text
+new RateLimitMiddleware([
+ 'identifier' => RateLimitMiddleware::IDENTIFIER_USER,
+ 'limit' => 1000,
+ 'window' => 3600, // 1 hour
+])
+```
+
+This requires authentication middleware to be loaded before rate limiting.
+The middleware checks for users implementing `Authentication\IdentityInterface`.
+
+### Route-based
+
+Apply different limits to different routes:
+
+``` text
+new RateLimitMiddleware([
+ 'identifier' => RateLimitMiddleware::IDENTIFIER_ROUTE,
+ 'limit' => 10,
+ 'window' => 60,
+])
+```
+
+This creates separate limits for each controller/action combination.
+
+### API Key / Token
+
+Track requests by API key or token:
+
+``` text
+new RateLimitMiddleware([
+ 'identifier' => RateLimitMiddleware::IDENTIFIER_API_KEY,
+ 'limit' => 5000,
+ 'window' => 3600,
+])
+```
+
+By default, the middleware looks for tokens in the `Authorization` and
+`X-API-Key` headers. You can customize which headers to check:
+
+``` text
+new RateLimitMiddleware([
+ 'identifier' => RateLimitMiddleware::IDENTIFIER_TOKEN,
+ 'tokenHeaders' => ['Authorization', 'X-API-Key', 'X-Auth-Token'],
+])
+```
+
+## Custom Identifiers
+
+You can create custom identifiers using a callback:
+
+``` php
+new RateLimitMiddleware([
+ 'identifierCallback' => function ($request) {
+ // Custom logic to identify the client
+ $tenant = $request->getHeader('X-Tenant-ID');
+ return 'tenant_' . $tenant[0];
+ },
+])
+```
+
+## Rate Limiting Strategies
+
+### Sliding Window
+
+The default strategy that provides smooth rate limiting by continuously
+adjusting the window based on request timing:
+
+``` text
+new RateLimitMiddleware([
+ 'strategy' => RateLimitMiddleware::STRATEGY_SLIDING_WINDOW,
+])
+```
+
+### Fixed Window
+
+Resets the counter at fixed intervals:
+
+``` text
+new RateLimitMiddleware([
+ 'strategy' => RateLimitMiddleware::STRATEGY_FIXED_WINDOW,
+])
+```
+
+### Token Bucket
+
+Allows for burst capacity while maintaining an average rate:
+
+``` text
+new RateLimitMiddleware([
+ 'strategy' => RateLimitMiddleware::STRATEGY_TOKEN_BUCKET,
+ 'limit' => 100, // bucket capacity
+ 'window' => 60, // refill rate
+])
+```
+
+### Custom Strategy
+
+You can use a custom rate limiter strategy by specifying the `strategyClass`
+option. Your class must implement `Cake\Http\RateLimiter\RateLimiterInterface`:
+
+``` text
+new RateLimitMiddleware([
+ 'strategyClass' => App\RateLimiter\MyCustomRateLimiter::class,
+])
+```
+
+The `strategyClass` option takes precedence over the `strategy` option.
+
+## Named Limiters
+
+For complex applications, you can define named limiter configurations
+and resolve them dynamically per request:
+
+``` php
+new RateLimitMiddleware([
+ 'limiters' => [
+ 'default' => [
+ 'limit' => 60,
+ 'window' => 60,
+ ],
+ 'api' => [
+ 'limit' => 1000,
+ 'window' => 3600,
+ ],
+ 'premium' => [
+ 'limit' => 10000,
+ 'window' => 3600,
+ ],
+ ],
+ 'limiterResolver' => function ($request) {
+ $user = $request->getAttribute('identity');
+ if ($user && $user->plan === 'premium') {
+ return 'premium';
+ }
+ if (str_starts_with($request->getPath(), '/api/')) {
+ return 'api';
+ }
+ return 'default';
+ },
+])
+```
+
+## Advanced Usage
+
+### Skip Rate Limiting
+
+Skip rate limiting for certain requests:
+
+``` php
+new RateLimitMiddleware([
+ 'skipCheck' => function ($request) {
+ // Skip rate limiting for health checks
+ return $request->getParam('action') === 'health';
+ },
+])
+```
+
+### Request Cost
+
+Assign different costs to different types of requests:
+
+``` php
+new RateLimitMiddleware([
+ 'costCallback' => function ($request) {
+ // POST requests cost 5x more
+ return $request->getMethod() === 'POST' ? 5 : 1;
+ },
+])
+```
+
+### Dynamic Limits
+
+Set different limits for different users or plans:
+
+``` php
+new RateLimitMiddleware([
+ 'limitCallback' => function ($request, $identifier) {
+ $user = $request->getAttribute('identity');
+ if ($user && $user->plan === 'premium') {
+ return 10000; // Premium users get higher limit
+ }
+ return 100; // Free tier limit
+ },
+])
+```
+
+### Custom Key Generation
+
+Customize how cache keys are generated:
+
+``` php
+new RateLimitMiddleware([
+ 'keyGenerator' => function ($request, $identifier) {
+ // Include the HTTP method in the key for per-method limits
+ return $identifier . '_' . $request->getMethod();
+ },
+])
+```
+
+### Resetting Rate Limits
+
+To programmatically reset a rate limit for a specific identifier, use the
+`reset()` method on the rate limiter strategy directly. This is useful for:
+
+- Admin actions to unblock users who were incorrectly rate-limited
+- Resetting limits when a user upgrades their plan
+- Clearing state between tests
+
+``` php
+use Cake\Cache\Cache;
+use Cake\Http\RateLimit\SlidingWindowRateLimiter;
+
+// Create a rate limiter with the same cache config as your middleware
+$limiter = new SlidingWindowRateLimiter(Cache::pool('default'));
+
+// Reset using the hashed identifier format
+$identifier = 'rate_limit_' . hash('xxh3', $userId);
+$limiter->reset($identifier);
+```
+
+> [!NOTE]
+> The identifier format used internally is `'rate_limit_' . hash('xxh3', $value)`
+> where `$value` is the raw identifier (IP address, user ID, etc.).
+
+## Rate Limit Headers
+
+When enabled, the middleware adds the following headers to responses:
+
+- `X-RateLimit-Limit` - The maximum number of requests allowed
+- `X-RateLimit-Remaining` - The number of requests remaining
+- `X-RateLimit-Reset` - Unix timestamp when the rate limit resets
+
+When a client exceeds the limit, a `Retry-After` header is also included
+(controlled by the `includeRetryAfter` option).
+
+## Multiple Rate Limiters
+
+You can apply multiple rate limiters with different configurations:
+
+``` php
+// Strict limit for login attempts
+$middlewareQueue->add(new RateLimitMiddleware([
+ 'identifier' => RateLimitMiddleware::IDENTIFIER_IP,
+ 'limit' => 5,
+ 'window' => 900, // 15 minutes
+ 'skipCheck' => function ($request) {
+ return $request->getParam('action') !== 'login';
+ },
+]));
+
+// General API rate limit
+$middlewareQueue->add(new RateLimitMiddleware([
+ 'identifier' => RateLimitMiddleware::IDENTIFIER_API_KEY,
+ 'limit' => 1000,
+ 'window' => 3600,
+]));
+```
+
+## Cache Configuration
+
+The rate limiter stores its data in cache. Make sure you have a persistent
+cache configured:
+
+``` text
+// In config/app.php
+'Cache' => [
+ 'rate_limit' => [
+ 'className' => 'Redis',
+ 'prefix' => 'rate_limit_',
+ 'duration' => '+1 hour',
+ ],
+],
+```
+
+Then use it in the middleware:
+
+``` text
+new RateLimitMiddleware([
+ 'cache' => 'rate_limit',
+])
+```
+
+> [!WARNING]
+> The `File` cache engine is not recommended for production use with
+> rate limiting as it may not handle concurrent requests properly.
diff --git a/docs/en/controllers/pages-controller.md b/docs/en/controllers/pages-controller.md
new file mode 100644
index 0000000000..ae7286c05b
--- /dev/null
+++ b/docs/en/controllers/pages-controller.md
@@ -0,0 +1,12 @@
+# The Pages Controller
+
+CakePHP's official skeleton app ships with a default controller **PagesController.php**.
+This is a simple and optional controller for serving up static content. The home page
+you see after installation is generated using this controller and the view
+file **templates/Pages/home.php**. If you make the view file
+**templates/Pages/about_us.php** you can access it using the URL
+**http://example.com/pages/about_us**. You are free to modify the Pages
+Controller to meet your needs.
+
+When you "bake" an app using Composer the Pages Controller is created in your
+**src/Controller/** folder.
diff --git a/docs/en/controllers/pagination.md b/docs/en/controllers/pagination.md
new file mode 100644
index 0000000000..20c44a1c14
--- /dev/null
+++ b/docs/en/controllers/pagination.md
@@ -0,0 +1,412 @@
+# Pagination
+
+One of the main obstacles of creating flexible and user-friendly web
+applications is designing an intuitive user interface. Many applications tend to
+grow in size and complexity quickly, and designers and programmers alike find
+they are unable to cope with displaying hundreds or thousands of records.
+Refactoring takes time, and performance and user satisfaction can suffer.
+
+Displaying a reasonable number of records per page has always been a critical
+part of every application and used to cause many headaches for developers.
+CakePHP eases the burden on the developer by providing a terse way to
+paginate data.
+
+Pagination in CakePHP controllers is done through the `paginate()` method. You
+then use `Cake\View\Helper\PaginatorHelper` in your view templates
+to generate pagination controls.
+
+## Basic Usage
+
+You can call `paginate()` using an ORM table instance or `Query` object:
+
+``` php
+public function index()
+{
+ // Paginate the ORM table.
+ $this->set('articles', $this->paginate($this->Articles));
+
+ // Paginate a select query
+ $query = $this->Articles->find('published')->contain('Comments');
+ $this->set('articles', $this->paginate($query));
+}
+```
+
+## Advanced Usage
+
+More complex use cases are supported by configuring the `$paginate`
+controller property or as the `$settings` argument to `paginate()`. These
+conditions serve as the basis for you pagination queries. They are augmented
+by the `sort`, `direction`, `limit`, and `page` parameters passed in
+from the URL:
+
+``` php
+class ArticlesController extends AppController
+{
+ protected array $paginate = [
+ 'limit' => 25,
+ 'order' => [
+ 'Articles.title' => 'asc',
+ ],
+ ];
+}
+```
+
+> [!TIP]
+> Default `order` options must be defined as an array.
+
+You can also use [Custom Find Methods](../orm/retrieving-data-and-resultsets#custom-find-methods) in pagination by using the `finder` option:
+
+``` php
+class ArticlesController extends AppController
+{
+ protected array $paginate = [
+ 'finder' => 'published',
+ ];
+}
+```
+
+Note: This only works with Table as string input in `$this->paginate('MyTable')`. Once you use `$this->MyTable->find()` as input for `paginate()`, you must directly use that Query object instead.
+
+If your finder method requires additional options you can pass those
+as values for the finder:
+
+``` php
+class ArticlesController extends AppController
+{
+ // find articles by tag
+ public function tags()
+ {
+ $tags = $this->request->getParam('pass');
+
+ $customFinderOptions = [
+ 'tags' => $tags
+ ];
+ // We're using the $settings argument to paginate() here.
+ // But the same structure could be used in $this->paginate
+ //
+ // Our custom finder is called findTagged inside ArticlesTable.php
+ // which is why we're using `tagged` as the key.
+ // Our finder should look like:
+ // public function findTagged(Query $query, array $tagged = [])
+ $settings = [
+ 'finder' => [
+ 'tagged' => $customFinderOptions
+ ]
+ ];
+ $articles = $this->paginate($this->Articles, $settings);
+ $this->set(compact('articles', 'tags'));
+ }
+}
+```
+
+In addition to defining general pagination values, you can define more than one
+set of pagination defaults in the controller. The name of each model can be used
+as a key in the `$paginate` property:
+
+``` php
+class ArticlesController extends AppController
+{
+ protected array $paginate = [
+ 'Articles' => [],
+ 'Authors' => [],
+ ];
+}
+```
+
+The values of the `Articles` and `Authors` keys could contain all the keys
+that a basic `$paginate` array would.
+
+`Controller::paginate()` returns an instance of `Cake\Datasource\Paging\PaginatedResultSet`
+which implements the `Cake\Datasource\Paging\PaginatedInterface`.
+
+This object contains the paginated records and the paging params.
+
+## Simple Pagination
+
+By default `Controller::paginate()` uses the `Cake\Datasource\Paging\NumericPaginator`
+class which does a `COUNT()` query to calculate the size of the result set so
+that page number links can be rendered. On very large datasets this count query
+can be very expensive. In situations where you only want to show 'Next' and 'Previous'
+links you can use the 'simple' paginator which does not do a count query:
+
+``` php
+class ArticlesController extends AppController
+{
+ protected array $paginate = [
+ 'className' => 'Simple', // Or use Cake\Datasource\Paging\SimplePaginator::class FQCN
+ ];
+}
+```
+
+When using the `SimplePaginator` you will not be able to generate page
+numbers, counter data, links to the last page, or total record count controls.
+
+
+
+## Paginating Multiple Queries
+
+You can paginate multiple models in a single controller action, using the
+`scope` option both in the controller's `$paginate` property and in the
+call to the `paginate()` method:
+
+``` php
+// Paginate property
+protected array $paginate = [
+ 'Articles' => ['scope' => 'article'],
+ 'Tags' => ['scope' => 'tag']
+];
+
+// In a controller action
+$articles = $this->paginate($this->Articles, ['scope' => 'article']);
+$tags = $this->paginate($this->Tags, ['scope' => 'tag']);
+$this->set(compact('articles', 'tags'));
+```
+
+The `scope` option will result in the paginator looking in
+scoped query string parameters. For example, the following URL could be used to
+paginate both tags and articles at the same time:
+
+ /dashboard?article[page]=1&tag[page]=3
+
+See the [Paginator Helper Multiple](../views/helpers/paginator#paginator-helper-multiple) section for how to generate scoped HTML
+elements and URLs for pagination.
+
+### Paginating the Same Model multiple Times
+
+To paginate the same model multiple times within a single controller action you
+need to define an alias for the model.:
+
+``` php
+// In a controller action
+$this->paginate = [
+ 'Articles' => [
+ 'scope' => 'published_articles',
+ 'limit' => 10,
+ 'order' => [
+ 'id' => 'desc',
+ ],
+ ],
+ 'UnpublishedArticles' => [
+ 'scope' => 'unpublished_articles',
+ 'limit' => 10,
+ 'order' => [
+ 'id' => 'desc',
+ ],
+ ],
+];
+
+$publishedArticles = $this->paginate(
+ $this->Articles->find('all', scope: 'published_articles')
+ ->where(['published' => true])
+);
+
+// Load an additional table object to allow differentiating in the paginator
+$unpublishedArticlesTable = $this->fetchTable('UnpublishedArticles', [
+ 'className' => 'App\Model\Table\ArticlesTable',
+ 'table' => 'articles',
+ 'entityClass' => 'App\Model\Entity\Article',
+]);
+
+$unpublishedArticles = $this->paginate(
+ $unpublishedArticlesTable->find('all', scope: 'unpublished_articles')
+ ->where(['published' => false])
+);
+```
+
+
+
+## Control which Fields Used for Ordering
+
+By default sorting can be done on any non-virtual column a table has. This is
+sometimes undesirable as it allows users to sort on un-indexed columns that can
+be expensive to order by. You can set the allowed list of fields that can be sorted
+using the `sortableFields` option. This option is required when you want to
+sort on any associated data, or computed fields that may be part of your
+pagination query:
+
+``` php
+protected array $paginate = [
+ 'sortableFields' => [
+ 'id', 'title', 'Users.username', 'created',
+ ],
+];
+```
+
+Any requests that attempt to sort on fields not in the allowed list will be
+ignored.
+
+## Advanced Sorting with SortableFieldsBuilder
+
+The `SortableFieldsBuilder` provides a powerful way to create user-friendly
+sort URLs while maintaining control over database ordering. Instead of exposing
+raw field names in URLs, you can map friendly keys to complex sorting logic with
+multi-column support and direction control.
+
+### Using the Builder
+
+You can configure sortable fields using a callable that receives a
+`SortableFieldsBuilder` instance:
+
+``` php
+use Cake\Datasource\Paging\SortField;
+use Cake\Datasource\Paging\SortableFieldsBuilder;
+
+protected array $paginate = [
+ 'sortableFields' => function (SortableFieldsBuilder $builder) {
+ return $builder
+ ->add('id', 'Articles.id')
+ ->add('title', 'Articles.title')
+ ->add('username', 'Users.username');
+ },
+];
+```
+
+This configuration allows users to sort using friendly keys like `?sort=username`
+instead of exposing the full field name `?sort=Users.username`.
+
+### Multi-Column Sorting
+
+The builder supports mapping a single sort key to multiple database fields with
+independent direction control. Use the `SortField` class to define complex
+sorting:
+
+``` php
+use Cake\Datasource\Paging\SortField;
+
+protected array $paginate = [
+ 'sortableFields' => function ($builder) {
+ return $builder
+ ->add('best-deal', [
+ SortField::desc('in_stock'),
+ SortField::asc('price'),
+ ])
+ ->add('popularity', [
+ SortField::desc('view_count'),
+ SortField::asc('title'),
+ ]);
+ },
+];
+```
+
+Now `?sort=best-deal` will order by in-stock items first (descending), then by
+price (ascending). When users toggle the direction (e.g., `?sort=best-deal&direction=asc`),
+all fields in the array will toggle their directions: in_stock becomes ASC and
+price becomes DESC.
+
+### Locked Sort Directions
+
+You can lock a sort direction to prevent users from toggling it. This is useful
+when a field should always be sorted in a specific direction:
+
+``` php
+protected array $paginate = [
+ 'sortableFields' => function ($builder) {
+ return $builder
+ ->add('latest', SortField::desc('created', locked: true))
+ ->add('id', SortField::asc('id', locked: true));
+ },
+];
+```
+
+With locked directions, the sort key will always use the specified direction
+regardless of the `direction` parameter in the URL.
+
+### Combined Sorting Keys
+
+In addition to the traditional `?sort=field&direction=asc` format, you can use
+combined sorting keys in URLs:
+
+``` text
+// These are equivalent
+?sort=title&direction=asc
+?sort=title-asc
+
+// These are equivalent
+?sort=price&direction=desc
+?sort=price-desc
+```
+
+This provides a cleaner URL format for applications that prefer it.
+
+### Simple Array Configuration
+
+For basic use cases where you just need to allow sorting on specific fields
+without mapping or multi-column support, you can still use the simple array
+format:
+
+``` php
+protected array $paginate = [
+ 'sortableFields' => [
+ 'id', 'title', 'Users.username', 'created',
+ ],
+];
+```
+
+## Limit the Maximum Number of Rows per Page
+
+The number of results that are fetched per page is exposed to the user as the
+`limit` parameter. It is generally undesirable to allow users to fetch all
+rows in a paginated set. The `maxLimit` option asserts that no one can set
+this limit too high from the outside. By default CakePHP limits the maximum
+number of rows that can be fetched to 100. If this default is not appropriate
+for your application, you can adjust it as part of the pagination options, for
+example reducing it to `10`:
+
+``` php
+protected array $paginate = [
+ // Other keys here.
+ 'maxLimit' => 10
+];
+```
+
+If the request's limit param is greater than this value, it will be reduced to
+the `maxLimit` value.
+
+## Out of Range Page Requests
+
+`Controller::paginate()` will throw a `NotFoundException` when trying to
+access a non-existent page, i.e. page number requested is greater than total
+page count.
+
+So you could either let the normal error page be rendered or use a try catch
+block and take appropriate action when a `NotFoundException` is caught:
+
+``` php
+use Cake\Http\Exception\NotFoundException;
+
+public function index()
+{
+ try {
+ $this->paginate();
+ } catch (NotFoundException $e) {
+ // Do something here like redirecting to first or last page.
+ // $e->getPrevious()->getAttributes('pagingParams') will give you required info.
+ }
+}
+```
+
+## Using a paginator class directly
+
+You can also use a paginator directly.:
+
+``` php
+// Create a paginator
+$paginator = new \Cake\Datasource\Paginator\NumericPaginator();
+
+// Paginate the model
+$results = $paginator->paginate(
+ // Query or table instance which you need to paginate
+ $this->fetchTable('Articles'),
+ // Request params
+ $this->request->getQueryParams(),
+ // Config array having the same structure as options as Controller::$paginate
+ [
+ 'finder' => 'latest',
+ ]
+);
+```
+
+## Pagination in the View
+
+Check the `Cake\View\Helper\PaginatorHelper` documentation for
+how to create links for pagination navigation.
diff --git a/docs/en/controllers/request-response.md b/docs/en/controllers/request-response.md
new file mode 100644
index 0000000000..c9b10140cd
--- /dev/null
+++ b/docs/en/controllers/request-response.md
@@ -0,0 +1,1472 @@
+# Request & Response Objects
+
+The request and response objects provide an abstraction around HTTP requests and
+responses. The request object in CakePHP allows you to introspect an incoming
+request, while the response object allows you to effortlessly create HTTP
+responses from your controllers.
+
+
+
+\$this-\>request
+
+
+
+
+
+## Request
+
+`class` Cake\\Http\\**ServerRequest**
+
+`ServerRequest` is the default request object used in CakePHP. It centralizes a
+number of features for interrogating and interacting with request data.
+On each request one Request is created and then passed by reference to the
+various layers of an application that use request data. By default the request
+is assigned to `$this->request`, and is available in Controllers, Cells, Views
+and Helpers. You can also access it in Components using the controller
+reference.
+
+::: info Changed in version 4.4.0
+The `ServerRequest` is available via DI. So you can get it from container or use it as a dependency for your service.
+:::
+
+Some of the duties `ServerRequest` performs include:
+
+- Processing the GET, POST, and FILES arrays into the data structures you are
+ familiar with.
+- Providing environment introspection pertaining to the request. Information
+ like the headers sent, the client's IP address, and the subdomain/domain
+ names the server your application is running on.
+- Providing access to request parameters both as array indexes and object
+ properties.
+
+CakePHP's request object implements the [PSR-7
+ServerRequestInterface](https://www.php-fig.org/psr/psr-7/) making it easier to
+use libraries from outside of CakePHP.
+
+
+
+### Request Parameters
+
+The request exposes routing parameters through the `getParam()` method:
+
+``` php
+$controllerName = $this->request->getParam('controller');
+```
+
+To get all routing parameters as an array use `getAttribute()`:
+
+``` php
+$parameters = $this->request->getAttribute('params');
+```
+
+All [Route Elements](../development/routing#route-elements) are accessed through this interface.
+
+In addition to [Route Elements](../development/routing#route-elements), you also often need access to
+[Passed Arguments](../development/routing#passed-arguments). These are both available on the request object as
+well:
+
+``` php
+// Passed arguments
+$passedArgs = $this->request->getParam('pass');
+```
+
+Will all provide you access to the passed arguments. There
+are several important/useful parameters that CakePHP uses internally, these
+are also all found in the routing parameters:
+
+- `plugin` The plugin handling the request. Will be null when there is no
+ plugin.
+- `controller` The controller handling the current request.
+- `action` The action handling the current request.
+- `prefix` The prefix for the current action. See [Prefix Routing](../development/routing#prefix-routing) for
+ more information.
+
+### Query String Parameters
+
+`method` Cake\\Http\\ServerRequest::**getQuery**($name, $default = null): mixed
+
+Query string parameters can be read using the `getQuery()` method:
+
+``` php
+// URL is /posts/index?page=1&sort=title
+$page = $this->request->getQuery('page');
+```
+
+You can either directly access the query property, or you can use
+`getQuery()` method to read the URL query array in an error-free manner.
+Any keys that do not exist will return `null`:
+
+``` php
+$foo = $this->request->getQuery('value_that_does_not_exist');
+// $foo === null
+
+// You can also provide default values
+$foo = $this->request->getQuery('does_not_exist', 'default val');
+```
+
+If you want to access all the query parameters you can use
+`getQueryParams()`:
+
+``` php
+$query = $this->request->getQueryParams();
+```
+
+You can use the casting utility functions to provide typesafe access to request
+data and other input:
+
+``` php
+use function Cake\Core\toBool;
+use function Cake\Core\toInt;
+use function Cake\Core\toString;
+use function Cake\I18n\toDate;
+use function Cake\I18n\toDateTime;
+
+// $active is bool|null.
+$active = toBool($this->request->getQuery('active'));
+
+// $page is int|null.
+$page = toInt($this->request->getQuery('page'));
+
+// $query is string|null.
+$query = toString($this->request->getQuery('query'));
+
+// Parse a date based on the format or null
+$date = toDate($this->request->getQuery('date'), 'Y-m-d');
+
+// Parse a datetime based on a format or null
+$date = toDateTime($this->request->getQuery('datetime'), 'Y-m-d H:i:s');
+```
+
+::: info Added in version 5.1.0
+Casting functions were added.
+:::
+
+### Request Body Data
+
+`method` Cake\\Http\\ServerRequest::**getData**($name, $default = null): mixed
+
+All POST data normally available through PHP's `$_POST` global variable can be
+accessed using `Cake\Http\ServerRequest::getData()`. For example:
+
+``` php
+// An input with a name attribute equal to 'title' is accessible at
+$title = $this->request->getData('title');
+```
+
+You can use a dot separated names to access nested data. For example:
+
+``` php
+$value = $this->request->getData('address.street_name');
+```
+
+For non-existent names the `$default` value will be returned:
+
+``` php
+$foo = $this->request->getData('value.that.does.not.exist');
+// $foo == null
+```
+
+You can also use [Body Parser Middleware](../controllers/middleware#body-parser-middleware) to parse request body of different
+content types into an array, so that it's accessible through `ServerRequest::getData()`.
+
+If you want to access all the data parameters you can use
+`getParsedBody()`:
+
+``` php
+$data = $this->request->getParsedBody();
+```
+
+
+
+### File Uploads
+
+Uploaded files can be accessed through the request body data, using the `Cake\Http\ServerRequest::getData()`
+method described above. For example, a file from an input element with a name attribute of `attachment`, can
+be accessed like this:
+
+``` php
+$attachment = $this->request->getData('attachment');
+```
+
+By default file uploads are represented in the request data as objects that implement
+[\Psr\Http\Message\UploadedFileInterface](https://www.php-fig.org/psr/psr-7/#16-uploaded-files). In the current
+implementation, the `$attachment` variable in the above example would by default hold an instance of
+`\Laminas\Diactoros\UploadedFile`.
+
+Accessing the uploaded file details is fairly simple, here's how you can obtain the same data as provided by the old
+style file upload array:
+
+``` php
+$name = $attachment->getClientFilename();
+$type = $attachment->getClientMediaType();
+$size = $attachment->getSize();
+$tmpName = $attachment->getStream()->getMetadata('uri');
+$error = $attachment->getError();
+```
+
+Moving the uploaded file from its temporary location to the desired target
+location, doesn't require manually accessing the temporary file, instead it can
+be easily done by using the objects `moveTo()` method:
+
+``` php
+$attachment->moveTo($targetPath);
+```
+
+In an HTTP environment, the `moveTo()` method will automatically validate
+whether the file is an actual uploaded file, and throw an exception in case
+necessary. In an CLI environment, where the concept of uploading files doesn't
+exist, it will allow to move the file that you've referenced irrespective of its
+origins, which makes testing file uploads possible.
+
+`method` Cake\\Http\\ServerRequest::**getUploadedFile**($path): UploadedFileInterface|null
+
+Returns the uploaded file at a specific path. The path uses the same dot syntax as the
+`Cake\Http\ServerRequest::getData()` method:
+
+``` php
+$attachment = $this->request->getUploadedFile('attachment');
+```
+
+Unlike `Cake\Http\ServerRequest::getData()`, `Cake\Http\ServerRequest::getUploadedFile()` would
+only return data when an actual file upload exists for the given path, if there is regular, non-file request body data
+present at the given path, then this method will return `null`, just like it would for any non-existent path.
+
+`method` Cake\\Http\\ServerRequest::**getUploadedFiles**(): array
+
+Returns all uploaded files in a normalized array structure. For the above example with the file input name of
+`attachment`, the structure would look like:
+
+``` php
+[
+ 'attachment' => object(Laminas\Diactoros\UploadedFile) {
+ // ...
+ }
+]
+```
+
+`method` Cake\\Http\\ServerRequest::**withUploadedFiles**(array $files): static
+
+This method sets the uploaded files of the request object, it accepts an array of objects that implement
+[\Psr\Http\Message\UploadedFileInterface](https://www.php-fig.org/psr/psr-7/#16-uploaded-files). It will
+replace all possibly existing uploaded files:
+
+``` php
+$files = [
+ 'MyModel' => [
+ 'attachment' => new \Laminas\Diactoros\UploadedFile(
+ $streamOrFile,
+ $size,
+ $errorStatus,
+ $clientFilename,
+ $clientMediaType
+ ),
+ 'anotherAttachment' => new \Laminas\Diactoros\UploadedFile(
+ '/tmp/hfz6dbn.tmp',
+ 123,
+ \UPLOAD_ERR_OK,
+ 'attachment.txt',
+ 'text/plain'
+ ),
+ ],
+];
+
+$this->request = $this->request->withUploadedFiles($files);
+```
+
+> [!NOTE]
+> Uploaded files that have been added to the request via this method, will *not* be available in the request body
+> data, ie you cannot retrieve them via `Cake\Http\ServerRequest::getData()`! If you need them in the
+> request data (too), then you have to set them via `Cake\Http\ServerRequest::withData()` or
+> `Cake\Http\ServerRequest::withParsedBody()`.
+
+### PUT, PATCH or DELETE Data
+
+`method` Cake\\Http\\ServerRequest::**getBody**(): StreamInterface
+
+When building REST services, you often accept request data on `PUT` and
+`DELETE` requests. Any `application/x-www-form-urlencoded` request body data
+will automatically be parsed and available via `$request->getData()` for `PUT` and
+`DELETE` requests. If you are accepting JSON or XML data, you can
+access the raw data with `getBody()`:
+
+``` php
+// Get the stream wrapper on the request body
+$body = $request->getBody();
+
+// Get the request body as a string
+$bodyString = (string)$request->getBody();
+```
+
+If your requests contain XML or JSON request content, you should consider using
+[Body Parser Middleware](../controllers/middleware#body-parser-middleware) to have CakePHP automatically parse those content
+types making the parsed data available in `$request->getData()` and
+`$request->getParsedBody()`.
+
+### Environment Variables (from \$\_SERVER and \$\_ENV)
+
+`method` Cake\\Http\\ServerRequest::**getEnv**($key, $default = null): string|null
+
+`ServerRequest::getEnv()` is a wrapper for `getenv()` global function and acts as
+a getter for environment variables without possible undefined keys:
+
+``` php
+$host = $this->request->getEnv('HTTP_HOST');
+```
+
+To access all the environment variables in a request use `getServerParams()`:
+
+``` php
+$env = $this->request->getServerParams();
+```
+
+`method` Cake\\Http\\ServerRequest::**withEnv**($key, $value): static
+
+`ServerRequest::withEnv()` is a wrapper for `putenv()` global function and acts as
+a setter for environment variables without having to modify globals
+`$_SERVER` and `$_ENV`:
+
+``` php
+// Set a value, generally helpful in testing.
+$this->request->withEnv('REQUEST_METHOD', 'POST');
+```
+
+### XML or JSON Data
+
+Applications employing [REST](../development/rest) often exchange data in
+non-URL-encoded post bodies. You can read input data in any format using
+`Cake\Http\ServerRequest::input()`. By providing a decoding function,
+you can receive the content in a deserialized format:
+
+``` php
+// Get JSON encoded data submitted to a PUT/POST action
+$jsonData = $this->request->input('json_decode');
+```
+
+Some deserializing methods require additional parameters when called, such as
+the 'as array' parameter on `json_decode`. If you want XML converted into a
+DOMDocument object, `Cake\Http\ServerRequest::input()` supports
+passing in additional parameters as well:
+
+``` php
+// Get XML encoded data submitted to a PUT/POST action
+$data = $this->request->input('Cake\Utility\Xml::build', ['return' => 'domdocument']);
+```
+
+### Path Information
+
+The request object also provides useful information about the paths in your
+application. The `base` and `webroot` attributes are useful for
+generating URLs, and determining whether or not your application is in a
+subdirectory. The attributes you can use are:
+
+``` php
+// Assume the current request URL is /subdir/articles/edit/1?page=1
+
+// Holds /subdir/articles/edit/1?page=1
+$here = $request->getRequestTarget();
+
+// Holds /subdir
+$base = $request->getAttribute('base');
+
+// Holds /subdir/
+$base = $request->getAttribute('webroot');
+```
+
+
+
+### Checking Request Conditions
+
+`method` Cake\\Http\\ServerRequest::**is**($type, $args...): bool
+
+The request object provides a way to inspect certain conditions in a given
+request. By using the `is()` method you can check a number of common
+conditions, as well as inspect other application specific request criteria:
+
+``` php
+$isPost = $this->request->is('post');
+```
+
+You can also extend the request detectors that are available, by using
+`Cake\Http\ServerRequest::addDetector()` to create new kinds of
+detectors. There are different types of detectors that you can create:
+
+- Environment value comparison - Compares a value fetched from `env()`
+ for equality with the provided value.
+- Header value comparison - If the specified header exists with the specified
+ value, or if the callable returns true.
+- Pattern value comparison - Pattern value comparison allows you to compare a
+ value fetched from `env()` to a regular expression.
+- Option based comparison - Option based comparisons use a list of options to
+ create a regular expression. Subsequent calls to add an already defined
+ options detector will merge the options.
+- Callback detectors - Callback detectors allow you to provide a 'callback' type
+ to handle the check. The callback will receive the request object as its only
+ parameter.
+
+`method` Cake\\Http\\ServerRequest::**addDetector**($name, $options): void
+
+Some examples would be:
+
+``` php
+// Add an environment detector.
+$this->request->addDetector(
+ 'post',
+ ['env' => 'REQUEST_METHOD', 'value' => 'POST']
+);
+
+// Add a pattern value detector.
+$this->request->addDetector(
+ 'iphone',
+ ['env' => 'HTTP_USER_AGENT', 'pattern' => '/iPhone/i']
+);
+
+// Add an option detector
+$this->request->addDetector('internalIp', [
+ 'env' => 'CLIENT_IP',
+ 'options' => ['192.168.0.101', '192.168.0.100']
+]);
+
+
+// Add a header detector with value comparison
+$this->request->addDetector('fancy', [
+ 'env' => 'CLIENT_IP',
+ 'header' => ['X-Fancy' => 1]
+]);
+
+// Add a header detector with callable comparison
+$this->request->addDetector('fancy', [
+ 'env' => 'CLIENT_IP',
+ 'header' => ['X-Fancy' => function ($value, $header) {
+ return in_array($value, ['1', '0', 'yes', 'no'], true);
+ }]
+]);
+
+// Add a callback detector. Must be a valid callable.
+$this->request->addDetector(
+ 'awesome',
+ function ($request) {
+ return $request->getParam('awesome');
+ }
+);
+
+// Add a detector that uses additional arguments.
+$this->request->addDetector(
+ 'csv',
+ [
+ 'accept' => ['text/csv'],
+ 'param' => '_ext',
+ 'value' => 'csv',
+ ]
+);
+```
+
+There are several built-in detectors that you can use:
+
+- `is('get')` Check to see whether the current request is a GET.
+- `is('put')` Check to see whether the current request is a PUT.
+- `is('patch')` Check to see whether the current request is a PATCH.
+- `is('post')` Check to see whether the current request is a POST.
+- `is('delete')` Check to see whether the current request is a DELETE.
+- `is('head')` Check to see whether the current request is HEAD.
+- `is('options')` Check to see whether the current request is OPTIONS.
+- `is('ajax')` Check to see whether the current request came with
+ X-Requested-With = XMLHttpRequest.
+- `is('ssl')` Check to see whether the request is via SSL.
+- `is('flash')` Check to see whether the request has a User-Agent of Flash.
+- `is('json')` Check to see whether the request URL has 'json' extension or the
+ Accept header is set to 'application/json'.
+- `is('xml')` Check to see whether the request URL has 'xml' extension or the Accept header is set to
+ 'application/xml' or 'text/xml'.
+
+`ServerRequest` also includes methods like
+`Cake\Http\ServerRequest::domain()`,
+`Cake\Http\ServerRequest::subdomains()` and
+`Cake\Http\ServerRequest::host()` to make applications that use
+subdomains simpler.
+
+### Session Data
+
+To access the session for a given request use the `getSession()` method or use the `session` attribute:
+
+``` php
+$session = $this->request->getSession();
+$session = $this->request->getAttribute('session');
+
+$data = $session->read('sessionKey');
+```
+
+For more information, see the [Sessions](../development/sessions) documentation for how
+to use the session object.
+
+### Host and Domain Name
+
+`method` Cake\\Http\\ServerRequest::**domain**($tldLength = 1): string
+
+Returns the domain name your application is running on:
+
+``` php
+// Prints 'example.org'
+echo $request->domain();
+```
+
+`method` Cake\\Http\\ServerRequest::**subdomains**($tldLength = 1): array
+
+Returns the subdomains your application is running on as an array:
+
+``` php
+// Returns ['my', 'dev'] for 'my.dev.example.org'
+$subdomains = $request->subdomains();
+```
+
+`method` Cake\\Http\\ServerRequest::**host**(): string|null
+
+Returns the host your application is on:
+
+``` php
+// Prints 'my.dev.example.org'
+echo $request->host();
+```
+
+### Reading the HTTP Method
+
+`method` Cake\\Http\\ServerRequest::**getMethod**(): string
+
+Returns the HTTP method the request was made with:
+
+``` php
+// Output POST
+echo $request->getMethod();
+```
+
+### Restricting Which HTTP method an Action Accepts
+
+`method` Cake\\Http\\ServerRequest::**allowMethod**($methods): bool
+
+Set allowed HTTP methods. If not matched, will throw
+`MethodNotAllowedException`. The 405 response will include the required
+`Allow` header with the passed methods:
+
+``` php
+public function delete()
+{
+ // Only accept POST and DELETE requests
+ $this->request->allowMethod(['post', 'delete']);
+ ...
+}
+```
+
+### Reading HTTP Headers
+
+Allows you to access any of the `HTTP_*` headers that were used
+for the request. For example:
+
+``` php
+// Get the header as a string
+$userAgent = $this->request->getHeaderLine('User-Agent');
+
+// Get an array of all values.
+$acceptHeader = $this->request->getHeader('Accept');
+
+// Check if a header exists
+$hasAcceptHeader = $this->request->hasHeader('Accept');
+```
+
+While some apache installs don't make the `Authorization` header accessible,
+CakePHP will make it available through apache specific methods as required.
+
+`method` Cake\\Http\\ServerRequest::**referer**($local = true): string|null
+
+Returns the referring address for the request.
+
+`method` Cake\\Http\\ServerRequest::**clientIp**(): string
+
+Returns the current visitor's IP address.
+
+### Trusting Proxy Headers
+
+If your application is behind a load balancer or running on a cloud service, you
+will often get the load balancer host, port and scheme in your requests. Often
+load balancers will also send `HTTP-X-Forwarded-*` headers with the original
+values. The forwarded headers will not be used by CakePHP out of the box. To
+have the request object use these headers set the `trustProxy` property to
+`true`:
+
+``` php
+$this->request->trustProxy = true;
+
+// These methods will now use the proxied headers.
+$port = $this->request->port();
+$host = $this->request->host();
+$scheme = $this->request->scheme();
+$clientIp = $this->request->clientIp();
+```
+
+Once proxies are trusted the `clientIp()` method will use the *last* IP
+address in the `X-Forwarded-For` header. If your application is behind
+multiple proxies, you can use `setTrustedProxies()` to define the IP addresses
+of proxies in your control:
+
+``` php
+$request->setTrustedProxies(['127.1.1.1', '127.8.1.3']);
+```
+
+After proxies are trusted `clientIp()` will use the first IP address in the
+`X-Forwarded-For` header providing it is the only value that isn't from a trusted
+proxy.
+
+### Checking Accept Headers
+
+`method` Cake\\Http\\ServerRequest::**accepts**($type = null): array|bool
+
+Find out which content types the client accepts, or check whether it accepts a
+particular type of content.
+
+Get all types:
+
+``` php
+$accepts = $this->request->accepts();
+```
+
+Check for a single type:
+
+``` php
+$acceptsJson = $this->request->accepts('application/json');
+```
+
+`method` Cake\\Http\\ServerRequest::**acceptLanguage**($language = null): array|bool
+
+Get all the languages accepted by the client,
+or check whether a specific language is accepted.
+
+Get the list of accepted languages:
+
+``` php
+$acceptsLanguages = $this->request->acceptLanguage();
+```
+
+Check whether a specific language is accepted:
+
+``` php
+$acceptsSpanish = $this->request->acceptLanguage('es-es');
+```
+
+
+
+### Reading Cookies
+
+Request cookies can be read through a number of methods:
+
+``` php
+// Get the cookie value, or null if the cookie is missing.
+$rememberMe = $this->request->getCookie('remember_me');
+
+// Read the value, or get the default of 0
+$rememberMe = $this->request->getCookie('remember_me', 0);
+
+// Get all cookies as an hash
+$cookies = $this->request->getCookieParams();
+
+// Get a CookieCollection instance
+$cookies = $this->request->getCookieCollection()
+```
+
+See the `Cake\Http\Cookie\CookieCollection` documentation for how
+to work with cookie collection.
+
+### Uploaded Files
+
+Requests expose the uploaded file data in `getData()` or
+`getUploadedFiles()` as `UploadedFileInterface` objects:
+
+``` php
+// Get a list of UploadedFile objects
+$files = $request->getUploadedFiles();
+
+// Read the file data.
+$files[0]->getStream();
+$files[0]->getSize();
+$files[0]->getClientFileName();
+
+// Move the file.
+$files[0]->moveTo($targetPath);
+```
+
+### Manipulating URIs
+
+Requests contain a URI object, which contains methods for interacting with the
+requested URI:
+
+``` php
+// Get the URI
+$uri = $request->getUri();
+
+// Read data out of the URI.
+$path = $uri->getPath();
+$query = $uri->getQuery();
+$host = $uri->getHost();
+```
+
+
+
+\$this-\>response
+
+
+
+## Response
+
+`class` Cake\\Http\\**Response**
+
+`Cake\Http\Response` is the default response class in CakePHP.
+It encapsulates a number of features and functionality for generating HTTP
+responses in your application. It also assists in testing, as it can be
+mocked/stubbed allowing you to inspect headers that will be sent.
+
+`Response` provides an interface to wrap the common response-related
+tasks such as:
+
+- Sending headers for redirects.
+- Sending content type headers.
+- Sending any header.
+- Sending the response body.
+
+### Dealing with Content Types
+
+`method` Cake\\Http\\Response::**withType**($contentType = null): static
+
+You can control the Content-Type of your application's responses with
+`Cake\Http\Response::withType()`. If your application needs to deal
+with content types that are not built into Response, you can map them with
+`setTypeMap()` as well:
+
+``` php
+// Add a vCard type
+$this->response->setTypeMap('vcf', ['text/v-card']);
+
+// Set the response Content-Type to vcard.
+$this->response = $this->response->withType('vcf');
+```
+
+Usually, you'll want to map additional content types in your controller's
+`~Controller::beforeFilter()` callback, so you can benefit from
+automatic view switching provided by [Controller Viewclasses](../controllers#controller-viewclasses).
+
+
+
+### Sending Files
+
+`method` Cake\\Http\\Response::**withFile**(string $path, array $options = []): static
+
+There are times when you want to send files as responses for your requests.
+You can accomplish that by using `Cake\Http\Response::withFile()`:
+
+``` php
+public function sendFile($id)
+{
+ $file = $this->Attachments->getFile($id);
+ $response = $this->response->withFile($file['path']);
+ // Return the response to prevent controller from trying to render
+ // a view.
+ return $response;
+}
+```
+
+As shown in the above example, you must pass the file path to the method.
+CakePHP will send a proper content type header if it's a known file type listed
+in CakeHttpResponse::\$\_mimeTypes. You can add new types prior to calling
+`Cake\Http\Response::withFile()` by using the
+`Cake\Http\Response::withType()` method.
+
+If you want, you can also force a file to be downloaded instead of displayed in
+the browser by specifying the options:
+
+``` php
+$response = $this->response->withFile(
+ $file['path'],
+ ['download' => true, 'name' => 'foo']
+);
+```
+
+The supported options are:
+
+name
+The name allows you to specify an alternate file name to be sent to
+the user.
+
+download
+A boolean value indicating whether headers should be set to force
+download.
+
+### Sending a String as File
+
+You can respond with a file that does not exist on the disk, such as a pdf or an
+ics generated on the fly from a string:
+
+``` php
+public function sendIcs()
+{
+ $icsString = $this->Calendars->generateIcs();
+ $response = $this->response;
+
+ // Inject string content into response body
+ $response = $response->withStringBody($icsString);
+
+ $response = $response->withType('ics');
+
+ // Optionally force file download
+ $response = $response->withDownload('filename_for_download.ics');
+
+ // Return response object to prevent controller from trying to render
+ // a view.
+ return $response;
+}
+```
+
+### Setting Headers
+
+`method` Cake\\Http\\Response::**withHeader**($header, $value)
+
+Setting headers is done with the `Cake\Http\Response::withHeader()`
+method. Like all of the PSR-7 interface methods, this method returns a *new*
+instance with the new header:
+
+``` php
+// Add/replace a header
+$response = $response->withHeader('X-Extra', 'My header');
+
+// Set multiple headers
+$response = $response->withHeader('X-Extra', 'My header')
+ ->withHeader('Location', 'http://example.com');
+
+// Append a value to an existing header
+$response = $response->withAddedHeader('Set-Cookie', 'remember_me=1');
+```
+
+Headers are not sent when set. Instead, they are held until the response is
+emitted by `Cake\Http\Server`.
+
+You can now use the convenience method
+`Cake\Http\Response::withLocation()` to directly set or get the
+redirect location header.
+
+### Setting the Body
+
+`method` Cake\\Http\\Response::**withStringBody**($string): static
+
+To set a string as the response body, do the following:
+
+``` php
+// Set a string into the body
+$response = $response->withStringBody('My Body');
+
+// If you want a json response
+$response = $response->withType('application/json')
+ ->withStringBody(json_encode(['Foo' => 'bar']));
+```
+
+`method` Cake\\Http\\Response::**withBody**($body)
+
+To set the response body, use the `withBody()` method, which is provided by the
+`Laminas\Diactoros\MessageTrait`:
+
+``` php
+$response = $response->withBody($stream);
+```
+
+Be sure that `$stream` is a `Psr\Http\Message\StreamInterface` object.
+See below on how to create a new stream.
+
+You can also stream responses from files using `Laminas\Diactoros\Stream` streams:
+
+``` php
+// To stream from a file
+use Laminas\Diactoros\Stream;
+
+$stream = new Stream('/path/to/file', 'rb');
+$response = $response->withBody($stream);
+```
+
+You can also stream responses from a callback using the `CallbackStream`. This
+is useful when you have resources like images, CSV files or PDFs you need to
+stream to the client:
+
+``` php
+// Streaming from a callback
+use Cake\Http\CallbackStream;
+
+// Create an image.
+$img = imagecreate(100, 100);
+// ...
+
+$stream = new CallbackStream(function () use ($img) {
+ imagepng($img);
+});
+$response = $response->withBody($stream);
+```
+
+### Setting the Character Set
+
+`method` Cake\\Http\\Response::**withCharset**($charset): static
+
+Sets the charset that will be used in the response:
+
+``` php
+$this->response = $this->response->withCharset('UTF-8');
+```
+
+### Interacting with Browser Caching
+
+`method` Cake\\Http\\Response::**withDisabledCache**(): static
+
+You sometimes need to force browsers not to cache the results of a controller
+action. `Cake\Http\Response::withDisabledCache()` is intended for just
+that:
+
+``` php
+public function index()
+{
+ // Disable caching
+ $this->response = $this->response->withDisabledCache();
+}
+```
+
+> [!WARNING]
+> Disabling caching from SSL domains while trying to send
+> files to Internet Explorer can result in errors.
+
+`method` Cake\\Http\\Response::**withCache**($since, $time = '+1 day'): static
+
+You can also tell clients that you want them to cache responses. By using
+`Cake\Http\Response::withCache()`:
+
+``` php
+public function index()
+{
+ // Enable caching
+ $this->response = $this->response->withCache('-1 minute', '+5 days');
+}
+```
+
+The above would tell clients to cache the resulting response for 5 days,
+hopefully speeding up your visitors' experience.
+The `withCache()` method sets the `Last-Modified` value to the first
+argument. `Expires` header and the `max-age` directive are set based on the
+second parameter. Cache-Control's `public` directive is set as well.
+
+
+
+### Fine Tuning HTTP Cache
+
+One of the best and easiest ways of speeding up your application is to use HTTP
+cache. Under this caching model, you are only required to help clients decide if
+they should use a cached copy of the response by setting a few headers such as
+modified time and response entity tag.
+
+Rather than forcing you to code the logic for caching and for invalidating
+(refreshing) it once the data has changed, HTTP uses two models, expiration and
+validation, which usually are much simpler to use.
+
+Apart from using `Cake\Http\Response::withCache()`, you can also use
+many other methods to fine-tune HTTP cache headers to take advantage of browser
+or reverse proxy caching.
+
+#### The Cache Control Header
+
+`method` Cake\\Http\\Response::**withSharable**($public, $time = null): static
+
+Used under the expiration model, this header contains multiple indicators that
+can change the way browsers or proxies use the cached content. A
+`Cache-Control` header can look like this:
+
+ Cache-Control: private, max-age=3600, must-revalidate
+
+`Response` class helps you set this header with some utility methods that will
+produce a final valid `Cache-Control` header. The first is the
+`withSharable()` method, which indicates whether a response is to be
+considered sharable across different users or clients. This method actually
+controls the `public` or `private` part of this header. Setting a response
+as private indicates that all or part of it is intended for a single user. To
+take advantage of shared caches, the control directive must be set as public.
+
+The second parameter of this method is used to specify a `max-age` for the
+cache, which is the number of seconds after which the response is no longer
+considered fresh:
+
+``` php
+public function view()
+{
+ // ...
+ // Set the Cache-Control as public for 3600 seconds
+ $this->response = $this->response->withSharable(true, 3600);
+}
+
+public function my_data()
+{
+ // ...
+ // Set the Cache-Control as private for 3600 seconds
+ $this->response = $this->response->withSharable(false, 3600);
+}
+```
+
+`Response` exposes separate methods for setting each of the directives in
+the `Cache-Control` header.
+
+#### The Expiration Header
+
+`method` Cake\\Http\\Response::**withExpires**($time): static
+
+You can set the `Expires` header to a date and time after which the response
+is no longer considered fresh. This header can be set using the
+`withExpires()` method:
+
+``` php
+public function view()
+{
+ $this->response = $this->response->withExpires('+5 days');
+}
+```
+
+This method also accepts a `DateTime` instance or any string that can
+be parsed by the `DateTime` class.
+
+#### The Etag Header
+
+`method` Cake\\Http\\Response::**withEtag**($tag, $weak = false): static
+
+Cache validation in HTTP is often used when content is constantly changing, and
+asks the application to only generate the response contents if the cache is no
+longer fresh. Under this model, the client continues to store pages in the
+cache, but it asks the application every time
+whether the resource has changed, instead of using it directly.
+This is commonly used with static resources such as images and other assets.
+
+The `withEtag()` method (called entity tag) is a string
+that uniquely identifies the requested resource, as a checksum does for a file,
+in order to determine whether it matches a cached resource.
+
+To take advantage of this header, you must either call the
+`isNotModified()` method manually or include the
+[Checking HTTP Cache](../controllers/components/check-http-cache) in your controller:
+
+``` php
+public function index()
+{
+ $articles = $this->Articles->find('all')->all();
+
+ // Simple checksum of the article contents.
+ // You should use a more efficient implementation
+ // in a real world application.
+ $checksum = md5(json_encode($articles));
+
+ $response = $this->response->withEtag($checksum);
+ if ($response->isNotModified($this->request)) {
+ return $response;
+ }
+
+ $this->response = $response;
+ // ...
+}
+```
+
+> [!NOTE]
+> Most proxy users should probably consider using the Last Modified Header
+> instead of Etags for performance and compatibility reasons.
+
+#### The Last Modified Header
+
+`method` Cake\\Http\\Response::**withModified**($time): static
+
+Also, under the HTTP cache validation model, you can set the `Last-Modified`
+header to indicate the date and time at which the resource was modified for the
+last time. Setting this header helps CakePHP tell caching clients whether the
+response was modified or not based on their cache.
+
+To take advantage of this header, you must either call the
+`isNotModified()` method manually or include the
+[Checking HTTP Cache](../controllers/components/check-http-cache) in your controller:
+
+``` php
+public function view()
+{
+ $article = $this->Articles->find()->first();
+ $response = $this->response->withModified($article->modified);
+ if ($response->isNotModified($this->request)) {
+ return $response;
+ }
+ $this->response;
+ // ...
+}
+```
+
+#### The Vary Header
+
+`method` Cake\\Http\\Response::**withVary**($header): static
+
+In some cases, you might want to serve different content using the same URL.
+This is often the case if you have a multilingual page or respond with different
+HTML depending on the browser. Under such circumstances you can use the `Vary`
+header:
+
+``` php
+$response = $this->response->withVary('User-Agent');
+$response = $this->response->withVary('Accept-Encoding', 'User-Agent');
+$response = $this->response->withVary('Accept-Language');
+```
+
+#### Sending Not-Modified Responses
+
+`method` Cake\\Http\\Response::**isNotModified**(Request $request): bool
+
+Compares the cache headers for the request object with the cache header from the
+response and determines whether it can still be considered fresh. If so, deletes
+the response content, and sends the 304 Not Modified header:
+
+``` php
+// In a controller action.
+if ($this->response->isNotModified($this->request)) {
+ return $this->response;
+}
+```
+
+
+
+### Setting Cookies
+
+Cookies can be added to response using either an array or a `Cake\Http\Cookie\Cookie`
+object:
+
+``` php
+use Cake\Http\Cookie\Cookie;
+use DateTime;
+
+// Add a cookie
+$this->response = $this->response->withCookie(Cookie::create(
+ 'remember_me',
+ 'yes',
+ // All keys are optional
+ [
+ 'expires' => new DateTime('+1 year'),
+ 'path' => '',
+ 'domain' => '',
+ 'secure' => false,
+ 'httponly' => false,
+ 'samesite' => null // Or one of CookieInterface::SAMESITE_* constants
+ ]
+));
+```
+
+See the [Creating Cookies](#creating-cookies) section for how to use the cookie object. You
+can use `withExpiredCookie()` to send an expired cookie in the response. This
+will make the browser remove its local cookie:
+
+``` php
+$this->response = $this->response->withExpiredCookie(new Cookie('remember_me'));
+```
+
+
+
+### Setting Cross Origin Request Headers (CORS)
+
+The `cors()` method returns a `CorsBuilder` instance which provides a fluent
+interface for defining [HTTP Access Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS)
+related headers:
+
+``` php
+$this->response = $this->response->cors($this->request)
+ ->allowOrigin(['*.cakephp.org'])
+ ->allowMethods(['GET', 'POST'])
+ ->allowHeaders(['X-CSRF-Token'])
+ ->allowCredentials()
+ ->exposeHeaders(['Link'])
+ ->maxAge(300)
+ ->build();
+```
+
+CORS related headers will only be applied to the response if the following
+criteria are met:
+
+1. The request has an `Origin` header.
+2. The request's `Origin` value matches one of the allowed Origin values.
+
+#### CorsBuilder Methods
+
+`class` Cake\\Http\\**CorsBuilder**
+
+The `CorsBuilder` provides the following methods for configuring CORS:
+
+`method` Cake\\Http\\CorsBuilder::**allowOrigin**(array|string $domains)
+
+`method` Cake\\Http\\CorsBuilder::**allowMethods**(array $methods)
+
+`method` Cake\\Http\\CorsBuilder::**allowHeaders**(array $headers)
+
+`method` Cake\\Http\\CorsBuilder::**allowCredentials**()
+
+`method` Cake\\Http\\CorsBuilder::**exposeHeaders**(array $headers)
+
+`method` Cake\\Http\\CorsBuilder::**maxAge**(string|int $age)
+
+`method` Cake\\Http\\CorsBuilder::**build**(): ResponseInterface
+
+#### Practical CORS Examples
+
+Here are some common CORS configurations:
+
+**API accepting requests from a SPA frontend**:
+
+``` php
+// In your controller
+public function beforeFilter(EventInterface $event)
+{
+ parent::beforeFilter($event);
+
+ if ($this->request->is('options')) {
+ // Handle preflight requests
+ $this->response = $this->response->cors($this->request)
+ ->allowOrigin(['https://app.example.com'])
+ ->allowMethods(['GET', 'POST', 'PUT', 'DELETE'])
+ ->allowHeaders(['Content-Type', 'Authorization'])
+ ->allowCredentials()
+ ->maxAge(86400)
+ ->build();
+
+ $event->setResult($this->response);
+ }
+}
+
+public function index()
+{
+ // Apply CORS to regular requests
+ $this->response = $this->response->cors($this->request)
+ ->allowOrigin(['https://app.example.com'])
+ ->allowCredentials()
+ ->build();
+
+ // Your regular controller logic...
+}
+```
+
+**Public API with relaxed CORS**:
+
+``` php
+$this->response = $this->response->cors($this->request)
+ ->allowOrigin('*')
+ ->allowMethods(['GET'])
+ ->exposeHeaders(['X-Total-Count', 'X-Page'])
+ ->maxAge(3600)
+ ->build();
+```
+
+#### Creating CORS Middleware
+
+For consistent CORS handling across your application, create a 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 {
+ // Handle preflight requests
+ if ($request->getMethod() === 'OPTIONS') {
+ $response = new \Cake\Http\Response();
+ $response = $response->cors($request)
+ ->allowOrigin(['*.myapp.com'])
+ ->allowMethods(['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
+ ->allowHeaders(['Content-Type', 'Authorization'])
+ ->allowCredentials()
+ ->maxAge(3600)
+ ->build();
+
+ return $response;
+ }
+
+ $response = $handler->handle($request);
+
+ // Add CORS headers to regular requests
+ return $response->cors($request)
+ ->allowOrigin(['*.myapp.com'])
+ ->allowCredentials()
+ ->build();
+ }
+}
+```
+
+Then add it to your application middleware stack in `src/Application.php`:
+
+``` php
+public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+{
+ $middlewareQueue
+ // Add CORS middleware early in the stack
+ ->add(new \App\Middleware\CorsMiddleware())
+ // ... other middleware
+ ->add(new ErrorHandlerMiddleware(Configure::read('Error')))
+ ->add(new AssetMiddleware([
+ 'cacheTime' => Configure::read('Asset.cacheTime'),
+ ]))
+ ->add(new RoutingMiddleware($this));
+
+ return $middlewareQueue;
+}
+```
+
+### Running logic after the Response has been sent
+
+In fastcgi based environments you can listen to the `Server.terminate` event
+to run logic **after** the response has been sent to the client. The
+`terminate` event will be passed a `request` and `response`. The
+`request` is fetched from the applications' DI container, or from
+`Router::getRequest()` if the DI container does not have a request registered.
+
+> [!WARNING]
+> In non fastcgi environments the `Server.terminate` event is fired before
+> the response is sent.
+
+::: info Added in version 5.1.0
+:::
+
+## Common Mistakes with Immutable Responses
+
+Response objects offer a number of methods that treat
+responses as immutable objects. Immutable objects help prevent difficult to
+track accidental side-effects, and reduce mistakes caused by method calls caused
+by refactoring that change ordering. While they offer a number of benefits,
+immutable objects can take some getting used to. Any method that starts with
+`with` operates on the response in an immutable fashion, and will **always**
+return a **new** instance. Forgetting to retain the modified instance is the most
+frequent mistake people make when working with immutable objects:
+
+``` php
+$this->response->withHeader('X-CakePHP', 'yes!');
+```
+
+In the above code, the response will be lacking the `X-CakePHP` header, as the
+return value of the `withHeader()` method was not retained. To correct the
+above code you would write:
+
+``` php
+$this->response = $this->response->withHeader('X-CakePHP', 'yes!');
+```
+
+## Cookie Collections
+
+`class` Cake\\Http\\Cookie\\**CookieCollection**
+
+`CookieCollection` objects are accessible from the request and response objects.
+They let you interact with groups of cookies using immutable patterns, which
+allow the immutability of the request and response to be preserved.
+
+
+
+### Creating Cookies
+
+`class` Cake\\Http\\Cookie\\**Cookie**
+
+`Cookie` objects can be defined through constructor objects, or by using the
+fluent interface that follows immutable patterns:
+
+``` php
+use Cake\Http\Cookie\Cookie;
+
+// All arguments in the constructor
+$cookie = new Cookie(
+ 'remember_me', // name
+ 1, // value
+ new DateTime('+1 year'), // expiration time, if applicable
+ '/', // path, if applicable
+ 'example.com', // domain, if applicable
+ false, // secure only?
+ true // http only ?
+);
+
+// Using the builder methods
+$cookie = (new Cookie('remember_me'))
+ ->withValue('1')
+ ->withExpiry(new DateTime('+1 year'))
+ ->withPath('/')
+ ->withDomain('example.com')
+ ->withSecure(false)
+ ->withHttpOnly(true);
+```
+
+Once you have created a cookie, you can add it to a new or existing
+`CookieCollection`:
+
+``` php
+use Cake\Http\Cookie\CookieCollection;
+
+// Create a new collection
+$cookies = new CookieCollection([$cookie]);
+
+// Add to an existing collection
+$cookies = $cookies->add($cookie);
+
+// Remove a cookie by name
+$cookies = $cookies->remove('remember_me');
+```
+
+> [!NOTE]
+> Remember that collections are immutable and adding cookies into, or removing
+> cookies from a collection, creates a *new* collection object.
+
+Cookie objects can be added to responses:
+
+``` php
+// Add one cookie
+$response = $this->response->withCookie($cookie);
+
+// Replace the entire cookie collection
+$response = $this->response->withCookieCollection($cookies);
+```
+
+Cookies set to responses can be encrypted using the
+[Encrypted Cookie Middleware](../controllers/middleware#encrypted-cookie-middleware).
+
+
+
+### Reading Cookies
+
+Once you have a `CookieCollection` instance, you can access the cookies it
+contains:
+
+``` php
+// Check if a cookie exists
+$cookies->has('remember_me');
+
+// Get the number of cookies in the collection
+count($cookies);
+
+// Get a cookie instance. Will throw an error if the cookie is not found
+$cookie = $cookies->get('remember_me');
+
+// Get a cookie or null
+$cookie = $cookies->remember_me;
+
+// Check if a cookie exists
+$exists = isset($cookies->remember_me)
+```
+
+Once you have a `Cookie` object you can interact with it's state and modify
+it. Keep in mind that cookies are immutable, so you'll need to update the
+collection if you modify a cookie:
+
+``` php
+// Get the value
+$value = $cookie->getValue()
+
+// Access data inside a JSON value
+$id = $cookie->read('User.id');
+
+// Check state
+$cookie->isHttpOnly();
+$cookie->isSecure();
+```
diff --git a/docs/en/core-libraries/app.md b/docs/en/core-libraries/app.md
new file mode 100644
index 0000000000..8066e164e5
--- /dev/null
+++ b/docs/en/core-libraries/app.md
@@ -0,0 +1,133 @@
+# App Class
+
+`class` Cake\\Core\\**App**
+
+The App class is responsible for resource location and path management.
+
+## Finding Classes
+
+### App::className()
+
+`static` Cake\\Core\\App::**className**($name, $type = '', $suffix = ''): string|null
+
+This method is used to resolve class names throughout CakePHP. It resolves
+the short form names CakePHP uses and returns the fully resolved class name:
+
+``` php
+// Resolve a short class name with the namespace + suffix.
+App::className('Flash', 'Controller/Component', 'Component');
+// Returns Cake\Controller\Component\FlashComponent
+
+// Resolve a plugin name.
+App::className('DebugKit.Toolbar', 'Controller/Component', 'Component');
+// Returns DebugKit\Controller\Component\ToolbarComponent
+
+// Names with \ in them will be returned unaltered.
+App::className('App\Cache\ComboCache');
+// Returns App\Cache\ComboCache
+```
+
+When resolving classes, the `App` namespace will be tried, and if the
+class does not exist the `Cake` namespace will be attempted. If both
+class names do not exist, `false` will be returned.
+
+## Finding Paths to Resources
+
+### App::path()
+
+`static` Cake\\Core\\App::**path**(string $package, ?string $plugin = null): array
+
+The method returns paths set using `App.paths` app config:
+
+``` php
+// Get the templates path set using ``App.paths.templates`` app config.
+App::path('templates');
+```
+
+The same way you can retrieve paths for `locales` and `plugins`.
+
+## Finding Paths to Namespaces
+
+### App::classPath()
+
+`static` Cake\\Core\\App::**classPath**(string $package, ?string $plugin = null): array
+
+Used to get locations for paths based on conventions:
+
+``` php
+// Get the path to Controller/ in your application
+App::classPath('Controller');
+```
+
+This can be done for all namespaces that are part of your application.
+
+`App::classPath()` will only return the default path, and will not be able to
+provide any information about additional paths the autoloader is configured
+for.
+
+### App::core()
+
+`static` Cake\\Core\\App::**core**(string $package): array
+
+Used for finding the path to a package inside CakePHP:
+
+``` php
+// Get the path to Cache engines.
+App::core('Cache/Engine');
+```
+
+## Locating Themes
+
+Since themes are plugins, you can use the methods above to get the path to
+a theme.
+
+## Loading Vendor Files
+
+Ideally vendor files should be autoloaded with `Composer`, if you have vendor
+files that cannot be autoloaded or installed with Composer you will need to use
+`require` to load them.
+
+If you cannot install a library with Composer, it is best to install each library in
+a directory following Composer's convention of `vendor/$author/$package`.
+If you had a library called AcmeLib, you could install it into
+`vendor/Acme/AcmeLib`. Assuming it did not use PSR-0 compatible classnames
+you could autoload the classes within it using `classmap` in your
+application's `composer.json`:
+
+``` json
+"autoload": {
+ "psr-4": {
+ "App\\": "src/",
+ "App\\Test\\": "tests/"
+ },
+ "classmap": [
+ "vendor/Acme/AcmeLib"
+ ]
+}
+```
+
+If your vendor library does not use classes, and instead provides functions, you
+can configure Composer to load these files at the beginning of each request
+using the `files` autoloading strategy:
+
+``` json
+"autoload": {
+ "psr-4": {
+ "App\\": "src/",
+ "App\\Test\\": "tests/"
+ },
+ "files": [
+ "vendor/Acme/AcmeLib/functions.php"
+ ]
+}
+```
+
+After configuring the vendor libraries you will need to regenerate your
+application's autoloader using:
+
+``` bash
+$ php composer.phar dump-autoload
+```
+
+If you happen to not be using Composer in your application, you will need to
+manually load all vendor libraries yourself.
diff --git a/docs/en/core-libraries/caching.md b/docs/en/core-libraries/caching.md
new file mode 100644
index 0000000000..20a47b816b
--- /dev/null
+++ b/docs/en/core-libraries/caching.md
@@ -0,0 +1,729 @@
+# Caching
+
+`class` Cake\\Cache\\**Cache**
+
+Caching can be used to make reading from expensive or slow resources faster, by
+maintaining a second copy of the required data in a faster or closer storage
+system. For example, you can store the results of expensive queries, or remote
+webservice access that doesn't frequently change in a cache. Once in the cache,
+reading data from the cache is much cheaper than accessing the remote resource.
+
+Caching in CakePHP is facilitated by the `Cache` class.
+This class provides a static interface and uniform API to
+interact with various Caching implementations. CakePHP
+provides several cache engines, and provides a simple interface if you need to
+build your own backend. The built-in caching engines are:
+
+- `File` File cache is a simple cache that uses local files. It
+ is the slowest cache engine, and doesn't provide as many features for
+ atomic operations. However, since disk storage is often quite cheap,
+ storing large objects, or elements that are infrequently written
+ work well in files.
+- `Memcached` Uses the [Memcached](https://php.net/memcached)
+ extension.
+- `Redis` Uses the [phpredis](https://github.com/phpredis/phpredis)
+ extension. Redis provides a fast and persistent cache system similar to
+ Memcached, also provides atomic operations.
+- `Apcu` APCu cache uses the PHP [APCu](https://php.net/apcu) extension.
+ This extension uses shared memory on the webserver to store objects.
+ This makes it very fast, and able to provide atomic read/write features.
+- `Array` Stores all data in an array. This engine does not provide
+ persistent storage and is intended for use in application test suites.
+- `Null` The null engine doesn't actually store anything and fails all read
+ operations.
+
+Regardless of the CacheEngine you choose to use, your application interacts with
+`Cake\Cache\Cache`.
+
+
+
+## Configuring Cache Engines
+
+### Cache::setConfig()
+
+`static` Cake\\Cache\\Cache::**setConfig**($key, $config = null): void
+
+Your application can configure any number of 'engines' during its bootstrap
+process. Cache engine configurations are defined in **config/app.php**.
+
+For optimal performance CakePHP requires two cache engines to be defined.
+
+- `_cake_core_` is used for storing file maps, and parsed results of
+ [Internationalization & Localization](../core-libraries/internationalization-and-localization) files.
+- `_cake_model_`, is used to store schema descriptions for your applications
+ models.
+
+Using multiple engine configurations also lets you incrementally change the
+storage as needed. For example in your **config/app.php** you could put the
+following:
+
+``` php
+// ...
+'Cache' => [
+ 'short' => [
+ 'className' => 'File',
+ 'duration' => '+1 hours',
+ 'path' => CACHE,
+ 'prefix' => 'cake_short_',
+ ],
+ // Using a fully namespaced name.
+ 'long' => [
+ 'className' => 'Cake\Cache\Engine\FileEngine',
+ 'duration' => '+1 week',
+ 'probability' => 100,
+ 'path' => CACHE . 'long' . DS,
+ ],
+]
+// ...
+```
+
+Configuration options can also be provided as a `DSN` string. This is
+useful when working with environment variables or `PaaS` providers:
+
+``` php
+Cache::setConfig('short', [
+ 'url' => 'memcached://user:password@cache-host/?timeout=3600&prefix=myapp_',
+]);
+```
+
+When using a DSN string you can define any additional parameters/options as
+query string arguments.
+
+You can also configure Cache engines at runtime:
+
+``` php
+// Using a short name
+Cache::setConfig('short', [
+ 'className' => 'File',
+ 'duration' => '+1 hours',
+ 'path' => CACHE,
+ 'prefix' => 'cake_short_'
+]);
+
+// Using a fully namespaced name.
+Cache::setConfig('long', [
+ 'className' => 'Cake\Cache\Engine\FileEngine',
+ 'duration' => '+1 week',
+ 'probability' => 100,
+ 'path' => CACHE . 'long' . DS,
+]);
+
+// Using a constructed object.
+$object = new FileEngine($config);
+Cache::setConfig('other', $object);
+```
+
+The name of these engine configurations ('short' and 'long') are used as the `$config`
+parameter for `Cake\Cache\Cache::write()` and
+`Cake\Cache\Cache::read()`. When configuring cache engines you can
+refer to the class name using the following syntaxes:
+
+``` php
+// Short name (in App\ or Cake namespaces)
+Cache::setConfig('long', ['className' => 'File']);
+
+// Plugin short name
+Cache::setConfig('long', ['className' => 'MyPlugin.SuperCache']);
+
+// Full namespace
+Cache::setConfig('long', ['className' => 'Cake\Cache\Engine\FileEngine']);
+
+// An object implementing CacheEngineInterface
+Cache::setConfig('long', ['className' => $myCache]);
+```
+
+> [!NOTE]
+> When using the FileEngine you might need to use the `mask` option to
+> ensure cache files are made with the correct permissions.
+
+### Engine Options
+
+Each engine accepts the following options:
+
+- `duration` Specify how long items in this cache configuration last.
+ Specified as a `strtotime()` compatible expression.
+- `groups` List of groups or 'tags' associated to every key stored in this
+ config. Useful when you need to delete a subset of data from a cache.
+- `prefix` Prepended to all entries. Good for when you need to share
+ a keyspace with either another cache config or another application.
+- `probability` Probability of hitting a cache gc cleanup. Setting to 0 will disable
+ `Cache::gc()` from ever being called automatically.
+
+### FileEngine Options
+
+FileEngine uses the following engine specific options:
+
+- `isWindows` Automatically populated with whether the host is windows or not
+- `lock` Should files be locked before writing to them?
+- `mask` The mask used for created files
+- `path` Path to where cachefiles should be saved. Defaults to system's temp dir.
+
+
+
+### RedisEngine Options
+
+RedisEngine uses the following engine specific options:
+
+- `port` The port your Redis server is running on.
+- `host` The host your Redis server is running on.
+- `database` The database number to use for connection.
+- `password` Redis server password.
+- `persistent` Should a persistent connection be made to Redis.
+- `timeout` Connection timeout for Redis.
+- `unix_socket` Path to a unix socket for Redis.
+- `tls` Connect to redis over TLS.
+- `ssl_key` The ssl private key used for TLS connections.
+- `ssl_ca` The ssl certificate authority file for TLS connections.
+- `ssl_cert` The ssl certificate used for TLS connections.
+- `cluster` Array of cluster server addresses for Redis Cluster support.
+
+::: info Added in version 5.1.0
+TLS connections were added in 5.1
+:::
+
+::: info Added in version 5.3.0
+Redis Cluster support was added in 5.3
+:::
+
+#### Redis Cluster Configuration
+
+To use Redis Cluster, configure the `cluster` option with an array of server addresses:
+
+``` php
+Cache::setConfig('redis_cluster', [
+ 'className' => 'Redis',
+ 'duration' => '+1 hours',
+ 'prefix' => 'cake_redis_',
+ 'cluster' => [
+ '127.0.0.1:7000',
+ '127.0.0.1:7001',
+ '127.0.0.1:7002',
+ ]
+]);
+```
+
+When using Redis Cluster, the `host` and `port` options are ignored. The engine will
+automatically handle key distribution and failover across the cluster nodes.
+
+### MemcacheEngine Options
+
+- `compress` Whether to compress data.
+- `username` Login to access the Memcache server.
+- `password` Password to access the Memcache server.
+- `persistent` The name of the persistent connection. All configurations using
+ the same persistent value will share a single underlying connection.
+- `serialize` The serializer engine used to serialize data. Available engines are php,
+ igbinary and json. Beside php, the memcached extension must be compiled with the
+ appropriate serializer support.
+- `servers` String or array of memcached servers. If an array MemcacheEngine will use
+ them as a pool.
+- `duration` Be aware that any duration greater than 30 days will be treated as real
+ Unix time value rather than an offset from current time.
+- `options` Additional options for the memcached client. Should be an array of option =\> value.
+ Use the `\Memcached::OPT_*` constants as keys.
+
+
+
+### Configuring Cache Fallbacks
+
+In the event that an engine is not available, such as the `FileEngine` trying
+to write to an unwritable folder or the `RedisEngine` failing to connect to
+Redis, the engine will fall back to the noop `NullEngine` and trigger a loggable
+error. This prevents the application from throwing an uncaught exception due to
+cache failure.
+
+You can configure Cache configurations to fall back to a specified config using
+the `fallback` configuration key:
+
+``` php
+Cache::setConfig('redis', [
+ 'className' => 'Redis',
+ 'duration' => '+1 hours',
+ 'prefix' => 'cake_redis_',
+ 'host' => '127.0.0.1',
+ 'port' => 6379,
+ 'fallback' => 'default',
+]);
+```
+
+If initializing the `RedisEngine` instance fails, the `redis` cache configuration
+would fall back to using the `default` cache configuration. If initializing the
+engine for the `default` cache configuration *also* fails, in this scenario the
+engine would fall back once again to the `NullEngine` and prevent the application
+from throwing an uncaught exception.
+
+You can turn off cache fallbacks with `false`:
+
+``` php
+Cache::setConfig('redis', [
+ 'className' => 'Redis',
+ 'duration' => '+1 hours',
+ 'prefix' => 'cake_redis_',
+ 'host' => '127.0.0.1',
+ 'port' => 6379,
+ 'fallback' => false
+]);
+```
+
+When there is no fallback cache failures will be raised as exceptions.
+
+### Cache::drop()
+
+`static` Cake\\Cache\\Cache::**drop**($key): bool
+
+Once a configuration is created you cannot change it. Instead you should drop
+the configuration and re-create it using `Cake\Cache\Cache::drop()` and
+`Cake\Cache\Cache::setConfig()`. Dropping a cache engine will remove
+the config and destroy the adapter if it was constructed.
+
+## Writing to a Cache
+
+### Cache::write()
+
+`static` Cake\\Cache\\Cache::**write**($key, $value, $config = 'default'): bool
+
+`Cache::write()` will write a \$value to the Cache. You can read or
+delete this value later by referring to it by `$key`. You may
+specify an optional configuration to store the cache in as well. If
+no `$config` is specified, default will be used. `Cache::write()`
+can store any type of object and is ideal for storing results of
+model finds:
+
+``` php
+$posts = Cache::read('posts');
+if ($posts === null) {
+ $posts = $someService->getAllPosts();
+ Cache::write('posts', $posts);
+}
+```
+
+Using `Cache::write()` and `Cache::read()` to reduce the number
+of trips made to the database to fetch posts.
+
+> [!NOTE]
+> If you plan to cache the result of queries made with the CakePHP ORM,
+> it is better to use the built-in cache capabilities of the Query object
+> as described in the [Caching Query Results](../orm/query-builder#caching-query-results) section
+
+### Cache::writeMany()
+
+`static` Cake\\Cache\\Cache::**writeMany**($data, $config = 'default'): bool
+
+You may find yourself needing to write multiple cache keys at once. While you
+can use multiple calls to `write()`, `writeMany()` allows CakePHP to use
+more efficient storage APIs where available. For example using `writeMany()`
+save multiple network connections when using Memcached:
+
+``` php
+$result = Cache::writeMany([
+ 'article-' . $slug => $article,
+ 'article-' . $slug . '-comments' => $comments
+]);
+
+// $result will contain
+['article-first-post' => true, 'article-first-post-comments' => true]
+```
+
+### Cache::add()
+
+`static` Cake\\Cache\\Cache::**add**(string $key, mixed $value, string $config = 'default'): bool
+
+Using `Cache::add()` will let you atomically set a key to a value if the key
+does not already exist in the cache. If the key already exists in the cache
+backend or the write fails, `add()` will return `false`:
+
+``` php
+// Set a key to act as a lock
+$result = Cache::add($lockKey, true);
+if (!$result) {
+ return;
+}
+// Do an action where there can only be one process active at a time.
+
+// Remove the lock key.
+Cache::delete($lockKey);
+```
+
+> [!WARNING]
+> File based caching does not support atomic writes.
+
+### Cache::remember()
+
+`static` Cake\\Cache\\Cache::**remember**($key, $callable, $config = 'default'): mixed
+
+Cache helps with read-through caching. If the named cache key exists,
+it will be returned. If the key does not exist, the callable will be invoked
+and the results stored in the cache at the provided key.
+
+For example, you often want to cache remote service call results. You could use
+`remember()` to make this simple:
+
+``` php
+class IssueService
+{
+ public function allIssues($repo)
+ {
+ return Cache::remember($repo . '-issues', function () use ($repo) {
+ return $this->fetchAll($repo);
+ });
+ }
+}
+```
+
+## Reading From a Cache
+
+### Cache::read()
+
+`static` Cake\\Cache\\Cache::**read**($key, $config = 'default'): mixed
+
+`Cache::read()` is used to read the cached value stored under
+`$key` from the `$config`. If `$config` is null the default
+config will be used. `Cache::read()` will return the cached value
+if it is a valid cache or `null` if the cache has expired or
+doesn't exist. Use strict comparison operators `===` or `!==`
+to check the success of the `Cache::read()` operation.
+
+For example:
+
+``` php
+$cloud = Cache::read('cloud');
+if ($cloud !== null) {
+ return $cloud;
+}
+
+// Generate cloud data
+// ...
+
+// Store data in cache
+Cache::write('cloud', $cloud);
+
+return $cloud;
+```
+
+Or if you are using another cache configuration called `short`, you can
+specify it in `Cache::read()` and `Cache::write()` calls as below:
+
+``` php
+// Read key "cloud", but from short configuration instead of default
+$cloud = Cache::read('cloud', 'short');
+if ($cloud === null) {
+ // Generate cloud data
+ // ...
+
+ // Store data in cache, using short cache configuration instead of default
+ Cache::write('cloud', $cloud, 'short');
+}
+
+return $cloud;
+```
+
+### Cache::readMany()
+
+`static` Cake\\Cache\\Cache::**readMany**($keys, $config = 'default'): iterable
+
+After you've written multiple keys at once, you'll probably want to read them as
+well. While you could use multiple calls to `read()`, `readMany()` allows
+CakePHP to use more efficient storage APIs where available. For example using
+`readMany()` save multiple network connections when using Memcached:
+
+``` php
+$result = Cache::readMany([
+ 'article-' . $slug,
+ 'article-' . $slug . '-comments'
+]);
+// $result will contain
+['article-first-post' => '...', 'article-first-post-comments' => '...']
+```
+
+## Deleting From a Cache
+
+### Cache::delete()
+
+`static` Cake\\Cache\\Cache::**delete**($key, $config = 'default'): bool
+
+`Cache::delete()` will allow you to completely remove a cached
+object from the store:
+
+``` php
+// Remove a key
+Cache::delete('my_key');
+```
+
+As of 4.4.0, the `RedisEngine` also provides a `deleteAsync()` method
+which uses the `UNLINK` operation to remove cache keys:
+
+``` php
+Cache::pool('redis')->deleteAsync('my_key');
+```
+
+### Cache::deleteMany()
+
+`static` Cake\\Cache\\Cache::**deleteMany**($keys, $config = 'default'): bool
+
+After you've written multiple keys at once, you may want to delete them. While
+you could use multiple calls to `delete()`, `deleteMany()` allows CakePHP to use
+more efficient storage APIs where available. For example using `deleteMany()`
+save multiple network connections when using Memcached:
+
+``` php
+$result = Cache::deleteMany([
+ 'article-' . $slug,
+ 'article-' . $slug . '-comments'
+]);
+// $result will contain
+['article-first-post' => true, 'article-first-post-comments' => true]
+```
+
+## Clearing Cached Data
+
+### Cache::clear()
+
+`static` Cake\\Cache\\Cache::**clear**($config = 'default'): bool
+
+Destroy all cached values for a cache configuration. In engines like: Apcu,
+Memcached, the cache configuration's prefix is used to remove
+cache entries. Make sure that different cache configurations have different
+prefixes:
+
+``` php
+// Will clear all keys.
+Cache::clear();
+```
+
+As of 4.4.0, the `RedisEngine` also provides a `clearBlocking()` method
+which uses the `UNLINK` operation to remove cache keys:
+
+``` php
+Cache::pool('redis')->clearBlocking();
+```
+
+> [!NOTE]
+> Because APCu uses isolated caches for webserver and CLI they
+> have to be cleared separately (CLI cannot clear webserver and vice versa).
+
+## Using Cache to Store Counters
+
+### Cache::increment()
+
+`static` Cake\\Cache\\Cache::**increment**($key, $offset = 1, $config = 'default'): int|false
+
+### Cache::decrement()
+
+`static` Cake\\Cache\\Cache::**decrement**($key, $offset = 1, $config = 'default'): int|false
+
+Counters in your application are good candidates for storage in a cache. As an
+example, a simple countdown for remaining 'slots' in a contest could be stored
+in Cache. The Cache class exposes atomic ways to increment/decrement counter
+values. Atomic operations are important for these values as it
+reduces the risk of contention, and ability for two users to simultaneously
+lower the value by one, resulting in an incorrect value.
+
+After setting an integer value you can manipulate it using `increment()` and
+`decrement()`:
+
+``` php
+Cache::write('initial_count', 10);
+
+// Later on
+Cache::decrement('initial_count');
+
+// Or
+Cache::increment('initial_count');
+```
+
+> [!NOTE]
+> Incrementing and decrementing do not work with FileEngine. You should use
+> APCu, Redis or Memcached instead.
+
+## Using Cache to Store Common Query Results
+
+You can greatly improve the performance of your application by putting results
+that infrequently change, or that are subject to heavy reads into the cache.
+A perfect example of this are the results from
+`Cake\ORM\Table::find()`. The Query object allows you to cache
+results using the `cache()` method. See the [Caching Query Results](../orm/query-builder#caching-query-results) section
+for more information.
+
+
+
+## Using Groups
+
+Sometimes you will want to mark multiple cache entries to belong to certain
+group or namespace. This is a common requirement for mass-invalidating keys
+whenever some information changes that is shared among all entries in the same
+group. This is possible by declaring the groups in cache configuration:
+
+``` php
+Cache::setConfig('site_home', [
+ 'className' => 'Redis',
+ 'duration' => '+999 days',
+ 'groups' => ['comment', 'article'],
+]);
+```
+
+### Cache::clearGroup()
+
+`method` Cake\\Cache\\Cache::**clearGroup**($group, $config = 'default'): bool
+
+Let's say you want to store the HTML generated for your homepage in cache, but
+would also want to automatically invalidate this cache every time a comment or
+post is added to your database. By adding the groups `comment` and `article`,
+we have effectively tagged any key stored into this cache configuration with
+both group names.
+
+For instance, whenever a new post is added, we could tell the Cache engine to
+remove all entries associated to the `article` group:
+
+``` php
+// src/Model/Table/ArticlesTable.php
+public function afterSave($event, $entity, $options = [])
+{
+ if ($entity->isNew()) {
+ Cache::clearGroup('article', 'site_home');
+ }
+}
+```
+
+### Cache::groupConfigs()
+
+`static` Cake\\Cache\\Cache::**groupConfigs**($group = null): array
+
+`groupConfigs()` can be used to retrieve mapping between group and
+configurations, i.e.: having the same group:
+
+``` php
+// src/Model/Table/ArticlesTable.php
+
+/**
+ * A variation of previous example that clears all Cache configurations
+ * having the same group
+ */
+public function afterSave($event, $entity, $options = [])
+{
+ if ($entity->isNew()) {
+ $configs = Cache::groupConfigs('article');
+ foreach ($configs['article'] as $config) {
+ Cache::clearGroup('article', $config);
+ }
+ }
+}
+```
+
+Groups are shared across all cache configs using the same engine and same
+prefix. If you are using groups and want to take advantage of group deletion,
+choose a common prefix for all your configs.
+
+## Globally Enable or Disable Cache
+
+### Cache::disable()
+
+`static` Cake\\Cache\\Cache::**disable**(): void
+
+You may need to disable all Cache read & writes when trying to figure out cache
+expiration related issues. You can do this using `enable()` and
+`disable()`:
+
+``` php
+// Disable all cache reads, and cache writes.
+Cache::disable();
+```
+
+Once disabled, all reads and writes will return `null`.
+
+### Cache::enable()
+
+`static` Cake\\Cache\\Cache::**enable**(): void
+
+Once disabled, you can use `enable()` to re-enable caching:
+
+``` php
+// Re-enable all cache reads, and cache writes.
+Cache::enable();
+```
+
+### Cache::enabled()
+
+`static` Cake\\Cache\\Cache::**enabled**(): bool
+
+If you need to check on the state of Cache, you can use `enabled()`.
+
+## Creating a Cache Engine
+
+You can provide custom `Cache` engines in `App\Cache\Engine` as well
+as in plugins using `$plugin\Cache\Engine`. Cache engines must be in a cache
+directory. If you had a cache engine named `MyCustomCacheEngine`
+it would be placed in either **src/Cache/Engine/MyCustomCacheEngine.php**.
+Or in **plugins/MyPlugin/src/Cache/Engine/MyCustomCacheEngine.php** as
+part of a plugin. Cache configs from plugins need to use the plugin
+dot syntax:
+
+``` php
+Cache::setConfig('custom', [
+ 'className' => 'MyPlugin.MyCustomCache',
+ // ...
+]);
+```
+
+Custom Cache engines must extend `Cake\Cache\CacheEngine` which
+defines a number of abstract methods as well as provides a few initialization
+methods.
+
+The required API for a CacheEngine is
+
+`class` Cake\\Cache\\**CacheEngine**
+
+`method` Cake\\Cache\\CacheEngine::**write**($key, $value)
+
+`method` Cake\\Cache\\CacheEngine::**read**($key)
+
+`method` Cake\\Cache\\CacheEngine::**delete**($key): bool
+
+`method` Cake\\Cache\\CacheEngine::**clear**($check): bool
+
+`method` Cake\\Cache\\CacheEngine::**clearGroup**($group): bool
+
+`method` Cake\\Cache\\CacheEngine::**decrement**($key, $offset = 1): int|false
+
+`method` Cake\\Cache\\CacheEngine::**increment**($key, $offset = 1): int|false
+
+
+
+## Cache Events
+
+::: info Added in version 5.3.0
+:::
+
+You can add event listeners to the following events:
+
+- `\Cake\Cache\Event\CacheBeforeGetEvent`
+- `\Cake\Cache\Event\CacheAfterGetEvent`
+- `\Cake\Cache\Event\CacheBeforeSetEvent`
+- `\Cake\Cache\Event\CacheAfterSetEvent`
+- `\Cake\Cache\Event\CacheBeforeAddEvent`
+- `\Cake\Cache\Event\CacheAfterAddEvent`
+- `\Cake\Cache\Event\CacheBeforeDecrementEvent`
+- `\Cake\Cache\Event\CacheAfterDecrementEvent`
+- `\Cake\Cache\Event\CacheBeforeDeleteEvent`
+- `\Cake\Cache\Event\CacheAfterDeleteEvent`
+- `\Cake\Cache\Event\CacheBeforeIncrementEvent`
+- `\Cake\Cache\Event\CacheAfterIncrementEvent`
+- `\Cake\Cache\Event\CacheClearedEvent`
+- `\Cake\Cache\Event\CacheGroupClearEvent`
+
+an example listener in your `src/Application.php` or plugin class would be:
+
+``` php
+public function events(EventManagerInterface $eventManager): EventManagerInterface
+{
+ $eventManager->on(CacheAfterGetEvent::NAME, function (CacheAfterGetEvent $event): void {
+ $key = $event->getKey();
+ $value = $event->getValue();
+ $success = $event->getResult();
+ });
+
+ return $eventManager;
+}
+```
+
+Different events have different context, so please check the methods inside the custom event class
+if you are looking for certain data.
diff --git a/docs/en/core-libraries/collections.md b/docs/en/core-libraries/collections.md
new file mode 100644
index 0000000000..646c1dfdbd
--- /dev/null
+++ b/docs/en/core-libraries/collections.md
@@ -0,0 +1,1428 @@
+# Collections
+
+`class` Cake\\Collection\\**Collection**
+
+The collection classes provide a set of tools to manipulate arrays or
+`Traversable` objects. If you have ever used underscore.js,
+you have an idea of what you can expect from the collection classes.
+
+Collection instances are immutable; modifying a collection will instead generate
+a new collection. This makes working with collection objects more predictable as
+operations are side-effect free.
+
+## Quick Example
+
+Collections can be created using an array or `Traversable` object. You'll also
+interact with collections every time you interact with the ORM in CakePHP.
+A simple use of a Collection would be:
+
+``` php
+use Cake\Collection\Collection;
+
+$items = ['a' => 1, 'b' => 2, 'c' => 3];
+$collection = new Collection($items);
+
+// Create a new collection containing elements
+// with a value greater than one.
+$overOne = $collection->filter(function ($value, $key, $iterator) {
+ return $value > 1;
+});
+```
+
+You can also use the `collection()` helper function instead of `new Collection()`:
+
+``` php
+$items = ['a' => 1, 'b' => 2, 'c' => 3];
+
+// These both make a Collection instance.
+$collectionA = new Collection($items);
+$collectionB = collection($items);
+```
+
+The benefit of the helper method is that it is easier to chain off of than
+`(new Collection($items))`.
+
+The `Cake\Collection\CollectionTrait` allows you to integrate
+collection-like features into any `Traversable` object you have in your
+application as well.
+
+## List of Methods
+
+| | | |
+|-----------------|--------------|--------------|
+| `any` | `append` | `appendItem` |
+| `avg` | `buffered` | `chunk` |
+| `chunkWithKeys` | `combine` | `compile` |
+| `contains` | `countBy` | `each` |
+| `every` | `extract` | `filter` |
+| `first` | `firstMatch` | `groupBy` |
+| `indexBy` | `insert` | `isEmpty` |
+| `last` | `listNested` | `map` |
+| `match` | `max` | `median` |
+| `min` | `nest` | `prepend` |
+| `prependItem` | `reduce` | `reject` |
+| `sample` | `shuffle` | `skip` |
+| `some` | `sortBy` | `stopWhen` |
+| `sumOf` | `take` | `through` |
+| `transpose` | `unfold` | `zip` |
+
+## Iterating
+
+### each()
+
+`method` Cake\\Collection\\Collection::**each**($callback)
+
+Collections can be iterated and/or transformed into new collections with the
+`each()` and `map()` methods. The `each()` method will not create a new
+collection, but will allow you to modify any objects within the collection:
+
+``` php
+$collection = new Collection($items);
+$collection = $collection->each(function ($value, $key) {
+ echo "Element $key: $value";
+});
+```
+
+The return of `each()` will be the collection object. Each will iterate the
+collection immediately applying the callback to each value in the collection.
+
+### map()
+
+`method` Cake\\Collection\\Collection::**map**($callback): CollectionInterface
+
+The `map()` method will create a new collection based on the output of the
+callback being applied to each object in the original collection:
+
+``` php
+$items = ['a' => 1, 'b' => 2, 'c' => 3];
+$collection = new Collection($items);
+
+$new = $collection->map(function ($value, $key) {
+ return $value * 2;
+});
+
+// $result contains [2, 4, 6];
+$result = $new->toList();
+
+// $result contains ['a' => 2, 'b' => 4, 'c' => 6];
+$result = $new->toArray();
+```
+
+The `map()` method will create a new iterator which lazily creates
+the resulting items when iterated.
+
+### extract()
+
+`method` Cake\\Collection\\Collection::**extract**($path): CollectionInterface
+
+One of the most common uses for a `map()` function is to extract a single
+column from a collection. If you are looking to build a list of elements
+containing the values for a particular property, you can use the `extract()`
+method:
+
+``` php
+$collection = new Collection($people);
+$names = $collection->extract('name');
+
+// $result contains ['mark', 'jose', 'barbara'];
+$result = $names->toList();
+```
+
+As with many other functions in the collection class, you are allowed to specify
+a dot-separated path for extracting columns. This example will return
+a collection containing the author names from a list of articles:
+
+``` php
+$collection = new Collection($articles);
+$names = $collection->extract('author.name');
+
+// $result contains ['Maria', 'Stacy', 'Larry'];
+$result = $names->toList();
+```
+
+Finally, if the property you are looking after cannot be expressed as a path,
+you can use a callback function to return it:
+
+``` php
+$collection = new Collection($articles);
+$names = $collection->extract(function ($article) {
+ return $article->author->name . ', ' . $article->author->last_name;
+});
+```
+
+Often, the properties you need to extract a common key present in multiple
+arrays or objects that are deeply nested inside other structures. For those
+cases you can use the `{*}` matcher in the path key. This matcher is often
+helpful when matching HasMany and BelongsToMany association data:
+
+``` php
+$data = [
+ [
+ 'name' => 'James',
+ 'phone_numbers' => [
+ ['number' => 'number-1'],
+ ['number' => 'number-2'],
+ ['number' => 'number-3'],
+ ],
+ ],
+ [
+ 'name' => 'James',
+ 'phone_numbers' => [
+ ['number' => 'number-4'],
+ ['number' => 'number-5'],
+ ],
+ ],
+];
+
+$numbers = (new Collection($data))->extract('phone_numbers.{*}.number');
+$result = $numbers->toList();
+// $result contains ['number-1', 'number-2', 'number-3', 'number-4', 'number-5']
+```
+
+This last example uses `toList()` unlike other examples, which is important
+when we're getting results with possibly duplicate keys. By using `toList()`
+we'll be guaranteed to get all values even if there are duplicate keys.
+
+Unlike `Cake\Utility\Hash::extract()` this method only supports the
+`{*}` wildcard. All other wildcard and attributes matchers are not supported.
+
+### combine()
+
+`method` Cake\\Collection\\Collection::**combine**($keyPath, $valuePath, $groupPath = null): CollectionInterface
+
+Collections allow you to create a new collection made from keys and values in
+an existing collection. Both the key and value paths can be specified with
+dot notation paths:
+
+``` php
+$items = [
+ ['id' => 1, 'name' => 'foo', 'parent' => 'a'],
+ ['id' => 2, 'name' => 'bar', 'parent' => 'b'],
+ ['id' => 3, 'name' => 'baz', 'parent' => 'a'],
+];
+$combined = (new Collection($items))->combine('id', 'name');
+$result = $combined->toArray();
+
+// $result contains
+[
+ 1 => 'foo',
+ 2 => 'bar',
+ 3 => 'baz',
+];
+```
+
+You can also optionally use a `groupPath` to group results based on a path:
+
+``` php
+$combined = (new Collection($items))->combine('id', 'name', 'parent');
+$result = $combined->toArray();
+
+// $result contains
+[
+ 'a' => [1 => 'foo', 3 => 'baz'],
+ 'b' => [2 => 'bar']
+];
+```
+
+Finally you can use *closures* to build keys/values/groups paths dynamically,
+for example when working with entities and dates (converted to `I18n\DateTime`
+instances by the ORM) you may want to group results by date:
+
+``` php
+$combined = (new Collection($entities))->combine(
+ 'id',
+ function ($entity) { return $entity; },
+ function ($entity) { return $entity->date->toDateString(); }
+);
+ $result = $combined->toArray();
+
+// $result contains
+[
+ 'date string like 2015-05-01' => ['entity1->id' => entity1, 'entity2->id' => entity2, ..., 'entityN->id' => entityN]
+ 'date string like 2015-06-01' => ['entity1->id' => entity1, 'entity2->id' => entity2, ..., 'entityN->id' => entityN]
+]
+```
+
+### stopWhen()
+
+`method` Cake\\Collection\\Collection::**stopWhen**(callable $c): CollectionInterface
+
+You can stop the iteration at any point using the `stopWhen()` method. Calling
+it in a collection will create a new one that will stop yielding results if the
+passed callable returns true for one of the elements:
+
+``` php
+$items = [10, 20, 50, 1, 2];
+$collection = new Collection($items);
+
+$new = $collection->stopWhen(function ($value, $key) {
+ // Stop on the first value bigger than 30
+ return $value > 30;
+});
+
+// $result contains [10, 20];
+$result = $new->toList();
+```
+
+### unfold()
+
+`method` Cake\\Collection\\Collection::**unfold**(callable $callback): CollectionInterface
+
+Sometimes the internal items of a collection will contain arrays or iterators
+with more items. If you wish to flatten the internal structure to iterate once
+over all elements you can use the `unfold()` method. It will create a new
+collection that will yield every single element nested in the collection:
+
+``` php
+$items = [[1, 2, 3], [4, 5]];
+$collection = new Collection($items);
+$new = $collection->unfold();
+
+// $result contains [1, 2, 3, 4, 5];
+$result = $new->toList();
+```
+
+When passing a callable to `unfold()` you can control what elements will be
+unfolded from each item in the original collection. This is useful for returning
+data from paginated services:
+
+``` php
+$pages = [1, 2, 3, 4];
+$collection = new Collection($pages);
+$items = $collection->unfold(function ($page, $key) {
+ // An imaginary web service that returns a page of results
+ return MyService::fetchPage($page)->toList();
+});
+
+$allPagesItems = $items->toList();
+```
+
+You can use the `yield` keyword inside `unfold()` to return as
+many elements for each item in the collection as you may need:
+
+``` php
+$oddNumbers = [1, 3, 5, 7];
+$collection = new Collection($oddNumbers);
+$new = $collection->unfold(function ($oddNumber) {
+ yield $oddNumber;
+ yield $oddNumber + 1;
+});
+
+// $result contains [1, 2, 3, 4, 5, 6, 7, 8];
+$result = $new->toList();
+```
+
+### chunk()
+
+`method` Cake\\Collection\\Collection::**chunk**($chunkSize): CollectionInterface
+
+When dealing with large amounts of items in a collection, it may make sense to
+process the elements in batches instead of one by one. For splitting
+a collection into multiple arrays of a certain size, you can use the `chunk()`
+function:
+
+``` php
+$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
+$collection = new Collection($items);
+$chunked = $collection->chunk(2);
+$chunked->toList(); // [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11]]
+```
+
+The `chunk` function is particularly useful when doing batch processing, for
+example with a database result:
+
+``` php
+$collection = new Collection($articles);
+$collection->map(function ($article) {
+ // Change a property in the article
+ $article->property = 'changed';
+ })
+ ->chunk(20)
+ ->each(function ($batch) {
+ myBulkSave($batch); // This function will be called for each batch
+ });
+```
+
+### chunkWithKeys()
+
+`method` Cake\\Collection\\Collection::**chunkWithKeys**($chunkSize): CollectionInterface
+
+Much like `chunk()`, `chunkWithKeys()` allows you to slice up
+a collection into smaller batches but with keys preserved. This is useful when
+chunking associative arrays:
+
+``` php
+$collection = new Collection([
+ 'a' => 1,
+ 'b' => 2,
+ 'c' => 3,
+ 'd' => [4, 5]
+]);
+$chunked = $collection->chunkWithKeys(2);
+$result = $chunked->toList();
+
+// $result contains
+[
+ ['a' => 1, 'b' => 2],
+ ['c' => 3, 'd' => [4, 5]]
+]
+```
+
+## Filtering
+
+### filter()
+
+`method` Cake\\Collection\\Collection::**filter**($callback): CollectionInterface
+
+Collections allow you to filter and create new collections based on
+the result of callback functions. You can use `filter()` to create a new
+collection of elements matching a criteria callback:
+
+``` php
+$collection = new Collection($people);
+$ladies = $collection->filter(function ($person, $key) {
+ return $person->gender === 'female';
+});
+$guys = $collection->filter(function ($person, $key) {
+ return $person->gender === 'male';
+});
+```
+
+### reject()
+
+`method` Cake\\Collection\\Collection::**reject**(callable $c): CollectionInterface
+
+The inverse of `filter()` is `reject()`. This method does a negative filter,
+removing elements that match the filter function:
+
+``` php
+$collection = new Collection($people);
+$ladies = $collection->reject(function ($person, $key) {
+ return $person->gender === 'male';
+});
+```
+
+### every()
+
+`method` Cake\\Collection\\Collection::**every**($callback): bool
+
+You can do truth tests with filter functions. To see if every element in
+a collection matches a test you can use `every()`:
+
+``` php
+$collection = new Collection($people);
+$allYoungPeople = $collection->every(function ($person) {
+ return $person->age < 21;
+});
+```
+
+### any()
+
+`method` Cake\\Collection\\Collection::**any**($callback): bool
+
+### some()
+
+`method` Cake\\Collection\\Collection::**some**($callback): bool
+
+You can see if the collection contains at least one element matching a filter
+function using the `any()` method:
+
+``` php
+$collection = new Collection($people);
+$hasYoungPeople = $collection->any(function ($person) {
+ return $person->age < 21;
+});
+```
+
+> [!NOTE]
+> The `some()` method is an alias of `any()`.
+
+### match()
+
+`method` Cake\\Collection\\Collection::**match**($conditions): CollectionInterface
+
+If you need to extract a new collection containing only the elements that
+contain a given set of properties, you should use the `match()` method:
+
+``` php
+$collection = new Collection($comments);
+$commentsFromMark = $collection->match(['user.name' => 'Mark']);
+```
+
+### firstMatch()
+
+`method` Cake\\Collection\\Collection::**firstMatch**($conditions): mixed
+
+The property name can be a dot-separated path. You can traverse into nested
+entities and match the values they contain. When you only need the first
+matching element from a collection, you can use `firstMatch()`:
+
+``` php
+$collection = new Collection($comments);
+$comment = $collection->firstMatch([
+ 'user.name' => 'Mark',
+ 'active' => true
+]);
+```
+
+As you can see from the above, both `match()` and `firstMatch()` allow you
+to provide multiple conditions to match on. In addition, the conditions can be
+for different paths, allowing you to express complex conditions to match
+against.
+
+## Aggregation
+
+### reduce()
+
+`method` Cake\\Collection\\Collection::**reduce**($callback, $initial): mixed
+
+The counterpart of a `map()` operation is usually a `reduce`. This
+function will help you build a single result out of all the elements in a
+collection:
+
+``` php
+$totalPrice = $collection->reduce(function ($accumulated, $orderLine) {
+ return $accumulated + $orderLine->price;
+}, 0);
+```
+
+In the above example, `$totalPrice` will be the sum of all single prices
+contained in the collection. Note the second argument for the `reduce()`
+function takes the initial value for the reduce operation you are
+performing:
+
+``` php
+$allTags = $collection->reduce(function ($accumulated, $article) {
+ return array_merge($accumulated, $article->tags);
+}, []);
+```
+
+### min()
+
+`method` Cake\\Collection\\Collection::**min**(string|callable $callback, $type = SORT_NUMERIC): mixed
+
+To extract the minimum value for a collection based on a property, just use the
+`min()` function. This will return the full element from the collection and
+not just the smallest value found:
+
+``` php
+$collection = new Collection($people);
+$youngest = $collection->min('age');
+
+echo $youngest->name;
+```
+
+You are also able to express the property to compare by providing a path or a
+callback function:
+
+``` php
+$collection = new Collection($people);
+$personYoungestChild = $collection->min(function ($person) {
+ return $person->child->age;
+});
+
+$personWithYoungestDad = $collection->min('dad.age');
+```
+
+### max()
+
+`method` Cake\\Collection\\Collection::**max**(string|callable $callback, $type = SORT_NUMERIC): mixed
+
+The same can be applied to the `max()` function, which will return a single
+element from the collection having the highest property value:
+
+``` php
+$collection = new Collection($people);
+$oldest = $collection->max('age');
+
+$personOldestChild = $collection->max(function ($person) {
+ return $person->child->age;
+});
+
+$personWithOldestDad = $collection->max('dad.age');
+```
+
+### sumOf()
+
+`method` Cake\\Collection\\Collection::**sumOf**($path = null): float|int
+
+Finally, the `sumOf()` method will return the sum of a property of all
+elements:
+
+``` php
+$collection = new Collection($people);
+$sumOfAges = $collection->sumOf('age');
+
+$sumOfChildrenAges = $collection->sumOf(function ($person) {
+ return $person->child->age;
+});
+
+$sumOfDadAges = $collection->sumOf('dad.age');
+```
+
+### avg()
+
+`method` Cake\\Collection\\Collection::**avg**($path = null): float|int|null
+
+Calculate the average value of the elements in the collection. Optionally
+provide a matcher path, or function to extract values to generate the average
+for:
+
+``` php
+$items = [
+ ['invoice' => ['total' => 100]],
+ ['invoice' => ['total' => 200]],
+];
+
+// $average contains 150
+$average = (new Collection($items))->avg('invoice.total');
+```
+
+### median()
+
+`method` Cake\\Collection\\Collection::**median**($path = null): float|int|null
+
+Calculate the median value of a set of elements. Optionally provide a matcher
+path, or function to extract values to generate the median for:
+
+``` php
+$items = [
+ ['invoice' => ['total' => 400]],
+ ['invoice' => ['total' => 500]],
+ ['invoice' => ['total' => 100]],
+ ['invoice' => ['total' => 333]],
+ ['invoice' => ['total' => 200]],
+];
+
+// $median contains 333
+$median = (new Collection($items))->median('invoice.total');
+```
+
+### Grouping and Counting
+
+### groupBy()
+
+`method` Cake\\Collection\\Collection::**groupBy**($callback): CollectionInterface
+
+Collection values can be grouped by different keys in a new collection when they
+share the same value for a property:
+
+``` php
+$students = [
+ ['name' => 'Mark', 'grade' => 9],
+ ['name' => 'Andrew', 'grade' => 10],
+ ['name' => 'Stacy', 'grade' => 10],
+ ['name' => 'Barbara', 'grade' => 9]
+];
+$collection = new Collection($students);
+$studentsByGrade = $collection->groupBy('grade');
+$result = $studentsByGrade->toArray();
+
+// $result contains
+[
+ 10 => [
+ ['name' => 'Andrew', 'grade' => 10],
+ ['name' => 'Stacy', 'grade' => 10]
+ ],
+ 9 => [
+ ['name' => 'Mark', 'grade' => 9],
+ ['name' => 'Barbara', 'grade' => 9]
+ ]
+]
+```
+
+As usual, it is possible to provide either a dot-separated path for nested
+properties or your own callback function to generate the groups dynamically:
+
+``` php
+$commentsByUserId = $comments->groupBy('user.id');
+
+$classResults = $students->groupBy(function ($student) {
+ return $student->grade > 6 ? 'approved' : 'denied';
+});
+```
+
+### countBy()
+
+`method` Cake\\Collection\\Collection::**countBy**($callback): CollectionInterface
+
+If you only wish to know the number of occurrences per group, you can do so by
+using the `countBy()` method. It takes the same arguments as `groupBy` so it
+should be already familiar to you:
+
+``` php
+$classResults = $students->countBy(function ($student) {
+ return $student->grade > 6 ? 'approved' : 'denied';
+});
+
+// Result could look like this when converted to array:
+['approved' => 70, 'denied' => 20]
+```
+
+### indexBy()
+
+`method` Cake\\Collection\\Collection::**indexBy**($callback): CollectionInterface
+
+There will be certain cases where you know an element is unique for the property
+you want to group by. If you wish a single result per group, you can use the
+function `indexBy()`:
+
+``` php
+$usersById = $users->indexBy('id');
+
+// When converted to array result could look like
+[
+ 1 => 'markstory',
+ 3 => 'jose_zap',
+ 4 => 'jrbasso'
+]
+```
+
+As with the `groupBy()` function you can also use a property path or
+a callback:
+
+``` php
+$articlesByAuthorId = $articles->indexBy('author.id');
+
+$filesByHash = $files->indexBy(function ($file) {
+ return md5($file);
+});
+```
+
+### zip()
+
+`method` Cake\\Collection\\Collection::**zip**($items): CollectionInterface
+
+The elements of different collections can be grouped together using the
+`zip()` method. It will return a new collection containing an array grouping
+the elements from each collection that are placed at the same position:
+
+``` php
+$odds = new Collection([1, 3, 5]);
+$pairs = new Collection([2, 4, 6]);
+$combined = $odds->zip($pairs)->toList(); // [[1, 2], [3, 4], [5, 6]]
+```
+
+You can also zip multiple collections at once:
+
+``` php
+$years = new Collection([2013, 2014, 2015, 2016]);
+$salaries = [1000, 1500, 2000, 2300];
+$increments = [0, 500, 500, 300];
+
+$rows = $years->zip($salaries, $increments);
+$result = $rows->toList();
+
+// $result contains
+[
+ [2013, 1000, 0],
+ [2014, 1500, 500],
+ [2015, 2000, 500],
+ [2016, 2300, 300]
+]
+```
+
+As you can already see, the `zip()` method is very useful for transposing
+multidimensional arrays:
+
+``` php
+$data = [
+ 2014 => ['jan' => 100, 'feb' => 200],
+ 2015 => ['jan' => 300, 'feb' => 500],
+ 2016 => ['jan' => 400, 'feb' => 600],
+];
+
+// Getting jan and feb data together
+
+$firstYear = new Collection(array_shift($data));
+$result = $firstYear->zip($data[0], $data[1])->toList();
+
+// Or $firstYear->zip(...$data) in PHP >= 5.6
+
+// $result contains
+[
+ [100, 300, 400],
+ [200, 500, 600]
+]
+```
+
+## Sorting
+
+### sortBy()
+
+`method` Cake\\Collection\\Collection::**sortBy**($callback, $order = SORT_DESC, $sort = SORT_NUMERIC): CollectionInterface
+
+Collection values can be sorted in ascending or descending order based on
+a column or custom function. To create a new sorted collection out of the values
+of another one, you can use `sortBy`:
+
+``` php
+$collection = new Collection($people);
+$sorted = $collection->sortBy('age');
+```
+
+As seen above, you can sort by passing the name of a column or property that
+is present in the collection values. You are also able to specify a property
+path instead using the dot notation. The next example will sort articles by
+their author's name:
+
+``` php
+$collection = new Collection($articles);
+$sorted = $collection->sortBy('author.name');
+```
+
+The `sortBy()` method is flexible enough to let you specify an extractor
+function that will let you dynamically select the value to use for comparing two
+different values in the collection:
+
+``` php
+$collection = new Collection($articles);
+$sorted = $collection->sortBy(function ($article) {
+ return $article->author->name . '-' . $article->title;
+});
+```
+
+In order to specify in which direction the collection should be sorted, you need
+to provide either `SORT_ASC` or `SORT_DESC` as the second parameter for
+sorting in ascending or descending direction respectively. By default,
+collections are sorted in descending direction:
+
+``` php
+$collection = new Collection($people);
+$sorted = $collection->sortBy('age', SORT_ASC);
+```
+
+Sometimes you will need to specify which type of data you are trying to compare
+so that you get consistent results. For this purpose, you should supply a third
+argument in the `sortBy()` function with one of the following constants:
+
+- **SORT_NUMERIC**: For comparing numbers
+- **SORT_STRING**: For comparing string values
+- **SORT_NATURAL**: For sorting string containing numbers and you'd like those
+ numbers to be order in a natural way. For example: showing "10" after "2".
+- **SORT_LOCALE_STRING**: For comparing strings based on the current locale.
+
+By default, `SORT_NUMERIC` is used:
+
+``` php
+$collection = new Collection($articles);
+$sorted = $collection->sortBy('title', SORT_ASC, SORT_NATURAL);
+```
+
+> [!WARNING]
+> It is often expensive to iterate sorted collections more than once. If you
+> plan to do so, consider converting the collection to an array or simply use
+> the `compile()` method on it.
+
+## Working with Tree Data
+
+### nest()
+
+`method` Cake\\Collection\\Collection::**nest**($idPath, $parentPath, $nestingKey = 'children'): CollectionInterface
+
+Not all data is meant to be represented in a linear way. Collections make it
+easier to construct and flatten hierarchical or nested structures. Creating
+a nested structure where children are grouped by a parent identifier property
+can be done with the `nest()` method.
+
+Two parameters are required for this function. The first one is the property
+representing the item identifier. The second parameter is the name of the
+property representing the identifier for the parent item:
+
+``` php
+$collection = new Collection([
+ ['id' => 1, 'parent_id' => null, 'name' => 'Birds'],
+ ['id' => 2, 'parent_id' => 1, 'name' => 'Land Birds'],
+ ['id' => 3, 'parent_id' => 1, 'name' => 'Eagle'],
+ ['id' => 4, 'parent_id' => 1, 'name' => 'Seagull'],
+ ['id' => 5, 'parent_id' => 6, 'name' => 'Clown Fish'],
+ ['id' => 6, 'parent_id' => null, 'name' => 'Fish'],
+]);
+$nested = $collection->nest('id', 'parent_id');
+$result = $nested->toList();
+
+// $result contains
+[
+ [
+ 'id' => 1,
+ 'parent_id' => null,
+ 'name' => 'Birds',
+ 'children' => [
+ ['id' => 2, 'parent_id' => 1, 'name' => 'Land Birds', 'children' => []],
+ ['id' => 3, 'parent_id' => 1, 'name' => 'Eagle', 'children' => []],
+ ['id' => 4, 'parent_id' => 1, 'name' => 'Seagull', 'children' => []],
+ ],
+ ],
+ [
+ 'id' => 6,
+ 'parent_id' => null,
+ 'name' => 'Fish',
+ 'children' => [
+ ['id' => 5, 'parent_id' => 6, 'name' => 'Clown Fish', 'children' => []],
+ ],
+ ],
+];
+```
+
+Children elements are nested inside the `children` property inside each of the
+items in the collection. This type of data representation is helpful for
+rendering menus or traversing elements up to certain level in the tree.
+
+### listNested()
+
+`method` Cake\\Collection\\Collection::**listNested**($order = 'desc', $nestingKey = 'children'): CollectionInterface
+
+The inverse of `nest()` is `listNested()`. This method allows you to flatten
+a tree structure back into a linear structure. It takes two parameters; the
+first one is the traversing mode (asc, desc or leaves), and the second one is
+the name of the property containing the children for each element in the
+collection.
+
+Taking the input the nested collection built in the previous example, we can
+flatten it:
+
+``` php
+$result = $nested->listNested()->toList();
+
+// $result contains
+[
+ ['id' => 1, 'parent_id' => null, 'name' => 'Birds', 'children' => [...]],
+ ['id' => 2, 'parent_id' => 1, 'name' => 'Land Birds'],
+ ['id' => 3, 'parent_id' => 1, 'name' => 'Eagle'],
+ ['id' => 4, 'parent_id' => 1, 'name' => 'Seagull'],
+ ['id' => 6, 'parent_id' => null, 'name' => 'Fish', 'children' => [...]],
+ ['id' => 5, 'parent_id' => 6, 'name' => 'Clown Fish']
+]
+```
+
+By default, the tree is traversed from the root to the leaves. You can also
+instruct it to only return the leaf elements in the tree:
+
+``` php
+$result = $nested->listNested('leaves')->toList();
+
+// $result contains
+[
+ ['id' => 2, 'parent_id' => 1, 'name' => 'Land Birds', 'children' => [], ],
+ ['id' => 3, 'parent_id' => 1, 'name' => 'Eagle', 'children' => [], ],
+ ['id' => 4, 'parent_id' => 1, 'name' => 'Seagull', 'children' => [], ],
+ ['id' => 5, 'parent_id' => 6, 'name' => 'Clown Fish', 'children' => [], ],
+]
+```
+
+Once you have converted a tree into a nested list, you can use the `printer()`
+method to configure how the list output should be formatted:
+
+``` php
+$result = $nested->listNested()->printer('name', 'id', '--')->toArray();
+
+// $result contains
+[
+ 1 => 'Birds',
+ 2 => '--Land Birds',
+ 3 => '--Eagle',
+ 4 => '--Seagull',
+ 6 => 'Fish',
+ 5 => '--Clown Fish',
+]
+```
+
+The `printer()` method also lets you use a callback to generate the keys and
+or values:
+
+``` php
+$nested->listNested()->printer(
+ function ($el) {
+ return $el->name;
+ },
+ function ($el) {
+ return $el->id;
+ }
+);
+```
+
+## Other Methods
+
+### isEmpty()
+
+`method` Cake\\Collection\\Collection::**isEmpty**(): bool
+
+Allows you to see if a collection contains any elements:
+
+``` php
+$collection = new Collection([]);
+// Returns true
+$collection->isEmpty();
+
+$collection = new Collection([1]);
+// Returns false
+$collection->isEmpty();
+```
+
+### contains()
+
+`method` Cake\\Collection\\Collection::**contains**($value): bool
+
+Collections allow you to quickly check if they contain one particular
+value: by using the `contains()` method:
+
+``` php
+$items = ['a' => 1, 'b' => 2, 'c' => 3];
+$collection = new Collection($items);
+$hasThree = $collection->contains(3);
+```
+
+Comparisons are performed using the `===` operator. If you wish to do looser
+comparison types you can use the `some()` method.
+
+### shuffle()
+
+`method` Cake\\Collection\\Collection::**shuffle**(): CollectionInterface
+
+Sometimes you may wish to show a collection of values in a random order. In
+order to create a new collection that will return each value in a randomized
+position, use the `shuffle`:
+
+``` php
+$collection = new Collection(['a' => 1, 'b' => 2, 'c' => 3]);
+
+// This could return [2, 3, 1]
+$collection->shuffle()->toList();
+```
+
+### transpose()
+
+`method` Cake\\Collection\\Collection::**transpose**(): CollectionInterface
+
+When you transpose a collection, you get a new collection containing a row made
+of the each of the original columns:
+
+``` php
+$items = [
+ ['Products', '2012', '2013', '2014'],
+ ['Product A', '200', '100', '50'],
+ ['Product B', '300', '200', '100'],
+ ['Product C', '400', '300', '200'],
+];
+$transpose = (new Collection($items))->transpose();
+$result = $transpose->toList();
+
+// $result contains
+[
+ ['Products', 'Product A', 'Product B', 'Product C'],
+ ['2012', '200', '300', '400'],
+ ['2013', '100', '200', '300'],
+ ['2014', '50', '100', '200'],
+]
+```
+
+### Withdrawing Elements
+
+### sample()
+
+`method` Cake\\Collection\\Collection::**sample**($length = 10): CollectionInterface
+
+Shuffling a collection is often useful when doing quick statistical analysis.
+Another common operation when doing this sort of task is withdrawing a few
+random values out of a collection so that more tests can be performed on those.
+For example, if you wanted to select 5 random users to which you'd like to apply
+some A/B tests to, you can use the `sample()` function:
+
+``` php
+$collection = new Collection($people);
+
+// Withdraw maximum 20 random users from this collection
+$testSubjects = $collection->sample(20);
+```
+
+`sample()` will take at most the number of values you specify in the first
+argument. If there are not enough elements in the collection to satisfy the
+sample, the full collection in a random order is returned.
+
+### take()
+
+`method` Cake\\Collection\\Collection::**take**($length, $offset): CollectionInterface
+
+Whenever you want to take a slice of a collection use the `take()` function,
+it will create a new collection with at most the number of values you specify in
+the first argument, starting from the position passed in the second argument:
+
+``` php
+$topFive = $collection->sortBy('age')->take(5);
+
+// Take 5 people from the collection starting from position 4
+$nextTopFive = $collection->sortBy('age')->take(5, 4);
+```
+
+Positions are zero-based, therefore the first position number is `0`.
+
+### skip()
+
+`method` Cake\\Collection\\Collection::**skip**($length): CollectionInterface
+
+While the second argument of `take()` can help you skip some elements before
+getting them from the collection, you can also use `skip()` for the same
+purpose as a way to take the rest of the elements after a certain position:
+
+``` php
+$collection = new Collection([1, 2, 3, 4]);
+$allExceptFirstTwo = $collection->skip(2)->toList(); // [3, 4]
+```
+
+### first()
+
+`method` Cake\\Collection\\Collection::**first**(): mixed
+
+One of the most common uses of `take()` is getting the first element in the
+collection. A shortcut method for achieving the same goal is using the
+`first()` method:
+
+``` php
+$collection = new Collection([5, 4, 3, 2]);
+$collection->first(); // Returns 5
+```
+
+### last()
+
+`method` Cake\\Collection\\Collection::**last**(): mixed
+
+Similarly, you can get the last element of a collection using the `last()`
+method:
+
+``` php
+$collection = new Collection([5, 4, 3, 2]);
+$collection->last(); // Returns 2
+```
+
+### Expanding Collections
+
+### append()
+
+`method` Cake\\Collection\\Collection::**append**(array|Traversable $items): CollectionInterface
+
+You can compose multiple collections into a single one. This enables you to
+gather data from various sources, concatenate it, and apply other collection
+functions to it very smoothly. The `append()` method will return a new
+collection containing the values from both sources:
+
+``` php
+$cakephpTweets = new Collection($tweets);
+$myTimeline = $cakephpTweets->append($phpTweets);
+
+// Tweets containing `cakefest` from both sources
+$myTimeline->filter(function ($tweet) {
+ return strpos($tweet, 'cakefest');
+});
+```
+
+### appendItem()
+
+`method` Cake\\Collection\\Collection::**appendItem**($value, $key): CollectionInterface
+
+Allows you to append an item with an optional key to the collection. If you
+specify a key that already exists in the collection, the value will not be
+overwritten:
+
+``` php
+$cakephpTweets = new Collection($tweets);
+$myTimeline = $cakephpTweets->appendItem($newTweet, 99);
+```
+
+### prepend()
+
+`method` Cake\\Collection\\Collection::**prepend**($items): CollectionInterface
+
+The `prepend()` method will return a new collection containing the values from
+both sources:
+
+``` php
+$cakephpTweets = new Collection($tweets);
+$myTimeline = $cakephpTweets->prepend($phpTweets);
+```
+
+### prependItem()
+
+`method` Cake\\Collection\\Collection::**prependItem**($value, $key): CollectionInterface
+
+Allows you to prepend an item with an optional key to the collection. If you
+specify a key that already exists in the collection, the value will not be
+overwritten:
+
+``` php
+$cakephpTweets = new Collection($tweets);
+$myTimeline = $cakephpTweets->prependItem($newTweet, 99);
+```
+
+> [!WARNING]
+> When appending from different sources, you can expect some keys from both
+> collections to be the same. For example, when appending two simple arrays.
+> This can present a problem when converting a collection to an array using
+> `toArray()`. If you do not want values from one collection to override
+> others in the previous one based on their key, make sure that you call
+> `toList()` in order to drop the keys and preserve all values.
+
+### Modifiying Elements
+
+### insert()
+
+`method` Cake\\Collection\\Collection::**insert**($path, $items): CollectionInterface
+
+At times, you may have two separate sets of data that you would like to insert
+the elements of one set into each of the elements of the other set. This is
+a very common case when you fetch data from a data source that does not support
+data-merging or joins natively.
+
+Collections offer an `insert()` method that will allow you to insert each of
+the elements in one collection into a property inside each of the elements of
+another collection:
+
+``` php
+$users = [
+ ['username' => 'mark'],
+ ['username' => 'juan'],
+ ['username' => 'jose']
+];
+
+$languages = [
+ ['PHP', 'Python', 'Ruby'],
+ ['Bash', 'PHP', 'Javascript'],
+ ['Javascript', 'Prolog']
+];
+
+$merged = (new Collection($users))->insert('skills', $languages);
+$result = $merged->toArray();
+
+// $result contains
+[
+ ['username' => 'mark', 'skills' => ['PHP', 'Python', 'Ruby']],
+ ['username' => 'juan', 'skills' => ['Bash', 'PHP', 'Javascript']],
+ ['username' => 'jose', 'skills' => ['Javascript', 'Prolog']]
+];
+```
+
+The first parameter for the `insert()` method is a dot-separated path of
+properties to follow so that the elements can be inserted at that position. The
+second argument is anything that can be converted to a collection object.
+
+Please observe that elements are inserted by the position they are found, thus,
+the first element of the second collection is merged into the first
+element of the first collection.
+
+If there are not enough elements in the second collection to insert into the
+first one, then the target property will not be present:
+
+``` php
+$languages = [
+ ['PHP', 'Python', 'Ruby'],
+ ['Bash', 'PHP', 'Javascript']
+];
+
+$merged = (new Collection($users))->insert('skills', $languages);
+$result = $merged->toArray();
+
+// $result contains
+[
+ ['username' => 'mark', 'skills' => ['PHP', 'Python', 'Ruby']],
+ ['username' => 'juan', 'skills' => ['Bash', 'PHP', 'Javascript']],
+ ['username' => 'jose']
+];
+```
+
+The `insert()` method can operate array elements or objects implementing the
+`ArrayAccess` interface.
+
+### Making Collection Methods Reusable
+
+Using closures for collection methods is great when the work to be done is small
+and focused, but it can get messy very quickly. This becomes more obvious when
+a lot of different methods need to be called or when the length of the closure
+methods is more than just a few lines.
+
+There are also cases when the logic used for the collection methods can be
+reused in multiple parts of your application. It is recommended that you
+consider extracting complex collection logic to separate classes. For example,
+imagine a lengthy closure like this one:
+
+``` php
+$collection
+ ->map(function ($row, $key) {
+ if (!empty($row['items'])) {
+ $row['total'] = collection($row['items'])->sumOf('price');
+ }
+
+ if (!empty($row['total'])) {
+ $row['tax_amount'] = $row['total'] * 0.25;
+ }
+
+ // More code here...
+
+ return $modifiedRow;
+ });
+```
+
+This can be refactored by creating another class:
+
+``` php
+class TotalOrderCalculator
+{
+ public function __invoke($row, $key)
+ {
+ if (!empty($row['items'])) {
+ $row['total'] = collection($row['items'])->sumOf('price');
+ }
+
+ if (!empty($row['total'])) {
+ $row['tax_amount'] = $row['total'] * 0.25;
+ }
+
+ // More code here...
+
+ return $modifiedRow;
+ }
+}
+
+// Use the logic in your map() call
+$collection->map(new TotalOrderCalculator)
+```
+
+### through()
+
+`method` Cake\\Collection\\Collection::**through**($callback): CollectionInterface
+
+Sometimes a chain of collection method calls can become reusable in other parts
+of your application, but only if they are called in that specific order. In
+those cases you can use `through()` in combination with a class implementing
+`__invoke` to distribute your handy data processing calls:
+
+``` php
+$collection
+ ->map(new ShippingCostCalculator)
+ ->map(new TotalOrderCalculator)
+ ->map(new GiftCardPriceReducer)
+ ->buffered()
+ ...
+```
+
+The above method calls can be extracted into a new class so they don't need to
+be repeated every time:
+
+``` php
+class FinalCheckOutRowProcessor
+{
+ public function __invoke($collection)
+ {
+ return $collection
+ ->map(new ShippingCostCalculator)
+ ->map(new TotalOrderCalculator)
+ ->map(new GiftCardPriceReducer)
+ ->buffered()
+ ...
+ }
+}
+
+// Now you can use the through() method to call all methods at once
+$collection->through(new FinalCheckOutRowProcessor);
+```
+
+### Optimizing Collections
+
+### buffered()
+
+`method` Cake\\Collection\\Collection::**buffered**(): CollectionInterface
+
+Collections often perform most operations that you create using its functions in
+a lazy way. This means that even though you can call a function, it does not
+mean it is executed right away. This is true for a great deal of functions in
+this class. Lazy evaluation allows you to save resources in situations
+where you don't use all the values in a collection. You might not use all the
+values when iteration stops early, or when an exception/failure case is reached
+early.
+
+Additionally, lazy evaluation helps speed up some operations. Consider the
+following example:
+
+``` php
+$collection = new Collection($oneMillionItems);
+$collection = $collection->map(function ($item) {
+ return $item * 2;
+});
+$itemsToShow = $collection->take(30);
+```
+
+Had the collections not been lazy, we would have executed one million operations,
+even though we only wanted to show 30 elements out of it. Instead, our map
+operation was only applied to the 30 elements we used. We can also
+derive benefits from this lazy evaluation for smaller collections when we
+do more than one operation on them. For example: calling `map()` twice and
+then `filter()`.
+
+Lazy evaluation comes with its downside too. You could be doing the same
+operations more than once if you optimize a collection prematurely. Consider
+this example:
+
+``` php
+$ages = $collection->extract('age');
+
+$youngerThan30 = $ages->filter(function ($item) {
+ return $item < 30;
+});
+
+$olderThan30 = $ages->filter(function ($item) {
+ return $item > 30;
+});
+```
+
+If we iterate both `youngerThan30` and `olderThan30`, the collection would
+unfortunately execute the `extract()` operation twice. This is because
+collections are immutable and the lazy-extracting operation would be done for
+both filters.
+
+Luckily we can overcome this issue with a single function. If you plan to reuse
+the values from certain operations more than once, you can compile the results
+into another collection using the `buffered()` function:
+
+``` php
+$ages = $collection->extract('age')->buffered();
+$youngerThan30 = ...
+$olderThan30 = ...
+```
+
+Now, when both collections are iterated, they will only call the
+extracting operation once.
+
+### Making Collections Rewindable
+
+The `buffered()` method is also useful for converting non-rewindable iterators
+into collections that can be iterated more than once:
+
+``` php
+public function results()
+{
+ ...
+ foreach ($transientElements as $e) {
+ yield $e;
+ }
+}
+$rewindable = (new Collection(results()))->buffered();
+```
+
+### Cloning Collections
+
+### compile()
+
+`method` Cake\\Collection\\Collection::**compile**($preserveKeys = true): CollectionInterface
+
+Sometimes you need to get a clone of the elements from another
+collection. This is useful when you need to iterate the same set from different
+places at the same time. In order to clone a collection out of another use the
+`compile()` method:
+
+``` php
+$ages = $collection->extract('age')->compile();
+
+foreach ($ages as $age) {
+ foreach ($collection as $element) {
+ echo h($element->name) . ' - ' . $age;
+ }
+}
+```
diff --git a/docs/en/core-libraries/email.md b/docs/en/core-libraries/email.md
new file mode 100644
index 0000000000..24dc83edce
--- /dev/null
+++ b/docs/en/core-libraries/email.md
@@ -0,0 +1,716 @@
+# Mailer
+
+`class` Cake\\Mailer\\**Mailer**(string|array|null $profile = null)
+
+`Mailer` is a convenience class for sending email. With this class you can send
+email from any place inside of your application.
+
+## Basic Usage
+
+First of all, you should ensure the class is loaded:
+
+``` php
+use Cake\Mailer\Mailer;
+```
+
+After you've loaded `Mailer`, you can send an email with the following:
+
+``` php
+$mailer = new Mailer('default');
+$mailer->setFrom(['me@example.com' => 'My Site'])
+ ->setTo('you@example.com')
+ ->setSubject('About')
+ ->deliver('My message');
+```
+
+Since `Mailer`'s setter methods return the instance of the class, you are able
+to set its properties with method chaining.
+
+`Mailer` has several methods for defining recipients - `setTo()`, `setCc()`,
+`setBcc()`, `addTo()`, `addCc()` and `addBcc()`. The main difference being
+that the first three will overwrite what was already set and the latter will just
+add more recipients to their respective field:
+
+``` php
+$mailer = new Mailer();
+$mailer->setTo('to@example.com', 'To Example');
+$mailer->addTo('to2@example.com', 'To2 Example');
+// The email's To recipients are: to@example.com and to2@example.com
+$mailer->setTo('test@example.com', 'ToTest Example');
+// The email's To recipient is: test@example.com
+```
+
+### Choosing the Sender
+
+When sending email on behalf of other people, it's often a good idea to define the
+original sender using the Sender header. You can do so using `setSender()`:
+
+``` php
+$mailer = new Mailer();
+$mailer->setSender('app@example.com', 'MyApp emailer');
+```
+
+> [!NOTE]
+> It's also a good idea to set the envelope sender when sending mail on another
+> person's behalf. This prevents them from getting any messages about
+> deliverability.
+
+
+
+## Configuration
+
+Mailer profiles and email transport settings are defined in your application's
+configuration files. The `Email` and `EmailTransport` keys define mailer
+profiles and email transport configurations respectively. During application
+bootstrap configuration settings are passed from `Configure` into the
+`Mailer` and `TransportFactory` classes using `setConfig()`. By defining
+profiles and transports, you can keep your application code free of
+configuration data, and avoid duplication that makes maintenance and deployment
+more difficult.
+
+To load a predefined configuration, you can use the `setProfile()` method or
+pass it to the constructor of `Mailer`:
+
+``` php
+$mailer = new Mailer();
+$mailer->setProfile('default');
+
+// Or in constructor
+$mailer = new Mailer('default');
+```
+
+Instead of passing a string which matches a preset configuration name, you can
+also just load an array of options:
+
+``` php
+$mailer = new Mailer();
+$mailer->setProfile(['from' => 'me@example.org', 'transport' => 'my_custom']);
+
+// Or in constructor
+$mailer = new Mailer(['from' => 'me@example.org', 'transport' => 'my_custom']);
+```
+
+
+
+### Configuration Profiles
+
+Defining delivery profiles allows you to consolidate common email settings into
+re-usable profiles. Your application can have as many profiles as necessary. The
+following configuration keys are used:
+
+- `'from'`: Mailer or array of sender. See `Mailer::setFrom()`.
+- `'sender'`: Mailer or array of real sender. See `Mailer::setSender()`.
+- `'to'`: Mailer or array of destination. See `Mailer::setTo()`.
+- `'cc'`: Mailer or array of carbon copy. See `Mailer::setCc()`.
+- `'bcc'`: Mailer or array of blind carbon copy. See `Mailer::setBcc()`.
+- `'replyTo'`: Mailer or array to reply the e-mail. See `Mailer::setReplyTo()`.
+- `'readReceipt'`: Mailer address or an array of addresses to receive the
+ receipt of read. See `Mailer::setReadReceipt()`.
+- `'returnPath'`: Mailer address or an array of addresses to return if have
+ some error. See `Mailer::setReturnPath()`.
+- `'messageId'`: Message ID of e-mail. See `Mailer::setMessageId()`.
+- `'subject'`: Subject of the message. See `Mailer::setSubject()`.
+- `'message'`: Content of message. Do not set this field if you are using rendered content.
+- `'priority'`: Priority of the email as numeric value (usually from 1 to 5 with 1 being the highest).
+- `'headers'`: Headers to be included. See `Mailer::setHeaders()`.
+- `'viewRenderer'`: If you are using rendered content, set the view classname.
+ See `ViewBuilder::setClassName()`.
+- `'template'`: If you are using rendered content, set the template name. See
+ `ViewBuilder::setTemplate()`.
+- `'theme'`: Theme used when rendering template. See `ViewBuilder::setTheme()`.
+- `'layout'`: If you are using rendered content, set the layout to render. See
+ `ViewBuilder::setTemplate()`.
+- `'autoLayout'`: If you want to render a template without layout, set this field to
+ `false`. See `ViewBuilder::disableAutoLayout()`.
+- `'viewVars'`: If you are using rendered content, set the array with
+ variables to be used in the view. See `Mailer::setViewVars()`.
+- `'attachments'`: List of files to attach. See `Mailer::setAttachments()`.
+- `'emailFormat'`: Format of email (html, text or both). See `Mailer::setEmailFormat()`.
+- `'transport'`: Transport configuration name. See [Email Transport](#email-transport).
+- `'log'`: Log level to log the email headers and message. `true` will use
+ LOG_DEBUG. See [Logging Levels](../core-libraries/logging#logging-levels). Note that logs will be emitted under the scope named `email`.
+ See also [Logging Scopes](../core-libraries/logging#logging-scopes).
+- `'helpers'`: Array of helpers used in the email template.
+ `ViewBuilder::setHelpers()`/`ViewBuilder::addHelpers()`.
+
+> [!NOTE]
+> The values of above keys using Mailer or array, like from, to, cc, etc will be passed
+> as first parameter of corresponding methods. The equivalent for:
+> `$mailer->setFrom('my@example.com', 'My Site')`
+> would be defined as `'from' => ['my@example.com' => 'My Site']` in your config
+
+## Setting Headers
+
+In `Mailer` you are free to set whatever headers you want. Do not forget to
+put the `X-` prefix for your custom headers.
+
+See `Mailer::setHeaders()` and `Mailer::addHeaders()`
+
+## Sending Templated Emails
+
+Emails are often much more than just a simple text message. In order
+to facilitate that, CakePHP provides a way to send emails using CakePHP's
+[view layer](../views).
+
+The templates for emails reside in a special folder `templates/email/` of your
+application. Mailer views can also use layouts and elements just like normal views:
+
+``` php
+$mailer = new Mailer();
+$mailer
+ ->setEmailFormat('html')
+ ->setTo('bob@example.com')
+ ->setFrom('app@domain.com')
+ ->viewBuilder()
+ ->setTemplate('welcome')
+ ->setLayout('fancy');
+
+$mailer->deliver();
+```
+
+The above would use **templates/email/html/welcome.php** for the view
+and **templates/layout/email/html/fancy.php** for the layout. You can
+send multipart templated email messages as well:
+
+``` php
+$mailer = new Mailer();
+$mailer
+ ->setEmailFormat('both')
+ ->setTo('bob@example.com')
+ ->setFrom('app@domain.com')
+ ->viewBuilder()
+ ->setTemplate('welcome')
+ ->setLayout('fancy');
+
+$mailer->deliver();
+```
+
+This would use the following template files:
+
+- **templates/email/text/welcome.php**
+- **templates/layout/email/text/fancy.php**
+- **templates/email/html/welcome.php**
+- **templates/layout/email/html/fancy.php**
+
+When sending templated emails you have the option of sending either
+`text`, `html` or `both`.
+
+You can set all view related config using the view builder instance got by
+`Mailer::viewBuilder()` similar to how you do the same in controller.
+
+You can set view variables with `Mailer::setViewVars()`:
+
+``` php
+$mailer = new Mailer('templated');
+$mailer->setViewVars(['value' => 12345]);
+```
+
+Or you can use the view builder methods `ViewBuilder::setVar()` and
+`ViewBuilder::setVars()`.
+
+In your email templates you can use these with:
+
+``` html
+
Here is your value: = $value ?>
+```
+
+You can use helpers in emails as well, much like you can in normal template files.
+By default only the `HtmlHelper` is loaded. You can load additional
+helpers using the `ViewBuilder::addHelpers()` method:
+
+``` php
+$mailer->viewBuilder()->addHelpers(['Html', 'Custom', 'Text']);
+```
+
+When adding helpers be sure to include 'Html' or it will be removed from the
+helpers loaded in your email template.
+
+> [!NOTE]
+> In versions prior to 4.3.0, you will need to use `setHelpers()` instead.
+
+If you want to send email using templates in a plugin you can use the familiar
+`plugin syntax` to do so:
+
+``` php
+$mailer = new Mailer();
+$mailer->viewBuilder()->setTemplate('Blog.new_comment');
+```
+
+The above would use template and layout from the Blog plugin as an example.
+
+In some cases, you might need to override the default template provided by plugins.
+You can do this using themes:
+
+``` php
+$mailer->viewBuilder()
+ ->setTemplate('Blog.new_comment')
+ ->setLayout('Blog.auto_message')
+ ->setTheme('TestTheme');
+```
+
+This allows you to override the `new_comment` template in your theme without
+modifying the Blog plugin. The template file needs to be created in the
+following path:
+**templates/plugin/TestTheme/plugin/Blog/email/text/new_comment.php**.
+
+## Sending Attachments
+
+### Mailer::setAttachments()
+
+`method` Cake\\Mailer\\Mailer::**setAttachments**($attachments)
+
+You can attach files to email messages as well. There are a few
+different formats depending on what kind of files you have, and how
+you want the filenames to appear in the recipient's mail client:
+
+1. Array: `$mailer->setAttachments(['/full/file/path/file.png'])` will
+ attach this file with the name file.png..
+
+2. Array with key:
+ `$mailer->setAttachments(['photo.png' => '/full/some_hash.png'])` will
+ attach some_hash.png with the name photo.png. The recipient will see
+ photo.png, not some_hash.png.
+
+3. Nested arrays:
+
+ ``` php
+ $mailer->setAttachments([
+ 'photo.png' => [
+ 'file' => '/full/some_hash.png',
+ 'mimetype' => 'image/png',
+ 'contentId' => 'my-unique-id',
+ ],
+ ]);
+ ```
+
+ The above will attach the file with different mimetype and with custom
+ Content ID (when set the content ID the attachment is transformed to inline).
+ The mimetype and contentId are optional in this form.
+
+ 3.1. When you are using the `contentId`, you can use the file in the HTML
+ body like ``.
+
+ 3.2. You can use the `contentDisposition` option to disable the
+ `Content-Disposition` header for an attachment. This is useful when
+ sending ical invites to clients using outlook.
+
+ 3.3 Instead of the `file` option you can provide the file contents as
+ a string using the `data` option. This allows you to attach files without
+ needing file paths to them.
+
+### Mailer::addAttachment()
+
+`method` Cake\\Mailer\\Mailer::**addAttachment**(\\Psr\\Http\\Message\\UploadedFileInterface|string $path, ?string $name, ?string $mimetype, ?string $contentId, ?bool $contentDisposition)
+
+You can also add attachments using the `addAttachment()` method.
+
+> \$mailer-\>addAttachment('/full/file/path/file.png');
+
+### Relaxing Address Validation Rules
+
+### Mailer::setEmailPattern()
+
+`method` Cake\\Mailer\\Mailer::**setEmailPattern**($pattern)
+
+If you are having validation issues when sending to non-compliant addresses, you
+can relax the pattern used to validate email addresses. This is sometimes
+necessary when dealing with some ISPs:
+
+``` php
+$mailer = new Mailer('default');
+
+// Relax the email pattern, so you can send
+// to non-conformant addresses.
+$mailer->setEmailPattern($newPattern);
+```
+
+## Sending Emails from CLI
+
+When sending emails within a CLI script (Shells, Tasks, ...) you should manually
+set the domain name for Mailer to use. It will serve as the host name for the
+message id (since there is no host name in a CLI environment):
+
+``` php
+$mailer->setDomain('www.example.org');
+// Results in message ids like ```` (valid)
+// Instead of ``` (invalid)
+```
+
+A valid message id can help to prevent emails ending up in spam folders.
+
+## Creating Reusable Emails
+
+Until now we have seen how to directly use the the `Mailer` class to create and
+send one emails. But main feature of mailer is to allow creating reusable emails
+throughout your application. They can also be used to contain multiple email
+configurations in one location. This helps keep your code DRYer and keeps email
+configuration noise out of other areas in your application.
+
+In this example we will be creating a `Mailer` that contains user-related
+emails. To create our `UserMailer`, create the file
+**src/Mailer/UserMailer.php**. The contents of the file should look like the
+following:
+
+``` php
+namespace App\Mailer;
+
+use Cake\Mailer\Mailer;
+
+class UserMailer extends Mailer
+{
+ public function welcome($user)
+ {
+ $this
+ ->setTo($user->email)
+ ->setSubject(sprintf('Welcome %s', $user->name))
+ ->viewBuilder()
+ ->setTemplate('welcome_mail'); // By default template with same name as method name is used.
+ }
+
+ public function resetPassword($user)
+ {
+ $this
+ ->setTo($user->email)
+ ->setSubject('Reset password')
+ ->setViewVars(['token' => $user->token]);
+ }
+}
+```
+
+In our example we have created two methods, one for sending a welcome email, and
+another for sending a password reset email. Each of these methods expect a user
+`Entity` and utilizes its properties for configuring each email.
+
+We are now able to use our `UserMailer` to send out our user-related emails
+from anywhere in our application. For example, if we wanted to send our welcome
+email we could do the following:
+
+``` php
+namespace App\Controller;
+
+use Cake\Mailer\MailerAwareTrait;
+
+class UsersController extends AppController
+{
+ use MailerAwareTrait;
+
+ public function register()
+ {
+ $user = $this->Users->newEmptyEntity();
+ if ($this->request->is('post')) {
+ $user = $this->Users->patchEntity($user, $this->request->getData())
+ if ($this->Users->save($user)) {
+ $this->getMailer('User')->send('welcome', [$user]);
+ }
+ }
+ $this->set('user', $user);
+ }
+}
+```
+
+If we wanted to completely separate sending a user their welcome email from our
+application's code, we can have our `UserMailer` subscribe to the
+`Model.afterSave` event. By subscribing to an event, we can keep our
+application's user-related classes completely free of email-related logic and
+instructions. For example, we could add the following to our `UserMailer`:
+
+``` php
+public function implementedEvents(): array
+{
+ return [
+ 'Model.afterSave' => 'onRegistration',
+ ];
+}
+
+public function onRegistration(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
+{
+ if ($entity->isNew()) {
+ $this->send('welcome', [$entity]);
+ }
+}
+```
+
+You can now register the mailer as an event listener and the
+`onRegistration()` method will be invoked every time the `Model.afterSave`
+event is fired:
+
+``` php
+// attach to Users event manager
+$this->Users->getEventManager()->on($this->getMailer('User'));
+```
+
+> [!NOTE]
+> For information on how to register event listener objects,
+> please refer to the [Registering Event Listeners](../core-libraries/events#registering-event-listeners) documentation.
+
+
+
+## Configuring Transports
+
+Email messages are delivered by transports. Different transports allow you to
+send messages via PHP's `mail()` function, SMTP servers, or not at all which
+is useful for debugging. Configuring transports allows you to keep configuration
+data out of your application code and makes deployment simpler as you can simply
+change the configuration data. An example transport configuration looks like:
+
+``` text
+// In config/app.php
+'EmailTransport' => [
+ // Sample Mail configuration
+ 'default' => [
+ 'className' => 'Mail',
+ ],
+ // Sample SMTP configuration
+ 'gmail' => [
+ 'host' => 'smtp.gmail.com',
+ 'port' => 587,
+ 'username' => 'my@gmail.com',
+ 'password' => 'secret',
+ 'className' => 'Smtp',
+ 'tls' => true,
+ ],
+],
+```
+
+Transports can also be configured at runtime using
+`TransportFactory::setConfig()`:
+
+``` php
+use Cake\Mailer\TransportFactory;
+
+// Define an SMTP transport
+TransportFactory::setConfig('gmail', [
+ 'host' => 'ssl://smtp.gmail.com',
+ 'port' => 465,
+ 'username' => 'my@gmail.com',
+ 'password' => 'secret',
+ 'className' => 'Smtp'
+]);
+```
+
+You can configure SSL SMTP servers, like Gmail. To do so, put the `ssl://`
+prefix in the host and configure the port value accordingly. You can also
+enable TLS SMTP using the `tls` option:
+
+``` php
+use Cake\Mailer\TransportFactory;
+
+TransportFactory::setConfig('gmail', [
+ 'host' => 'smtp.gmail.com',
+ 'port' => 587,
+ 'username' => 'my@gmail.com',
+ 'password' => 'secret',
+ 'className' => 'Smtp',
+ 'tls' => true
+]);
+```
+
+The above configuration would enable TLS communication for email messages.
+
+To configure your mailer to use a specific transport you can use
+`Cake\Mailer\Mailer::setTransport()` method or have the transport
+in your configuration:
+
+``` php
+// Use a named transport already configured using TransportFactory::setConfig()
+$mailer->setTransport('gmail');
+
+// Use a constructed object.
+$mailer->setTransport(new \Cake\Mailer\Transport\DebugTransport());
+```
+
+> [!WARNING]
+> You will need to have access for less secure apps enabled in your Google
+> account for this to work:
+> [Allowing less secure apps to access your
+> account](https://support.google.com/accounts/answer/6010255).
+
+> [!NOTE]
+> [Gmail SMTP settings](https://support.google.com/a/answer/176600?hl=en).
+
+> [!NOTE]
+> To use SSL + SMTP, you will need to have the SSL configured in your PHP
+> install.
+
+Configuration options can also be provided as a `DSN` string. This is
+useful when working with environment variables or `PaaS` providers:
+
+``` css
+TransportFactory::setConfig('default', [
+ 'url' => 'smtp://my@gmail.com:secret@smtp.gmail.com:587?tls=true',
+]);
+```
+
+When using a DSN string you can define any additional parameters/options as
+query string arguments.
+
+### Mailer::drop()
+
+`static` Cake\\Mailer\\Mailer::**drop**($key)
+
+Once configured, transports cannot be modified. In order to modify a transport
+you must first drop it and then reconfigure it.
+
+### Creating Custom Transports
+
+You are able to create your custom transports for situations such as send email using services
+like SendGrid, MailGun or Postmark. To create your transport, first create the file
+**src/Mailer/Transport/ExampleTransport.php** (where Example is the name of your
+transport). To start, your file should look like:
+
+``` php
+namespace App\Mailer\Transport;
+
+use Cake\Mailer\AbstractTransport;
+use Cake\Mailer\Message;
+
+class ExampleTransport extends AbstractTransport
+{
+ public function send(Message $message): array
+ {
+ // Do something.
+ }
+}
+```
+
+You must implement the method `send(Message $message)` with your custom logic.
+
+## Sending emails without using Mailer
+
+The `Mailer` is a higher level abstraction class which acts as a bridge between
+the `Cake\Mailer\Message`, `Cake\Mailer\Renderer` and `Cake\Mailer\AbstractTransport`
+classes to configure emails with a fluent interface.
+
+If you want you can use these classes directly with the `Mailer` too.
+
+For example:
+
+``` php
+$render = new \Cake\Mailer\Renderer();
+$render->viewBuilder()
+ ->setTemplate('custom')
+ ->setLayout('sparkly');
+
+$message = new \Cake\Mailer\Message();
+$message
+ ->setFrom('admin@cakephp.org')
+ ->setTo('user@foo.com')
+ ->setBody($render->render());
+
+$transport = new \Cake\Mailer\Transport\MailTransport();
+$result = $transport->send($message);
+```
+
+You can even skip using the `Renderer` and set the message body directly
+using `Message::setBodyText()` and `Message::setBodyHtml()` methods.
+
+
+
+## Testing Mailers
+
+To test mailers, add `Cake\TestSuite\EmailTrait` to your test case.
+The `EmailTrait` uses PHPUnit hooks to replace your application's email transports
+with a proxy that intercepts email messages and allows you to do assertions
+on the mail that would be delivered.
+
+Add the trait to your test case to start testing emails, and load routes if your
+emails need to generate URLs:
+
+``` php
+namespace App\Test\TestCase\Mailer;
+
+use App\Mailer\WelcomeMailer;
+use App\Model\Entity\User;
+
+use Cake\TestSuite\EmailTrait;
+use Cake\TestSuite\TestCase;
+
+class WelcomeMailerTestCase extends TestCase
+{
+ use EmailTrait;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->loadRoutes();
+ }
+}
+```
+
+Let's assume we have a mailer that delivers welcome emails when a new user
+registers. We want to check that the subject and body contain the user's name:
+
+``` php
+// in our WelcomeMailerTestCase class.
+public function testName()
+{
+ $user = new User([
+ 'name' => 'Alice Alittea',
+ 'email' => 'alice@example.org',
+ ]);
+ $mailer = new WelcomeMailer();
+ $mailer->send('welcome', [$user]);
+
+ $this->assertMailSentTo($user->email);
+ $this->assertMailContainsText('Hi ' . $user->name);
+ $this->assertMailContainsText('Welcome to CakePHP!');
+}
+```
+
+### Assertion methods
+
+The `Cake\TestSuite\EmailTrait` trait provides the following assertions:
+
+``` php
+// Asserts an expected number of emails were sent
+$this->assertMailCount($count);
+
+// Asserts that no emails were sent
+$this->assertNoMailSent();
+
+// Asserts an email was sent to an address
+$this->assertMailSentTo($address);
+
+// Asserts an email was sent from an address
+$this->assertMailSentFrom($emailAddress);
+$this->assertMailSentFrom([$emailAddress => $displayName]);
+
+// Asserts an email contains expected contents
+$this->assertMailContains($contents);
+
+// Asserts an email contains expected html contents
+$this->assertMailContainsHtml($contents);
+
+// Asserts an email contains expected text contents
+$this->assertMailContainsText($contents);
+
+// Asserts an email contains the expected value within an Message getter (for example, "subject")
+$this->assertMailSentWith($expected, $parameter);
+
+// Asserts an email at a specific index was sent to an address
+$this->assertMailSentToAt($at, $address);
+
+// Asserts an email at a specific index was sent from an address
+$this->assertMailSentFromAt($at, $address);
+
+// Asserts an email at a specific index contains expected contents
+$this->assertMailContainsAt($at, $contents);
+
+// Asserts an email at a specific index contains expected html contents
+$this->assertMailContainsHtmlAt($at, $contents);
+
+// Asserts an email at a specific index contains expected text contents
+$this->assertMailContainsTextAt($at, $contents);
+
+// Asserts an email contains an attachment
+$this->assertMailContainsAttachment('test.png');
+
+// Asserts an email at a specific index contains the expected value within an Message getter (for example, "cc")
+$this->assertMailSentWithAt($at, $expected, $parameter);
+
+// Asserts an email contains a substring in the subject.
+$this->assertMailSubjectContains('Free Offer');
+
+// Asserts an email at the specific index contains a substring in the subject.
+$this->assertMailSubjectContainsAt(1, 'Free Offer');
+```
diff --git a/docs/en/core-libraries/events.md b/docs/en/core-libraries/events.md
new file mode 100644
index 0000000000..6c1b953553
--- /dev/null
+++ b/docs/en/core-libraries/events.md
@@ -0,0 +1,667 @@
+# Events System
+
+Creating maintainable applications is both a science and an art. It is
+well-known that a key for having good quality code is making your objects
+loosely coupled and strongly cohesive at the same time. Cohesion means that
+all methods and properties for a class are strongly related to the class
+itself and it is not trying to do the job other objects should be doing,
+while loosely coupling is the measure of how little a class is "wired"
+to external objects, and how much that class is depending on them.
+
+There are certain cases where you need to cleanly communicate with other parts
+of an application, without having to hard code dependencies, thus losing
+cohesion and increasing class coupling. Using the Observer pattern, which allows
+objects to notify other objects and anonymous listeners about changes is
+a useful pattern to achieve this goal.
+
+Listeners in the observer pattern can subscribe to events and choose to act upon
+them if they are relevant. If you have used JavaScript, there is a good chance
+that you are already familiar with event driven programming.
+
+CakePHP emulates several aspects of how events are triggered and managed in
+popular JavaScript libraries such as jQuery. In the CakePHP implementation, an
+event object is dispatched to all listeners. The event object holds information
+about the event, and provides the ability to stop event propagation at any
+point. Listeners can register themselves or can delegate this task to other
+objects and have the chance to alter the state and the event itself for the rest
+of the callbacks.
+
+The event subsystem is at the heart of Model, Behavior, Controller, View and
+Helper callbacks. If you've ever used any of them, you are already somewhat
+familiar with events in CakePHP.
+
+## Example Event Usage
+
+Let's suppose you are building a Cart plugin, and you'd like to focus on just
+handling order logic. You don't really want to include shipping logic, emailing
+the user or decrementing the item from the stock, but these are important tasks
+to the people using your plugin. If you were not using events, you may try to
+implement this by attaching behaviors to models, or adding components to your
+controllers. Doing so represents a challenge most of the time, since you
+would have to come up with the code for externally loading those behaviors or
+attaching hooks to your plugin controllers.
+
+Instead, you can use events to allow you to cleanly separate the concerns of
+your code and allow additional concerns to hook into your plugin using events.
+For example, in your Cart plugin you have an Orders model that deals with
+creating orders. You'd like to notify the rest of the application that an order
+has been created. To keep your Orders model clean you could use events:
+
+``` php
+// Cart/Model/Table/OrdersTable.php
+namespace Cart\Model\Table;
+
+use Cake\Event\Event;
+use Cake\ORM\Table;
+
+class OrdersTable extends Table
+{
+ public function place($order)
+ {
+ if ($this->save($order)) {
+ $this->Cart->remove($order);
+ $event = new Event('Order.afterPlace', $this, [
+ 'order' => $order
+ ]);
+ $this->getEventManager()->dispatch($event);
+
+ return true;
+ }
+
+ return false;
+ }
+}
+```
+
+The above code allows you to notify the other parts of the application
+that an order has been created. You can then do tasks like send email
+notifications, update stock, log relevant statistics and other tasks in separate
+objects that focus on those concerns.
+
+## Accessing Event Managers
+
+In CakePHP events are triggered against event managers. Event managers are
+available in every Table, View and Controller using `getEventManager()`:
+
+``` php
+$events = $this->getEventManager();
+```
+
+Each model has a separate event manager, while the View and Controller
+share one. This allows model events to be self contained, and allow components
+or controllers to act upon events created in the view if necessary.
+
+### Global Event Manager
+
+In addition to instance level event managers, CakePHP provides a global event
+manager that allows you to listen to any event fired in an application. This is
+useful when attaching listeners to a specific instance might be cumbersome or
+difficult. The global manager is a singleton instance of
+`Cake\Event\EventManager`. Listeners attached to the global
+dispatcher will be fired before instance listeners at the same priority. You can
+access the global manager using a static method:
+
+``` php
+// In any configuration file or piece of code that executes before the event
+use Cake\Event\EventManager;
+
+EventManager::instance()->on(
+ 'Order.afterPlace',
+ $aCallback
+);
+```
+
+One important thing you should consider is that there are events that will be
+triggered having the same name but different subjects, so checking it in the
+event object is usually required in any function that gets attached globally in
+order to prevent some bugs. Remember that with the flexibility of using the
+global manager, some additional complexity is incurred.
+
+`Cake\Event\EventManager::dispatch()` method accepts the event
+object as an argument and notifies all listener and callbacks passing this
+object along. The listeners will handle all the extra logic around the
+`afterPlace` event, you can log the time, send emails, update user statistics
+possibly in separate objects and even delegating it to offline tasks if you have
+the need.
+
+
+
+### Tracking Events
+
+To keep a list of events that are fired on a particular `EventManager`, you
+can enable event tracking. To do so, simply attach an
+`Cake\Event\EventList` to the manager:
+
+``` php
+EventManager::instance()->setEventList(new EventList());
+```
+
+After firing an event on the manager, you can retrieve it from the event list:
+
+``` php
+$eventsFired = EventManager::instance()->getEventList();
+$firstEvent = $eventsFired[0];
+```
+
+Tracking can be disabled by removing the event list or calling
+`Cake\Event\EventManager::trackEvents(false)`.
+
+## Core Events
+
+There are a number of core events within the framework which your application
+can listen to. Each layer of CakePHP emits events that you can use in your
+application.
+
+- [ORM/Model events](../orm/table-objects#table-callbacks)
+- [Controller events](../controllers#controller-life-cycle)
+- [View events](../views#view-events)
+
+### `Server.terminate`
+
+The `Server.terminate` event is triggered after the response has been sent to the
+client. This event is useful for performing tasks that should be done after the
+response has been sent, such as logging or sending emails.
+
+You can listen to this event using an event manager instance:
+
+``` php
+use Cake\Event\EventManager;
+
+EventManager::instance()->on('Server.terminate', function ($event) {
+ // Perform tasks that should be done after the response has been
+ // sent to the client.
+});
+```
+
+Or using the `events` hook in your Application/Plugin class:
+
+``` php
+use Cake\Event\EventManagerInterface;
+
+public function events(EventManagerInterface $eventManager): EventManagerInterface
+{
+ $eventManager->on('Server.terminate', function ($event) {
+ // Perform tasks that should be done after the response has been
+ // sent to the client.
+ });
+
+ return $eventManager;
+}
+```
+
+> [!TIP]
+> This is called even if an exception is thrown during the request, e.g. on 404 pages.
+
+> [!NOTE]
+> The `Server.terminate` event only works for PHP-FPM implementations which
+> support the `fastcgi_finish_request` function.
+
+### `Command.beforeExecute` & `Command.afterExecute`
+
+::: info Added in version 5.0.0
+The `Command.beforeExecute` & `Command.afterExecute` events were added in 5.0
+:::
+
+The `Command.beforeExecute` & `Command.afterExecute` events are triggered before and after
+a command has been executed. Both events contain the `args` which are passed to the command
+and the `afterExecute` event also contains the `exitCode` which is returned by the command.
+
+You can listen to this event using an event manager instance:
+
+``` php
+use Cake\Event\EventManager;
+
+EventManager::instance()->on('Command.beforeExecute', function ($event, $args) {
+ $command = $event->getSubject();
+ // Do stuff here
+});
+
+EventManager::instance()->on('Command.afterExecute', function ($event, $args, $result) {
+ $command = $event->getSubject();
+ // Do stuff here
+});
+```
+
+Or using the `events` hook in your Application/Plugin class:
+
+``` php
+use Cake\Event\EventManagerInterface;
+
+public function events(EventManagerInterface $eventManager): EventManagerInterface
+{
+ $eventManager->on('Command.beforeExecute', function ($event, $args) {
+ $command = $event->getSubject();
+ // Do stuff here
+ });
+
+ $eventManager->on('Command.afterExecute', function ($event, $args, $result) {
+ $command = $event->getSubject();
+ // Do stuff here
+ });
+
+ return $eventManager;
+}
+```
+
+
+
+## Registering Listeners
+
+Listeners are the preferred way to register callbacks for an event. This is done
+by implementing the `Cake\Event\EventListenerInterface` interface
+in any class you wish to register some callbacks. Classes implementing it need
+to provide the `implementedEvents()` method. This method must return an
+associative array with all event names that the class will handle.
+
+To continue our previous example, let's imagine we have a UserStatistic class
+responsible for calculating a user's purchasing history, and compiling into
+global site statistics. This is a great place to use a listener class. Doing so
+allows you to concentrate the statistics logic in one place and react to events
+as necessary. Our `UserStatistics` listener might start out like:
+
+``` php
+namespace App\Event;
+
+use Cake\Event\EventListenerInterface;
+
+class UserStatistic implements EventListenerInterface
+{
+ public function implementedEvents(): array
+ {
+ return [
+ // Custom event names let you design your application events
+ // as required.
+ 'Order.afterPlace' => 'updateBuyStatistic',
+ ];
+ }
+
+ public function updateBuyStatistic($event)
+ {
+ // Code to update statistics
+ }
+}
+
+// From your controller, attach the UserStatistic object to the Order's event manager
+$statistics = new UserStatistic();
+$this->Orders->getEventManager()->on($statistics);
+```
+
+As you can see in the above code, the `on()` function will accept instances
+of the `EventListener` interface. Internally, the event manager will use
+`implementedEvents()` to attach the correct callbacks.
+
+::: info Added in version 5.1.0
+The `events` hook was added to the `BaseApplication` as well as the `BasePlugin` class
+:::
+
+As of CakePHP 5.1 it is recommended to register event listeners by adding them via the `events` hook in your application or plugin class:
+
+``` php
+namespace App;
+
+use App\Event\UserStatistic;
+use Cake\Event\EventManagerInterface;
+use Cake\Http\BaseApplication;
+
+class Application extends BaseApplication
+{
+ // The rest of your Application class
+
+ public function events(EventManagerInterface $eventManager): EventManagerInterface
+ {
+ $statistics = new UserStatistic();
+ $eventManager->on($statistics);
+
+ return $eventManager;
+ }
+}
+```
+
+### Registering Anonymous Listeners
+
+While event listener objects are generally a better way to implement listeners,
+you can also bind any `callable` as an event listener. For example if we
+wanted to put any orders into the log files, we could use a simple anonymous
+function to do so:
+
+``` php
+use Cake\Log\Log;
+
+// From within a controller, or during application bootstrap.
+$this->Orders->getEventManager()->on('Order.afterPlace', function ($event) {
+ Log::write(
+ 'info',
+ 'A new order was placed with id: ' . $event->getSubject()->id
+ );
+});
+```
+
+In addition to anonymous functions you can use any other callable type that PHP
+supports:
+
+``` php
+$events = [
+ 'email-sending' => 'EmailSender::sendBuyEmail',
+ 'inventory' => [$this->InventoryManager, 'decrement'],
+];
+foreach ($events as $callable) {
+ $eventManager->on('Order.afterPlace', $callable);
+}
+```
+
+When working with plugins that don't trigger specific events, you can leverage
+event listeners on the default events. Lets take an example 'UserFeedback'
+plugin which handles feedback forms from users. From your application you would
+like to know when a Feedback record has been saved and ultimately act on it. You
+can listen to the global `Model.afterSave` event. However, you can take
+a more direct approach and only listen to the event you really need:
+
+``` php
+// You can create the following before the
+// save operation, ie. config/bootstrap.php
+use Cake\Datasource\FactoryLocator;
+// If sending emails
+use Cake\Mailer\Email;
+
+FactoryLocator::get('Table')->get('ThirdPartyPlugin.Feedbacks')
+ ->getEventManager()
+ ->on('Model.afterSave', function($event, $entity)
+ {
+ // For example we can send an email to the admin
+ $email = new Email('default');
+ $email->setFrom(['info@yoursite.com' => 'Your Site'])
+ ->setTo('admin@yoursite.com')
+ ->setSubject('New Feedback - Your Site')
+ ->send('Body of message');
+ });
+```
+
+You can use this same approach to bind listener objects.
+
+### Interacting with Existing Listeners
+
+Assuming several event listeners have been registered the presence or absence
+of a particular event pattern can be used as the basis of some action.:
+
+``` php
+// Attach listeners to EventManager.
+$this->getEventManager()->on('User.Registration', [$this, 'userRegistration']);
+$this->getEventManager()->on('User.Verification', [$this, 'userVerification']);
+$this->getEventManager()->on('User.Authorization', [$this, 'userAuthorization']);
+
+// Somewhere else in your application.
+$events = $this->getEventManager()->matchingListeners('Verification');
+if (!empty($events)) {
+ // Perform logic related to presence of 'Verification' event listener.
+ // For example removing the listener if present.
+ $this->getEventManager()->off('User.Verification');
+} else {
+ // Perform logic related to absence of 'Verification' event listener
+}
+```
+
+> [!NOTE]
+> The pattern passed to the `matchingListeners` method is case sensitive.
+
+
+
+### Establishing Priorities
+
+In some cases you might want to control the order that listeners are invoked.
+For instance, if we go back to our user statistics example. It would be ideal if
+this listener was called at the end of the stack. By calling it at the end of
+the listener stack, we can ensure that the event was not cancelled, and that no
+other listeners raised exceptions. We can also get the final state of the
+objects in the case that other listeners have modified the subject or event
+object.
+
+Priorities are defined as an integer when adding a listener. The higher the
+number, the later the method will be fired. The default priority for all
+listeners is `10`. If you need your method to be run earlier, using any value
+below this default will work. On the other hand if you desire to run the
+callback after the others, using a number above `10` will do.
+
+If two callbacks happen to have the same priority value, they will be executed
+with a the order they were attached. You set priorities using the `on()`
+method for callbacks, and declaring it in the `implementedEvents()` function
+for event listeners:
+
+``` php
+// Setting priority for a callback
+$callback = [$this, 'doSomething'];
+$this->getEventManager()->on(
+ 'Order.afterPlace',
+ ['priority' => 2],
+ $callback
+);
+
+// Setting priority for a listener
+class UserStatistic implements EventListenerInterface
+{
+ public function implementedEvents(): array
+ {
+ return [
+ 'Order.afterPlace' => [
+ 'callable' => 'updateBuyStatistic',
+ 'priority' => 100
+ ],
+ ];
+ }
+}
+```
+
+As you see, the main difference for `EventListener` objects is that you need
+to use an array for specifying the callable method and the priority preference.
+The `callable` key is a special array entry that the manager will read to know
+what function in the class it should be calling.
+
+### Getting Event Data as Function Parameters
+
+When events have data provided in their constructor, the provided data is
+converted into arguments for the listeners. An example from the View layer is
+the afterRender callback:
+
+``` php
+$this->getEventManager()
+ ->dispatch(new Event('View.afterRender', $this, ['view' => $viewFileName]));
+```
+
+The listeners of the `View.afterRender` callback should have the following
+signature:
+
+``` javascript
+function (EventInterface $event, $viewFileName)
+```
+
+Each value provided to the Event constructor will be converted into function
+parameters in the order they appear in the data array. If you use an associative
+array, the result of `array_values` will determine the function argument
+order.
+
+> [!NOTE]
+> Unlike in 2.x, converting event data to listener arguments is the default
+> behavior and cannot be disabled.
+
+## Dispatching Events
+
+Once you have obtained an instance of an event manager you can dispatch events
+using `Cake\Event\EventManager::dispatch()`. This method takes an
+instance of the `Cake\Event\Event` class. Let's look at dispatching
+an event:
+
+``` php
+// An event listener has to be instantiated before dispatching an event.
+// Create a new event and dispatch it.
+$event = new Event('Order.afterPlace', $this, [
+ 'order' => $order
+]);
+$this->getEventManager()->dispatch($event);
+```
+
+`Cake\Event\Event` accepts 3 arguments in its constructor. The
+first one is the event name, you should try to keep this name as unique as
+possible, while making it readable. We suggest a convention as follows:
+`Layer.eventName` for general events happening at a layer level (for example,
+`Controller.startup`, `View.beforeRender`) and `Layer.Class.eventName` for
+events happening in specific classes on a layer, for example
+`Model.User.afterRegister` or `Controller.Courses.invalidAccess`.
+
+The second argument is the `subject`, meaning the object associated to the
+event, usually when it is the same class triggering events about itself, using
+`$this` will be the most common case. Although a Component could trigger
+controller events too. The subject class is important because listeners will get
+immediate access to the object properties and have the chance to inspect or
+change them on the fly.
+
+Finally, the third argument is any additional event data. This can be any data
+you consider useful to pass around so listeners can act upon it. While this can
+be an argument of any type, we recommend passing an associative array.
+
+The `Cake\Event\EventManager::dispatch()` method accepts an event
+object as an argument and notifies all subscribed listeners.
+
+
+
+### Stopping Events
+
+Much like DOM events, you may want to stop an event to prevent additional
+listeners from being notified. You can see this in action during model callbacks
+(for example, beforeSave) in which it is possible to stop the saving operation if
+the code detects it cannot proceed any further.
+
+In order to stop events you can either return `false` in your callbacks or
+call the `stopPropagation()` method on the event object:
+
+``` php
+public function doSomething($event)
+{
+ // ...
+ return false; // Stops the event
+}
+
+public function updateBuyStatistic($event)
+{
+ // ...
+ $event->stopPropagation();
+}
+```
+
+Stopping an event will prevent any additional callbacks from being called.
+Additionally the code triggering the event may behave differently based on the
+event being stopped or not. Generally it does not make sense to stop 'after'
+events, but stopping 'before' events is often used to prevent the entire
+operation from occurring.
+
+To check if an event was stopped, you call the `isStopped()` method in the
+event object:
+
+``` php
+public function place($order)
+{
+ $event = new Event('Order.beforePlace', $this, ['order' => $order]);
+ $this->getEventManager()->dispatch($event);
+ if ($event->isStopped()) {
+ return false;
+ }
+ if ($this->Orders->save($order)) {
+ // ...
+ }
+ // ...
+}
+```
+
+In the previous example the order would not get saved if the event is stopped
+during the `beforePlace` process.
+
+### Getting Event Results
+
+Every time a callback returns a non-null non-false value, it gets stored in the
+`$result` property of the event object. This is useful when you want to allow
+callbacks to modify the event execution. Let's take again our `beforePlace`
+example and let callbacks modify the `$order` data.
+
+Event results can be altered either using the event object result property
+directly or returning the value in the callback itself:
+
+``` php
+// A listener callback
+public function doSomething($event)
+{
+ // ...
+ $alteredData = $event->getData('order') + $moreData;
+
+ return $alteredData;
+}
+
+// Another listener callback
+public function doSomethingElse($event)
+{
+ // ...
+ $event->setResult(['order' => $alteredData] + $this->result());
+}
+
+// Using the event result
+public function place($order)
+{
+ $event = new Event('Order.beforePlace', $this, ['order' => $order]);
+ $this->getEventManager()->dispatch($event);
+ if (!empty($event->getResult()['order'])) {
+ $order = $event->getResult()['order'];
+ }
+ if ($this->Orders->save($order)) {
+ // ...
+ }
+ // ...
+}
+```
+
+It is possible to alter any event object property and have the new data passed
+to the next callback. In most of the cases, providing objects as event data or
+result and directly altering the object is the best solution as the reference is
+kept the same and modifications are shared across all callback calls.
+
+### Removing Callbacks and Listeners
+
+If for any reason you want to remove any callback from the event manager just
+call the `Cake\Event\EventManager::off()` method using as
+arguments the first two parameters you used for attaching it:
+
+``` php
+// Attaching a function
+$this->getEventManager()->on('My.event', [$this, 'doSomething']);
+
+// Detaching the function
+$this->getEventManager()->off('My.event', [$this, 'doSomething']);
+
+// Attaching an anonymous function.
+$myFunction = function ($event) { ... };
+$this->getEventManager()->on('My.event', $myFunction);
+
+// Detaching the anonymous function
+$this->getEventManager()->off('My.event', $myFunction);
+
+// Adding a EventListener
+$listener = new MyEventLister();
+$this->getEventManager()->on($listener);
+
+// Detaching a single event key from a listener
+$this->getEventManager()->off('My.event', $listener);
+
+// Detaching all callbacks implemented by a listener
+$this->getEventManager()->off($listener);
+```
+
+Events are a great way of separating concerns in your application and make
+classes both cohesive and decoupled from each other. Events can be utilized to
+de-couple application code and make extensible plugins.
+
+Keep in mind that with great power comes great responsibility. Using too many
+events can make debugging harder and require additional integration testing.
+
+## Additional Reading
+
+- [Behaviors](../orm/behaviors)
+- [Command Objects](../console-commands/commands)
+- [Components](../controllers/components)
+- [Helpers](../views/helpers)
+- [Testing Events](../development/testing#testing-events)
diff --git a/docs/en/core-libraries/form.md b/docs/en/core-libraries/form.md
new file mode 100644
index 0000000000..7c1e740fbc
--- /dev/null
+++ b/docs/en/core-libraries/form.md
@@ -0,0 +1,243 @@
+# Modelless Forms
+
+`class` Cake\\Form\\**Form**
+
+Most of the time you will have forms backed by [ORM entities](../orm/entities)
+and [ORM tables](../orm/table-objects) or other persistent stores,
+but there are times when you'll need to validate user input and then perform an
+action if the data is valid. The most common example of this is a contact form.
+
+## Creating a Form
+
+Generally when using the Form class you'll want to use a subclass to define your
+form. This makes testing easier, and lets you re-use your form. Forms are put
+into **src/Form** and usually have `Form` as a class suffix. For example,
+a simple contact form would look like:
+
+``` php
+// in src/Form/ContactForm.php
+namespace App\Form;
+
+use Cake\Form\Form;
+use Cake\Form\Schema;
+use Cake\Validation\Validator;
+
+class ContactForm extends Form
+{
+ protected function _buildSchema(Schema $schema): Schema
+ {
+ return $schema->addField('name', 'string')
+ ->addField('email', ['type' => 'string'])
+ ->addField('body', ['type' => 'text']);
+ }
+
+ public function validationDefault(Validator $validator): Validator
+ {
+ $validator->minLength('name', 10)
+ ->email('email');
+
+ return $validator;
+ }
+
+ // Prior to 5.3.0 This should be
+ // `protected function _execute(array $data): bool`
+ protected function process(array $data): bool
+ {
+ // Send an email.
+ return true;
+ }
+}
+```
+
+In the above example we see the 3 hook methods that forms provide:
+
+- `_buildSchema` is used to define the schema data that is used by FormHelper
+ to create an HTML form. You can define field type, length, and precision.
+- `validationDefault` Gets a `Cake\Validation\Validator` instance
+ that you can attach validators to.
+- `process` lets you define the behavior you want to happen when
+ `execute()` is called and the data is valid.
+
+You can always define additional public methods as you need as well.
+
+::: info Changed in version 5.3.0
+The `_execute` method was deprecated, and replaced by `process`.
+:::
+
+## Processing Request Data
+
+Once you've defined your form, you can use it in your controller to process
+and validate request data:
+
+``` php
+// In a controller
+namespace App\Controller;
+
+use App\Controller\AppController;
+use App\Form\ContactForm;
+
+class ContactController extends AppController
+{
+ public function index()
+ {
+ $contact = new ContactForm();
+ if ($this->request->is('post')) {
+ if ($contact->execute($this->request->getData())) {
+ $this->Flash->success('We will get back to you soon.');
+ } else {
+ $this->Flash->error('There was a problem submitting your form.');
+ }
+ }
+ $this->set('contact', $contact);
+ }
+}
+```
+
+In the above example, we use the `execute()` method to run our form's
+`process()` method only when the data is valid, and set flash messages
+accordingly. If we want to use a non-default validation set we can use the
+`validate` option:
+
+``` php
+if ($contact->execute($this->request->getData(), 'update')) {
+ // Handle form success.
+}
+```
+
+This option can also be set to `false` to disable validation.
+
+We could have also used the `validate()` method to only validate
+the request data:
+
+``` php
+$isValid = $form->validate($this->request->getData());
+
+// You can also use other validation sets. The following
+// would use the rules defined by `validationUpdate()`
+$isValid = $form->validate($this->request->getData(), 'update');
+```
+
+## Setting Form Values
+
+You can set default values for modelless forms using the `setData()` method.
+Values set with this method will overwrite existing data in the form object:
+
+``` php
+// In a controller
+namespace App\Controller;
+
+use App\Controller\AppController;
+use App\Form\ContactForm;
+
+class ContactController extends AppController
+{
+ public function index()
+ {
+ $contact = new ContactForm();
+ if ($this->request->is('post')) {
+ if ($contact->execute($this->request->getData())) {
+ $this->Flash->success('We will get back to you soon.');
+ } else {
+ $this->Flash->error('There was a problem submitting your form.');
+ }
+ }
+
+ if ($this->request->is('get')) {
+ $contact->setData([
+ 'name' => 'John Doe',
+ 'email' => 'john.doe@example.com'
+ ]);
+ }
+
+ $this->set('contact', $contact);
+ }
+}
+```
+
+Values should only be defined if the request method is GET, otherwise
+you will overwrite your previous POST Data which might have validation errors
+that need corrections. You can use `set()` to add or replace individual fields
+or a subset of fields:
+
+``` php
+// Set one field.
+$contact->set('name', 'John Doe');
+
+// Set multiple fields;
+$contact->set([
+ 'name' => 'John Doe',
+ 'email' => 'john.doe@example.com',
+]);
+```
+
+## Getting Form Errors
+
+Once a form has been validated you can retrieve the errors from it:
+
+``` php
+$errors = $form->getErrors();
+/* $errors contains
+[
+ 'name' => ['length' => 'Name must be at least two characters long'],
+ 'email' => ['format' => 'A valid email address is required'],
+]
+*/
+
+$error = $form->getError('email');
+/* $error contains
+[
+ 'format' => 'A valid email address is required',
+]
+*/
+```
+
+## Invalidating Individual Form Fields from Controller
+
+It is possible to invalidate individual fields from the controller without the
+use of the Validator class. The most common use case for this is when the
+validation is done on a remote server. In such case, you must manually
+invalidate the fields accordingly to the feedback from the remote server:
+
+``` php
+// in src/Form/ContactForm.php
+public function setErrors($errors)
+{
+ $this->_errors = $errors;
+}
+```
+
+According to how the validator class would have returned the errors, `$errors`
+must be in this format:
+
+``` php
+['fieldName' => ['validatorName' => 'The error message to display']]
+```
+
+Now you will be able to invalidate form fields by setting the fieldName, then
+set the error messages:
+
+``` php
+// In a controller
+$contact = new ContactForm();
+$contact->setErrors(['email' => ['_required' => 'Your email is required']]);
+```
+
+Proceed to Creating HTML with FormHelper to see the results.
+
+## Creating HTML with FormHelper
+
+Once you've created a Form class, you'll likely want to create an HTML form for
+it. FormHelper understands Form objects just like ORM entities:
+
+``` php
+echo $this->Form->create($contact);
+echo $this->Form->control('name');
+echo $this->Form->control('email');
+echo $this->Form->control('body');
+echo $this->Form->button('Submit');
+echo $this->Form->end();
+```
+
+The above would create an HTML form for the `ContactForm` we defined earlier.
+HTML forms created with FormHelper will use the defined schema and validator to
+determine field types, maxlengths, and validation errors.
diff --git a/docs/en/core-libraries/global-constants-and-functions.md b/docs/en/core-libraries/global-constants-and-functions.md
new file mode 100644
index 0000000000..ae9eefb398
--- /dev/null
+++ b/docs/en/core-libraries/global-constants-and-functions.md
@@ -0,0 +1,264 @@
+# Constants & Functions
+
+While most of your day-to-day work in CakePHP will be utilizing core classes and
+methods, CakePHP features a number of global convenience functions that may come
+in handy. Many of these functions are for use with CakePHP classes (loading
+model or component classes), but many others make working with arrays or
+strings a little easier.
+
+We'll also cover some of the constants available in CakePHP applications. Using
+these constants will help make upgrades more smooth, but are also convenient
+ways to point to certain files or directories in your CakePHP application.
+
+## Global Functions
+
+Here are CakePHP's globally available functions. Most of them are just
+convenience wrappers for other CakePHP functionality, such as debugging and
+translating content. By default only namespaced functions are autoloaded,
+however you can optionally load global aliases by adding:
+
+``` text
+require CAKE . 'functions.php';
+```
+
+To your application's `config/bootstrap.php`. Doing this will load global
+aliases for *all* functions listed below.
+
+### __()
+
+`function` **__(string $string_id, [$formatArgs])**
+
+This function handles localization in CakePHP applications. The
+`$string_id` identifies the ID for a translation. You can supply
+additional arguments to replace placeholders in your string:
+
+``` text
+__('You have {0} unread messages', $number);
+```
+
+You can also provide a name-indexed array of replacements:
+
+``` text
+__('You have {unread} unread messages', ['unread' => $number]);
+```
+
+> [!NOTE]
+> Check out the
+> [Internationalization & Localization](../core-libraries/internationalization-and-localization) section for
+> more information.
+
+### __d()
+
+`function` **__d(string $domain, string $msg, mixed $args = null)**
+
+Allows you to override the current domain for a single message lookup.
+
+Useful when internationalizing a plugin:
+`echo __d('plugin_name', 'This is my plugin');`
+
+> [!NOTE]
+> Make sure to use the underscored version of the plugin name here as domain.
+
+### __dn()
+
+`function` **__dn(string $domain, string $singular, string $plural, integer $count, mixed $args = null)**
+
+Allows you to override the current domain for a single plural message
+lookup. Returns correct plural form of message identified by `$singular`
+and `$plural` for count `$count` from domain `$domain`.
+
+### __dx()
+
+`function` **__dx(string $domain, string $context, string $msg, mixed $args = null)**
+
+Allows you to override the current domain for a single message lookup. It
+also allows you to specify a context.
+
+The context is a unique identifier for the translations string that makes it
+unique within the same domain.
+
+### __dxn()
+
+`function` **__dxn(string $domain, string $context, string $singular, string $plural, integer $count, mixed $args = null)**
+
+Allows you to override the current domain for a single plural message
+lookup. It also allows you to specify a context. Returns correct plural
+form of message identified by `$singular` and `$plural` for count
+`$count` from domain `$domain`. Some languages have more than one form
+for plural messages dependent on the count.
+
+The context is a unique identifier for the translations string that makes it
+unique within the same domain.
+
+### __n()
+
+`function` **__n(string $singular, string $plural, integer $count, mixed $args = null)**
+
+Returns correct plural form of message identified by `$singular` and
+`$plural` for count `$count`. Some languages have more than one form for
+plural messages dependent on the count.
+
+### __x()
+
+`function` **__x(string $context, string $msg, mixed $args = null)**
+
+The context is a unique identifier for the translations string that makes it
+unique within the same domain.
+
+### __xn()
+
+`function` **__xn(string $context, string $singular, string $plural, integer $count, mixed $args = null)**
+
+Returns correct plural form of message identified by `$singular` and
+`$plural` for count `$count` from domain `$domain`. It also allows you
+to specify a context. Some languages have more than one form for plural
+messages dependent on the count.
+
+The context is a unique identifier for the translations string that makes it
+unique within the same domain.
+
+### collection()
+
+`function` **collection(mixed $items)**
+
+Convenience wrapper for instantiating a new `Cake\Collection\Collection`
+object, wrapping the passed argument. The `$items` parameter takes either
+a `Traversable` object or an array.
+
+### debug()
+
+`function` **debug(mixed $var, boolean $showHtml = null, $showFrom = true)**
+
+If the core `$debug` variable is `true`, `$var` is printed out.
+If `$showHTML` is `true` or left as `null`, the data is rendered to be
+browser-friendly. If `$showFrom` is not set to `false`, the debug output
+will start with the line from which it was called. Also see
+[Debugging](../development/debugging)
+
+### dd()
+
+`function` **dd(mixed $var, boolean $showHtml = null)**
+
+It behaves like `debug()`, but execution is also halted.
+If the core `$debug` variable is `true`, `$var` is printed.
+If `$showHTML` is `true` or left as `null`, the data is rendered to be
+browser-friendly. Also see [Debugging](../development/debugging)
+
+### pr()
+
+`function` **pr(mixed $var)**
+
+Convenience wrapper for `print_r()`, with the addition of
+wrapping `
` tags around the output.
+
+### pj()
+
+`function` **pj(mixed $var)**
+
+JSON pretty print convenience function, with the addition of
+wrapping `
` tags around the output.
+
+It is meant for debugging the JSON representation of objects and arrays.
+
+### env()
+
+`function` **env(string $key, string $default = null)**
+
+Gets an environment variable from available sources. Used as a backup if
+`$_SERVER` or `$_ENV` are disabled.
+
+This function also emulates `PHP_SELF` and `DOCUMENT_ROOT` on
+unsupporting servers. In fact, it's a good idea to always use `env()`
+instead of `$_SERVER` or `getenv()` (especially if you plan to
+distribute the code), since it's a full emulation wrapper.
+
+### h()
+
+`function` **h(string $text, boolean $double = true, string $charset = null)**
+
+Convenience wrapper for `htmlspecialchars()`.
+
+### pluginSplit()
+
+`function` **pluginSplit(string $name, boolean $dotAppend = false, string $plugin = null)**
+
+Splits a dot syntax plugin name into its plugin and class name. If `$name`
+does not have a dot, then index 0 will be `null`.
+
+Commonly used like `list($plugin, $name) = pluginSplit('Users.User');`
+
+### namespaceSplit()
+
+`function` **namespaceSplit(string $class)**
+
+Split the namespace from the classname.
+
+Commonly used like `list($namespace, $className) = namespaceSplit('Cake\Core\App');`
+
+## Core Definition Constants
+
+Most of the following constants refer to paths in your application.
+
+`constant` Cake\\Core\\**APP**
+
+Absolute path to your application directory, including a trailing slash.
+
+`constant` Cake\\Core\\**APP_DIR**
+
+Equals `app` or the name of your application directory.
+
+`constant` Cake\\Core\\**CACHE**
+
+Path to the cache files directory. It can be shared between hosts in a
+multi-server setup.
+
+`constant` Cake\\Core\\**CAKE**
+
+Path to the cake directory.
+
+`constant` Cake\\Core\\**CAKE_CORE_INCLUDE_PATH**
+
+Path to the root lib directory.
+
+`constant` Cake\\Core\\**CONFIG**
+
+Path to the config directory.
+
+`constant` Cake\\Core\\**CORE_PATH**
+
+Path to the CakePHP directory with ending directory slash.
+
+`constant` Cake\\Core\\**DS**
+
+Short for PHP's `DIRECTORY_SEPARATOR`, which is `/` on Linux and `\`
+on Windows.
+
+`constant` Cake\\Core\\**LOGS**
+
+Path to the logs directory.
+
+`constant` Cake\\Core\\**RESOURCES**
+
+Path to the resources directory.
+
+`constant` Cake\\Core\\**ROOT**
+
+Path to the root directory.
+
+`constant` Cake\\Core\\**TESTS**
+
+Path to the tests directory.
+
+`constant` Cake\\Core\\**TMP**
+
+Path to the temporary files directory.
+
+`constant` Cake\\Core\\**WWW_ROOT**
+
+Full path to the webroot.
+
+## Timing Definition Constants
+
+`constant` Cake\\Core\\**TIME_START**
+
+Unix timestamp in microseconds as a float from when the application started.
diff --git a/docs/en/core-libraries/hash.md b/docs/en/core-libraries/hash.md
new file mode 100644
index 0000000000..ef0cc57f83
--- /dev/null
+++ b/docs/en/core-libraries/hash.md
@@ -0,0 +1,951 @@
+# Hash
+
+`class` Cake\\Utility\\**Hash**
+
+Array management, if done right, can be a very powerful and useful
+tool for building smarter, more optimized code. CakePHP offers a
+very useful set of static utilities in the Hash class that allow you
+to do just that.
+
+CakePHP's Hash class can be called from any model or controller in
+the same way Inflector is called. Example: `Hash::combine()`.
+
+
+
+## Hash Path Syntax
+
+The path syntax described below is used by all the methods in `Hash`. Not all
+parts of the path syntax are available in all methods. A path expression is
+made of any number of tokens. Tokens are composed of two groups. Expressions,
+are used to traverse the array data, while matchers are used to qualify
+elements. You apply matchers to expression elements.
+
+### Expression Types
+
+| Expression | Definition |
+|----|----|
+| `{n}` | Represents a numeric key. Will match any string or numeric key. |
+| `{s}` | Represents a string. Will match any string value including numeric string values. |
+| `{*}` | Matches any value. |
+| `Foo` | Matches keys with the exact same value. |
+
+All expression elements are supported by all methods. In addition to expression
+elements, you can use attribute matching with certain methods. They are `extract()`,
+`combine()`, `format()`, `check()`, `map()`, `reduce()`,
+`apply()`, `sort()`, `insert()`, `remove()` and `nest()`.
+
+### Attribute Matching Types
+
+| Matcher | Definition |
+|----|----|
+| `[id]` | Match elements with a given array key. |
+| `[id=2]` | Match elements with id equal to 2. |
+| `[id!=2]` | Match elements with id not equal to 2. |
+| `[id>2]` | Match elements with id greater than 2. |
+| `[id>=2]` | Match elements with id greater than or equal to 2. |
+| `[id<2]` | Match elements with id less than 2 |
+| `[id<=2]` | Match elements with id less than or equal to 2. |
+| `[text=/.../]` | Match elements that have values matching the regular expression inside `...`. |
+
+### Hash::get()
+
+`static` Cake\\Utility\\Hash::**get**(array|ArrayAccess $data, $path, $default = null): mixed
+
+`get()` is a simplified version of `extract()`, it only supports direct
+path expressions. Paths with `{n}`, `{s}`, `{*}` or matchers are not
+supported. Use `get()` when you want exactly one value out of an array. If
+a matching path is not found the default value will be returned.
+
+### Hash::extract()
+
+`static` Cake\\Utility\\Hash::**extract**(array|ArrayAccess $data, $path): ArrayAccess|array
+
+`Hash::extract()` supports all expression, and matcher components of
+[Hash Path Syntax](#hash-path-syntax). You can use extract to retrieve data from arrays
+or object implementing `ArrayAccess` interface, along arbitrary paths
+quickly without having to loop through the data structures. Instead you
+use path expressions to qualify which elements you want returned :
+
+``` php
+// Common Usage:
+$users = [
+ ['id' => 1, 'name' => 'mark'],
+ ['id' => 2, 'name' => 'jane'],
+ ['id' => 3, 'name' => 'sally'],
+ ['id' => 4, 'name' => 'jose'],
+];
+$results = Hash::extract($users, '{n}.id');
+// $results equals:
+// [1,2,3,4];
+```
+
+`static` Hash::**insert**(array $data, $path, $values = null): ArrayAccess|array
+
+Inserts `$values` into an array as defined by `$path`:
+
+``` php
+$a = [
+ 'pages' => ['name' => 'page']
+];
+$result = Hash::insert($a, 'files', ['name' => 'files']);
+// $result now looks like:
+[
+ [pages] => [
+ [name] => page
+ ]
+ [files] => [
+ [name] => files
+ ]
+]
+```
+
+ You can use paths using `{n}`, `{s}` and `{*}` to insert data into multiple
+points:
+
+``` php
+$users = Hash::insert($users, '{n}.new', 'value');
+```
+
+Attribute matchers work with `insert()` as well:
+
+``` php
+$data = [
+ 0 => ['up' => true, 'Item' => ['id' => 1, 'title' => 'first']],
+ 1 => ['Item' => ['id' => 2, 'title' => 'second']],
+ 2 => ['Item' => ['id' => 3, 'title' => 'third']],
+ 3 => ['up' => true, 'Item' => ['id' => 4, 'title' => 'fourth']],
+ 4 => ['Item' => ['id' => 5, 'title' => 'fifth']],
+];
+$result = Hash::insert($data, '{n}[up].Item[id=4].new', 9);
+/* $result now looks like:
+ [
+ ['up' => true, 'Item' => ['id' => 1, 'title' => 'first']],
+ ['Item' => ['id' => 2, 'title' => 'second']],
+ ['Item' => ['id' => 3, 'title' => 'third']],
+ ['up' => true, 'Item' => ['id' => 4, 'title' => 'fourth', 'new' => 9]],
+ ['Item' => ['id' => 5, 'title' => 'fifth']],
+ ]
+*/
+```
+
+### Hash::remove()
+
+`static` Cake\\Utility\\Hash::**remove**(array $data, $path): ArrayAccess|array
+
+Removes all elements from an array that match `$path`:
+
+``` php
+$a = [
+ 'pages' => ['name' => 'page'],
+ 'files' => ['name' => 'files']
+];
+$result = Hash::remove($a, 'files');
+/* $result now looks like:
+ [
+ [pages] => [
+ [name] => page
+ ]
+
+ ]
+*/
+```
+
+Using `{n}`, `{s}` and `{*}` will allow you to remove multiple values at once.
+You can also use attribute matchers with `remove()`:
+
+``` php
+$data = [
+ 0 => ['clear' => true, 'Item' => ['id' => 1, 'title' => 'first']],
+ 1 => ['Item' => ['id' => 2, 'title' => 'second']],
+ 2 => ['Item' => ['id' => 3, 'title' => 'third']],
+ 3 => ['clear' => true, 'Item' => ['id' => 4, 'title' => 'fourth']],
+ 4 => ['Item' => ['id' => 5, 'title' => 'fifth']],
+];
+$result = Hash::remove($data, '{n}[clear].Item[id=4]');
+/* $result now looks like:
+ [
+ ['clear' => true, 'Item' => ['id' => 1, 'title' => 'first']],
+ ['Item' => ['id' => 2, 'title' => 'second']],
+ ['Item' => ['id' => 3, 'title' => 'third']],
+ ['clear' => true],
+ ['Item' => ['id' => 5, 'title' => 'fifth']],
+ ]
+*/
+```
+
+### Hash::combine()
+
+`static` Cake\\Utility\\Hash::**combine**(array $data, $keyPath, $valuePath = null, $groupPath = null): array
+
+Creates an associative array using a `$keyPath` as the path to build its keys,
+and optionally `$valuePath` as path to get the values. If `$valuePath` is not
+specified, or doesn't match anything, values will be initialized to null.
+You can optionally group the values by what is obtained when following the
+path specified in `$groupPath`:
+
+``` php
+$a = [
+ [
+ 'User' => [
+ 'id' => 2,
+ 'group_id' => 1,
+ 'Data' => [
+ 'user' => 'mariano.iglesias',
+ 'name' => 'Mariano Iglesias'
+ ]
+ ]
+ ],
+ [
+ 'User' => [
+ 'id' => 14,
+ 'group_id' => 2,
+ 'Data' => [
+ 'user' => 'phpnut',
+ 'name' => 'Larry E. Masters'
+ ]
+ ]
+ ],
+];
+
+$result = Hash::combine($a, '{n}.User.id');
+/* $result now looks like:
+ [
+ [2] =>
+ [14] =>
+ ]
+*/
+
+$result = Hash::combine($a, '{n}.User.id', '{n}.User.Data.user');
+/* $result now looks like:
+ [
+ [2] => 'mariano.iglesias'
+ [14] => 'phpnut'
+ ]
+*/
+
+$result = Hash::combine($a, '{n}.User.id', '{n}.User.Data');
+/* $result now looks like:
+ [
+ [2] => [
+ [user] => mariano.iglesias
+ [name] => Mariano Iglesias
+ ]
+ [14] => [
+ [user] => phpnut
+ [name] => Larry E. Masters
+ ]
+ ]
+*/
+
+$result = Hash::combine($a, '{n}.User.id', '{n}.User.Data.name');
+/* $result now looks like:
+ [
+ [2] => Mariano Iglesias
+ [14] => Larry E. Masters
+ ]
+*/
+
+$result = Hash::combine($a, '{n}.User.id', '{n}.User.Data', '{n}.User.group_id');
+/* $result now looks like:
+ [
+ [1] => [
+ [2] => [
+ [user] => mariano.iglesias
+ [name] => Mariano Iglesias
+ ]
+ ]
+ [2] => [
+ [14] => [
+ [user] => phpnut
+ [name] => Larry E. Masters
+ ]
+ ]
+ ]
+*/
+
+$result = Hash::combine($a, '{n}.User.id', '{n}.User.Data.name', '{n}.User.group_id');
+/* $result now looks like:
+ [
+ [1] => [
+ [2] => Mariano Iglesias
+ ]
+ [2] => [
+ [14] => Larry E. Masters
+ ]
+ ]
+*/
+
+// As of 3.9.0 $keyPath can be null
+$result = Hash::combine($a, null, '{n}.User.Data.name');
+/* $result now looks like:
+ [
+ [0] => Mariano Iglesias
+ [1] => Larry E. Masters
+ ]
+*/
+```
+
+You can provide arrays for both `$keyPath` and `$valuePath`. If you do this,
+the first value will be used as a format string, for values extracted by the
+other paths:
+
+``` php
+$result = Hash::combine(
+ $a,
+ '{n}.User.id',
+ ['%s: %s', '{n}.User.Data.user', '{n}.User.Data.name'],
+ '{n}.User.group_id'
+);
+/* $result now looks like:
+ [
+ [1] => [
+ [2] => mariano.iglesias: Mariano Iglesias
+ ]
+ [2] => [
+ [14] => phpnut: Larry E. Masters
+ ]
+ ]
+*/
+
+$result = Hash::combine(
+ $a,
+ ['%s: %s', '{n}.User.Data.user', '{n}.User.Data.name'],
+ '{n}.User.id'
+);
+/* $result now looks like:
+ [
+ [mariano.iglesias: Mariano Iglesias] => 2
+ [phpnut: Larry E. Masters] => 14
+ ]
+*/
+```
+
+### Hash::format()
+
+`static` Cake\\Utility\\Hash::**format**(array $data, array $paths, $format): array|null
+
+Returns a series of values extracted from an array, formatted with a
+format string:
+
+``` php
+$data = [
+ [
+ 'Person' => [
+ 'first_name' => 'Nate',
+ 'last_name' => 'Abele',
+ 'city' => 'Boston',
+ 'state' => 'MA',
+ 'something' => '42'
+ ]
+ ],
+ [
+ 'Person' => [
+ 'first_name' => 'Larry',
+ 'last_name' => 'Masters',
+ 'city' => 'Boondock',
+ 'state' => 'TN',
+ 'something' => '{0}'
+ ]
+ ],
+ [
+ 'Person' => [
+ 'first_name' => 'Garrett',
+ 'last_name' => 'Woodworth',
+ 'city' => 'Venice Beach',
+ 'state' => 'CA',
+ 'something' => '{1}'
+ ]
+ ]
+];
+
+$res = Hash::format($data, ['{n}.Person.first_name', '{n}.Person.something'], '%2$d, %1$s');
+/*
+[
+ [0] => 42, Nate
+ [1] => 0, Larry
+ [2] => 0, Garrett
+]
+*/
+
+$res = Hash::format($data, ['{n}.Person.first_name', '{n}.Person.something'], '%1$s, %2$d');
+/*
+[
+ [0] => Nate, 42
+ [1] => Larry, 0
+ [2] => Garrett, 0
+]
+*/
+```
+
+### Hash::contains()
+
+`static` Cake\\Utility\\Hash::**contains**(array $data, array $needle): bool
+
+Determines if one Hash or array contains the exact keys and values
+of another:
+
+``` php
+$a = [
+ 0 => ['name' => 'main'],
+ 1 => ['name' => 'about']
+];
+$b = [
+ 0 => ['name' => 'main'],
+ 1 => ['name' => 'about'],
+ 2 => ['name' => 'contact'],
+ 'a' => 'b',
+];
+
+$result = Hash::contains($a, $a);
+// true
+$result = Hash::contains($a, $b);
+// false
+$result = Hash::contains($b, $a);
+// true
+```
+
+### Hash::check()
+
+`static` Cake\\Utility\\Hash::**check**(array $data, string $path = null): bool
+
+Checks if a particular path is set in an array:
+
+``` php
+$set = [
+ 'My Index 1' => ['First' => 'The first item']
+];
+$result = Hash::check($set, 'My Index 1.First');
+// $result == true
+
+$result = Hash::check($set, 'My Index 1');
+// $result == true
+
+$set = [
+ 'My Index 1' => [
+ 'First' => [
+ 'Second' => [
+ 'Third' => [
+ 'Fourth' => 'Heavy. Nesting.'
+ ]
+ ]
+ ]
+ ]
+];
+$result = Hash::check($set, 'My Index 1.First.Second');
+// $result == true
+
+$result = Hash::check($set, 'My Index 1.First.Second.Third');
+// $result == true
+
+$result = Hash::check($set, 'My Index 1.First.Second.Third.Fourth');
+// $result == true
+
+$result = Hash::check($set, 'My Index 1.First.Seconds.Third.Fourth');
+// $result == false
+```
+
+### Hash::filter()
+
+`static` Cake\\Utility\\Hash::**filter**(array $data, $callback = ['Hash', 'filter']): array
+
+Filters empty elements out of array, excluding '0'. You can also supply a
+custom `$callback` to filter the array elements. The callback should
+return `false` to remove elements from the resulting array:
+
+``` php
+$data = [
+ '0',
+ false,
+ true,
+ 0,
+ ['one thing', 'I can tell you', 'is you got to be', false]
+];
+$res = Hash::filter($data);
+
+/* $res now looks like:
+ [
+ [0] => 0
+ [2] => true
+ [3] => 0
+ [4] => [
+ [0] => one thing
+ [1] => I can tell you
+ [2] => is you got to be
+ ]
+ ]
+*/
+```
+
+### Hash::flatten()
+
+`static` Cake\\Utility\\Hash::**flatten**(array $data, string $separator = '.'): array
+
+Collapses a multi-dimensional array into a single dimension:
+
+``` php
+$arr = [
+ [
+ 'Post' => ['id' => '1', 'title' => 'First Post'],
+ 'Author' => ['id' => '1', 'user' => 'Kyle'],
+ ],
+ [
+ 'Post' => ['id' => '2', 'title' => 'Second Post'],
+ 'Author' => ['id' => '3', 'user' => 'Crystal'],
+ ],
+];
+$res = Hash::flatten($arr);
+/* $res now looks like:
+ [
+ [0.Post.id] => 1
+ [0.Post.title] => First Post
+ [0.Author.id] => 1
+ [0.Author.user] => Kyle
+ [1.Post.id] => 2
+ [1.Post.title] => Second Post
+ [1.Author.id] => 3
+ [1.Author.user] => Crystal
+ ]
+*/
+```
+
+### Hash::expand()
+
+`static` Cake\\Utility\\Hash::**expand**(array $data, string $separator = '.'): array
+
+Expands an array that was previously flattened with
+`Hash::flatten()`:
+
+``` php
+$data = [
+ '0.Post.id' => 1,
+ '0.Post.title' => First Post,
+ '0.Author.id' => 1,
+ '0.Author.user' => Kyle,
+ '1.Post.id' => 2,
+ '1.Post.title' => Second Post,
+ '1.Author.id' => 3,
+ '1.Author.user' => Crystal,
+];
+$res = Hash::expand($data);
+/* $res now looks like:
+[
+ [
+ 'Post' => ['id' => '1', 'title' => 'First Post'],
+ 'Author' => ['id' => '1', 'user' => 'Kyle'],
+ ],
+ [
+ 'Post' => ['id' => '2', 'title' => 'Second Post'],
+ 'Author' => ['id' => '3', 'user' => 'Crystal'],
+ ],
+];
+*/
+```
+
+### Hash::merge()
+
+`static` Cake\\Utility\\Hash::**merge**(array $data, array $merge[, array $n]): array
+
+This function can be thought of as a hybrid between PHP's
+`array_merge` and `array_merge_recursive`. The difference to the two
+is that if an array key contains another array then the function
+behaves recursive (unlike `array_merge`) but does not do if for keys
+containing strings (unlike `array_merge_recursive`).
+
+> [!NOTE]
+> This function will work with an unlimited amount of arguments and
+> typecasts non-array parameters into arrays.
+
+``` php
+$array = [
+ [
+ 'id' => '48c2570e-dfa8-4c32-a35e-0d71cbdd56cb',
+ 'name' => 'mysql raleigh-workshop-08 < 2008-09-05.sql ',
+ 'description' => 'Importing an sql dump',
+ ],
+ [
+ 'id' => '48c257a8-cf7c-4af2-ac2f-114ecbdd56cb',
+ 'name' => 'pbpaste | grep -i Unpaid | pbcopy',
+ 'description' => 'Remove all lines that say "Unpaid".',
+ ]
+];
+$arrayB = 4;
+$arrayC = [0 => "test array", "cats" => "dogs", "people" => 1267];
+$arrayD = ["cats" => "felines", "dog" => "angry"];
+$res = Hash::merge($array, $arrayB, $arrayC, $arrayD);
+
+/* $res now looks like:
+[
+ [0] => [
+ [id] => 48c2570e-dfa8-4c32-a35e-0d71cbdd56cb
+ [name] => mysql raleigh-workshop-08 < 2008-09-05.sql
+ [description] => Importing an sql dump
+ ]
+ [1] => [
+ [id] => 48c257a8-cf7c-4af2-ac2f-114ecbdd56cb
+ [name] => pbpaste | grep -i Unpaid | pbcopy
+ [description] => Remove all lines that say "Unpaid".
+ ]
+ [2] => 4
+ [3] => test array
+ [cats] => felines
+ [people] => 1267
+ [dog] => angry
+]
+*/
+```
+
+### Hash::numeric()
+
+`static` Cake\\Utility\\Hash::**numeric**(array $data): bool
+
+Checks to see if all the values in the array are numeric:
+
+``` php
+$data = ['one'];
+$res = Hash::numeric(array_keys($data));
+// $res is true
+
+$data = [1 => 'one'];
+$res = Hash::numeric($data);
+// $res is false
+```
+
+### Hash::dimensions()
+
+`static` Cake\\Utility\\Hash::**dimensions**(array $data): int
+
+Counts the dimensions of an array. This method will only
+consider the dimension of the first element in the array:
+
+``` php
+$data = ['one', '2', 'three'];
+$result = Hash::dimensions($data);
+// $result == 1
+
+$data = ['1' => '1.1', '2', '3'];
+$result = Hash::dimensions($data);
+// $result == 1
+
+$data = ['1' => ['1.1' => '1.1.1'], '2', '3' => ['3.1' => '3.1.1']];
+$result = Hash::dimensions($data);
+// $result == 2
+
+$data = ['1' => '1.1', '2', '3' => ['3.1' => '3.1.1']];
+$result = Hash::dimensions($data);
+// $result == 1
+
+$data = ['1' => ['1.1' => '1.1.1'], '2', '3' => ['3.1' => ['3.1.1' => '3.1.1.1']]];
+$result = Hash::dimensions($data);
+// $result == 2
+```
+
+### Hash::maxDimensions()
+
+`static` Cake\\Utility\\Hash::**maxDimensions**(array $data): int
+
+Similar to `~Hash::dimensions()`, however this method returns,
+the deepest number of dimensions of any element in the array:
+
+``` php
+$data = ['1' => '1.1', '2', '3' => ['3.1' => '3.1.1']];
+$result = Hash::maxDimensions($data);
+// $result == 2
+
+$data = ['1' => ['1.1' => '1.1.1'], '2', '3' => ['3.1' => ['3.1.1' => '3.1.1.1']]];
+$result = Hash::maxDimensions($data);
+// $result == 3
+```
+
+### Hash::map()
+
+`static` Cake\\Utility\\Hash::**map**(array $data, $path, $function): array
+
+Creates a new array, by extracting `$path`, and mapping `$function`
+across the results. You can use both expression and matching elements with
+this method:
+
+``` php
+// Call the noop function $this->noop() on every element of $data
+$result = Hash::map($data, "{n}", [$this, 'noop']);
+
+public function noop(array $array)
+{
+ // Do stuff to array and return the result
+ return $array;
+}
+```
+
+### Hash::reduce()
+
+`static` Cake\\Utility\\Hash::**reduce**(array $data, $path, $function): mixed
+
+Creates a single value, by extracting `$path`, and reducing the extracted
+results with `$function`. You can use both expression and matching elements
+with this method.
+
+### Hash::apply()
+
+`static` Cake\\Utility\\Hash::**apply**(array $data, $path, $function): mixed
+
+Apply a callback to a set of extracted values using `$function`. The function
+will get the extracted values as the first argument:
+
+``` php
+$data = [
+ ['date' => '01-01-2016', 'booked' => true],
+ ['date' => '01-01-2016', 'booked' => false],
+ ['date' => '02-01-2016', 'booked' => true]
+];
+$result = Hash::apply($data, '{n}[booked=true].date', 'array_count_values');
+/* $result now looks like:
+ [
+ '01-01-2016' => 1,
+ '02-01-2016' => 1,
+ ]
+*/
+```
+
+### Hash::sort()
+
+`static` Cake\\Utility\\Hash::**sort**(array $data, $path, $dir, $type = 'regular'): array
+
+Sorts an array by any value, determined by a [Hash Path Syntax](#hash-path-syntax)
+Only expression elements are supported by this method:
+
+``` php
+$a = [
+ 0 => ['Person' => ['name' => 'Jeff']],
+ 1 => ['Shirt' => ['color' => 'black']]
+];
+$result = Hash::sort($a, '{n}.Person.name', 'asc');
+/* $result now looks like:
+ [
+ [0] => [
+ [Shirt] => [
+ [color] => black
+ ]
+ ]
+ [1] => [
+ [Person] => [
+ [name] => Jeff
+ ]
+ ]
+ ]
+*/
+```
+
+`$dir` can be either `asc` or `desc`. `$type`
+can be one of the following values:
+
+- `regular` for regular sorting.
+- `numeric` for sorting values as their numeric equivalents.
+- `string` for sorting values as their string value.
+- `natural` for sorting values in a human friendly way. Will
+ sort `foo10` below `foo2` as an example.
+
+### Hash::diff()
+
+`static` Cake\\Utility\\Hash::**diff**(array $data, array $compare): array
+
+Computes the difference between two arrays:
+
+``` php
+$a = [
+ 0 => ['name' => 'main'],
+ 1 => ['name' => 'about']
+];
+$b = [
+ 0 => ['name' => 'main'],
+ 1 => ['name' => 'about'],
+ 2 => ['name' => 'contact']
+];
+
+$result = Hash::diff($a, $b);
+/* $result now looks like:
+ [
+ [2] => [
+ [name] => contact
+ ]
+ ]
+*/
+```
+
+### Hash::mergeDiff()
+
+`static` Cake\\Utility\\Hash::**mergeDiff**(array $data, array $compare): array
+
+This function merges two arrays and pushes the differences in
+data to the bottom of the resultant array.
+
+**Example 1**
+:
+
+``` php
+$array1 = ['ModelOne' => ['id' => 1001, 'field_one' => 'a1.m1.f1', 'field_two' => 'a1.m1.f2']];
+$array2 = ['ModelOne' => ['id' => 1003, 'field_one' => 'a3.m1.f1', 'field_two' => 'a3.m1.f2', 'field_three' => 'a3.m1.f3']];
+$res = Hash::mergeDiff($array1, $array2);
+
+/* $res now looks like:
+ [
+ [ModelOne] => [
+ [id] => 1001
+ [field_one] => a1.m1.f1
+ [field_two] => a1.m1.f2
+ [field_three] => a3.m1.f3
+ ]
+ ]
+*/
+```
+
+**Example 2**
+:
+
+``` php
+$array1 = ["a" => "b", 1 => 20938, "c" => "string"];
+$array2 = ["b" => "b", 3 => 238, "c" => "string", ["extra_field"]];
+$res = Hash::mergeDiff($array1, $array2);
+/* $res now looks like:
+ [
+ [a] => b
+ [1] => 20938
+ [c] => string
+ [b] => b
+ [3] => 238
+ [4] => [
+ [0] => extra_field
+ ]
+ ]
+*/
+```
+
+### Hash::normalize()
+
+`static` Cake\\Utility\\Hash::**normalize**(array $data, $assoc = true, $default = null): array
+
+Normalizes an array. If `$assoc` is `true`, the resulting array will be
+normalized to be an associative array. Numeric keys with values, will be
+converted to string keys with `$default` values. Normalizing an array,
+makes using the results with `Hash::merge()` easier:
+
+``` php
+$a = ['Tree', 'CounterCache',
+ 'Upload' => [
+ 'folder' => 'products',
+ 'fields' => ['image_1_id', 'image_2_id']
+ ]
+];
+$result = Hash::normalize($a);
+/* $result now looks like:
+ [
+ [Tree] => null
+ [CounterCache] => null
+ [Upload] => [
+ [folder] => products
+ [fields] => [
+ [0] => image_1_id
+ [1] => image_2_id
+ ]
+ ]
+ ]
+*/
+
+$b = [
+ 'Cacheable' => ['enabled' => false],
+ 'Limit',
+ 'Bindable',
+ 'Validator',
+ 'Transactional',
+];
+$result = Hash::normalize($b);
+/* $result now looks like:
+ [
+ [Cacheable] => [
+ [enabled] => false
+ ]
+
+ [Limit] => null
+ [Bindable] => null
+ [Validator] => null
+ [Transactional] => null
+ ]
+*/
+```
+
+::: info Changed in version 4.5.0
+The `$default` parameter was added.
+:::
+
+### Hash::nest()
+
+`static` Cake\\Utility\\Hash::**nest**(array $data, array $options = []): array
+
+Takes a flat array set, and creates a nested, or threaded data structure.
+
+**Options:**
+
+- `children` The key name to use in the result set for children. Defaults
+ to 'children'.
+- `idPath` The path to a key that identifies each entry. Should be
+ compatible with `Hash::extract()`. Defaults to `{n}.$alias.id`
+- `parentPath` The path to a key that identifies the parent of each entry.
+ Should be compatible with `Hash::extract()`. Defaults to `{n}.$alias.parent_id`
+- `root` The id of the desired top-most result.
+
+For example, if you had the following array of data:
+
+``` php
+$data = [
+ ['ThreadPost' => ['id' => 1, 'parent_id' => null]],
+ ['ThreadPost' => ['id' => 2, 'parent_id' => 1]],
+ ['ThreadPost' => ['id' => 3, 'parent_id' => 1]],
+ ['ThreadPost' => ['id' => 4, 'parent_id' => 1]],
+ ['ThreadPost' => ['id' => 5, 'parent_id' => 1]],
+ ['ThreadPost' => ['id' => 6, 'parent_id' => null]],
+ ['ThreadPost' => ['id' => 7, 'parent_id' => 6]],
+ ['ThreadPost' => ['id' => 8, 'parent_id' => 6]],
+ ['ThreadPost' => ['id' => 9, 'parent_id' => 6]],
+ ['ThreadPost' => ['id' => 10, 'parent_id' => 6]]
+];
+
+$result = Hash::nest($data, ['root' => 6]);
+/* $result now looks like:
+ [
+ (int) 0 => [
+ 'ThreadPost' => [
+ 'id' => (int) 6,
+ 'parent_id' => null
+ ],
+ 'children' => [
+ (int) 0 => [
+ 'ThreadPost' => [
+ 'id' => (int) 7,
+ 'parent_id' => (int) 6
+ ],
+ 'children' => []
+ ],
+ (int) 1 => [
+ 'ThreadPost' => [
+ 'id' => (int) 8,
+ 'parent_id' => (int) 6
+ ],
+ 'children' => []
+ ],
+ (int) 2 => [
+ 'ThreadPost' => [
+ 'id' => (int) 9,
+ 'parent_id' => (int) 6
+ ],
+ 'children' => []
+ ],
+ (int) 3 => [
+ 'ThreadPost' => [
+ 'id' => (int) 10,
+ 'parent_id' => (int) 6
+ ],
+ 'children' => []
+ ]
+ ]
+ ]
+ ]
+ */
+```
diff --git a/docs/en/core-libraries/httpclient.md b/docs/en/core-libraries/httpclient.md
new file mode 100644
index 0000000000..85536682e7
--- /dev/null
+++ b/docs/en/core-libraries/httpclient.md
@@ -0,0 +1,611 @@
+# Http Client
+
+`class` Cake\\Http\\**Client**(mixed $config = [])
+
+CakePHP includes a PSR-18 compliant HTTP client which can be used for
+making requests. It is a great way to communicate with webservices, and
+remote APIs.
+
+## Doing Requests
+
+Doing requests is simple and straight forward. Doing a GET request looks like:
+
+``` php
+use Cake\Http\Client;
+
+$http = new Client();
+
+// Simple get
+$response = $http->get('http://example.com/test.html');
+
+// Simple get with querystring
+$response = $http->get('http://example.com/search', ['q' => 'widget']);
+
+// Simple get with querystring & additional headers
+$response = $http->get('http://example.com/search', ['q' => 'widget'], [
+ 'headers' => ['X-Requested-With' => 'XMLHttpRequest'],
+]);
+```
+
+Doing POST and PUT requests is equally simple:
+
+``` php
+// Send a POST request with application/x-www-form-urlencoded encoded data
+$http = new Client();
+$response = $http->post('http://example.com/posts/add', [
+ 'title' => 'testing',
+ 'body' => 'content in the post',
+]);
+
+// Send a PUT request with application/x-www-form-urlencoded encoded data
+$response = $http->put('http://example.com/posts/add', [
+ 'title' => 'testing',
+ 'body' => 'content in the post',
+]);
+
+// Other methods as well.
+$http->delete(/* ... */);
+$http->head(/* ... */);
+$http->patch(/* ... */);
+```
+
+If you have created a PSR-7 request object you can send it using
+`sendRequest()`:
+
+``` php
+use Cake\Http\Client;
+use Cake\Http\Client\Request as ClientRequest;
+
+$request = new ClientRequest(
+ 'http://example.com/search',
+ ClientRequest::METHOD_GET
+);
+$http = new Client();
+$response = $http->sendRequest($request);
+```
+
+## Creating Multipart Requests with Files
+
+You can include files in request bodies by including a filehandle in the array:
+
+``` php
+$http = new Client();
+$response = $http->post('http://example.com/api', [
+ 'image' => fopen('/path/to/a/file', 'r'),
+]);
+```
+
+The filehandle will be read until its end; it will not be rewound before being read.
+
+### Building Multipart Request Bodies
+
+There may be times when you need to build a request body in a very specific way.
+In these situations you can often use `Cake\Http\Client\FormData` to craft
+the specific multipart HTTP request you want:
+
+``` php
+use Cake\Http\Client\FormData;
+
+$data = new FormData();
+
+// Create an XML part
+$xml = $data->newPart('xml', $xmlString);
+// Set the content type.
+$xml->type('application/xml');
+$data->add($xml);
+
+// Create a file upload with addFile()
+// This will append the file to the form data as well.
+$file = $data->addFile('upload', fopen('/some/file.txt', 'r'));
+$file->contentId('abc123');
+$file->disposition('attachment');
+
+// Send the request.
+$response = $http->post(
+ 'http://example.com/api',
+ (string)$data,
+ ['headers' => ['Content-Type' => $data->contentType()]]
+);
+```
+
+## Sending Request Bodies
+
+When dealing with REST APIs you often need to send request bodies that are not
+form encoded. Http\Client exposes this through the type option:
+
+``` php
+// Send a JSON request body.
+$http = new Client();
+$response = $http->post(
+ 'http://example.com/tasks',
+ json_encode($data),
+ ['type' => 'json']
+);
+```
+
+The `type` key can either be a one of 'json', 'xml' or a full mime type.
+When using the `type` option, you should provide the data as a string. If you're
+doing a GET request that needs both querystring parameters and a request body
+you can do the following:
+
+``` php
+// Send a JSON body in a GET request with query string parameters.
+$http = new Client();
+$response = $http->get(
+ 'http://example.com/tasks',
+ ['q' => 'test', '_content' => json_encode($data)],
+ ['type' => 'json']
+);
+```
+
+
+
+## Request Method Options
+
+Each HTTP method takes an `$options` parameter which is used to provide
+addition request information. The following keys can be used in `$options`:
+
+- `headers` - Array of additional headers
+- `cookie` - Array of cookies to use.
+- `proxy` - Array of proxy information.
+- `auth` - Array of authentication data, the `type` key is used to delegate to
+ an authentication strategy. By default Basic auth is used.
+- `ssl_verify_peer` - defaults to `true`. Set to `false` to disable SSL certification
+ verification (not recommended).
+- `ssl_verify_peer_name` - defaults to `true`. Set to `false` to disable
+ host name verification when verifying SSL certificates (not recommended).
+- `ssl_verify_depth` - defaults to 5. Depth to traverse in the CA chain.
+- `ssl_verify_host` - defaults to `true`. Validate the SSL certificate against the host name.
+- `ssl_cafile` - defaults to built in cafile. Overwrite to use custom CA bundles.
+- `timeout` - Duration to wait before timing out in seconds.
+- `type` - Send a request body in a custom content type. Requires `$data` to
+ either be a string, or the `_content` option to be set when doing GET
+ requests.
+- `redirect` - Number of redirects to follow. Defaults to `false`.
+- `curl` - An array of additional curl options (if the curl adapter is used),
+ for example, `[CURLOPT_SSLKEY => 'key.pem']`.
+
+The options parameter is always the 3rd parameter in each of the HTTP methods.
+They can also be used when constructing `Client` to create
+[scoped clients](#http_client_scoped_client).
+
+## Authentication
+
+`Cake\Http\Client` supports a few different authentication systems. Different
+authentication strategies can be added by developers. Auth strategies are called
+before the request is sent, and allow headers to be added to the request
+context.
+
+### Using Basic Authentication
+
+An example of basic authentication:
+
+``` php
+$http = new Client();
+$response = $http->get('http://example.com/profile/1', [], [
+ 'auth' => ['username' => 'mark', 'password' => 'secret'],
+]);
+```
+
+By default `Cake\Http\Client` will use basic authentication if there is no
+`'type'` key in the auth option.
+
+### Using Digest Authentication
+
+An example of basic authentication:
+
+``` php
+$http = new Client();
+$response = $http->get('http://example.com/profile/1', [], [
+ 'auth' => [
+ 'type' => 'digest',
+ 'username' => 'mark',
+ 'password' => 'secret',
+ 'realm' => 'myrealm',
+ 'nonce' => 'onetimevalue',
+ 'qop' => 1,
+ 'opaque' => 'someval',
+ ],
+]);
+```
+
+By setting the 'type' key to 'digest', you tell the authentication subsystem to
+use digest authentication. Digest authentication supports the following
+algorithms:
+
+- MD5
+- SHA-256
+- SHA-512-256
+- MD5-sess
+- SHA-256-sess
+- SHA-512-256-sess
+
+The algorithm will be automatically chosen based on the server challenge.
+
+### OAuth 1 Authentication
+
+Many modern web-services require OAuth authentication to access their APIs.
+The included OAuth authentication assumes that you already have your consumer
+key and consumer secret:
+
+``` php
+$http = new Client();
+$response = $http->get('http://example.com/profile/1', [], [
+ 'auth' => [
+ 'type' => 'oauth',
+ 'consumerKey' => 'bigkey',
+ 'consumerSecret' => 'secret',
+ 'token' => '...',
+ 'tokenSecret' => '...',
+ 'realm' => 'tickets',
+ ],
+]);
+```
+
+### OAuth 2 Authentication
+
+Because OAuth2 is often a single header, there is not a specialized
+authentication adapter. Instead you can create a client with the access token:
+
+``` php
+$http = new Client([
+ 'headers' => ['Authorization' => 'Bearer ' . $accessToken],
+]);
+$response = $http->get('https://example.com/api/profile/1');
+```
+
+### Proxy Authentication
+
+Some proxies require authentication to use them. Generally this authentication
+is Basic, but it can be implemented by any authentication adapter. By default
+Http\Client will assume Basic authentication, unless the type key is set:
+
+``` php
+$http = new Client();
+$response = $http->get('http://example.com/test.php', [], [
+ 'proxy' => [
+ 'username' => 'mark',
+ 'password' => 'testing',
+ 'proxy' => '127.0.0.1:8080',
+ ],
+]);
+```
+
+The second proxy parameter must be a string with an IP or a domain without
+protocol. The username and password information will be passed through the
+request headers, while the proxy string will be passed through
+[stream_context_create()](https://php.net/manual/en/function.stream-context-create.php).
+
+
+
+## Creating Scoped Clients
+
+Having to re-type the domain name, authentication and proxy settings can become
+tedious & error prone. To reduce the chance for mistake and relieve some of the
+tedium, you can create scoped clients:
+
+``` php
+// Create a scoped client.
+$http = new Client([
+ 'host' => 'api.example.com',
+ 'scheme' => 'https',
+ 'auth' => ['username' => 'mark', 'password' => 'testing'],
+]);
+
+// Do a request to api.example.com
+$response = $http->get('/test.php');
+```
+
+If your scoped client only needs information from the URL you can use
+`createFromUrl()`:
+
+``` php
+$http = Client::createFromUrl('https://api.example.com/v1/test');
+```
+
+The above would create a client instance with the `protocol`, `host`, and
+`basePath` options set.
+
+The following information can be used when creating a scoped client:
+
+- host
+- basePath
+- scheme
+- proxy
+- auth
+- port
+- cookies
+- timeout
+- ssl_verify_peer
+- ssl_verify_depth
+- ssl_verify_host
+
+Any of these options can be overridden by specifying them when doing requests.
+host, scheme, proxy, port are overridden in the request URL:
+
+``` php
+// Using the scoped client we created earlier.
+$response = $http->get('http://foo.com/test.php');
+```
+
+The above will replace the domain, scheme, and port. However, this request will
+continue using all the other options defined when the scoped client was created.
+See [Http Client Request Options](#http_client_request_options) for more information on the options
+supported.
+
+## Setting and Managing Cookies
+
+Http\Client can also accept cookies when making requests. In addition to
+accepting cookies, it will also automatically store valid cookies set in
+responses. Any response with cookies, will have them stored in the originating
+instance of Http\Client. The cookies stored in a Client instance are
+automatically included in future requests to domain + path combinations that
+match:
+
+``` php
+$http = new Client([
+ 'host' => 'cakephp.org'
+]);
+
+// Do a request that sets some cookies
+$response = $http->get('/');
+
+// Cookies from the first request will be included
+// by default.
+$response2 = $http->get('/changelogs');
+```
+
+You can always override the auto-included cookies by setting them in the
+request's `$options` parameters:
+
+``` php
+// Replace a stored cookie with a custom value.
+$response = $http->get('/changelogs', [], [
+ 'cookies' => ['sessionid' => '123abc'],
+]);
+```
+
+You can add cookie objects to the client after creating it using the `addCookie()`
+method:
+
+``` php
+use Cake\Http\Cookie\Cookie;
+
+$http = new Client([
+ 'host' => 'cakephp.org'
+]);
+$http->addCookie(new Cookie('session', 'abc123'));
+```
+
+## Client Events
+
+`Client` will emit events when requests are sent. The
+`HttpClient.beforeSend` event is fired before a request is sent, and
+`HttpClient.afterSend` is fired after a request is sent. You can modify the
+request, or set a response in a `beforeSend` listener. The `afterSend` event
+is triggered for all requests, even those that have their responses set by
+a `beforeSend` event.
+
+
+
+## Response Objects
+
+`class` Cake\\Http\\Client\\**Response**
+
+Response objects have a number of methods for inspecting the response data.
+
+### Reading Response Bodies
+
+You read the entire response body as a string:
+
+``` php
+// Read the entire response as a string.
+$response->getStringBody();
+```
+
+You can also access the stream object for the response and use its methods:
+
+``` php
+// Get a Psr\Http\Message\StreamInterface containing the response body
+$stream = $response->getBody();
+
+// Read a stream 100 bytes at a time.
+while (!$stream->eof()) {
+ echo $stream->read(100);
+}
+```
+
+
+
+### Reading JSON and XML Response Bodies
+
+Since JSON and XML responses are commonly used, response objects provide a way
+to use accessors to read decoded data. JSON data is decoded into an array, while
+XML data is decoded into a `SimpleXMLElement` tree:
+
+``` php
+// Get some XML
+$http = new Client();
+$response = $http->get('http://example.com/test.xml');
+$xml = $response->getXml();
+
+// Get some JSON
+$http = new Client();
+$response = $http->get('http://example.com/test.json');
+$json = $response->getJson();
+```
+
+The decoded response data is stored in the response object, so accessing it
+multiple times has no additional cost.
+
+### Accessing Response Headers
+
+You can access headers through a few different methods. Header names are always
+treated as case-insensitive values when accessing them through methods:
+
+``` php
+// Get all the headers as an associative array.
+$response->getHeaders();
+
+// Get a single header as an array.
+$response->getHeader('content-type');
+
+// Get a header as a string
+$response->getHeaderLine('content-type');
+
+// Get the response encoding
+$response->getEncoding();
+```
+
+### Accessing Cookie Data
+
+You can read cookies with a few different methods depending on how much
+data you need about the cookies:
+
+``` php
+// Get all cookies (full data)
+$response->getCookies();
+
+// Get a single cookie's value.
+$response->getCookie('session_id');
+
+// Get a the complete data for a single cookie
+// includes value, expires, path, httponly, secure keys.
+$response->getCookieData('session_id');
+```
+
+### Checking the Status Code
+
+Response objects provide a few methods for checking status codes:
+
+``` php
+// Was the response a 20x
+$response->isOk();
+
+// Was the response a 30x
+$response->isRedirect();
+
+// Get the status code
+$response->getStatusCode();
+```
+
+## Changing Transport Adapters
+
+By default `Http\Client` will prefer using a `curl` based transport adapter.
+If the curl extension is not available a stream based adapter will be used
+instead. You can force select a transport adapter using a constructor option:
+
+``` php
+use Cake\Http\Client\Adapter\Stream;
+
+$http = new Client(['adapter' => Stream::class]);
+```
+
+## Events
+
+The HTTP client triggers couple of events before and after sending a request
+which allows you to modify either the request or response or do other tasks like
+caching, logging etc.
+
+### HttpClient.beforeSend
+
+``` php
+// Somewhere before calling one of the HTTP client's methods which makes a request
+$http->getEventManager()->on(
+ 'HttpClient.beforeSend',
+ function (
+ \Cake\Http\Client\ClientEvent $event,
+ \Cake\Http\Client\Request $request,
+ array $adapterOptions,
+ int $redirects
+ ) {
+ // Modify the request
+ $event->setRequest(....);
+ // Modify the adapter options
+ $event->setAdapterOptions(....);
+
+ // Skip making the actual request by returning a response.
+ // You can use $event->setResult($response) to achieve the same.
+ return new \Cake\Http\Client\Response(body: 'something');
+ }
+);
+```
+
+### HttpClient.afterSend
+
+``` php
+// Somewhere before calling one of the HTTP client's methods which makes a request
+$http->getEventManager()->on(
+ 'HttpClient.afterSend',
+ function (
+ \Cake\Http\Client\ClientEvent $event,
+ \Cake\Http\Client\Request $request,
+ array $adapterOptions,
+ int $redirects,
+ bool $requestSent // Indicates whether the request was actually sent
+ // or response returned from ``beforeSend`` event
+ ) {
+ // Get the response
+ $response = $event->getResponse();
+
+ // Return a new/modified response.
+ // You can use $event->setResult($response) to achieve the same.
+ return new \Cake\Http\Client\Response(body: 'something');
+ }
+);
+```
+
+
+
+## Testing
+
+In tests you will often want to create mock responses to external APIs. You can
+use the `HttpClientTrait` to define responses to the requests your application
+is making:
+
+``` php
+use Cake\Http\TestSuite\HttpClientTrait;
+use Cake\TestSuite\TestCase;
+
+class CartControllerTests extends TestCase
+{
+ use HttpClientTrait;
+
+ public function testCheckout()
+ {
+ // Mock a POST request that will be made.
+ $this->mockClientPost(
+ 'https://example.com/process-payment',
+ $this->newClientResponse(200, [], json_encode(['ok' => true]))
+ );
+ $this->post("/cart/checkout");
+ // Do assertions.
+ }
+}
+```
+
+There are methods to mock the most commonly used HTTP methods:
+
+``` php
+$this->mockClientGet(/* ... */);
+$this->mockClientPatch(/* ... */);
+$this->mockClientPost(/* ... */);
+$this->mockClientPut(/* ... */);
+$this->mockClientDelete(/* ... */);
+```
+
+### Response::newClientResponse()
+
+`method` Cake\\Http\\TestSuite\\Response::**newClientResponse**(int $code = 200, array $headers = [], string $body = '')
+
+As seen above you can use the `newClientResponse()` method to create responses
+for the requests your application will make. The headers need to be a list of
+strings:
+
+``` php
+$headers = [
+ 'Content-Type: application/json',
+ 'Connection: close',
+];
+$response = $this->newClientResponse(200, $headers, $body)
+```
diff --git a/docs/en/core-libraries/inflector.md b/docs/en/core-libraries/inflector.md
new file mode 100644
index 0000000000..a22af1b32d
--- /dev/null
+++ b/docs/en/core-libraries/inflector.md
@@ -0,0 +1,261 @@
+# Inflector
+
+`class` Cake\\Utility\\**Inflector**
+
+The Inflector class takes a string and can manipulate it to handle word
+variations such as pluralization or camelizing and is normally accessed
+statically. Example:
+`Inflector::pluralize('example')` returns "examples".
+
+You can try out the inflections online at [inflector.cakephp.org](https://inflector.cakephp.org/) or [sandbox.dereuromark.de](https://sandbox.dereuromark.de/sandbox/inflector).
+
+
+
+## Summary of Inflector Methods and Their Output
+
+Quick summary of the Inflector built-in methods and the results they output
+when provided a multi-word argument:
+
+
+
+
+
+
+
+
+
+
Method
+
Argument
+
Output
+
+
+
+
+
pluralize()
+
BigApple
+
BigApples
+
+
+
big_apple
+
big_apples
+
+
+
singularize()
+
BigApples
+
BigApple
+
+
+
big_apples
+
big_apple
+
+
+
camelize()
+
big_apples
+
BigApples
+
+
+
big apple
+
BigApple
+
+
+
underscore()
+
BigApples
+
big_apples
+
+
+
Big Apples
+
big apples
+
+
+
humanize()
+
big_apples
+
Big Apples
+
+
+
bigApple
+
BigApple
+
+
+
classify()
+
big_apples
+
BigApple
+
+
+
big apple
+
BigApple
+
+
+
dasherize()
+
BigApples
+
big-apples
+
+
+
big apple
+
big apple
+
+
+
tableize()
+
BigApple
+
big_apples
+
+
+
Big Apple
+
big apples
+
+
+
variable()
+
big_apple
+
bigApple
+
+
+
big apples
+
bigApples
+
+
+
+
+## Creating Plural & Singular Forms
+
+### Inflector::singularize()
+
+`static` Cake\\Utility\\Inflector::**singularize**($singular): string
+
+### Inflector::pluralize()
+
+`static` Cake\\Utility\\Inflector::**pluralize**($singular): string
+
+Both `pluralize` and `singularize()` work on most English nouns. If you need
+to support other languages, you can use [Inflection Configuration](#inflection-configuration) to
+customize the rules used:
+
+``` php
+// Apples
+echo Inflector::pluralize('Apple');
+```
+
+> [!NOTE]
+> `pluralize()` should not be used on a noun that is already in its plural form.
+
+``` php
+// Person
+echo Inflector::singularize('People');
+```
+
+> [!NOTE]
+> `singularize()` should not be used on a noun that is already in its singular form.
+
+## Creating CamelCase and under_scored Forms
+
+### Inflector::camelize()
+
+`static` Cake\\Utility\\Inflector::**camelize**($underscored): string
+
+### Inflector::underscore()
+
+`static` Cake\\Utility\\Inflector::**underscore**($camelCase): string
+
+These methods are useful when creating class names, or property names:
+
+``` php
+// ApplePie
+Inflector::camelize('Apple_pie')
+
+// apple_pie
+Inflector::underscore('ApplePie');
+```
+
+It should be noted that underscore will only convert camelCase formatted words.
+Words that contains spaces will be lower-cased, but will not contain an
+underscore.
+
+## Creating Human Readable Forms
+
+### Inflector::humanize()
+
+`static` Cake\\Utility\\Inflector::**humanize**($underscored): string
+
+This method is useful when converting underscored forms into "Title Case" forms
+for human readable values:
+
+``` php
+// Apple Pie
+Inflector::humanize('apple_pie');
+```
+
+## Creating Table and Class Name Forms
+
+### Inflector::classify()
+
+`static` Cake\\Utility\\Inflector::**classify**($underscored): string
+
+### Inflector::dasherize()
+
+`static` Cake\\Utility\\Inflector::**dasherize**($dashed): string
+
+### Inflector::tableize()
+
+`static` Cake\\Utility\\Inflector::**tableize**($camelCase): string
+
+When generating code, or using CakePHP's conventions you may need to inflect
+table names or class names:
+
+``` php
+// UserProfileSetting
+Inflector::classify('user_profile_settings');
+
+// user-profile-setting
+Inflector::dasherize('UserProfileSetting');
+
+// user_profile_settings
+Inflector::tableize('UserProfileSetting');
+```
+
+## Creating Variable Names
+
+### Inflector::variable()
+
+`static` Cake\\Utility\\Inflector::**variable**($underscored): string
+
+Variable names are often useful when doing meta-programming tasks that involve
+generating code or doing work based on conventions:
+
+``` php
+// applePie
+Inflector::variable('apple_pie');
+```
+
+
+
+## Inflection Configuration
+
+CakePHP's naming conventions can be really nice - you can name your database
+table `big_boxes`, your model `BigBoxes`, your controller
+`BigBoxesController`, and everything just works together automatically. The
+way CakePHP knows how to tie things together is by *inflecting* the words
+between their singular and plural forms.
+
+There are occasions (especially for our non-English speaking friends) where you
+may run into situations where CakePHP's inflector (the class that pluralizes,
+singularizes, camelCases, and under_scores) might not work as you'd like. If
+CakePHP won't recognize your Foci or Fish, you can tell CakePHP about your
+special cases.
+
+### Loading Custom Inflections
+
+### Inflector::rules()
+
+`static` Cake\\Utility\\Inflector::**rules**($type, $rules, $reset = false): void
+
+Define new inflection and transliteration rules for Inflector to use. Often,
+this method is used in your **config/bootstrap.php**:
+
+``` php
+Inflector::rules('singular', ['/^(bil)er$/i' => '\1', '/^(inflec|contribu)tors$/i' => '\1ta']);
+Inflector::rules('uninflected', ['singulars']);
+Inflector::rules('irregular', ['phylum' => 'phyla']); // The key is singular form, value is plural form
+```
+
+The supplied rules will be merged into the respective inflection sets defined in
+`Cake/Utility/Inflector`, with the added rules taking precedence over the core
+rules. You can use `Inflector::reset()` to clear rules and restore the
+original Inflector state.
diff --git a/docs/en/core-libraries/internationalization-and-localization.md b/docs/en/core-libraries/internationalization-and-localization.md
new file mode 100644
index 0000000000..b9d0842ecd
--- /dev/null
+++ b/docs/en/core-libraries/internationalization-and-localization.md
@@ -0,0 +1,751 @@
+# Internationalization & Localization
+
+One of the best ways for an application to reach a larger audience is to cater
+to multiple languages. This can often prove to be a daunting task, but the
+internationalization and localization features in CakePHP make it much easier.
+
+First, it's important to understand some terminology. *Internationalization*
+refers to the ability of an application to be localized. The term *localization*
+refers to the adaptation of an application to meet specific language (or
+culture) requirements (i.e. a "locale"). Internationalization and localization
+are often abbreviated as i18n and l10n respectively; 18 and 10 are the number
+of characters between the first and last character.
+
+## Setting Up Translations
+
+There are only a few steps to go from a single-language application to a
+multi-lingual application, the first of which is to make use of the
+`__()` function in your code. Below is an example of some code for a
+single-language application:
+
+``` html
+
Popular Articles
+```
+
+To internationalize your code, all you need to do is to wrap strings in
+`__()` like so:
+
+``` html
+
= __('Popular Articles') ?>
+```
+
+Doing nothing else, these two code examples are functionally identical - they
+will both send the same content to the browser. The `__()` function
+will translate the passed string if a translation is available, or return it
+unmodified.
+
+### Language Files
+
+Translations can be made available by using language files stored in the
+application. The default format for CakePHP translation files is the
+[Gettext](https://en.wikipedia.org/wiki/Gettext) format. Files need to be
+placed under **resources/locales/** and within this directory, there should be a
+subfolder for each language the application needs to support:
+
+ resources/
+ locales/
+ en_US/
+ default.po
+ en_GB/
+ default.po
+ validation.po
+ es/
+ default.po
+
+The default domain is 'default', therefore the locale folder should at least
+contain the **default.po** file as shown above. A domain refers to any arbitrary
+grouping of translation messages. When no group is used, then the default group
+is selected.
+
+The core strings messages extracted from the CakePHP library can be stored
+separately in a file named **cake.po** in **resources/locales/**.
+The [CakePHP localized library](https://github.com/cakephp/localized) houses
+translations for the client-facing translated strings in the core (the cake
+domain). To use these files, link or copy them into their expected location:
+**resources/locales/\/cake.po**. If your locale is incomplete or incorrect,
+please submit a PR in this repository to fix it.
+
+Plugins can also contain translation files, the convention is to use the
+`under_scored` version of the plugin name as the domain for the translation
+messages:
+
+ MyPlugin/
+ resources/
+ locales/
+ fr/
+ my_plugin.po
+ additional.po
+ de/
+ my_plugin.po
+
+Translation folders can be the two or three letter ISO code of the language or
+the full ICU locale name such as `fr_FR`, `es_AR`, `da_DK` which contains
+both the language and the country where it is spoken.
+
+See for the full list of locales.
+
+::: info Changed in version 4.5.0
+As of 4.5.0 plugins can contain multiple translation domains. Use `MyPlugin.additional` to reference plugin domains.
+:::
+
+An example translation file could look like this:
+
+``` pot
+msgid "My name is {0}"
+msgstr "Je m'appelle {0}"
+
+msgid "I'm {0,number} years old"
+msgstr "J'ai {0,number} ans"
+```
+
+> [!NOTE]
+> Translations are cached - Make sure that you always clear the cache after
+> making changes to translations! You can either use the
+> [cache tool](../console-commands/cache) and run for example
+> `bin/cake cache clear _cake_core_`, or manually clear the `tmp/cache/persistent`
+> folder (if using file based caching).
+
+### Extract Pot Files with I18n Shell
+
+To create the pot files from \_\_() and other internationalized types of
+messages that can be found in the application code, you can use the i18n command.
+Please read the [following chapter](../console-commands/i18n) to
+learn more.
+
+### Setting the Default Locale
+
+The default locale can be set in your **config/app.php** file by setting
+`App.defaultLocale`:
+
+``` text
+'App' => [
+ ...
+ 'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'),
+ ...
+]
+```
+
+This will control several aspects of the application, including the default
+translations language, the date format, number format and currency whenever any
+of those is displayed using the localization libraries that CakePHP provides.
+
+### Changing the Locale at Runtime
+
+To change the language for translated strings you can call this method:
+
+``` php
+use Cake\I18n\I18n;
+
+I18n::setLocale('de_DE');
+```
+
+This will also change how numbers and dates are formatted when using one of the
+localization tools.
+
+## Using Translation Functions
+
+CakePHP provides several functions that will help you internationalize your
+application. The most frequently used one is `__()`. This function
+is used to retrieve a single translation message or return the same string if no
+translation was found:
+
+``` text
+echo __('Popular Articles');
+```
+
+If you need to group your messages, for example, translations inside a plugin,
+you can use the `__d()` function to fetch messages from another
+domain:
+
+``` text
+echo __d('my_plugin', 'Trending right now');
+```
+
+> [!NOTE]
+> If you want to translate plugins that are vendor namespaced, you must use
+> the domain string `vendor/plugin_name`. But the related language file
+> will become `plugins///resources/locales//plugin_name.po`
+> inside your plugin folder.
+
+Sometimes translations strings can be ambiguous for people translating them.
+This can happen if two strings are identical but refer to different things. For
+example, 'letter' has multiple meanings in English. To solve that problem, you
+can use the `__x()` function:
+
+``` text
+echo __x('written communication', 'He read the first letter');
+
+echo __x('alphabet learning', 'He read the first letter');
+```
+
+The first argument is the context of the message and the second is the message
+to be translated.
+
+``` pot
+msgctxt "written communication"
+msgid "He read the first letter"
+msgstr "Er las den ersten Brief"
+```
+
+### Using Variables in Translation Messages
+
+Translation functions allow you to interpolate variables into the messages using
+special markers defined in the message itself or in the translated string:
+
+``` text
+echo __("Hello, my name is {0}, I'm {1} years old", ['Sara', 12]);
+```
+
+Markers are numeric, and correspond to the keys in the passed array. You can
+also pass variables as independent arguments to the function:
+
+``` text
+echo __("Small step for {0}, Big leap for {1}", 'Man', 'Humanity');
+```
+
+All translation functions support placeholder replacements:
+
+``` text
+__d('validation', 'The field {0} cannot be left empty', 'Name');
+
+__x('alphabet', 'He read the letter {0}', 'Z');
+```
+
+The `'` (single quote) character acts as an escape code in translation
+messages. Any variables between single quotes will not be replaced and is
+treated as literal text. For example:
+
+``` text
+__("This variable '{0}' be replaced.", 'will not');
+```
+
+By using two adjacent quotes your variables will be replaced properly:
+
+``` text
+__("This variable ''{0}'' be replaced.", 'will');
+```
+
+These functions take advantage of the
+[ICU MessageFormatter](https://php.net/manual/en/messageformatter.format.php)
+so you can translate messages and localize dates, numbers and currency at the
+same time:
+
+``` text
+echo __(
+ 'Hi {0}, your balance on the {1,date} is {2,number,currency}',
+ ['Charles', new DateTime('2014-01-13 11:12:00'), 1354.37]
+);
+
+// Returns
+Hi Charles, your balance on the Jan 13, 2014, 11:12 AM is $ 1,354.37
+```
+
+Numbers in placeholders can be formatted as well with fine grain control of the
+output:
+
+``` text
+echo __(
+ 'You have traveled {0,number} kilometers in {1,number,integer} weeks',
+ [5423.344, 5.1]
+);
+
+// Returns
+You have traveled 5,423.34 kilometers in 5 weeks
+
+echo __('There are {0,number,#,###} people on earth', 6.1 * pow(10, 8));
+
+// Returns
+There are 6,100,000,000 people on earth
+```
+
+This is the list of formatter specifiers you can put after the word `number`:
+
+- `integer`: Removes the decimal part
+- `currency`: Puts the locale currency symbol and rounds decimals
+- `percent`: Formats the number as a percentage
+
+Dates can also be formatted by using the word `date` after the placeholder
+number. A list of extra options follows:
+
+- `short`
+- `medium`
+- `long`
+- `full`
+
+The word `time` after the placeholder number is also accepted and it
+understands the same options as `date`.
+
+You can also use named placeholders like `{name}` in the message strings.
+When using named placeholders, pass the placeholder and replacement in an array using key/value pairs,
+for example:
+
+``` text
+// echos: Hi. My name is Sara. I'm 12 years old.
+echo __("Hi. My name is {name}. I'm {age} years old.", ['name' => 'Sara', 'age' => 12]);
+```
+
+### Plurals
+
+One crucial part of internationalizing your application is getting your messages
+pluralized correctly depending on the language they are shown. CakePHP provides
+a couple ways to correctly select plurals in your messages.
+
+#### Using ICU Plural Selection
+
+The first one is taking advantage of the `ICU` message format that comes by
+default in the translation functions. In the translations file you could have
+the following strings
+
+``` pot
+msgid "{0,plural,=0{No records found} =1{Found 1 record} other{Found # records}}"
+msgstr "{0,plural,=0{Ningún resultado} =1{1 resultado} other{# resultados}}"
+
+msgid "{placeholder,plural,=0{No records found} =1{Found 1 record} other{Found {1} records}}"
+msgstr "{placeholder,plural,=0{Ningún resultado} =1{1 resultado} other{{1} resultados}}"
+```
+
+And in the application use the following code to output either of the
+translations for such string:
+
+``` text
+__('{0,plural,=0{No records found }=1{Found 1 record} other{Found # records}}', [0]);
+
+// Returns "Ningún resultado" as the argument {0} is 0
+
+__('{0,plural,=0{No records found} =1{Found 1 record} other{Found # records}}', [1]);
+
+// Returns "1 resultado" because the argument {0} is 1
+
+__('{placeholder,plural,=0{No records found} =1{Found 1 record} other{Found {1} records}}', [0, 'many', 'placeholder' => 2])
+
+// Returns "many resultados" because the argument {placeholder} is 2 and
+// argument {1} is 'many'
+```
+
+A closer look to the format we just used will make it evident how messages are
+built:
+
+``` text
+{ [count placeholder],plural, case1{message} case2{message} case3{...} ... }
+```
+
+The `[count placeholder]` can be the array key number of any of the variables
+you pass to the translation function. It will be used for selecting the correct
+plural form.
+
+Note that to reference `[count placeholder]` within `{message}` you have to
+use `#`.
+
+You can of course use simpler message ids if you don't want to type the full
+plural selection sequence in your code
+
+``` pot
+msgid "search.results"
+msgstr "{0,plural,=0{Ningún resultado} =1{1 resultado} other{{1} resultados}}"
+```
+
+Then use the new string in your code:
+
+``` text
+__('search.results', [2, 2]);
+
+// Returns: "2 resultados"
+```
+
+The latter version has the downside that there is a need to have a translation
+messages file even for the default language, but has the advantage that it makes
+the code more readable and leaves the complicated plural selection strings in
+the translation files.
+
+Sometimes using direct number matching in plurals is impractical. For example,
+languages like Arabic require a different plural when you refer
+to few things and other plural form for many things. In those cases you can
+use the ICU matching aliases. Instead of writing:
+
+``` text
+=0{No results} =1{...} other{...}
+```
+
+You can do:
+
+``` css
+zero{No Results} one{One result} few{...} many{...} other{...}
+```
+
+Make sure you read the
+[Language Plural Rules Guide](https://unicode-org.github.io/cldr-staging/charts/37/supplemental/language_plural_rules.html)
+to get a complete overview of the aliases you can use for each language.
+
+#### Using Gettext Plural Selection
+
+The second plural selection format accepted is using the built-in capabilities
+of Gettext. In this case, plurals will be stored in the `.po`
+file by creating a separate message translation line per plural form:
+
+``` pot
+# One message identifier for singular
+msgid "One file removed"
+# Another one for plural
+msgid_plural "{0} files removed"
+# Translation in singular
+msgstr[0] "Un fichero eliminado"
+# Translation in plural
+msgstr[1] "{0} ficheros eliminados"
+```
+
+When using this other format, you are required to use another translation
+function:
+
+``` php
+// Returns: "10 ficheros eliminados"
+$count = 10;
+__n('One file removed', '{0} files removed', $count, $count);
+
+// It is also possible to use it inside a domain
+__dn('my_plugin', 'One file removed', '{0} files removed', $count, $count);
+```
+
+The number inside `msgstr[]` is the number assigned by Gettext for the plural
+form of the language. Some languages have more than two plural forms, for
+example Croatian:
+
+``` pot
+msgid "One file removed"
+msgid_plural "{0} files removed"
+msgstr[0] "{0} datoteka je uklonjena"
+msgstr[1] "{0} datoteke su uklonjene"
+msgstr[2] "{0} datoteka je uklonjeno"
+```
+
+Please visit the [Launchpad languages page](https://translations.launchpad.net/+languages)
+for a detailed explanation of the plural form numbers for each language.
+
+## Creating Your Own Translators
+
+If you need to diverge from CakePHP conventions regarding where and how
+translation messages are stored, you can create your own translation message
+loader. The easiest way to create your own translator is by defining a loader
+for a single domain and locale:
+
+``` php
+use Cake\I18n\Package;
+// Prior to 4.2 you need to use Aura\Intl\Package
+
+I18n::setTranslator('animals', function () {
+ $package = new Package(
+ 'default', // The formatting strategy (ICU)
+ 'default' // The fallback domain
+ );
+ $package->setMessages([
+ 'Dog' => 'Chien',
+ 'Cat' => 'Chat',
+ 'Bird' => 'Oiseau'
+ ...
+ ]);
+
+ return $package;
+}, 'fr_FR');
+```
+
+The above code can be added to your **config/bootstrap.php** so that
+translations can be found before any translation function is used. The absolute
+minimum that is required for creating a translator is that the loader function
+should return a `Cake\I18n\Package` object (prior to 4.2 it should be an `Aura\Intl\Package` object).
+Once the code is in place you can use the translation functions as usual:
+
+``` php
+I18n::setLocale('fr_FR');
+__d('animals', 'Dog'); // Returns "Chien"
+```
+
+As you see, `Package` objects take translation messages as an array. You can
+pass the `setMessages()` method however you like: with inline code, including
+another file, calling another function, etc. CakePHP provides a few loader
+functions you can reuse if you just need to change where messages are loaded.
+For example, you can still use **.po** files, but loaded from another location:
+
+``` php
+use Cake\I18n\MessagesFileLoader as Loader;
+
+// Load messages from resources/locales/folder/sub_folder/filename.po
+I18n::setTranslator(
+ 'animals',
+ new Loader('filename', 'folder/sub_folder', 'po'),
+ 'fr_FR'
+);
+```
+
+### Creating Message Parsers
+
+It is possible to continue using the same conventions CakePHP uses, but use
+a message parser other than `PoFileParser`. For example, if you wanted to load
+translation messages using `YAML`, you will first need to created the parser
+class:
+
+``` php
+namespace App\I18n\Parser;
+
+class YamlFileParser
+{
+ public function parse($file)
+ {
+ return yaml_parse_file($file);
+ }
+}
+```
+
+The file should be created in the **src/I18n/Parser** directory of your
+application. Next, create the translations file under
+**resources/locales/fr_FR/animals.yaml**
+
+``` yaml
+Dog: Chien
+Cat: Chat
+Bird: Oiseau
+```
+
+And finally, configure the translation loader for the domain and locale:
+
+``` php
+use Cake\I18n\MessagesFileLoader as Loader;
+
+I18n::setTranslator(
+ 'animals',
+ new Loader('animals', 'fr_FR', 'yaml'),
+ 'fr_FR'
+);
+```
+
+
+
+### Creating Generic Translators
+
+Configuring translators by calling `I18n::setTranslator()` for each domain and
+locale you need to support can be tedious, specially if you need to support more
+than a few different locales. To avoid this problem, CakePHP lets you define
+generic translator loaders for each domain.
+
+Imagine that you wanted to load all translations for the default domain and for
+any language from an external service:
+
+``` php
+use Cake\I18n\Package;
+// Prior to 4.2 you need to use Aura\Intl\Package
+
+I18n::config('default', function ($domain, $locale) {
+ $locale = Locale::parseLocale($locale);
+ $lang = $locale['language'];
+ $messages = file_get_contents("http://example.com/translations/$lang.json");
+
+ return new Package(
+ 'default', // Formatter
+ null, // Fallback (none for default domain)
+ json_decode($messages, true)
+ )
+});
+```
+
+The above example calls an example external service to load a JSON file with the
+translations and then just build a `Package` object for any locale that is
+requested in the application.
+
+If you'd like to change how packages are loaded for all packages, that don't
+have specific loaders set you can replace the fallback package loader by using
+the `_fallback` package:
+
+``` php
+I18n::config('_fallback', function ($domain, $locale) {
+ // Custom code that yields a package here.
+});
+```
+
+### Plurals and Context in Custom Translators
+
+The arrays used for `setMessages()` can be crafted to instruct the translator
+to store messages under different domains or to trigger Gettext-style plural
+selection. The following is an example of storing translations for the same key
+in different contexts:
+
+``` php
+[
+ 'He reads the letter {0}' => [
+ 'alphabet' => 'Él lee la letra {0}',
+ 'written communication' => 'Él lee la carta {0}',
+ ],
+]
+```
+
+Similarly, you can express Gettext-style plurals using the messages array by
+having a nested array key per plural form:
+
+``` php
+[
+ 'I have read one book' => 'He leído un libro',
+ 'I have read {0} books' => [
+ 'He leído un libro',
+ 'He leído {0} libros',
+ ],
+]
+```
+
+### Using Different Formatters
+
+In previous examples we have seen that Packages are built using `default` as
+first argument, and it was indicated with a comment that it corresponded to the
+formatter to be used. Formatters are classes responsible for interpolating
+variables in translation messages and selecting the correct plural form.
+
+If you're dealing with a legacy application, or you don't need the power offered
+by the ICU message formatting, CakePHP also provides the `sprintf` formatter:
+
+``` text
+return Package('sprintf', 'fallback_domain', $messages);
+```
+
+The messages to be translated will be passed to the `sprintf()` function for
+interpolating the variables:
+
+``` text
+__('Hello, my name is %s and I am %d years old', 'José', 29);
+```
+
+It is possible to set the default formatter for all translators created by
+CakePHP before they are used for the first time. This does not include manually
+created translators using the `setTranslator()` and `config()` methods:
+
+``` php
+I18n::setDefaultFormatter('sprintf');
+```
+
+## Localizing Dates and Numbers
+
+When outputting Dates and Numbers in your application, you will often need that
+they are formatted according to the preferred format for the country or region
+that you wish your page to be displayed.
+
+In order to change how dates and numbers are displayed you just need to change
+the current locale setting and use the right classes:
+
+``` php
+use Cake\I18n\I18n;
+use Cake\I18n\DateTime;
+use Cake\I18n\Number;
+
+I18n::setLocale('fr-FR');
+
+$date = new DateTime('2015-04-05 23:00:00');
+
+echo $date; // Displays 05/04/2015 23:00
+
+echo Number::format(524.23); // Displays 524,23
+```
+
+Make sure you read the [Date & Time](../core-libraries/time) and [Number](../core-libraries/number)
+sections to learn more about formatting options.
+
+By default dates returned for the ORM results use the `Cake\I18n\DateTime` class,
+so displaying them directly in you application will be affected by changing the
+current locale.
+
+
+
+### Parsing Localized Datetime Data
+
+When accepting localized data from the request, it is nice to accept datetime
+information in a user's localized format. In a controller, or
+[Middleware](../controllers/middleware) you can configure the Date, Time, and
+DateTime types to parse localized formats:
+
+``` php
+use Cake\Database\TypeFactory;
+
+// Enable default locale format parsing.
+TypeFactory::build('datetime')->useLocaleParser();
+
+// Configure a custom datetime format parser format.
+TypeFactory::build('datetime')->useLocaleParser()->setLocaleFormat('dd-M-y');
+
+// You can also use IntlDateFormatter constants.
+TypeFactory::build('datetime')->useLocaleParser()
+ ->setLocaleFormat([IntlDateFormatter::SHORT, -1]);
+```
+
+The default parsing format is the same as the default string format.
+
+
+
+### Converting Request Data from the User's Timezone
+
+When handling data from users in different timezones you will need to convert
+the datetimes in request data into your application's timezone. You can use
+`setUserTimezone()` from a controller or [Middleware](../controllers/middleware) to
+make this process simpler:
+
+``` php
+// Set the user's timezone
+TypeFactory::build('datetime')->setUserTimezone($user->timezone);
+```
+
+Once set, when your application creates or updates entities from request data,
+the ORM will automatically convert datetime values from the user's timezone into
+your application's timezone. This ensures that your application is always
+working in the timezone defined in `App.defaultTimezone`.
+
+If your application handles datetime information in a number of actions you can
+use a middleware to define both timezone conversion and locale parsing:
+
+``` php
+namespace App\Middleware;
+
+use Cake\Database\TypeFactory;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+class DatetimeMiddleware implements MiddlewareInterface
+{
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ // Get the user from the request.
+ // This example assumes your user entity has a timezone attribute.
+ $user = $request->getAttribute('identity');
+ if ($user) {
+ TypeFactory::build('datetime')
+ ->useLocaleParser()
+ ->setUserTimezone($user->timezone);
+ }
+
+ return $handler->handle($request);
+ }
+}
+```
+
+## Automatically Choosing the Locale Based on Request Data
+
+By using the `LocaleSelectorMiddleware` in your application, CakePHP will
+automatically set the locale based on the current user:
+
+``` php
+// in src/Application.php
+use Cake\I18n\Middleware\LocaleSelectorMiddleware;
+
+// Update the middleware function, adding the new middleware
+public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+{
+ // Add middleware and set the valid locales
+ $middlewareQueue->add(new LocaleSelectorMiddleware(['en_US', 'fr_FR']));
+ // To accept any locale header value
+ $middlewareQueue->add(new LocaleSelectorMiddleware(['*']));
+}
+```
+
+The `LocaleSelectorMiddleware` will use the `Accept-Language` header to
+automatically set the user's preferred locale. You can use the locale list
+option to restrict which locales will automatically be used.
+
+## Translate Content/Entities
+
+If you want to translate content/entities then you should look at the [Translate Behavior](../orm/behaviors/translate).
diff --git a/docs/en/core-libraries/logging.md b/docs/en/core-libraries/logging.md
new file mode 100644
index 0000000000..211b709a6d
--- /dev/null
+++ b/docs/en/core-libraries/logging.md
@@ -0,0 +1,593 @@
+# Logging
+
+While CakePHP core Configure Class settings can really help you see
+what's happening under the hood, there are certain times that
+you'll need to log data to the disk in order to find out what's
+going on. With technologies like SOAP, AJAX, and REST APIs, debugging can be
+rather difficult.
+
+Logging can also be a way to find out what's been going on in your
+application over time. What search terms are being used? What sorts
+of errors are my users being shown? How often is a particular query
+being executed?
+
+Logging data in CakePHP is done with the `log()` function. It is provided by the
+`LogTrait`, which is the common ancestor for many CakePHP classes. If the
+context is a CakePHP class (Controller, Component, View,...), you can log your
+data. You can also use `Log::write()` directly. See [Writing To Logs](#writing-to-logs).
+
+
+
+## Logging Configuration
+
+Configuring `Log` should be done during your application's bootstrap phase.
+The **config/app.php** file is intended for just this. You can define
+as many or as few loggers as your application needs. Loggers should be
+configured using `Cake\Log\Log`. An example would be:
+
+``` php
+use Cake\Log\Engine\FileLog;
+use Cake\Log\Log;
+
+// Classname using logger 'class' constant
+Log::setConfig('info', [
+ 'className' => FileLog::class,
+ 'path' => LOGS,
+ 'levels' => ['info'],
+ 'file' => 'info',
+]);
+
+// Short classname
+Log::setConfig('debug', [
+ 'className' => 'File',
+ 'path' => LOGS,
+ 'levels' => ['notice', 'debug'],
+ 'file' => 'debug',
+]);
+
+// Fully namespaced name.
+Log::setConfig('error', [
+ 'className' => 'Cake\Log\Engine\FileLog',
+ 'path' => LOGS,
+ 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
+ 'file' => 'error',
+]);
+```
+
+The above creates three loggers, named `info`, `debug` and `error`.
+Each is configured to handle different levels of messages. They also store their
+log messages in separate files, so we can separate debug/notice/info logs
+from more serious errors. See the section on [Logging Levels](#logging-levels) for more
+information on the different levels and what they mean.
+
+Once a configuration is created you cannot change it. Instead you should drop
+the configuration and re-create it using `Cake\Log\Log::drop()` and
+`Cake\Log\Log::setConfig()`.
+
+It is also possible to create loggers by providing a closure. This is useful
+when you need full control over how the logger object is built. The closure
+has to return the constructed logger instance. For example:
+
+``` php
+Log::setConfig('special', function () {
+ return new \Cake\Log\Engine\FileLog(['path' => LOGS, 'file' => 'log']);
+});
+```
+
+Configuration options can also be provided as a `DSN` string. This is
+useful when working with environment variables or `PaaS` providers:
+
+``` php
+Log::setConfig('error', [
+ 'url' => 'file:///full/path/to/logs/?levels[]=warning&levels[]=error&file=error',
+]);
+```
+
+> [!WARNING]
+> If you do not configure logging engines, log messages will not be stored.
+
+## Error and Exception Logging
+
+Errors and Exceptions can also be logged. By configuring the corresponding
+values in your **config/app.php** file. Errors will be displayed when debug is
+`true` and logged when debug is `false`. To log uncaught exceptions, set the
+`log` option to `true`. See [Configuration](../development/configuration) for more
+information.
+
+
+
+## Writing to Logs
+
+Writing to the log files can be done in two different ways. The first
+is to use the static `Cake\Log\Log::write()` method:
+
+``` php
+Log::write('debug', 'Something did not work');
+```
+
+The second is to use the `log()` shortcut function available on any
+class using the `LogTrait`. Calling `log()` will internally call
+`Log::write()`:
+
+``` php
+// Executing this inside a class using LogTrait
+$this->log('Something did not work!', 'debug');
+```
+
+All configured log streams are written to sequentially each time
+`Cake\Log\Log::write()` is called. If you have not configured any
+logging engines `log()` will return `false` and no log messages will be
+written.
+
+### Using Placeholders in Messages
+
+If you need to log dynamically defined data, you can use placeholders in your
+log messages and provide an array of key/value pairs in the `$context`
+parameter:
+
+``` php
+// Will log `Could not process for userid=1`
+Log::write('error', 'Could not process for userid={user}', ['user' => $user->id]);
+```
+
+Placeholders that do not have keys defined will not be replaced. If you need to
+use a literal braced word, you must escape the placeholder:
+
+``` php
+// Will log `No {replace}`
+Log::write('error', 'No \\{replace}', ['replace' => 'no']);
+```
+
+If you include objects in your logging placeholders those objects must implement
+one of the following methods:
+
+- `__toString()`
+- `toArray()`
+- `__debugInfo()`
+
+
+
+### Using Levels
+
+CakePHP supports the standard POSIX set of logging levels. Each level represents
+an increasing level of severity:
+
+- Emergency: system is unusable
+- Alert: action must be taken immediately
+- Critical: critical conditions
+- Error: error conditions
+- Warning: warning conditions
+- Notice: normal but significant condition
+- Info: informational messages
+- Debug: debug-level messages
+
+You can refer to these levels by name when configuring loggers, and when writing
+log messages. Alternatively, you can use convenience methods like
+`Cake\Log\Log::error()` to clearly indicate the logging
+level. Using a level that is not in the above levels will result in an
+exception.
+
+> [!NOTE]
+> When `levels` is set to an empty value in a logger's configuration, it
+> will take messages of any level.
+
+
+
+### Logging Scopes
+
+Often times you'll want to configure different logging behavior for different
+subsystems or parts of your application. Take for example an e-commerce shop.
+You'll probably want to handle logging for orders and payments differently than
+you do other less critical logs.
+
+CakePHP exposes this concept as logging scopes. When log messages are written
+you can include a scope name. If there is a configured logger for that scope,
+the log messages will be directed to those loggers. For example:
+
+``` php
+use Cake\Log\Engine\FileLog;
+
+// Configure logs/shops.log to receive all levels, but only
+// those with `orders` and `payments` scope.
+Log::setConfig('shops', [
+ 'className' => FileLog::class,
+ 'path' => LOGS,
+ 'levels' => [],
+ 'scopes' => ['orders', 'payments'],
+ 'file' => 'shops.log',
+]);
+
+// Configure logs/payments.log to receive all levels, but only
+// those with `payments` scope.
+Log::setConfig('payments', [
+ 'className' => FileLog::class,
+ 'path' => LOGS,
+ 'levels' => [],
+ 'scopes' => ['payments'],
+ 'file' => 'payments.log',
+]);
+
+Log::warning('this gets written only to shops.log', ['scope' => ['orders']]);
+Log::warning('this gets written to both shops.log and payments.log', ['scope' => ['payments']]);
+```
+
+Scopes can also be passed as a single string or a numerically indexed array.
+Note that using this form will limit the ability to pass more data as context:
+
+``` php
+Log::warning('This is a warning', ['orders']);
+Log::warning('This is a warning', 'payments');
+```
+
+> [!NOTE]
+> When `scopes` is set to an empty array or `null` in a logger's
+> configuration, it will take messages of any scope. Setting it to `false`
+> will only match messages without scope.
+
+
+
+## Logging to Files
+
+As its name implies `FileLog` writes log messages to files. The level of log
+message being written determines the name of the file the message is stored in.
+If a level is not supplied, `LOG_ERR` is used which writes to the
+error log. The default log location is **logs/\$level.log**:
+
+``` php
+// Executing this inside a CakePHP class
+$this->log("Something didn't work!");
+
+// Results in this being appended to logs/error.log
+// 2007-11-02 10:22:02 Error: Something didn't work!
+```
+
+The configured directory must be writable by the web server user in
+order for logging to work correctly.
+
+You can configure additional/alternate FileLog locations when configuring
+a logger. FileLog accepts a `path` which allows for
+custom paths to be used:
+
+``` php
+Log::setConfig('custom_path', [
+ 'className' => 'File',
+ 'path' => '/path/to/custom/place/'
+]);
+```
+
+`FileLog` engine takes the following options:
+
+- `size` Used to implement basic log file rotation. If log file size
+ reaches specified size the existing file is renamed by appending timestamp
+ to filename and new log file is created. Can be integer bytes value or
+ human readable string values like '10MB', '100KB' etc. Defaults to 10MB.
+- `rotate` Log files are rotated specified times before being removed.
+ If value is 0, old versions are removed rather then rotated. Defaults to 10.
+- `mask` Set the file permissions for created files. If left empty the default
+ permissions are used.
+
+> [!NOTE]
+> Missing directories will be automatically created to avoid
+> unnecessary errors thrown when using the FileEngine.
+
+
+
+## Logging to Syslog
+
+In production environments it is highly recommended that you setup your system to
+use syslog instead of the file logger. This will perform much better as any
+writes will be done in a (almost) non-blocking fashion and your operating system
+logger can be configured separately to rotate files, pre-process writes or use
+a completely different storage for your logs.
+
+Using syslog is pretty much like using the default FileLog engine, you just need
+to specify `Syslog` as the engine to be used for logging. The following
+configuration snippet will replace the default logger with syslog, this should
+be done in the **config/bootstrap.php** file:
+
+``` php
+Log::setConfig('default', [
+ 'engine' => 'Syslog'
+]);
+```
+
+The configuration array accepted for the Syslog logging engine understands the
+following keys:
+
+- `format`: An sprintf template string with two placeholders, the first one
+ for the error level, and the second for the message itself. This key is
+ useful to add additional information about the server or process in the
+ logged message. For example: `%s - Web Server 1 - %s` will look like
+ `error - Web Server 1 - An error occurred in this request` after
+ replacing the placeholders. This option is deprecated. You should use
+ [Logging Formatters](#logging-formatters) instead.
+- `prefix`: An string that will be prefixed to every logged message.
+- `flag`: An integer flag to be used for opening the connection to the
+ logger, by default `LOG_ODELAY` will be used. See `openlog` documentation
+ for more options
+- `facility`: The logging slot to use in syslog. By default `LOG_USER` is
+ used. See `syslog` documentation for more options
+
+## Creating Log Engines
+
+Log engines can be part of your application, or part of
+plugins. If for example you had a database logger called
+`DatabaseLog`. As part of your application it would be placed in
+**src/Log/Engine/DatabaseLog.php**. As part of a plugin it would be placed in
+**plugins/LoggingPack/src/Log/Engine/DatabaseLog.php**. To configure log
+engine you should use `Cake\Log\Log::setConfig()`. For example
+configuring our DatabaseLog would look like:
+
+``` php
+// For src/Log
+Log::setConfig('otherFile', [
+ 'className' => 'Database',
+ 'model' => 'LogEntry',
+ // ...
+]);
+
+// For plugin called LoggingPack
+Log::setConfig('otherFile', [
+ 'className' => 'LoggingPack.Database',
+ 'model' => 'LogEntry',
+ // ...
+]);
+```
+
+When configuring a log engine the `className` parameter is used to
+locate and load the log handler. All of the other configuration
+properties are passed to the log engine's constructor as an array. :
+
+``` php
+namespace App\Log\Engine;
+use Cake\Log\Engine\BaseLog;
+
+class DatabaseLog extends BaseLog
+{
+ public function __construct(array $config = [])
+ {
+ parent::__construct($config);
+ // ...
+ }
+
+ public function log($level, string $message, array $context = [])
+ {
+ // Write to the database.
+ }
+}
+```
+
+CakePHP requires that all logging engine implement `Psr\Log\LoggerInterface`.
+The class `Cake\Log\Engine\BaseLog` is an easy way to satisfy the
+interface as it only requires you to implement the `log()` method.
+
+
+
+## Logging Formatters
+
+Logging formatters allow you to control how log messages are formatted
+independent of the storage engine. Each core provided logging engine comes with
+a formatter configured to maintain backwards compatible output. However, you can
+adjust the formatters to fit your requirements. Formatters are configured
+alongside the logging engine:
+
+``` php
+use Cake\Log\Engine\SyslogLog;
+use App\Log\Formatter\CustomFormatter;
+
+// Simple formatting configuration with no options.
+Log::setConfig('error', [
+ 'className' => SyslogLog::class,
+ 'formatter' => CustomFormatter::class,
+]);
+
+// Configure a formatter with additional options.
+Log::setConfig('error', [
+ 'className' => SyslogLog::class,
+ 'formatter' => [
+ 'className' => CustomFormatter::class,
+ 'key' => 'value',
+ ],
+]);
+```
+
+To implement your own logging formatter you need to extend
+`Cake\Log\Format\AbstractFormatter` or one of its subclasses. The primary
+method you need to implement is `format($level, $message, $context)` which is
+responsible for formatting log messages.
+
+
+
+## Testing Logs
+
+To test logging, add `Cake\TestSuite\LogTestTrait` to your test case. The
+`LogTestTrait` uses PHPUnit hooks to attach log engines that intercept the log
+messages your application is making. Once you have captured logs you can perform
+assertions on log messages your application is emitting. For example:
+
+``` php
+namespace App\Test\TestCase\Controller;
+
+use Cake\TestSuite\LogTestTrait;
+use Cake\TestSuite\TestCase;
+
+class UsersControllerTest extends TestCase
+{
+ use LogTestTrait;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->setupLog([
+ 'error' => ['scopes' => ['app.security']]
+ ]);
+ }
+
+ public function testResetPassword()
+ {
+ $this->post('/users/resetpassword', ['email' => 'bob@example.com']);
+ $this->assertLogMessageContains('info', 'bob@example.com reset password', 'app.security');
+ }
+}
+```
+
+You use `setupLog()` to define the log messages you wish to capture and
+perform assertions on. After logs have been emitted you can make assertions on
+the contents of logs, or the absence of them:
+
+- `assertLogMessage(string $level, string $expectedMessage, ?string $scope = null, string $failMsg = '')` Assert that a log message was found.
+- `assertLogMessageContains(string $level, string $expectedMessage, ?string $scope = null, string $failMsg = '')` Assert that a log message contains the
+ substring.
+- `assertLogAbsent(string $level, ?string $failMsg = '')` Assert that no log
+ messages of the given level were captured.
+
+The `LogTestTrait` will automatically clean up any loggers that were
+configured.
+
+## Log API
+
+`class` Cake\\Log\\**Log**
+
+### Log::setConfig()
+
+`static` Cake\\Log\\Log::**setConfig**($key, $config): void
+
+param string \$name
+Name for the logger being connected, used
+to drop a logger later on.
+
+param array \$config
+Array of configuration information and
+constructor arguments for the logger.
+
+Get or set the configuration for a Logger. See [Log Configuration](#log-configuration) for
+more information.
+
+### Log::configured()
+
+`static` Cake\\Log\\Log::**configured**(): array
+
+returns
+An array of configured loggers.
+
+Get the names of the configured loggers.
+
+### Log::drop()
+
+`static` Cake\\Log\\Log::**drop**($name): bool
+
+param string \$name
+Name of the logger you wish to no longer receive
+messages.
+
+### Log::write()
+
+`static` Cake\\Log\\Log::**write**($level, $message, $scope = []): bool
+
+Write a message into all the configured loggers.
+`$level` indicates the level of log message being created.
+`$message` is the message of the log entry being written to.
+`$scope` is the scope(s) a log message is being created in.
+
+### Log::levels()
+
+`static` Cake\\Log\\Log::**levels**(): array
+
+Call this method without arguments, eg: Log::levels() to obtain current
+level configuration.
+
+### Convenience Methods
+
+The following convenience methods were added to log \$message with the
+appropriate log level.
+
+#### Log::emergency()
+
+`static` Cake\\Log\\Log::**emergency**($message, $scope = []): bool
+
+#### Log::alert()
+
+`static` Cake\\Log\\Log::**alert**($message, $scope = []): bool
+
+#### Log::critical()
+
+`static` Cake\\Log\\Log::**critical**($message, $scope = []): bool
+
+#### Log::error()
+
+`static` Cake\\Log\\Log::**error**($message, $scope = []): bool
+
+#### Log::warning()
+
+`static` Cake\\Log\\Log::**warning**($message, $scope = []): bool
+
+#### Log::notice()
+
+`static` Cake\\Log\\Log::**notice**($message, $scope = []): bool
+
+#### Log::info()
+
+`static` Cake\\Log\\Log::**info**($message, $scope = []): bool
+
+#### Log::debug()
+
+`static` Cake\\Log\\Log::**debug**($message, $scope = []): bool
+
+## Logging Trait
+
+> A trait that provides shortcut methods for logging
+
+### Log::log()
+
+`method` Cake\\Log\\Log::**log**($msg, $level = LOG_ERR): bool
+
+## Using Monolog
+
+Monolog is a popular logger for PHP. Since it implements the same interfaces as
+the CakePHP loggers, you can use them in your application as the default
+logger.
+
+After installing Monolog using composer, configure the logger using the
+`Log::setConfig()` method:
+
+``` php
+// config/bootstrap.php
+
+use Monolog\Logger;
+use Monolog\Handler\StreamHandler;
+
+Log::setConfig('default', function () {
+ $log = new Logger('app');
+ $log->pushHandler(new StreamHandler('path/to/your/combined.log'));
+
+ return $log;
+});
+
+// Optionally stop using the now redundant default loggers
+Log::drop('debug');
+Log::drop('error');
+```
+
+Use similar methods if you want to configure a different logger for your console:
+
+``` php
+// config/bootstrap_cli.php
+
+use Monolog\Logger;
+use Monolog\Handler\StreamHandler;
+
+Log::setConfig('default', function () {
+ $log = new Logger('cli');
+ $log->pushHandler(new StreamHandler('path/to/your/combined-cli.log'));
+
+ return $log;
+});
+
+// Optionally stop using the now redundant default CLI loggers
+Configure::delete('Log.debug');
+Configure::delete('Log.error');
+```
+
+> [!NOTE]
+> When using a console specific logger, make sure to conditionally configure
+> your application logger. This will prevent duplicate log entries.
diff --git a/docs/en/core-libraries/number.md b/docs/en/core-libraries/number.md
new file mode 100644
index 0000000000..fdc5704919
--- /dev/null
+++ b/docs/en/core-libraries/number.md
@@ -0,0 +1,348 @@
+# Number
+
+`class` Cake\\I18n\\**Number**
+
+If you need `NumberHelper` functionalities outside of a `View`,
+use the `Number` class:
+
+``` php
+namespace App\Controller;
+
+use Cake\I18n\Number;
+
+class UsersController extends AppController
+{
+ public function initialize(): void
+ {
+ parent::initialize();
+ $this->loadComponent('Authentication.Authentication');
+ }
+
+ public function afterLogin()
+ {
+ $identity = $this->Authentication->getIdentity();
+ $storageUsed = $identity->storage_used;
+ if ($storageUsed > 5000000) {
+ // Notify users of quota
+ $this->Flash->success(__('You are using {0} storage', Number::toReadableSize($storageUsed)));
+ }
+ }
+}
+```
+
+
+
+All of these functions return the formatted number; they do not
+automatically echo the output into the view.
+
+## Formatting Currency Values
+
+### Number::currency()
+
+`method` Cake\\I18n\\Number::**currency**(mixed $value, string $currency = null, array $options = []): string
+
+This method is used to display a number in common currency formats
+(EUR, GBP, USD), based on the 3-letter ISO 4217 currency code. Usage in a view looks like:
+
+``` php
+// Called as NumberHelper
+echo $this->Number->currency($value, $currency);
+
+// Called as Number
+echo Number::currency($value, $currency);
+```
+
+The first parameter, `$value`, should be a floating point number
+that represents the amount of money you are expressing. The second
+parameter is a string used to choose a predefined currency formatting
+scheme:
+
+| \$currency | 1234.56, formatted by currency type |
+|------------|-------------------------------------|
+| EUR | €1.234,56 |
+| GBP | £1,234.56 |
+| USD | \$1,234.56 |
+
+The third parameter is an array of options for further defining the
+output. The following options are available:
+
+| Option | Description |
+|----|----|
+| before | Text to display before the rendered number. |
+| after | Text to display after the rendered number. |
+| zero | The text to use for zero values; can be a string or a number. ie. 0, 'Free!'. |
+| places | Number of decimal places to use, ie. 2 |
+| precision | Maximal number of decimal places to use, ie. 2 |
+| locale | The locale name to use for formatting number, ie. "fr_FR". |
+| fractionSymbol | String to use for fraction numbers, ie. ' cents'. |
+| fractionPosition | Either 'before' or 'after' to place the fraction symbol. |
+| pattern | An ICU number pattern to use for formatting the number ie. \#,###.00 |
+| useIntlCode | Set to `true` to replace the currency symbol with the international currency code. |
+
+If `$currency` value is `null`, the default currency will be retrieved from
+`Cake\I18n\Number::defaultCurrency()`. To format currencies in an
+accounting format you should set the currency format:
+
+``` php
+Number::setDefaultCurrencyFormat(Number::FORMAT_CURRENCY_ACCOUNTING);
+```
+
+## Setting the Default Currency
+
+### Number::setDefaultCurrency()
+
+`method` Cake\\I18n\\Number::**setDefaultCurrency**($currency): void
+
+Setter for the default currency. This removes the need to always pass the
+currency to `Cake\I18n\Number::currency()` and change all
+currency outputs by setting other default. If `$currency` is set to `null`,
+it will clear the currently stored value.
+
+## Getting the Default Currency
+
+### Number::getDefaultCurrency()
+
+`method` Cake\\I18n\\Number::**getDefaultCurrency**(): string
+
+Getter for the default currency. If default currency was set earlier using
+`setDefaultCurrency()`, then that value will be returned. By default, it will
+retrieve the `intl.default_locale` ini value if set and `'en_US'` if not.
+
+## Formatting Floating Point Numbers
+
+### Number::precision()
+
+`method` Cake\\I18n\\Number::**precision**(float $value, int $precision = 3, array $options = []): string
+
+This method displays a number with the specified amount of
+precision (decimal places). It will round in order to maintain the
+level of precision defined. :
+
+``` php
+// Called as NumberHelper
+echo $this->Number->precision(456.91873645, 2);
+
+// Outputs
+456.92
+
+// Called as Number
+echo Number::precision(456.91873645, 2);
+```
+
+## Formatting Percentages
+
+### Number::toPercentage()
+
+`method` Cake\\I18n\\Number::**toPercentage**(mixed $value, int $precision = 2, array $options = []): string
+
+| Option | Description |
+|----|----|
+| multiply | Boolean to indicate whether the value has to be multiplied by 100. Useful for decimal percentages. |
+
+Like `Cake\I18n\Number::precision()`, this method formats a number
+according to the supplied precision (where numbers are rounded to meet the
+given precision). This method also expresses the number as a percentage
+and appends the output with a percent sign. :
+
+``` php
+// Called as NumberHelper. Output: 45.69%
+echo $this->Number->toPercentage(45.691873645);
+
+// Called as Number. Output: 45.69%
+echo Number::toPercentage(45.691873645);
+
+// Called with multiply. Output: 45.7%
+echo Number::toPercentage(0.45691, 1, [
+ 'multiply' => true
+]);
+```
+
+## Interacting with Human Readable Values
+
+### Number::toReadableSize()
+
+`method` Cake\\I18n\\Number::**toReadableSize**(string $size): string
+
+This method formats data sizes in human readable forms. It provides
+a shortcut way to convert bytes to KB, MB, GB, and TB. The size is
+displayed with a two-digit precision level, according to the size
+of data supplied (i.e. higher sizes are expressed in larger
+terms):
+
+``` php
+// Called as NumberHelper
+echo $this->Number->toReadableSize(0); // 0 Byte
+echo $this->Number->toReadableSize(1024); // 1 KB
+echo $this->Number->toReadableSize(1321205.76); // 1.26 MB
+echo $this->Number->toReadableSize(5368709120); // 5 GB
+
+// Called as Number
+echo Number::toReadableSize(0); // 0 Byte
+echo Number::toReadableSize(1024); // 1 KB
+echo Number::toReadableSize(1321205.76); // 1.26 MB
+echo Number::toReadableSize(5368709120); // 5 GB
+```
+
+## Formatting Numbers
+
+### Number::format()
+
+`method` Cake\\I18n\\Number::**format**(mixed $value, array $options = []): string
+
+This method gives you much more control over the formatting of
+numbers for use in your views (and is used as the main method by
+most of the other NumberHelper methods). Using this method might
+looks like:
+
+``` php
+// Called as NumberHelper
+$this->Number->format($value, $options);
+
+// Called as Number
+Number::format($value, $options);
+```
+
+The `$value` parameter is the number that you are planning on
+formatting for output. With no `$options` supplied, the number
+1236.334 would output as 1,236. Note that the default precision is
+zero decimal places.
+
+The `$options` parameter is where the real magic for this method
+resides.
+
+- If you pass an integer then this becomes the amount of precision
+ or places for the function.
+- If you pass an associated array, you can use the following keys:
+
+| Option | Description |
+|----|----|
+| places | Number of decimal places to use, ie. 2 |
+| precision | Maximum number of decimal places to use, ie. 2 |
+| pattern | An ICU number pattern to use for formatting the number ie. \#,###.00 |
+| locale | The locale name to use for formatting number, ie. "fr_FR". |
+| before | Text to display before the rendered number. |
+| after | Text to display after the rendered number. |
+
+Example:
+
+``` php
+// Called as NumberHelper
+echo $this->Number->format('123456.7890', [
+ 'places' => 2,
+ 'before' => '¥ ',
+ 'after' => ' !'
+]);
+// Output '¥ 123,456.79 !'
+
+echo $this->Number->format('123456.7890', [
+ 'locale' => 'fr_FR'
+]);
+// Output '123 456,79 !'
+
+// Called as Number
+echo Number::format('123456.7890', [
+ 'places' => 2,
+ 'before' => '¥ ',
+ 'after' => ' !'
+]);
+// Output '¥ 123,456.79 !'
+
+echo Number::format('123456.7890', [
+ 'locale' => 'fr_FR'
+]);
+// Output '123 456,79 !'
+```
+
+### Number::ordinal()
+
+`method` Cake\\I18n\\Number::**ordinal**(mixed $value, array $options = []): string
+
+This method will output an ordinal number.
+
+Examples:
+
+``` php
+echo Number::ordinal(1);
+// Output '1st'
+
+echo Number::ordinal(2);
+// Output '2nd'
+
+echo Number::ordinal(2, [
+ 'locale' => 'fr_FR'
+]);
+// Output '2e'
+
+echo Number::ordinal(410);
+// Output '410th'
+```
+
+## Format Differences
+
+### Number::formatDelta()
+
+`method` Cake\\I18n\\Number::**formatDelta**(mixed $value, array $options = []): string
+
+This method displays differences in value as a signed number:
+
+``` php
+// Called as NumberHelper
+$this->Number->formatDelta($value, $options);
+
+// Called as Number
+Number::formatDelta($value, $options);
+```
+
+The `$value` parameter is the number that you are planning on
+formatting for output. With no `$options` supplied, the number
+1236.334 would output as 1,236. Note that the default precision is
+zero decimal places.
+
+The `$options` parameter takes the same keys as `Number::format()` itself:
+
+| Option | Description |
+|-----------|------------------------------------------------------------|
+| places | Number of decimal places to use, ie. 2 |
+| precision | Maximum number of decimal places to use, ie. 2 |
+| locale | The locale name to use for formatting number, ie. "fr_FR". |
+| before | Text to display before the rendered number. |
+| after | Text to display after the rendered number. |
+
+Example:
+
+``` php
+// Called as NumberHelper
+echo $this->Number->formatDelta('123456.7890', [
+ 'places' => 2,
+ 'before' => '[',
+ 'after' => ']'
+]);
+// Output '[+123,456.79]'
+
+// Called as Number
+echo Number::formatDelta('123456.7890', [
+ 'places' => 2,
+ 'before' => '[',
+ 'after' => ']'
+]);
+// Output '[+123,456.79]'
+```
+
+
+
+## Configure formatters
+
+### Number::config()
+
+`method` Cake\\I18n\\Number::**config**(string $locale, int $type = NumberFormatter::DECIMAL, array $options = []): void
+
+This method allows you to configure formatter defaults which persist across calls
+to various methods.
+
+Example:
+
+``` php
+Number::config('en_IN', \NumberFormatter::CURRENCY, [
+ 'pattern' => '#,##,##0'
+]);
+```
diff --git a/docs/en/core-libraries/plugin.md b/docs/en/core-libraries/plugin.md
new file mode 100644
index 0000000000..182b41c31b
--- /dev/null
+++ b/docs/en/core-libraries/plugin.md
@@ -0,0 +1,58 @@
+# Plugin Class
+
+`class` Cake\\Core\\**Plugin**
+
+The Plugin class is responsible for resource location and path management of plugins.
+
+## Locating Plugins
+
+### Plugin::path()
+
+`static` Cake\\Core\\Plugin::**path**(string $plugin): string
+
+Plugins can be located with Plugin. Using `Plugin::path('DebugKit');`
+for example, will give you the full path to the DebugKit plugin:
+
+``` php
+$path = Plugin::path('DebugKit');
+```
+
+## Check if a Plugin is Loaded
+
+You can check dynamically inside your code if a specific plugin has been loaded:
+
+``` php
+$isLoaded = Plugin::isLoaded('DebugKit');
+```
+
+Use `Plugin::loaded()` if you want to get a list of all currently loaded plugins.
+
+## Finding Paths to Namespaces
+
+### Plugin::classPath()
+
+`static` Cake\\Core\\Plugin::**classPath**(string $plugin): string
+
+Used to get the location of the plugin's class files:
+
+``` php
+$path = App::classPath('DebugKit');
+```
+
+## Finding Paths to Resources
+
+### Plugin::templatePath()
+
+`static` Cake\\Core\\Plugin::**templatePath**(string $plugin): string
+
+The method returns the path to the plugins' templates:
+
+``` php
+$path = Plugin::templatePath('DebugKit');
+```
+
+The same goes for the config path:
+
+``` php
+$path = Plugin::configPath('DebugKit');
+```
diff --git a/docs/en/core-libraries/registry-objects.md b/docs/en/core-libraries/registry-objects.md
new file mode 100644
index 0000000000..6215c70862
--- /dev/null
+++ b/docs/en/core-libraries/registry-objects.md
@@ -0,0 +1,57 @@
+# Registry Objects
+
+The registry classes provide a simple way to create and retrieve loaded
+instances of a given object type. There are registry classes for Components,
+Helpers, Tasks, and Behaviors.
+
+While the examples below will use Components, the same behavior can be expected
+for Helpers, Behaviors, and Tasks in addition to Components.
+
+## Loading Objects
+
+Objects can be loaded on-the-fly using add\()
+Example:
+
+``` php
+$this->loadComponent('Acl.Acl');
+$this->addHelper('Flash')
+```
+
+This will result in the `Acl` property and `Flash` helper being loaded.
+Configuration can also be set on-the-fly. Example:
+
+``` php
+$this->loadComponent('Cookie', ['name' => 'sweet']);
+```
+
+Any keys and values provided will be passed to the Component's constructor. The
+one exception to this rule is `className`. Classname is a special key that is
+used to alias objects in a registry. This allows you to have component names
+that do not reflect the classnames, which can be helpful when extending core
+components:
+
+``` php
+$this->Flash = $this->loadComponent('Flash', ['className' => 'MyCustomFlash']);
+$this->Flash->error(); // Actually using MyCustomFlash::error();
+```
+
+## Triggering Callbacks
+
+Callbacks are not provided by registry objects. You should use the
+[events system](../core-libraries/events) to dispatch any events/callbacks
+for your application.
+
+## Disabling Callbacks
+
+In previous versions, collection objects provided a `disable()` method to disable
+objects from receiving callbacks. You should use the features in the events system to
+accomplish this now. For example, you could disable component callbacks in the
+following way:
+
+``` php
+// Remove MyComponent from callbacks.
+$this->getEventManager()->off($this->MyComponent);
+
+// Re-enable MyComponent for callbacks.
+$this->getEventManager()->on($this->MyComponent);
+```
diff --git a/docs/en/core-libraries/security.md b/docs/en/core-libraries/security.md
new file mode 100644
index 0000000000..d28286c75a
--- /dev/null
+++ b/docs/en/core-libraries/security.md
@@ -0,0 +1,111 @@
+# Security Utility
+
+`class` Cake\\Utility\\**Security**
+
+The [security library](https://api.cakephp.org/5.x/class-Cake.Utility.Security.html)
+handles basic security measures such as providing methods for
+hashing and encrypting data.
+
+## Encrypting and Decrypting Data
+
+### Security::encrypt()
+
+`static` Cake\\Utility\\Security::**encrypt**($text, $key, $hmacSalt = null): string
+
+### Security::decrypt()
+
+`static` Cake\\Utility\\Security::**decrypt**($cipher, $key, $hmacSalt = null): string|null
+
+Encrypt `$text` using AES-256. The `$key` should be a value with a
+lots of variance in the data much like a good password. The returned result
+will be the encrypted value with an HMAC checksum.
+
+The [openssl](https://php.net/openssl) extension is required for encrypting/decrypting.
+
+An example use would be:
+
+``` php
+// Assuming key is stored somewhere it can be re-used for
+// decryption later.
+$key = 'wt1U5MACWJFTXGenFoZoiLwQGrLgdbHA';
+$result = Security::encrypt($value, $key);
+```
+
+If you do not supply an HMAC salt, the value of `Security::getSalt()` will be used.
+Encrypted values can be decrypted using
+`Cake\Utility\Security::decrypt()`.
+
+This method should **never** be used to store passwords.
+
+Decrypt a previously encrypted value. The `$key` and `$hmacSalt`
+parameters must match the values used to encrypt or decryption will fail. An
+example use would be:
+
+``` php
+// Assuming the key is stored somewhere it can be re-used for
+// Decryption later.
+$key = 'wt1U5MACWJFTXGenFoZoiLwQGrLgdbHA';
+
+$cipher = $user->secrets;
+$result = Security::decrypt($cipher, $key);
+```
+
+If the value cannot be decrypted due to changes in the key or HMAC salt
+`false` will be returned.
+
+## Hashing Data
+
+### Security::hash()
+
+`static` Cake\\Utility\\Security::**hash**( $string, $type = NULL, $salt = false ): string
+
+Create a hash from string using given method. Fallback on next
+available method. If `$salt` is set to `true`, the application's salt
+value will be used:
+
+``` php
+// Using the application's salt value
+$sha1 = Security::hash('CakePHP Framework', 'sha1', true);
+
+// Using a custom salt value
+$sha1 = Security::hash('CakePHP Framework', 'sha1', 'my-salt');
+
+// Using the default hash algorithm
+$hash = Security::hash('CakePHP Framework');
+```
+
+The `hash()` method supports the following hashing strategies:
+
+- md5
+- sha1
+- sha256
+
+And any other hash algorithm that PHP's `hash()` function supports.
+
+> [!WARNING]
+> You should not be using `hash()` for passwords in new applications.
+> Instead you should use the `DefaultPasswordHasher` class which uses bcrypt
+> by default.
+
+## Getting Secure Random Data
+
+### Security::randomBytes()
+
+`static` Cake\\Utility\\Security::**randomBytes**($length): string
+
+Get `$length` number of bytes from a secure random source. This function draws
+data from one of the following sources:
+
+- PHP's `random_bytes` function.
+- `openssl_random_pseudo_bytes` from the SSL extension.
+
+If neither source is available a warning will be emitted and an unsafe value
+will be used for backwards compatibility reasons.
+
+### Security::randomString()
+
+`static` Cake\\Utility\\Security::**randomString**($length): string
+
+Get a random string `$length` long from a secure random source. This method
+draws from the same random source as `randomBytes()` and will encode the data
+as a hexadecimal string.
diff --git a/docs/en/core-libraries/text.md b/docs/en/core-libraries/text.md
new file mode 100644
index 0000000000..221653f2d8
--- /dev/null
+++ b/docs/en/core-libraries/text.md
@@ -0,0 +1,451 @@
+# Text
+
+`class` Cake\\Utility\\**Text**
+
+The Text class includes convenience methods for creating and manipulating
+strings and is normally accessed statically. Example:
+`Text::uuid()`.
+
+If you need `Cake\View\Helper\TextHelper` functionalities outside
+of a `View`, use the `Text` class:
+
+``` php
+namespace App\Controller;
+
+use Cake\Utility\Text;
+
+class UsersController extends AppController
+{
+ public function initialize(): void
+ {
+ parent::initialize();
+ $this->loadComponent('Auth')
+ };
+
+ public function afterLogin()
+ {
+ $message = $this->Users->find('new_message')->first();
+ if (!empty($message)) {
+ // Notify user of new message
+ $this->Flash->success(__(
+ 'You have a new message: {0}',
+ Text::truncate($message['Message']['body'], 255, ['html' => true])
+ ));
+ }
+ }
+}
+```
+
+## Convert Strings into ASCII
+
+### Text::transliterate()
+
+`static` Cake\\Utility\\Text::**transliterate**($string, $transliteratorId = null): string
+
+Transliterate by default converts all characters in provided string into
+equivalent ASCII characters. The method expects UTF-8 encoding. The character
+conversion can be controlled using transliteration identifiers which you can
+pass using the `$transliteratorId` argument or change the default identifier
+string using `Text::setTransliteratorId()`. ICU transliteration identifiers
+are basically of form `:` and you can specify
+multiple conversion pairs separated by `;`. You can find more info about
+transliterator identifiers
+[here](https://unicode-org.github.io/icu/userguide/transforms/general/#transliterator-identifiers):
+
+``` php
+// apple puree
+Text::transliterate('apple purée');
+
+// Ubermensch (only latin characters are transliterated)
+Text::transliterate('Übérmensch', 'Latin-ASCII;');
+```
+
+## Creating URL Safe Strings
+
+### Text::slug()
+
+`static` Cake\\Utility\\Text::**slug**(string $string, array|string $options = []): string
+
+Slug transliterates all characters into ASCII versions and converting unmatched
+characters and spaces to dashes. The slug method expects UTF-8 encoding.
+
+You can provide an array of options that controls slug. `$options` can also be
+a string in which case it will be used as replacement string. The supported
+options are:
+
+- `replacement` Replacement string, defaults to '-'.
+
+- `transliteratorId` A valid tranliterator id string. If default `null`
+ `Text::$_defaultTransliteratorId` to be used.
+ If `false` no transliteration will be done, only non words will be removed.
+
+- `preserve` Specific non-word character to preserve. Defaults to `null`.
+ For example, this option can be set to '.' to generate clean file names:
+
+ ``` php
+ // apple-puree
+ Text::slug('apple purée');
+
+ // apple_puree
+ Text::slug('apple purée', '_');
+
+ // foo-bar.tar.gz
+ Text::slug('foo bar.tar.gz', ['preserve' => '.']);
+ ```
+
+## Generating UUIDs
+
+### Text::uuid()
+
+`static` Cake\\Utility\\Text::**uuid**(): string
+
+The UUID method is used to generate unique identifiers as per `4122`. The
+UUID is a 128-bit string in the format of
+`485fc381-e790-47a3-9794-1337c0a8fe68`. :
+
+``` php
+Text::uuid(); // 485fc381-e790-47a3-9794-1337c0a8fe68
+```
+
+::: info Added in version 5.3.0
+You can now configure a custom UUID generator using dependency injection.
+:::
+
+Starting from CakePHP 5.3.0, you can configure a custom UUID generator by
+setting a closure in your configuration:
+
+``` php
+// In your config/app.php or config/bootstrap.php
+use Cake\Core\Configure;
+
+Configure::write('Text.uuidGenerator', function () {
+ // Return your custom UUID string
+ return \MyUuidLibrary::generate();
+});
+```
+
+This allows you to integrate your own UUID generation strategy or use
+third-party UUID libraries. When a custom generator is configured, it will
+be used instead of the default UUID generation method.
+
+## Simple String Parsing
+
+### Text::tokenize()
+
+`static` Cake\\Utility\\Text::**tokenize**(string $data, string $separator = ',', string $leftBound = '(', string $rightBound = ')'): array
+
+Tokenizes a string using `$separator`, ignoring any instance of `$separator`
+that appears between `$leftBound` and `$rightBound`.
+
+This method can be useful when splitting up data that has regular formatting
+such as tag lists:
+
+``` php
+$data = "cakephp 'great framework' php";
+$result = Text::tokenize($data, ' ', "'", "'");
+// Result contains
+['cakephp', "'great framework'", 'php'];
+```
+
+### Text::parseFileSize()
+
+`method` Cake\\Utility\\Text::**parseFileSize**(string $size, mixed $default = false): mixed
+
+This method unformats a number from a human-readable byte size to an integer
+number of bytes:
+
+``` php
+$int = Text::parseFileSize('2GB');
+```
+
+## Formatting Strings
+
+### Text::insert()
+
+`static` Cake\\Utility\\Text::**insert**(string $str, array $data, array $options = []): string
+
+The insert method is used to create string templates and to allow for key/value
+replacements:
+
+``` php
+Text::insert(
+ 'My name is :name and I am :age years old.',
+ ['name' => 'Bob', 'age' => '65']
+);
+// Returns: "My name is Bob and I am 65 years old."
+```
+
+### Text::cleanInsert()
+
+`static` Cake\\Utility\\Text::**cleanInsert**(string $str, array $options): string
+
+Cleans up a `Text::insert` formatted string with given `$options` depending
+on the 'clean' key in `$options`. The default method used is text but html is
+also available. The goal of this function is to replace all whitespace and
+unneeded markup around placeholders that did not get replaced by
+`Text::insert`.
+
+You can use the following options in the options array:
+
+``` php
+$options = [
+ 'clean' => [
+ 'method' => 'text', // or html
+ ],
+ 'before' => '',
+ 'after' => ''
+];
+```
+
+## Wrapping Text
+
+### Text::wrap()
+
+`static` Cake\\Utility\\Text::**wrap**(string $text, array|int $options = []): string
+
+Wraps a block of text to a set width and indents blocks as well.
+Can intelligently wrap text so words are not sliced across lines:
+
+``` php
+$text = 'This is the song that never ends.';
+$result = Text::wrap($text, 22);
+
+// Returns
+This is the song that
+never ends.
+```
+
+You can provide an array of options that control how wrapping is done. The
+supported options are:
+
+- `width` The width to wrap to. Defaults to 72.
+- `wordWrap` Whether or not to wrap whole words. Defaults to `true`.
+- `indent` The character to indent lines with. Defaults to ''.
+- `indentAt` The line number to start indenting text. Defaults to 0.
+
+### Text::wrapBlock()
+
+`static` Cake\\Utility\\Text::**wrapBlock**(string $text, array|int $options = []): string
+
+If you need to ensure that the total width of the generated block won't
+exceed a certain length even with internal indentation, you need to use
+`wrapBlock()` instead of `wrap()`. This is particularly useful to generate
+text for the console for example. It accepts the same options as `wrap()`:
+
+``` php
+$text = 'This is the song that never ends. This is the song that never ends.';
+$result = Text::wrapBlock($text, [
+ 'width' => 22,
+ 'indent' => ' → ',
+ 'indentAt' => 1
+]);
+
+// Returns
+This is the song that
+ → never ends. This
+ → is the song that
+ → never ends.
+```
+
+
+
+## Highlighting Substrings
+
+### Text::highlight()
+
+`method` Cake\\Utility\\Text::**highlight**(string $text, array|string $phrase, array $options = []): string
+
+Highlights `$phrase` in `$text` using the `$options['format']` string
+specified or a default string.
+
+Options:
+
+- `format` string - The piece of HTML with the phrase that will be
+ highlighted
+- `html` bool - If `true`, will ignore any HTML tags, ensuring that only
+ the correct text is highlighted
+
+Example:
+
+``` php
+// Called as TextHelper
+echo $this->Text->highlight(
+ $lastSentence,
+ 'using',
+ ['format' => '\1']
+);
+
+// Called as Text
+use Cake\Utility\Text;
+
+echo Text::highlight(
+ $lastSentence,
+ 'using',
+ ['format' => '\1']
+);
+```
+
+Output:
+
+> Highlights \$phrase in \$text \using\ the
+> \$options\['format'\] string specified or a default string.
+
+## Truncating Text
+
+### Text::truncate()
+
+`method` Cake\\Utility\\Text::**truncate**(string $text, int $length = 100, array $options = []): string
+
+If `$text` is longer than `$length`, this method truncates it at `$length`
+and adds a suffix consisting of `'ellipsis'`, if defined. If `'exact'` is
+passed as `false`, the truncation will occur at the first whitespace after the
+point at which `$length` is exceeded. If `'html'` is passed as `true`,
+HTML tags will be respected and will not be cut off.
+
+`$options` is used to pass all extra parameters, and has the following
+possible keys by default, all of which are optional:
+
+``` php
+[
+ 'ellipsis' => '...',
+ 'exact' => true,
+ 'html' => false
+]
+```
+
+Example:
+
+``` php
+// Called as TextHelper
+echo $this->Text->truncate(
+ 'The killer crept forward and tripped on the rug.',
+ 22,
+ [
+ 'ellipsis' => '...',
+ 'exact' => false
+ ]
+);
+
+// Called as Text
+use Cake\Utility\Text;
+
+echo Text::truncate(
+ 'The killer crept forward and tripped on the rug.',
+ 22,
+ [
+ 'ellipsis' => '...',
+ 'exact' => false
+ ]
+);
+```
+
+Output:
+
+ The killer crept...
+
+## Truncating the Tail of a String
+
+### Text::tail()
+
+`method` Cake\\Utility\\Text::**tail**(string $text, int $length = 100, array $options = []): string
+
+If `$text` is longer than `$length`, this method removes an initial
+substring with length consisting of the difference and prepends a prefix
+consisting of `'ellipsis'`, if defined. If `'exact'` is passed as `false`,
+the truncation will occur at the first whitespace prior to the point at which
+truncation would otherwise take place.
+
+`$options` is used to pass all extra parameters, and has the following
+possible keys by default, all of which are optional:
+
+``` php
+[
+ 'ellipsis' => '...',
+ 'exact' => true
+]
+```
+
+Example:
+
+``` php
+$sampleText = 'I packed my bag and in it I put a PSP, a PS3, a TV, ' .
+ 'a C# program that can divide by zero, death metal t-shirts'
+
+// Called as TextHelper
+echo $this->Text->tail(
+ $sampleText,
+ 70,
+ [
+ 'ellipsis' => '...',
+ 'exact' => false
+ ]
+);
+
+// Called as Text
+use Cake\Utility\Text;
+
+echo Text::tail(
+ $sampleText,
+ 70,
+ [
+ 'ellipsis' => '...',
+ 'exact' => false
+ ]
+);
+```
+
+Output:
+
+ ...a TV, a C# program that can divide by zero, death metal t-shirts
+
+## Extracting an Excerpt
+
+### Text::excerpt()
+
+`method` Cake\\Utility\\Text::**excerpt**(string $text, string $phrase, int $radius = 100, string $ellipsis = '…'): string
+
+Extracts an excerpt from `$text` surrounding the `$phrase` with a number
+of characters on each side determined by `$radius`, and prefix/suffix with
+`$ellipsis`. This method is especially handy for search results. The query
+string or keywords can be shown within the resulting document. :
+
+``` php
+// Called as TextHelper
+echo $this->Text->excerpt($lastParagraph, 'method', 50, '...');
+
+// Called as Text
+use Cake\Utility\Text;
+
+echo Text::excerpt($lastParagraph, 'method', 50, '...');
+```
+
+Output:
+
+ ... by $radius, and prefix/suffix with $ellipsis. This method is especially
+ handy for search results. The query...
+
+## Converting an Array to Sentence Form
+
+### Text::toList()
+
+`method` Cake\\Utility\\Text::**toList**(array $list, ?string $and = null, $separator = ', '): string
+
+Creates a comma-separated list where the last two items are joined with 'and':
+
+``` php
+$colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
+
+// Called as TextHelper
+echo $this->Text->toList($colors);
+
+// Called as Text
+use Cake\Utility\Text;
+
+echo Text::toList($colors);
+```
+
+Output:
+
+ red, orange, yellow, green, blue, indigo and violet
+
+
diff --git a/docs/en/core-libraries/time.md b/docs/en/core-libraries/time.md
new file mode 100644
index 0000000000..2e5793f59a
--- /dev/null
+++ b/docs/en/core-libraries/time.md
@@ -0,0 +1,513 @@
+# Date & Time
+
+`class` Cake\\I18n\\**DateTime**
+
+If you need `TimeHelper` functionalities outside of a `View`,
+use the `DateTime` class:
+
+``` php
+use Cake\I18n\DateTime;
+
+class UsersController extends AppController
+{
+ public function initialize(): void
+ {
+ parent::initialize();
+ $this->loadComponent('Authentication.Authentication');
+ }
+
+ public function afterLogin()
+ {
+ $identity = $this->Authentication->getIdentity();
+ $time = new DateTime($identity->date_of_birth);
+ if ($time->isToday()) {
+ // Greet user with a happy birthday message
+ $this->Flash->success(__('Happy birthday to you...'));
+ }
+ }
+}
+```
+
+Under the hood, CakePHP uses [Chronos](https://github.com/cakephp/chronos)
+to power its `DateTime` utility. Anything you can do with `Chronos` and
+PHP's `DateTimeImmutable`, you can do with `DateTime`.
+
+For more details on Chronos please see [the API documentation](https://api.cakephp.org/chronos/).
+
+
+
+## Creating DateTime Instances
+
+`DateTime` are immutable objects as immutability prevents accidental changes
+to data, and avoids order based dependency issues.
+
+There are a few ways to create `DateTime` instances:
+
+``` php
+use Cake\I18n\DateTime;
+
+// Create from a string datetime.
+$time = DateTime::createFromFormat(
+ 'Y-m-d H:i:s',
+ '2021-01-31 22:11:30',
+ 'America/New_York'
+);
+
+// Create from a timestamp and set timezone
+$time = DateTime::createFromTimestamp(1612149090, 'America/New_York');
+
+// Get the current time.
+$time = DateTime::now();
+
+// Or just use 'new'
+$time = new DateTime('2021-01-31 22:11:30', 'America/New_York');
+
+$time = new DateTime('2 hours ago');
+```
+
+The `DateTime` class constructor can take any parameter that the internal `DateTimeImmutable`
+PHP class can. When passing a number or numeric string, it will be interpreted
+as a UNIX timestamp.
+
+In test cases, you can mock out `now()` using `setTestNow()`:
+
+``` php
+// Fixate time.
+$time = new DateTime('2021-01-31 22:11:30');
+DateTime::setTestNow($time);
+
+// Outputs '2021-01-31 22:11:30'
+$now = DateTime::now();
+echo $now->i18nFormat('yyyy-MM-dd HH:mm:ss');
+
+// Outputs '2021-01-31 22:11:30'
+$now = DateTime::parse('now');
+echo $now->i18nFormat('yyyy-MM-dd HH:mm:ss');
+```
+
+## Manipulation
+
+Remember, `DateTime` instance always return a new instance from setters
+instead of modifying itself:
+
+``` php
+$time = DateTime::now();
+
+// Create and reassign a new instance
+$newTime = $time->year(2013)
+ ->month(10)
+ ->day(31);
+// Outputs '2013-10-31 22:11:30'
+echo $newTime->i18nFormat('yyyy-MM-dd HH:mm:ss');
+```
+
+You can also use the methods provided by PHP's built-in `DateTime` class:
+
+``` php
+$time = $time->setDate(2013, 10, 31);
+```
+
+Failing to reassign the new `DateTime` instances will result in the
+original, unmodified instance being used:
+
+``` php
+$time->year(2013)
+ ->month(10)
+ ->day(31);
+// Outputs '2021-01-31 22:11:30'
+echo $time->i18nFormat('yyyy-MM-dd HH:mm:ss');
+```
+
+You can create another instance with modified dates, through subtraction and
+addition of their components:
+
+``` php
+$time = DateTime::create(2021, 1, 31, 22, 11, 30);
+$newTime = $time->subDays(5)
+ ->addHours(-2)
+ ->addMonths(1);
+// Outputs '2/26/21, 8:11 PM'
+echo $newTime;
+
+// Using strtotime strings.
+$newTime = $time->modify('+1 month -5 days -2 hours');
+// Outputs '2/26/21, 8:11 PM'
+echo $newTime;
+```
+
+You can get the internal components of a date by accessing its properties:
+
+``` php
+$time = DateTime::create(2021, 1, 31, 22, 11, 30);
+echo $time->year; // 2021
+echo $time->month; // 1
+echo $time->day; // 31
+echo $time->timezoneName; // America/New_York
+```
+
+## Formatting
+
+### DateTime::setJsonEncodeFormat()
+
+`static` Cake\\I18n\\DateTime::**setJsonEncodeFormat**($format): void
+
+This method sets the default format used when converting an object to json:
+
+``` php
+DateTime::setJsonEncodeFormat('yyyy-MM-dd HH:mm:ss'); // For any immutable DateTime
+Date::setJsonEncodeFormat('yyyy-MM-dd HH:mm:ss'); // For any mutable Date
+
+$time = DateTime::parse('2021-01-31 22:11:30');
+echo json_encode($time); // Outputs '2021-01-31 22:11:30'
+
+Date::setJsonEncodeFormat(static function($time) {
+ return $time->format(DATE_ATOM);
+});
+```
+
+> [!NOTE]
+> This method must be called statically.
+
+> [!NOTE]
+> Be aware that this is not a PHP Datetime string format! You need to use a
+> ICU date formatting string as specified in the following resource:
+> .
+
+::: info Changed in version 4.1.0
+The `callable` parameter type was added.
+:::
+
+### DateTime::i18nFormat()
+
+`method` Cake\\I18n\\DateTime::**i18nFormat**($format = null, $timezone = null, $locale = null): string|int
+
+A very common thing to do with `Time` instances is to print out formatted
+dates. CakePHP makes this a snap:
+
+``` php
+$time = DateTime::parse('2021-01-31 22:11:30');
+
+// Prints a localized datetime stamp. Outputs '1/31/21, 10:11 PM'
+echo $time;
+
+// Outputs '1/31/21, 10:11 PM' for the en-US locale
+echo $time->i18nFormat();
+
+// Use the full date and time format. Outputs 'Sunday, January 31, 2021 at 10:11:30 PM Eastern Standard Time'
+echo $time->i18nFormat(\IntlDateFormatter::FULL);
+
+// Use full date but short time format. Outputs 'Sunday, January 31, 2021 at 10:11 PM'
+echo $time->i18nFormat([\IntlDateFormatter::FULL, \IntlDateFormatter::SHORT]);
+
+// Outputs '2021-Jan-31 22:11:30'
+echo $time->i18nFormat('yyyy-MMM-dd HH:mm:ss');
+```
+
+It is possible to specify the desired format for the string to be displayed.
+You can either pass [IntlDateFormatter constants](https://www.php.net/manual/en/class.intldateformatter.php) as the first
+argument of this function, or pass a full ICU date formatting string as
+specified in the following resource:
+.
+
+You can also format dates with non-gregorian calendars:
+
+``` php
+// On ICU version 66.1
+$time = DateTime::create(2021, 1, 31, 22, 11, 30);
+
+// Outputs 'Sunday, Bahman 12, 1399 AP at 10:11:30 PM Eastern Standard Time'
+echo $time->i18nFormat(\IntlDateFormatter::FULL, null, 'en-IR@calendar=persian');
+
+// Outputs 'Sunday, January 31, 3 Reiwa at 10:11:30 PM Eastern Standard Time'
+echo $time->i18nFormat(\IntlDateFormatter::FULL, null, 'en-JP@calendar=japanese');
+
+// Outputs 'Sunday, Twelfth Month 19, 2020(geng-zi) at 10:11:30 PM Eastern Standard Time'
+echo $time->i18nFormat(\IntlDateFormatter::FULL, null, 'en-CN@calendar=chinese');
+
+// Outputs 'Sunday, Jumada II 18, 1442 AH at 10:11:30 PM Eastern Standard Time'
+echo $time->i18nFormat(\IntlDateFormatter::FULL, null, 'en-SA@calendar=islamic');
+```
+
+The following calendar types are supported:
+
+- japanese
+- buddhist
+- chinese
+- persian
+- indian
+- islamic
+- hebrew
+- coptic
+- ethiopic
+
+> [!NOTE]
+> For constant strings i.e. IntlDateFormatter::FULL Intl uses ICU library
+> that feeds its data from CLDR () which version
+> may vary depending on PHP installation and give different results.
+
+### DateTime::nice()
+
+`method` Cake\\I18n\\DateTime::**nice**(): string
+
+Print out a predefined 'nice' format:
+
+``` php
+$time = DateTime::parse('2021-01-31 22:11:30', new \DateTimeZone('America/New_York'));
+
+// Outputs 'Jan 31, 2021, 10:11 PM' in en-US
+echo $time->nice();
+```
+
+You can alter the timezone in which the date is displayed without altering the
+`DateTime` object itself. This is useful when you store dates in one timezone, but
+want to display them in a user's own timezone:
+
+``` php
+// Outputs 'Monday, February 1, 2021 at 4:11:30 AM Central European Standard Time'
+echo $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Paris');
+
+// Outputs 'Monday, February 1, 2021 at 12:11:30 PM Japan Standard Time'
+echo $time->i18nFormat(\IntlDateFormatter::FULL, 'Asia/Tokyo');
+
+// Timezone is unchanged. Outputs 'America/New_York'
+echo $time->timezoneName;
+```
+
+Leaving the first parameter as `null` will use the default formatting string:
+
+``` php
+// Outputs '2/1/21, 4:11 AM'
+echo $time->i18nFormat(null, 'Europe/Paris');
+```
+
+Finally, it is possible to use a different locale for displaying a date:
+
+``` php
+// Outputs 'lundi 1 février 2021 à 04:11:30 heure normale d’Europe centrale'
+echo $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Paris', 'fr-FR');
+
+// Outputs '1 févr. 2021 à 04:11'
+echo $time->nice('Europe/Paris', 'fr-FR');
+```
+
+### Setting the Default Locale and Format String
+
+The default locale in which dates are displayed when using `nice`
+`i18nFormat` is taken from the directive
+[intl.default_locale](https://www.php.net/manual/en/intl.configuration.php#ini.intl.default-locale).
+You can, however, modify this default at runtime:
+
+``` php
+DateTime::setDefaultLocale('es-ES');
+Date::setDefaultLocale('es-ES');
+
+// Outputs '31 ene. 2021 22:11'
+echo $time->nice();
+```
+
+From now on, datetimes will be displayed in the Spanish preferred format unless
+a different locale is specified directly in the formatting method.
+
+Likewise, it is possible to alter the default formatting string to be used for
+`i18nFormat`:
+
+``` php
+DateTime::setToStringFormat(\IntlDateFormatter::SHORT); // For any DateTime
+Date::setToStringFormat(\IntlDateFormatter::SHORT); // For any Date
+
+// The same method exists on Date, and DateTime
+DateTime::setToStringFormat([
+ \IntlDateFormatter::FULL,
+ \IntlDateFormatter::SHORT
+]);
+// Outputs 'Sunday, January 31, 2021 at 10:11 PM'
+echo $time;
+
+// The same method exists on Date and DateTime
+DateTime::setToStringFormat("EEEE, MMMM dd, yyyy 'at' KK:mm:ss a");
+// Outputs 'Sunday, January 31, 2021 at 10:11:30 PM'
+echo $time;
+```
+
+It is recommended to always use the constants instead of directly passing a date
+format string.
+
+> [!NOTE]
+> Be aware that this is not a PHP Datetime string format! You need to use a
+> ICU date formatting string as specified in the following resource:
+> .
+
+### Formatting Relative Times
+
+### DateTime::timeAgoInWords()
+
+`method` Cake\\I18n\\DateTime::**timeAgoInWords**(array $options = []): string
+
+Often it is useful to print times relative to the present:
+
+``` php
+$time = new DateTime('Jan 31, 2021');
+// On June 12, 2021, this would output '4 months, 1 week, 6 days ago'
+echo $time->timeAgoInWords(
+ ['format' => 'MMM d, YYY', 'end' => '+1 year']
+);
+```
+
+The `end` option lets you define at which point after which relative times
+should be formatted using the `format` option. The `accuracy` option lets
+us control what level of detail should be used for each interval range:
+
+``` php
+// Outputs '4 months ago'
+echo $time->timeAgoInWords([
+ 'accuracy' => ['month' => 'month'],
+ 'end' => '1 year'
+]);
+```
+
+By setting `accuracy` to a string, you can specify what is the maximum level
+of detail you want output:
+
+``` php
+$time = new DateTime('+23 hours');
+// Outputs 'in about a day'
+echo $time->timeAgoInWords([
+ 'accuracy' => 'day'
+]);
+```
+
+## Conversion
+
+### DateTime::toQuarter()
+
+`method` Cake\\I18n\\DateTime::**toQuarter**(): int|array
+
+### DateTime::toQuarterRange()
+
+`method` Cake\\I18n\\DateTime::**toQuarterRange**(): array
+
+Once created, you can convert `DateTime` instances into timestamps or quarter
+values:
+
+``` php
+$time = new DateTime('2021-01-31');
+echo $time->toQuarter(); // Outputs '1'
+echo $time->toUnixString(); // Outputs '1612069200'
+```
+
+::: info Added in version 5.3.0
+The `toQuarterRange()` method was added.
+:::
+
+You can also get the date range for a quarter:
+
+``` php
+$time = new DateTime('2021-01-31');
+$range = $time->toQuarterRange();
+// Outputs ['2021-01-01', '2021-03-31']
+
+$time = new DateTime('2021-12-25');
+$range = $time->toQuarterRange();
+// Outputs ['2021-10-01', '2021-12-31']
+```
+
+## Comparing With the Present
+
+### DateTime::isYesterday()
+
+`method` Cake\\I18n\\DateTime::**isYesterday**()
+
+### DateTime::isThisWeek()
+
+`method` Cake\\I18n\\DateTime::**isThisWeek**()
+
+### DateTime::isThisMonth()
+
+`method` Cake\\I18n\\DateTime::**isThisMonth**()
+
+### DateTime::isThisYear()
+
+`method` Cake\\I18n\\DateTime::**isThisYear**()
+
+You can compare a `DateTime` instance with the present in a variety of ways:
+
+``` php
+$time = new DateTime('+3 days');
+
+debug($time->isYesterday());
+debug($time->isThisWeek());
+debug($time->isThisMonth());
+debug($time->isThisYear());
+```
+
+Each of the above methods will return `true`/`false` based on whether or
+not the `DateTime` instance matches the present.
+
+## Comparing With Intervals
+
+### DateTime::isWithinNext()
+
+`method` Cake\\I18n\\DateTime::**isWithinNext**($interval)
+
+You can see if a `DateTime` instance falls within a given range using
+`wasWithinLast()` and `isWithinNext()`:
+
+``` php
+$time = new DateTime('+3 days');
+
+// Within 2 days. Outputs 'false'
+debug($time->isWithinNext('2 days'));
+
+// Within 2 next weeks. Outputs 'true'
+debug($time->isWithinNext('2 weeks'));
+```
+
+### DateTime::wasWithinLast()
+
+`method` Cake\\I18n\\DateTime::**wasWithinLast**($interval)
+
+You can also compare a `DateTime` instance within a range in the past:
+
+``` php
+$time = new DateTime('-72 hours');
+
+// Within past 2 days. Outputs 'false'
+debug($time->wasWithinLast('2 days'));
+
+// Within past 3 days. Outputs 'true'
+debug($time->wasWithinLast('3 days'));
+
+// Within past 2 weeks. Outputs 'true'
+debug($time->wasWithinLast('2 weeks'));
+```
+
+
+
+## Date
+
+`class` Cake\\I18n\\**Date**
+
+The immutable `Date` class in CakePHP represents calendar dates unaffected by
+time and timezones. The `Date` class wraps the `Cake\Chronos\ChronosDate` class.
+
+> [!NOTE]
+> Unlike the `DateTime` class, `Date` does not extends the `DateTimeInterface`.
+> So you cannot cannot directly compare a `Date` instance with a `DateTime` instance.
+> But you can do comparisons like `$dateTime->toNative() > $date->toNative()`.
+
+## Time
+
+`class` Cake\\I18n\\**Time**
+
+The `Time` class represents clock times independent of date or time zones
+Similar to the `DateTime` and `` `Date `` classes, the `Time` class is also immutable.
+It wraps the `Cake\Chronos\ChronosTime` class.
+
+## Accepting Localized Request Data
+
+When creating text inputs that manipulate dates, you'll probably want to accept
+and parse localized datetime strings. See the [Parsing Localized Dates](../core-libraries/internationalization-and-localization#parsing-localized-dates).
+
+## Supported Timezones
+
+CakePHP supports all valid PHP timezones. For a list of supported timezones, [see this page](https://php.net/manual/en/timezones.php).
diff --git a/docs/en/core-libraries/validation.md b/docs/en/core-libraries/validation.md
new file mode 100644
index 0000000000..97cbcfcb3d
--- /dev/null
+++ b/docs/en/core-libraries/validation.md
@@ -0,0 +1,677 @@
+# Validation
+
+The validation package in CakePHP provides features to build validators that can
+validate arbitrary arrays of data with ease. You can find a [list of available
+Validation rules in the API](https://api.cakephp.org/5.x/class-Cake.Validation.Validation.html).
+
+
+
+## Creating Validators
+
+`class` Cake\\Validation\\**Validator**
+
+Validator objects define the rules that apply to a set of fields.
+Validator objects contain a mapping between fields and validation sets. In
+turn, the validation sets contain a collection of rules that apply to the field
+they are attached to. Creating a validator is simple:
+
+``` php
+use Cake\Validation\Validator;
+
+$validator = new Validator();
+```
+
+Once created, you can start defining sets of rules for the fields you want to
+validate:
+
+``` php
+$validator
+ ->requirePresence('title')
+ ->notEmptyString('title', 'Please fill this field')
+ ->add('title', [
+ 'length' => [
+ 'rule' => ['minLength', 10],
+ 'message' => 'Titles need to be at least 10 characters long',
+ ]
+ ])
+ ->allowEmptyDateTime('published')
+ ->add('published', 'boolean', [
+ 'rule' => 'boolean',
+ ])
+ ->requirePresence('body')
+ ->add('body', 'length', [
+ 'rule' => ['minLength', 50],
+ 'message' => 'Articles must have a substantial body.',
+ ]);
+```
+
+As seen in the example above, validators are built with a fluent interface that
+allows you to define rules for each field you want to validate.
+
+There were a few methods called in the example above, so let's go over the
+various features. The `add()` method allows you to add new rules to
+a validator. You can either add rules individually or in groups as seen above.
+
+### Requiring Field Presence
+
+The `requirePresence()` method requires the field to be present in any
+validated array. If the field is absent, validation will fail. The
+`requirePresence()` method has 4 modes:
+
+- `true` The field's presence is always required.
+- `false` The field's presence is not required.
+- `create` The field's presence is required when validating a **create**
+ operation.
+- `update` The field's presence is required when validating an **update**
+ operation.
+
+By default, `true` is used. Key presence is checked by using
+`array_key_exists()` so that null values will count as present. You can set
+the mode using the second parameter:
+
+``` php
+$validator->requirePresence('author_id', 'create');
+```
+
+If you have multiple fields that are required, you can define them as a list:
+
+``` php
+// Define multiple fields for create
+$validator->requirePresence(['author_id', 'title'], 'create');
+
+// Define multiple fields for mixed modes
+$validator->requirePresence([
+ 'author_id' => [
+ 'mode' => 'create',
+ 'message' => 'An author is required.',
+ ],
+ 'published' => [
+ 'mode' => 'update',
+ 'message' => 'The published state is required.',
+ ],
+]);
+```
+
+### Allowing Empty Fields
+
+Validators offer several methods to control which fields accept empty values and
+which empty values are accepted and not forwarded to other validation rules for
+the named field. CakePHP provides empty value support for different shapes
+of data:
+
+1. `allowEmptyString()` Should be used when you want to only accept
+ an empty string.
+2. `allowEmptyArray()` Should be used when you want to accept an array.
+3. `allowEmptyDate()` Should be used when you want to accept an empty string,
+ or an array that is marshalled into a date field.
+4. `allowEmptyTime()` Should be used when you want to accept an empty string,
+ or an array that is marshalled into a time field.
+5. `allowEmptyDateTime()` Should be used when you want to accept an empty
+ string or an array that is marshalled into a datetime or timestamp field.
+6. `allowEmptyFile()` Should be used when you want to accept an array that
+ contains an empty uploaded file.
+
+You also can use following specific validators: `notEmptyString()`, `notEmptyArray()`, `notEmptyFile()`, `notEmptyDate()`, `notEmptyTime()`, `notEmptyDateTime()`.
+
+The `allowEmpty*` methods support a `when` parameter that allows you to control
+when a field can or cannot be empty:
+
+- `false` The field is not allowed to be empty.
+- `create` The field can be empty when validating a **create**
+ operation.
+- `update` The field can be empty when validating an **update**
+ operation.
+- A callback that returns `true` or `false` to indicate whether a field is
+ allowed to be empty. See the [Conditional Validation](#conditional-validation) section for examples on
+ how to use this parameter.
+
+An example of these methods in action is:
+
+``` php
+$validator->allowEmptyDateTime('published')
+ ->allowEmptyString('title', 'Title cannot be empty', false)
+ ->allowEmptyString('body', 'Body cannot be empty', 'update')
+ ->allowEmptyFile('header_image', 'update');
+ ->allowEmptyDateTime('posted', 'update');
+```
+
+### Adding Validation Rules
+
+The `Validator` class provides methods that make building validators simple
+and expressive. For example adding validation rules to a username could look
+like:
+
+``` php
+$validator = new Validator();
+$validator
+ ->email('username')
+ ->ascii('username')
+ ->lengthBetween('username', [4, 8]);
+```
+
+See the [Validator API documentation](https://api.cakephp.org/5.x/class-Cake.Validation.Validator.html) for the
+full set of validator methods.
+
+
+
+### Using Custom Validation Rules
+
+In addition to using methods on the `Validator`, and coming from providers, you
+can also use any callable, including anonymous functions, as validation rules:
+
+``` php
+// Use a global function
+$validator->add('title', 'custom', [
+ 'rule' => 'validate_title',
+ 'message' => 'The title is not valid'
+]);
+
+// Use an array callable that is not in a provider
+$validator->add('title', 'custom', [
+ 'rule' => [$this, 'method'],
+ 'message' => 'The title is not valid'
+]);
+
+// Use a closure
+$extra = 'Some additional value needed inside the closure';
+$validator->add('title', 'custom', [
+ 'rule' => function ($value, $context) use ($extra) {
+ // Custom logic that returns true/false
+ },
+ 'message' => 'The title is not valid'
+]);
+
+// Use a rule from a custom provider
+$validator->add('title', 'custom', [
+ 'rule' => 'customRule',
+ 'provider' => 'custom',
+ 'message' => 'The title is not unique enough'
+]);
+```
+
+Closures or callable methods will receive 2 arguments when called. The first
+will be the value for the field being validated. The second is a context array
+containing data related to the validation process:
+
+- **data**: The original data passed to the validation method, useful if you
+ plan to create rules comparing values.
+- **providers**: The complete list of rule provider objects, useful if you
+ need to create complex rules by calling multiple providers.
+- **newRecord**: Whether the validation call is for a new record or
+ a preexisting one.
+- **entity**: The entity being validated if provided to `validate()`.
+
+Closures should return boolean true if the validation passes. If it fails,
+return boolean false or for a custom error message return a string, see the
+[Conditional/Dynamic Error Messages](#dynamic_validation_error_messages)
+section for further details.
+
+::: info Changed in version 5.3.0
+The `entity` key was added to validation context.
+:::
+
+
+
+### Conditional/Dynamic Error Messages
+
+Validation rule methods, being it [custom callables](#custom-validation-rules),
+or [methods supplied by providers](#adding-validation-providers), can either
+return a boolean, indicating whether the validation succeeded, or they can return
+a string, which means that the validation failed, and that the returned string
+should be used as the error message.
+
+Possible existing error messages defined via the `message` option will be
+overwritten by the ones returned from the validation rule method:
+
+``` php
+$validator->add('length', 'custom', [
+ 'rule' => function ($value, $context) {
+ if (!$value) {
+ return false;
+ }
+
+ if ($value < 10) {
+ return 'Error message when value is less than 10';
+ }
+
+ if ($value > 20) {
+ return 'Error message when value is greater than 20';
+ }
+
+ return true;
+ },
+ 'message' => 'Generic error message used when `false` is returned'
+]);
+```
+
+
+
+### Conditional Validation
+
+When defining validation rules, you can use the `on` key to define when
+a validation rule should be applied. If left undefined, the rule will always be
+applied. Other valid values are `create` and `update`. Using one of these
+values will make the rule apply to only create or update operations.
+
+Additionally, you can provide a callable function that will determine whether or
+not a particular rule should be applied:
+
+``` php
+$validator->add('picture', 'file', [
+ 'rule' => ['mimeType', ['image/jpeg', 'image/png']],
+ 'on' => function ($context) {
+ return !empty($context['data']['show_profile_picture']);
+ }
+]);
+```
+
+You can access the other submitted field values using the `$context['data']`
+array. The above example will make the rule for 'picture' optional depending on
+whether the value for `show_profile_picture` is empty. You could also use the
+`uploadedFile` validation rule to create optional file upload inputs:
+
+``` php
+$validator->add('picture', 'file', [
+ 'rule' => ['uploadedFile', ['optional' => true]],
+]);
+```
+
+The `allowEmpty*`, `notEmpty*` and `requirePresence()` methods will also
+accept a callback function as their last argument. If present, the callback
+determines whether or not the rule should be applied. For example, a field is
+sometimes allowed to be empty:
+
+``` php
+$validator->allowEmptyString('tax', 'This field is required', function ($context) {
+ return !$context['data']['is_taxable'];
+});
+```
+
+Likewise, a field can be required to be populated when certain conditions are
+met:
+
+``` php
+$validator->notEmptyString('email_frequency', 'This field is required', function ($context) {
+ return !empty($context['data']['wants_newsletter']);
+});
+```
+
+In the above example, the `email_frequency` field cannot be left empty if the
+the user wants to receive the newsletter.
+
+Further it's also possible to require a field to be present under certain
+conditions only:
+
+``` php
+$validator->requirePresence('full_name', function ($context) {
+ if (isset($context['data']['action'])) {
+ return $context['data']['action'] === 'subscribe';
+ }
+
+ return false;
+});
+$validator->requirePresence('email');
+```
+
+This would require the `full_name` field to be present only in case the user
+wants to create a subscription, while the `email` field would always be
+required.
+
+The `$context` parameter passed to custom conditional callbacks contains the
+following keys:
+
+- `data` The data being validated.
+- `newRecord` a boolean indicating whether a new or existing record is being
+ validated.
+- `field` The current field being validated.
+- `providers` The validation providers attached to the current validator.
+
+### Marking Rules as the Last to Run
+
+When fields have multiple rules, each validation rule will be run even if the
+previous one has failed. This allows you to collect as many validation errors as
+you can in a single pass. If you want to stop execution after
+a specific rule has failed, you can set the `last` option to `true`:
+
+``` php
+$validator = new Validator();
+$validator
+ ->add('body', [
+ 'minLength' => [
+ 'rule' => ['minLength', 10],
+ 'last' => true,
+ 'message' => 'Comments must have a substantial body.',
+ ],
+ 'maxLength' => [
+ 'rule' => ['maxLength', 250],
+ 'message' => 'Comments cannot be too long.',
+ ],
+ ]);
+```
+
+If the minLength rule fails in the example above, the maxLength rule will not be
+run.
+
+## Make Rules 'last' by default
+
+You can have the `last` option automatically applied to each rule you can use
+the `setStopOnFailure()` method to enable this behavior:
+
+``` php
+public function validationDefault(Validator $validator): Validator
+{
+ $validator
+ ->setStopOnFailure()
+ ->requirePresence('email', 'create')
+ ->notBlank('email')
+ ->email('email');
+
+ return $validator;
+}
+```
+
+When enabled all fields will stop validation on the first failing rule instead
+of checking all possible rules. In this case only a single error message will
+appear under the form field.
+
+
+
+### Adding Validation Providers
+
+The `Validator`, `ValidationSet` and `ValidationRule` classes do not
+provide any validation methods themselves. Validation rules come from
+'providers'. You can bind any number of providers to a Validator object.
+Validator instances come with a 'default' provider setup automatically. The
+default provider is mapped to the `Cake\Validation\Validation`
+class. This makes it simple to use the methods on that class as validation
+rules. When using Validators and the ORM together, additional providers are
+configured for the table and entity objects. You can use the `setProvider()`
+method to add any additional providers your application needs:
+
+``` php
+$validator = new Validator();
+
+// Use an object instance.
+$validator->setProvider('custom', $myObject);
+
+// Use a class name. Methods must be static.
+$validator->setProvider('custom', 'App\Model\Validation');
+```
+
+Validation providers can be objects, or class names. If a class name is used the
+methods must be static. To use a provider other than 'default', be sure to set
+the `provider` key in your rule:
+
+``` php
+// Use a rule from the table provider
+$validator->add('title', 'custom', [
+ 'rule' => 'customTableMethod',
+ 'provider' => 'table'
+]);
+```
+
+If you wish to add a `provider` to all `Validator` objects that are created
+in the future, you can use the `addDefaultProvider()` method as follows:
+
+``` php
+use Cake\Validation\Validator;
+
+// Use an object instance.
+Validator::addDefaultProvider('custom', $myObject);
+
+// Use a class name. Methods must be static.
+Validator::addDefaultProvider('custom', 'App\Model\Validation');
+```
+
+> [!NOTE]
+> DefaultProviders must be added before the `Validator` object is created
+> therefore **config/bootstrap.php** is the best place to set up your
+> default providers.
+
+You can use the [Localized plugin](https://github.com/cakephp/localized) to
+get providers based on countries. With this plugin, you'll be able to validate
+model fields, depending on a country, ie:
+
+``` php
+namespace App\Model\Table;
+
+use Cake\ORM\Table;
+use Cake\Validation\Validator;
+
+class PostsTable extends Table
+{
+ public function validationDefault(Validator $validator): Validator
+ {
+ // add the provider to the validator
+ $validator->setProvider('fr', 'Cake\Localized\Validation\FrValidation');
+ // use the provider in a field validation rule
+ $validator->add('phoneField', 'myCustomRuleNameForPhone', [
+ 'rule' => 'phone',
+ 'provider' => 'fr'
+ ]);
+
+ return $validator;
+ }
+}
+```
+
+The localized plugin uses the two letter ISO code of the countries for
+validation, like en, fr, de.
+
+There are a few methods that are common to all classes, defined through the
+[ValidationInterface interface](https://github.com/cakephp/localized/blob/master/src/Validation/ValidationInterface.php):
+
+``` text
+phone() to check a phone number
+postal() to check a postal code
+personId() to check a country specific person ID
+```
+
+### Nesting Validators
+
+When validating [Modelless Forms](../core-libraries/form) with nested data, or when working
+with models that contain array data types, it is necessary to validate the
+nested data you have. CakePHP makes it simple to add validators to specific
+attributes. For example, assume you are working with a non-relational database
+and need to store an article and its comments:
+
+``` php
+$data = [
+ 'title' => 'Best article',
+ 'comments' => [
+ ['comment' => ''],
+ ],
+];
+```
+
+To validate the comments you would use a nested validator:
+
+``` php
+$validator = new Validator();
+$validator->add('title', 'not-blank', ['rule' => 'notBlank']);
+
+$commentValidator = new Validator();
+$commentValidator->add('comment', 'not-blank', ['rule' => 'notBlank']);
+
+// Connect the nested validators.
+$validator->addNestedMany('comments', $commentValidator);
+
+// Get all errors including those from nested validators.
+$validator->validate($data);
+```
+
+You can create 1:1 'relationships' with `addNested()` and 1:N 'relationships'
+with `addNestedMany()`. With both methods, the nested validator's errors will
+contribute to the parent validator's errors and influence the final result.
+Like other validator features, nested validators support error messages and
+conditional application:
+
+``` php
+$validator->addNestedMany(
+ 'comments',
+ $commentValidator,
+ 'Invalid comment',
+ 'create'
+);
+```
+
+The error message for a nested validator can be found in the `_nested` key.
+
+
+
+### Creating Reusable Validators
+
+While defining validators inline where they are used makes for good example
+code, it doesn't lead to maintainable applications. Instead, you should
+create `Validator` sub-classes for your reusable validation logic:
+
+``` php
+// In src/Model/Validation/ContactValidator.php
+namespace App\Model\Validation;
+
+use Cake\Validation\Validator;
+
+class ContactValidator extends Validator
+{
+ public function __construct()
+ {
+ parent::__construct();
+ // Add validation rules here.
+ }
+}
+```
+
+## Validating Data
+
+Now that you've created a validator and added the rules you want to it, you can
+start using it to validate data. Validators are able to validate array
+data. For example, if you wanted to validate a contact form before creating and
+sending an email you could do the following:
+
+``` php
+use Cake\Validation\Validator;
+
+$validator = new Validator();
+$validator
+ ->requirePresence('email')
+ ->add('email', 'validFormat', [
+ 'rule' => 'email',
+ 'message' => 'E-mail must be valid',
+ ])
+ ->requirePresence('name')
+ ->notEmptyString('name', 'We need your name.')
+ ->requirePresence('comment')
+ ->notEmptyString('comment', 'You need to give a comment.');
+
+$errors = $validator->validate($this->request->getData());
+if (empty($errors)) {
+ // Send an email.
+}
+```
+
+The `getErrors()` method will return a non-empty array when there are validation
+failures. The returned array of errors will be structured like:
+
+``` php
+$errors = [
+ 'email' => ['E-mail must be valid'],
+];
+```
+
+If you have multiple errors on a single field, an array of error messages will
+be returned per field. By default the `getErrors()` method applies rules for
+the 'create' mode. If you'd like to apply 'update' rules you can do the
+following:
+
+``` php
+$errors = $validator->validate($this->request->getData(), false);
+if (!$errors) {
+ // Send an email.
+}
+```
+
+> [!NOTE]
+> If you need to validate entities you should use methods like
+> `Cake\ORM\Table::newEntity()`,
+> `Cake\ORM\Table::newEntities()`,
+> `Cake\ORM\Table::patchEntity()`,
+> `Cake\ORM\Table::patchEntities()`
+> as they are designed for that.
+
+## Validating Entity Data
+
+Validation is meant for checking request data coming from forms or other user
+interfaces used to populate the entities.
+
+The request data is validated automatically when using the `newEntity()`,
+`newEntities()`, `patchEntity()` or `patchEntities()` methods of `Table` class:
+
+``` php
+// In the ArticlesController class
+$article = $this->Articles->newEntity($this->request->getData());
+if ($article->getErrors()) {
+ // Do work to show error messages.
+}
+```
+
+Similarly, when you need to validate multiple entities at a time, you can
+use the `newEntities()` method:
+
+``` php
+// In the ArticlesController class
+$entities = $this->Articles->newEntities($this->request->getData());
+foreach ($entities as $entity) {
+ if (!$entity->getErrors()) {
+ $this->Articles->save($entity);
+ }
+}
+```
+
+The `newEntity()`, `patchEntity()`, `newEntities()` and `patchEntities()`
+methods allow you to specify which associations are validated, and which
+validation sets to apply using the `options` parameter:
+
+``` php
+$valid = $this->Articles->newEntity($article, [
+ 'associated' => [
+ 'Comments' => [
+ 'associated' => ['User'],
+ 'validate' => 'special',
+ ],
+ ],
+]);
+```
+
+Apart from validating user provided data maintaining integrity of data regardless
+where it came from is important. To solve this problem CakePHP offers a second
+level of validation which is called "application rules". You can read more about
+them in the [Applying Application Rules](../orm/validation#application-rules) section.
+
+## Core Validation Rules
+
+CakePHP provides a basic suite of validation methods in the `Validation`
+class. The Validation class contains a variety of static methods that provide
+validators for several common validation situations.
+
+The [API documentation](https://api.cakephp.org/5.x/class-Cake.Validation.Validation.html) for the
+`Validation` class provides a good list of the validation rules that are
+available, and their basic usage.
+
+Some of the validation methods accept additional parameters to define boundary
+conditions or valid options. You can provide these boundary conditions and
+options as follows:
+
+``` php
+$validator = new Validator();
+$validator
+ ->add('title', 'minLength', [
+ 'rule' => ['minLength', 10],
+ ])
+ ->add('rating', 'validValue', [
+ 'rule' => ['range', 1, 5],
+ ]);
+```
+
+Core rules that take additional parameters should have an array for the
+`rule` key that contains the rule as the first element, and the additional
+parameters as the remaining parameters.
diff --git a/docs/en/core-libraries/xml.md b/docs/en/core-libraries/xml.md
new file mode 100644
index 0000000000..3993a274b0
--- /dev/null
+++ b/docs/en/core-libraries/xml.md
@@ -0,0 +1,202 @@
+# Xml
+
+`class` Cake\\Utility\\**Xml**
+
+The Xml class allows you to transform arrays into SimpleXMLElement or
+DOMDocument objects, and back into arrays again.
+
+## Loading XML documents
+
+### Xml::build()
+
+`static` Cake\\Utility\\Xml::**build**($input, array $options = []): SimpleXMLElement|DOMDocument
+
+You can load XML-ish data using `Xml::build()`. Depending on your
+`$options` parameter, this method will return a SimpleXMLElement (default)
+or DOMDocument object. You can use `Xml::build()` to build XML
+objects from a variety of sources. For example, you can load XML from
+strings:
+
+``` php
+$text = '
+
+ 1
+ Best post
+ ...
+';
+$xml = Xml::build($text);
+```
+
+You can also build Xml objects from local files by overriding the default option:
+
+``` php
+// Local file
+$xml = Xml::build('/home/awesome/unicorns.xml', ['readFile' => true]);
+```
+
+You can also build Xml objects using an array:
+
+``` php
+$data = [
+ 'post' => [
+ 'id' => 1,
+ 'title' => 'Best post',
+ 'body' => ' ... ',
+ ]
+];
+$xml = Xml::build($data);
+```
+
+If your input is invalid, the Xml class will throw an exception:
+
+``` php
+$xmlString = 'What is XML?';
+try {
+ $xmlObject = Xml::build($xmlString); // Here will throw an exception
+} catch (\Cake\Utility\Exception\XmlException $e) {
+ throw new InternalErrorException();
+}
+```
+
+> [!NOTE]
+> [DOMDocument](https://php.net/domdocument) and
+> [SimpleXML](https://php.net/simplexml) implement different APIs.
+> Be sure to use the correct methods on the object you request from Xml.
+
+## Loading HTML documents
+
+HTML documents can be parsed into `SimpleXmlElement` or `DOMDocument`
+objects with `loadHtml()`:
+
+``` php
+$html = Xml::loadHtml($htmlString, ['return' => 'domdocument']);
+```
+
+By default entity loading and huge document parsing are disabled. These modes
+can be enabled with the `loadEntities` and `parseHuge` options respectively.
+
+## Transforming a XML String in Array
+
+### Xml::toArray()
+
+`static` Cake\\Utility\\Xml::**toArray**($obj): array
+
+Converting XML strings into arrays is simple with the Xml class as well. By
+default you'll get a SimpleXml object back:
+
+``` php
+$xmlString = 'value';
+$xmlArray = Xml::toArray(Xml::build($xmlString));
+```
+
+If your XML is invalid a `Cake\Utility\Exception\XmlException` will be raised.
+
+## Transforming an Array into a String of XML
+
+``` php
+$xmlArray = ['root' => ['child' => 'value']];
+// You can use Xml::build() too.
+$xmlObject = Xml::fromArray($xmlArray, ['format' => 'tags']);
+$xmlString = $xmlObject->asXML();
+```
+
+Your array must have only one element in the "top level" and it can not be
+numeric. If the array is not in this format, Xml will throw an exception.
+Examples of invalid arrays:
+
+``` text
+// Top level with numeric key
+[
+ ['key' => 'value']
+];
+
+// Multiple keys in top level
+[
+ 'key1' => 'first value',
+ 'key2' => 'other value'
+];
+```
+
+By default array values will be output as XML tags. If you want to define
+attributes or text values you can prefix the keys that are supposed to be
+attributes with `@`. For value text, use `@` as the key:
+
+``` php
+$xmlArray = [
+ 'project' => [
+ '@id' => 1,
+ 'name' => 'Name of project, as tag',
+ '@' => 'Value of project',
+ ],
+];
+$xmlObject = Xml::fromArray($xmlArray);
+$xmlString = $xmlObject->asXML();
+```
+
+The content of `$xmlString` will be:
+
+``` php
+
+Value of projectName of project, as tag
+```
+
+### Using Namespaces
+
+To use XML Namespaces, create a key in your array with the name `xmlns:`
+in a generic namespace or input the prefix `xmlns:` in a custom namespace. See
+the samples:
+
+``` php
+$xmlArray = [
+ 'root' => [
+ 'xmlns:' => 'https://cakephp.org',
+ 'child' => 'value',
+ ]
+];
+$xml1 = Xml::fromArray($xmlArray);
+
+$xmlArray(
+ 'root' => [
+ 'tag' => [
+ 'xmlns:pref' => 'https://cakephp.org',
+ 'pref:item' => [
+ 'item 1',
+ 'item 2'
+ ]
+ ]
+ ]
+);
+$xml2 = Xml::fromArray($xmlArray);
+```
+
+The value of `$xml1` and `$xml2` will be, respectively:
+
+``` php
+
+value
+
+
+item 1item 2
+```
+
+### Creating a Child
+
+After you have created your XML document, you just use the native interfaces for
+your document type to add, remove, or manipulate child nodes:
+
+``` php
+// Using SimpleXML
+$myXmlOriginal = 'value';
+$xml = Xml::build($myXmlOriginal);
+$xml->root->addChild('young', 'new value');
+
+// Using DOMDocument
+$myXmlOriginal = 'value';
+$xml = Xml::build($myXmlOriginal, ['return' => 'domdocument']);
+$child = $xml->createElement('young', 'new value');
+$xml->firstChild->appendChild($child);
+```
+
+> [!TIP]
+> After manipulating your XML using SimpleXMLElement or DomDocument you can
+> use `Xml::toArray()` without a problem.
diff --git a/docs/en/debug-kit.md b/docs/en/debug-kit.md
new file mode 100644
index 0000000000..0dd94f3d73
--- /dev/null
+++ b/docs/en/debug-kit.md
@@ -0,0 +1,3 @@
+# Debug Kit
+
+This page has [moved](https://book.cakephp.org/debugkit/5.x/en/).
diff --git a/docs/en/deployment.md b/docs/en/deployment.md
new file mode 100644
index 0000000000..3eff1c1885
--- /dev/null
+++ b/docs/en/deployment.md
@@ -0,0 +1,140 @@
+# Deployment
+
+Once your app is ready to be deployed there are a few things you should do.
+
+## Moving files
+
+You can clone your repository onto your production server and then checkout the
+commit/tag you want to run. Then, run `composer install`. While this requires
+some knowledge about git and an existing install of `git` and `composer`
+this process will take care about library dependencies and file and folder
+permissions.
+
+Be aware that when deploying via FTP you will have to fix file and
+folder permissions.
+
+You can also use this deployment technique to setup a staging or demo-server
+(pre-production) and keep it in sync with your local environment.
+
+## Adjusting Configuration
+
+You'll want to make a few adjustments to your application's configuration for
+a production environment. The value of `debug` is extremely important.
+Turning debug = `false` disables a number of development features that should
+never be exposed to the Internet at large. Disabling debug changes the following
+features:
+
+- Debug messages, created with `pr()`, `debug()` and `dd()` are
+ disabled.
+- Core CakePHP caches duration are defaulted to 365 days, instead of 10 seconds
+ as in development.
+- Error views are less informative, and generic error pages are displayed
+ instead of detailed error messages with stack traces.
+- PHP Warnings and Errors are not displayed.
+
+In addition to the above, many plugins and application extensions use `debug`
+to modify their behavior.
+
+You can check against an environment variable to set the debug level dynamically
+between environments. This will avoid deploying an application with debug
+`true` and also save yourself from having to change the debug level each time
+before deploying to a production environment.
+
+For example, you can set an environment variable in your Apache configuration:
+
+ SetEnv CAKEPHP_DEBUG 1
+
+And then you can set the debug level dynamically in **app_local.php**:
+
+``` php
+$debug = (bool)getenv('CAKEPHP_DEBUG');
+
+return [
+ 'debug' => $debug,
+ .....
+];
+```
+
+It is recommended that you put configuration that is shared across all
+of your application's environments in **config/app.php**. For configuration that
+varies between environments either use **config/app_local.php** or environment
+variables.
+
+## Check Your Security
+
+If you're throwing your application out into the wild, it's a good idea to make
+sure it doesn't have any obvious leaks:
+
+- Ensure you are using the [Csrf Middleware](security/csrf#csrf-middleware) component or middleware.
+- You may want to enable the [Form Protection Component](controllers/components/form-protection) component.
+ It can help prevent several types of form tampering and reduce the possibility
+ of mass-assignment issues.
+- Ensure your models have the correct [Validation](core-libraries/validation) rules
+ enabled.
+- Check that only your `webroot/` directory is publicly visible, and that your
+ secrets (such as your app salt, and any security keys) are private and unique
+ as well.
+
+## Set Document Root
+
+Setting the document root correctly on your application is an important step to
+keeping your code secure and your application safer. CakePHP applications
+should have the document root set to the application's `webroot`. This
+makes the application and configuration files inaccessible through a URL.
+Setting the document root is different for different webservers. See the
+[Url Rewriting](installation#without-url-rewriting) documentation for webserver specific
+information.
+
+In all cases you will want to set the virtual host/domain's document to be
+`webroot/`. This removes the possibility of files outside of the webroot
+directory being executed.
+
+
+
+## Improve Your Application's Performance
+
+Class loading can take a big share of your application's processing time.
+In order to avoid this problem, it is recommended that you run this command in
+your production server once the application is deployed:
+
+ php composer.phar dumpautoload -o
+
+Since handling static assets, such as images, JavaScript and CSS files of
+plugins, through the `Dispatcher` is incredibly inefficient, it is strongly
+recommended to symlink them for production. This can be done by using
+the `plugin` command:
+
+ bin/cake plugin assets symlink
+
+The above command will symlink the `webroot/` directory of all loaded plugins
+to appropriate path in the app's `webroot/` directory.
+
+If your filesystem doesn't allow creating symlinks the directories will be
+copied instead of being symlinked. You can also explicitly copy the directories
+using:
+
+ bin/cake plugin assets copy
+
+CakePHP uses `assert()` internally to provide runtime type checking and
+provide better error messages during development. You can have PHP skip these
+assertions by updating your `php.ini` to include:
+
+``` ini
+; Turn off assert() code generation.
+zend.assertions = -1
+```
+
+Skipping code generation for `assert()` will yield faster runtime performance,
+and is recommended for applications that have good test coverage or that are
+using a static analyzer.
+
+## Deploying an update
+
+On each deploy you'll likely have a few tasks to co-ordinate on your web server. Some typical ones
+are:
+
+1. Install dependencies with `composer install`. Avoid using `composer update` when doing deploys as you could get unexpected versions of packages.
+2. Run database [migrations](migrations) with either the Migrations plugin
+ or another tool.
+3. Clear model schema cache with `bin/cake schema_cache clear`. The [Schema Cache Tool](console-commands/schema-cache)
+ has more information on this command.
diff --git a/docs/en/development/application.md b/docs/en/development/application.md
new file mode 100644
index 0000000000..4d05a51899
--- /dev/null
+++ b/docs/en/development/application.md
@@ -0,0 +1,84 @@
+# Application
+
+The `Application` is the heart of your application. It controls
+how your application is configured, and what plugins, middleware, console
+commands and routes are included.
+
+You can find your `Application` class at **src/Application.php**. By default
+it will be pretty slim and only define a few default
+[Middleware](../controllers/middleware). Applications can define the following hook
+methods:
+
+- `bootstrap` Used to load [configuration files](../development/configuration), define constants and other global functions.
+ By default this will include **config/bootstrap.php**. This is the ideal place
+ to load [Plugins](../plugins) and global [event listeners](../core-libraries/events).
+- `routes` Used to load [routes](../development/routing). By default this
+ will include **config/routes.php**.
+- `middleware` Used to add [middleware](../controllers/middleware) to your application.
+- `console` Used to add [console commands](../console-commands) to your
+ application. By default this will automatically discover console commands in
+ your application and all plugins.
+
+## Bootstrapping your Application
+
+If you have any additional configuration needs, you should add them to your
+application's **config/bootstrap.php** file. This file is included before each
+request, and CLI command.
+
+This file is ideal for a number of common bootstrapping tasks:
+
+- Defining convenience functions.
+- Declaring constants.
+- Defining cache configuration.
+- Defining logging configuration.
+- Loading custom inflections.
+- Loading configuration files.
+
+It might be tempting to place formatting functions there in order to use them in
+your controllers. As you'll see in the [Controllers](../controllers) and [Views](../views)
+sections there are better ways you add custom logic to your application.
+
+
+
+### Application::bootstrap()
+
+In addition to the **config/bootstrap.php** file which should be used to
+configure low-level concerns of your application, you can also use the
+`Application::bootstrap()` hook method to load/initialize plugins, and attach
+global event listeners:
+
+``` php
+// in src/Application.php
+namespace App;
+
+use Cake\Http\BaseApplication;
+
+class Application extends BaseApplication
+{
+ public function bootstrap()
+ {
+ // Call the parent to `require_once` config/bootstrap.php
+ parent::bootstrap();
+
+ // CakePHP has the ability to fallback to using the `Cake\ORM\Table`
+ // class to represent your database tables when a related class is
+ // not created for that table. But using this "auto-tables" feature
+ // can make debugging more difficult in some scenarios. So we disable
+ // this feature except for the CLI environment (since the classes
+ // would not be present when using the `bake` code generation tool).
+ if (PHP_SAPI !== 'cli') {
+ FactoryLocator::add(
+ 'Table',
+ (new TableLocator())->allowFallbackClass(false)
+ );
+ }
+
+ // Load MyPlugin
+ $this->addPlugin('MyPlugin');
+ }
+}
+```
+
+Loading plugins and events in `Application::bootstrap()` makes
+[Integration Testing](../development/testing#integration-testing) easier as events and routes will be re-processed on
+each test method.
diff --git a/docs/en/development/configuration.md b/docs/en/development/configuration.md
new file mode 100644
index 0000000000..6166111267
--- /dev/null
+++ b/docs/en/development/configuration.md
@@ -0,0 +1,594 @@
+# Configuration
+
+While conventions remove the need to configure all of CakePHP, you'll still need
+to configure a few things like your database credentials.
+
+Additionally, there are optional configuration options that allow you to swap
+out default values & implementations with ones tailored to your application.
+
+
+
+app.php, app_local.example.php
+
+
+
+
+
+configuration
+
+
+
+## Configuring your Application
+
+Configuration is generally stored in either PHP or INI files, and loaded during
+the application bootstrap. CakePHP comes with one configuration file by default,
+but if required you can add additional configuration files and load them in
+your application's bootstrap code. `Cake\Core\Configure` is used
+for global configuration, and classes like `Cache` provide `setConfig()`
+methods to make configuration simple and transparent.
+
+The application skeleton features a **config/app.php** file which should contain
+configuration that doesn't vary across the various environments your application
+is deployed in. The **config/app_local.php** file should contain the
+configuration data that varies between environments and should be managed by
+configuration management, or your deployment tooling. Both of these files reference environment variables
+through the `env()` function that enables configuration values to set through
+the server environment.
+
+### Loading Additional Configuration Files
+
+If your application has many configuration options it can be helpful to split
+configuration into multiple files. After creating each of the files in your
+**config/** directory you can load them in **bootstrap.php**:
+
+``` php
+use Cake\Core\Configure;
+use Cake\Core\Configure\Engine\PhpConfig;
+
+Configure::setConfig('default', new PhpConfig());
+Configure::load('app', 'default', false);
+Configure::load('other_config', 'default');
+```
+
+
+
+## Environment Variables
+
+Many modern cloud providers, like Heroku, let you define environment
+variables for configuration data. You can configure your CakePHP through
+environment variables in the [12factor app style](https://12factor.net/).
+Environment variables allow your application to require less state making your
+application easier to manage when it is deployed across a number of
+environments.
+
+As you can see in your **app.php**, the `env()` function is used to read
+configuration from the environment, and build the application configuration.
+CakePHP uses `DSN` strings for databases, logs, email transports and cache
+configurations allowing you to easily vary these libraries in each environment.
+
+For local development, CakePHP leverages [dotenv](https://github.com/josegonzalez/php-dotenv) to make local development
+automatically reload environment variables. Use composer to require this library
+and then there is a block of code in `bootstrap.php` that needs to be
+uncommented to harness it.
+
+You will see a `config/.env.example` in your
+application. By copying this file into `config/.env` and customizing the
+values you can configure your application.
+
+You should avoid committing the `config/.env` file to your repository and
+instead use the `config/.env.example` as a template with placeholder values so
+everyone on your team knows what environment variables are in use and what
+should go in each one.
+
+Once your environment variables have been set, you can use `env()` to read
+data from the environment:
+
+``` php
+$debug = env('APP_DEBUG', false);
+```
+
+The second value passed to the env function is the default value. This value
+will be used if no environment variable exists for the given key.
+
+
+
+### General Configuration
+
+Below is a description of the variables and how they affect your CakePHP
+application.
+
+debug
+Changes CakePHP debugging output. `false` = Production mode. No error
+messages, errors, or warnings shown. `true` = Errors and warnings shown.
+
+App.namespace
+The namespace to find app classes under.
+
+> [!NOTE]
+> When changing the namespace in your configuration, you will also
+> need to update your **composer.json** file to use this namespace
+> as well. Additionally, create a new autoloader by running
+> `php composer.phar dumpautoload`.
+
+
+
+App.baseUrl
+Un-comment this definition if you **don’t** plan to use Apache’s
+mod_rewrite with CakePHP. Don’t forget to remove your .htaccess
+files too.
+
+App.base
+The base directory the app resides in. If `false` this
+will be auto detected. If not `false`, ensure your string starts
+with a / and does NOT end with a /. For example, /basedir is a valid
+App.base.
+
+App.encoding
+Define what encoding your application uses. This encoding
+is used to generate the charset in the layout, and encode entities.
+It should match the encoding values specified for your database.
+
+App.webroot
+The webroot directory.
+
+App.wwwRoot
+The file path to webroot.
+
+App.fullBaseUrl
+The fully qualified domain name (including protocol) to your application's
+root. This is used when generating absolute URLs. By default this value
+is generated using the `$_SERVER` environment. However, you should define it
+manually to optimize performance or if you are concerned about people
+manipulating the `Host` header.
+In a CLI context (from command) the fullBaseUrl cannot be read from \$\_SERVER,
+as there is no webserver involved. You do need to specify it yourself if
+you do need to generate URLs from a shell (for example, when sending emails).
+
+App.imageBaseUrl
+Web path to the public images directory under webroot. If you are using
+a `CDN` you should set this value to the CDN's location.
+
+App.cssBaseUrl
+Web path to the public css directory under webroot. If you are using
+a `CDN` you should set this value to the CDN's location.
+
+App.jsBaseUrl
+Web path to the public js directory under webroot. If you are using
+a `CDN` you should set this value to the CDN's location.
+
+App.paths
+Configure paths for non class based resources. Supports the
+`plugins`, `templates`, `locales` subkeys, which allow the definition
+of paths for plugins, view templates and locale files respectively.
+
+App.uploadedFilesAsObjects
+Defines whether uploaded files are being represented as objects (`true`),
+or arrays (`false`). This option is being treated as enabled by default.
+See the [File Uploads section](../controllers/request-response#request-file-uploads) in the Request &
+Response Objects chapter for more information.
+
+Security.salt
+A random string used in hashing. This value is also used as the
+HMAC salt when doing symmetric encryption.
+
+Asset.timestamp
+Appends a timestamp which is last modified time of the particular
+file at the end of asset files URLs (CSS, JavaScript, Image) when
+using proper helpers. Valid values:
+
+- (bool) `false` - Doesn't do anything (default)
+- (bool) `true` - Appends the timestamp when debug is `true`
+- (string) 'force' - Always appends the timestamp.
+
+Asset.cacheTime
+Sets the asset cache time. This determines the http header `Cache-Control`'s
+`max-age`, and the http header's `Expire`'s time for assets.
+This can take anything that you version of PHP's [strtotime function](https://php.net/manual/en/function.strtotime.php) can take.
+The default is `+1 day`.
+
+
+
+### Using a CDN
+
+To use a CDN for loading your static assets, change `App.imageBaseUrl`,
+`App.cssBaseUrl`, `App.jsBaseUrl` to point the CDN URI, for example:
+`https://mycdn.example.com/` (note the trailing `/`).
+
+All images, scripts and styles loaded via HtmlHelper will prepend the absolute
+CDN path, matching the same relative path used in the application. Please note
+there is a specific use case when using plugin based assets: plugins will not
+use the plugin's prefix when absolute `...BaseUrl` URI is used, for example By
+default:
+
+- `$this->Helper->assetUrl('TestPlugin.logo.png')` resolves to `test_plugin/logo.png`
+
+If you set `App.imageBaseUrl` to `https://mycdn.example.com/`:
+
+- `$this->Helper->assetUrl('TestPlugin.logo.png')` resolves to `https://mycdn.example.com/logo.png`.
+
+### Database Configuration
+
+See the [Database Configuration](../orm/database-basics#database-configuration) for information
+on configuring your database connections.
+
+### Caching Configuration
+
+See the [Caching Configuration](../core-libraries/caching#cache-configuration) for information on
+configuring caching in CakePHP.
+
+### Error and Exception Handling Configuration
+
+See the [Error and Exception Configuration](../development/errors#error-configuration) for
+information on configuring error and exception handlers.
+
+### Logging Configuration
+
+See the [Log Configuration](../core-libraries/logging#log-configuration) for information on configuring logging in
+CakePHP.
+
+### Email Configuration
+
+See the [Email Configuration](../core-libraries/email#email-configuration) for information on
+configuring email presets in CakePHP.
+
+### Session Configuration
+
+See the [Session Configuration](../development/sessions#session-configuration) for information on configuring session
+handling in CakePHP.
+
+### Routing configuration
+
+See the [Routes Configuration](../development/routing#routes-configuration) for more information
+on configuring routing and creating routes for your application.
+
+
+
+## Additional Class Paths
+
+Additional class paths are setup through the autoloaders your application uses.
+When using `composer` to generate your autoloader, you could do the following,
+to provide fallback paths for controllers in your application:
+
+``` json
+"autoload": {
+ "psr-4": {
+ "App\\Controller\\": "/path/to/directory/with/controller/folders/",
+ "App\\": "src/"
+ }
+}
+```
+
+The above would setup paths for both the `App` and `App\Controller`
+namespace. The first key will be searched, and if that path does not contain the
+class/file the second key will be searched. You can also map a single namespace
+to multiple directories with the following:
+
+``` json
+"autoload": {
+ "psr-4": {
+ "App\\": ["src/", "/path/to/directory/"]
+ }
+}
+```
+
+### Plugin, View Template and Locale Paths
+
+Since plugins, view templates and locales are not classes, they cannot have an
+autoloader configured. CakePHP provides three Configure variables to setup additional
+paths for these resources. In your **config/app.php** you can set these variables:
+
+``` text
+return [
+ // More configuration
+ 'App' => [
+ 'paths' => [
+ 'plugins' => [
+ ROOT . DS . 'plugins' . DS,
+ '/path/to/other/plugins/',
+ ],
+ 'templates' => [
+ ROOT . DS . 'templates' . DS,
+ ROOT . DS . 'templates2' . DS,
+ ],
+ 'locales' => [
+ ROOT . DS . 'resources' . DS . 'locales' . DS,
+ ],
+ ],
+ ],
+];
+```
+
+Paths should end with a directory separator, or they will not work properly.
+
+## Inflection Configuration
+
+See the [Inflection Configuration](../core-libraries/inflector#inflection-configuration) docs for more information.
+
+## Configure Class
+
+`class` Cake\\Core\\**Configure**
+
+CakePHP's Configure class can be used to store and retrieve
+application or runtime specific values. Be careful, this class
+allows you to store anything in it, then use it in any other part
+of your code: a sure temptation to break the MVC pattern CakePHP
+was designed for. The main goal of Configure class is to keep
+centralized variables that can be shared between many objects.
+Remember to try to live by "convention over configuration" and you
+won't end up breaking the MVC structure CakePHP provides.
+
+### Writing Configuration data
+
+`static` Cake\\Core\\Configure::**write**($key, $value): void
+
+Use `write()` to store data in the application's configuration:
+
+``` php
+Configure::write('Company.name', 'Pizza, Inc.');
+Configure::write('Company.slogan', 'Pizza for your body and soul');
+```
+
+> [!NOTE]
+> The `dot notation` used in the `$key` parameter can be used to
+> organize your configuration settings into logical groups.
+
+The above example could also be written in a single call:
+
+``` php
+Configure::write('Company', [
+ 'name' => 'Pizza, Inc.',
+ 'slogan' => 'Pizza for your body and soul'
+]);
+```
+
+You can use `Configure::write('debug', $bool)` to switch between debug and
+production modes on the fly.
+
+> [!NOTE]
+> Any configuration changes done using `Configure::write()` are in memory
+> and will not persist across requests.
+
+### Reading Configuration Data
+
+`static` Cake\\Core\\Configure::**read**($key = null, $default = null): mixed
+
+Used to read configuration data from the application. If a key is supplied, the
+data is returned. Using our examples from write() above, we can read that data
+back:
+
+``` php
+// Returns 'Pizza Inc.'
+Configure::read('Company.name');
+
+// Returns 'Pizza for your body and soul'
+Configure::read('Company.slogan');
+
+Configure::read('Company');
+// Returns:
+['name' => 'Pizza, Inc.', 'slogan' => 'Pizza for your body and soul'];
+
+// Returns 'fallback' as Company.nope is undefined.
+Configure::read('Company.nope', 'fallback');
+```
+
+If `$key` is left null, all values in Configure will be returned.
+
+`static` Cake\\Core\\Configure::**readOrFail**($key): mixed
+
+Reads configuration data just like `Cake\Core\Configure::read()`
+but expects to find a key/value pair. In case the requested pair does not
+exist, a `RuntimeException` will be thrown:
+
+``` php
+Configure::readOrFail('Company.name'); // Yields: 'Pizza, Inc.'
+Configure::readOrFail('Company.geolocation'); // Will throw an exception
+
+Configure::readOrFail('Company');
+
+// Yields:
+['name' => 'Pizza, Inc.', 'slogan' => 'Pizza for your body and soul'];
+```
+
+### Checking to see if Configuration Data is Defined
+
+`static` Cake\\Core\\Configure::**check**($key): bool
+
+Used to check if a key/path exists and has non-null value:
+
+``` php
+$exists = Configure::check('Company.name');
+```
+
+### Deleting Configuration Data
+
+`static` Cake\\Core\\Configure::**delete**($key): void
+
+Used to delete information from the application's configuration:
+
+``` php
+Configure::delete('Company.name');
+```
+
+### Reading & Deleting Configuration Data
+
+`static` Cake\\Core\\Configure::**consume**($key): mixed
+
+Read and delete a key from Configure. This is useful when you want to
+combine reading and deleting values in a single operation.
+
+`static` Cake\\Core\\Configure::**consumeOrFail**($key): mixed
+
+Consumes configuration data just like `Cake\Core\Configure::consume()`
+but expects to find a key/value pair. In case the requested pair does not
+exist, a `RuntimeException` will be thrown:
+
+``` php
+Configure::consumeOrFail('Company.name'); // Yields: 'Pizza, Inc.'
+Configure::consumeOrFail('Company.geolocation'); // Will throw an exception
+
+Configure::consumeOrFail('Company');
+
+// Yields:
+['name' => 'Pizza, Inc.', 'slogan' => 'Pizza for your body and soul'];
+```
+
+## Reading and writing configuration files
+
+`static` Cake\\Core\\Configure::**setConfig**($name, $engine)
+
+CakePHP comes with two built-in configuration file engines.
+`Cake\Core\Configure\Engine\PhpConfig` is able to read PHP config
+files, in the same format that Configure has historically read.
+`Cake\Core\Configure\Engine\IniConfig` is able to read ini config
+files. See the [PHP documentation](https://php.net/parse_ini_file) for more
+information on the specifics of ini files. To use a core config engine, you'll
+need to attach it to Configure using `Configure::config()`:
+
+``` php
+use Cake\Core\Configure\Engine\PhpConfig;
+
+// Read config files from config
+Configure::config('default', new PhpConfig());
+
+// Read config files from another path.
+Configure::config('default', new PhpConfig('/path/to/your/config/files/'));
+```
+
+You can have multiple engines attached to Configure, each reading different
+kinds or sources of configuration files. You can interact with attached engines
+using a few other methods on Configure. To check which engine aliases are
+attached you can use `Configure::configured()`:
+
+``` php
+// Get the array of aliases for attached engines.
+Configure::configured();
+
+// Check if a specific engine is attached
+Configure::configured('default');
+```
+
+`static` Cake\\Core\\Configure::**drop**($name): bool
+
+You can also remove attached engines. `Configure::drop('default')`
+would remove the default engine alias. Any future attempts to load configuration
+files with that engine would fail:
+
+``` php
+Configure::drop('default');
+```
+
+
+
+### Loading Configuration Files
+
+`static` Cake\\Core\\Configure::**load**($key, $config = 'default', $merge = true): bool
+
+Once you've attached a config engine to Configure you can load configuration
+files:
+
+``` php
+// Load my_file.php using the 'default' engine object.
+Configure::load('my_file', 'default');
+```
+
+Loaded configuration files merge their data with the existing runtime
+configuration in Configure. This allows you to overwrite and add new values into
+the existing runtime configuration. By setting `$merge` to `true`, values
+will not ever overwrite the existing configuration.
+
+> [!WARNING]
+> When merging configuration files with \$merge = true, dot notation in keys is
+> not expanded:
+>
+> ``` php
+> // config1.php
+> 'Key1' => [
+> 'Key2' => [
+> 'Key3' => ['NestedKey1' => 'Value'],
+> ],
+> ],
+>
+> // config2.php
+> 'Key1.Key2' => [
+> 'Key3' => ['NestedKey2' => 'Value2'],
+> ]
+>
+> Configure::load('config1', 'default');
+> Configure::load('config2', 'default', true);
+>
+> // Now Key1.Key2.Key3 has the value ['NestedKey2' => 'Value2']
+> // instead of ['NestedKey1' => 'Value', 'NestedKey2' => 'Value2']
+> ```
+
+### Creating or Modifying Configuration Files
+
+`static` Cake\\Core\\Configure::**dump**($key, $config = 'default', $keys = []): bool
+
+Dumps all or some of the data in Configure into a file or storage system
+supported by a config engine. The serialization format is decided by the config
+engine attached as \$config. For example, if the 'default' engine is
+a `Cake\Core\Configure\Engine\PhpConfig`, the generated file will be
+a PHP configuration file loadable by the
+`Cake\Core\Configure\Engine\PhpConfig`
+
+Given that the 'default' engine is an instance of PhpConfig.
+Save all data in Configure to the file \`my_config.php\`:
+
+``` php
+Configure::dump('my_config', 'default');
+```
+
+Save only the error handling configuration:
+
+``` php
+Configure::dump('error', 'default', ['Error', 'Exception']);
+```
+
+`Configure::dump()` can be used to either modify or overwrite
+configuration files that are readable with `Configure::load()`
+
+### Storing Runtime Configuration
+
+`static` Cake\\Core\\Configure::**store**($name, $cacheConfig = 'default', $data = null): bool
+
+You can also store runtime configuration values for use in a future request.
+Since configure only remembers values for the current request, you will
+need to store any modified configuration information if you want to
+use it in subsequent requests:
+
+``` php
+// Store the current configuration in the 'user_1234' key in the 'default' cache.
+Configure::store('user_1234', 'default');
+```
+
+Stored configuration data is persisted in the named cache configuration. See the
+[Caching](../core-libraries/caching) documentation for more information on caching.
+
+### Restoring Runtime Configuration
+
+`static` Cake\\Core\\Configure::**restore**($name, $cacheConfig = 'default'): bool
+
+Once you've stored runtime configuration, you'll probably need to restore it
+so you can access it again. `Configure::restore()` does exactly that:
+
+``` php
+// Restore runtime configuration from the cache.
+Configure::restore('user_1234', 'default');
+```
+
+When restoring configuration information it's important to restore it with
+the same key, and cache configuration as was used to store it. Restored
+information is merged on top of the existing runtime configuration.
+
+### Configuration Engines
+
+CakePHP provides the ability to load configuration files from a number of
+different sources, and features a pluggable system for [creating your own
+configuration engines](https://api.cakephp.org/5.x/interface-Cake.Core.Configure.ConfigEngineInterface.html).
+The built in configuration engines are:
+
+- [JsonConfig](https://api.cakephp.org/5.x/class-Cake.Core.Configure.Engine.JsonConfig.html)
+- [IniConfig](https://api.cakephp.org/5.x/class-Cake.Core.Configure.Engine.IniConfig.html)
+- [PhpConfig](https://api.cakephp.org/5.x/class-Cake.Core.Configure.Engine.PhpConfig.html)
+
+By default your application will use `PhpConfig`.
diff --git a/docs/en/development/debugging.md b/docs/en/development/debugging.md
new file mode 100644
index 0000000000..e675c16b8d
--- /dev/null
+++ b/docs/en/development/debugging.md
@@ -0,0 +1,234 @@
+# Debugging
+
+Debugging is an inevitable and necessary part of any development
+cycle. While CakePHP doesn't offer any tools that directly connect
+with any IDE or editor, CakePHP does provide several tools to
+assist in debugging and exposing what is running under the hood of
+your application.
+
+## Basic Debugging
+
+`function` **debug(mixed $var, boolean $showHtml = null, $showFrom = true)**
+
+The `debug()` function is a globally available function that works
+similarly to the PHP function `print_r()`. The `debug()` function
+allows you to show the contents of a variable in a number of
+different ways. First, if you'd like data to be shown in an
+HTML-friendly way, set the second parameter to `true`. The function
+also prints out the line and file it is originating from by
+default.
+
+Output from this function is only shown if the core `$debug` variable
+has been set to `true`.
+
+Also see `dd()`, `pr()` and `pj()`.
+
+`function` **stackTrace()**
+
+The `stackTrace()` function is available globally, and allows you to output
+a stack trace wherever the function is called.
+
+`function` **breakpoint()**
+
+If you have [Psysh](https://psysh.org/) installed you can use this
+function in CLI environments to open an interactive console with the current
+local scope:
+
+``` text
+// Some code
+eval(breakpoint());
+```
+
+Will open an interactive console that can be used to check local variables
+and execute other code. You can exit the interactive debugger and resume the
+original execution by running `quit` or `q` in the interactive session.
+
+## Using the Debugger Class
+
+`class` Cake\\Error\\**Debugger**
+
+To use the debugger, first ensure that `Configure::read('debug')` is set to
+`true`. You can use `filter_var(env('DEBUG', true), FILTER_VALIDATE_BOOLEAN),` in **config/app.php** file to ensure that `debug`
+is a boolean.
+
+The following configuration options can be set in **config/app.php** to change how
+`Debugger` behaves:
+
+- `Debugger.editor` Choose the which editor URL format you want to use.
+ By default atom, emacs, macvim, phpstorm, sublime, textmate, and vscode are
+ available. You can add additional editor link formats using
+ `Debugger::addEditor()` during your application bootstrap.
+- `Debugger.editorBasePath` The path that replaces `ROOT` when generating
+ editor links.
+- `Debugger.outputMask` A mapping of `key` to `replacement` values that
+ `Debugger` should replace in dumped data and logs generated by `Debugger`.
+
+::: info Added in version 5.3.0
+The `Debugger.editorBasePath` configure option was added.
+:::
+
+## Outputting Values
+
+`static` Cake\\Error\\Debugger::**dump**($var, $depth = 3): void
+
+Dump prints out the contents of a variable. It will print out all
+properties and methods (if any) of the supplied variable:
+
+``` php
+$foo = [1,2,3];
+
+Debugger::dump($foo);
+
+// Outputs
+array(
+ 1,
+ 2,
+ 3
+)
+
+// Simple object
+$car = new Car();
+
+Debugger::dump($car);
+
+// Outputs
+object(Car) {
+ color => 'red'
+ make => 'Toyota'
+ model => 'Camry'
+ mileage => (int)15000
+}
+```
+
+### Masking Data
+
+When dumping data with `Debugger` or rendering error pages, you may want to
+hide sensitive keys like passwords or API keys. In your **config/bootstrap.php**
+you can mask specific keys:
+
+``` css
+Debugger::setOutputMask([
+ 'password' => 'xxxxx',
+ 'awsKey' => 'yyyyy',
+]);
+```
+
+As of 4.1.0 you can use the `Debugger.outputMask` configuration value to set
+output masks.
+
+## Logging With Stack Traces
+
+`static` Cake\\Error\\Debugger::**log**($var, $level = 7, $depth = 3): void
+
+Creates a detailed stack trace log at the time of invocation. The
+`log()` method prints out data similar to that done by
+`Debugger::dump()`, but to the debug.log instead of the output
+buffer. Note your **tmp** directory (and its contents) must be
+writable by the web server for `log()` to work correctly.
+
+## Generating Stack Traces
+
+`static` Cake\\Error\\Debugger::**trace**($options): array|string
+
+Returns the current stack trace. Each line of the trace includes
+the calling method, including which file and line the call
+originated from:
+
+``` text
+// In PostsController::index()
+pr(Debugger::trace());
+
+// Outputs
+PostsController::index() - APP/Controller/DownloadsController.php, line 48
+Dispatcher::_invoke() - CORE/src/Routing/Dispatcher.php, line 265
+Dispatcher::dispatch() - CORE/src/Routing/Dispatcher.php, line 237
+[main] - APP/webroot/index.php, line 84
+```
+
+Above is the stack trace generated by calling `Debugger::trace()` in
+a controller action. Reading the stack trace bottom to top shows
+the order of currently running functions (stack frames).
+
+## Getting an Excerpt From a File
+
+`static` Cake\\Error\\Debugger::**excerpt**($file, $line, $context): array
+
+Grab an excerpt from the file at \$path (which is an absolute
+filepath), highlights line number \$line with \$context number of
+lines around it. :
+
+``` php
+pr(Debugger::excerpt(ROOT . DS . LIBS . 'debugger.php', 321, 2));
+
+// Will output the following.
+Array
+(
+ [0] => * @access public
+ [1] => */
+ [2] => function excerpt($file, $line, $context = 2) {
+
+ [3] => $data = $lines = array();
+ [4] => $data = @explode("\n", file_get_contents($file));
+)
+```
+
+Although this method is used internally, it can be handy if you're
+creating your own error messages or log entries for custom
+situations.
+
+`static` Debugger::**getType**($var): string
+
+Get the type of a variable. Objects will return their class name
+
+## Editor Integration
+
+Exception and error pages can contain URLs that directly open in your editor or
+IDE. CakePHP ships with URL formats for several popular editors, and you can add
+additional editor formats if required during application bootstrap:
+
+``` text
+// Generate links for vscode.
+Debugger::setEditor('vscode')
+
+// Add a custom format
+// Format strings will have the {file} and {line}
+// placeholders replaced.
+Debugger::addEditor('custom', 'thing://open={file}&line={line}');
+
+// You can also use a closure to generate URLs
+Debugger::addEditor('custom', function ($file, $line) {
+ return "thing://open={$file}&line={$line}";
+});
+```
+
+## Using Logging to Debug
+
+Logging messages is another good way to debug applications, and you can use
+`Cake\Log\Log` to do logging in your application. All objects that
+use `LogTrait` have an instance method `log()` which can be used
+to log messages:
+
+``` php
+$this->log('Got here', 'debug');
+```
+
+The above would write `Got here` into the debug log. You can use log entries
+to help debug methods that involve redirects or complicated loops. You can also
+use `Cake\Log\Log::write()` to write log messages. This method can be called
+statically anywhere in your application one Log has been loaded:
+
+``` php
+// At the top of the file you want to log in.
+use Cake\Log\Log;
+
+// Anywhere that Log has been imported.
+Log::debug('Got here');
+```
+
+## Debug Kit
+
+DebugKit is a plugin that provides a number of good debugging tools. It
+primarily provides a toolbar in the rendered HTML, that provides a plethora of
+information about your application and the current request. See the [DebugKit
+Documentation](https://book.cakephp.org/debugkit/) for how to install and use
+DebugKit.
diff --git a/docs/en/development/dependency-injection.md b/docs/en/development/dependency-injection.md
new file mode 100644
index 0000000000..33e6867b07
--- /dev/null
+++ b/docs/en/development/dependency-injection.md
@@ -0,0 +1,398 @@
+# Dependency Injection
+
+The CakePHP service container enables you to manage class dependencies for your
+application services through dependency injection. Dependency injection
+automatically "injects" an object's dependencies via the constructor without
+having to manually instantiate them.
+
+You can use the service container to define 'application services'. These
+classes can use models and interact with other objects like loggers and mailers
+to build re-usable workflows and business logic for your application.
+
+CakePHP will use the `DI container` in the following situations:
+
+- Constructing controllers.
+- Calling actions on your controllers.
+- Constructing Components.
+- Constructing Console Commands.
+- Constructing Middleware by classname.
+
+## Controller Example
+
+``` php
+// In src/Controller/UsersController.php
+class UsersController extends AppController
+{
+ // The $users service will be created via the service container.
+ public function ssoCallback(UsersService $users)
+ {
+ if ($this->request->is('post')) {
+ // Use the UsersService to create/get the user from a
+ // Single Signon Provider.
+ $user = $users->ensureExists($this->request->getData());
+ }
+ }
+}
+
+// In src/Application.php
+public function services(ContainerInterface $container): void
+{
+ $container->add(UsersService::class);
+}
+```
+
+In this example, the `UsersController::ssoCallback()` action needs to fetch
+a user from a Single-Sign-On provider and ensure it exists in the local
+database. Because this service is injected into our controller, we can easily
+swap the implementation out with a mock object or a dummy sub-class when
+testing.
+
+## Command Example
+
+``` php
+// In src/Command/CheckUsersCommand.php
+use Cake\Console\CommandFactoryInterface;
+
+class CheckUsersCommand extends Command
+{
+ public function __construct(protected UsersService $users, ?CommandFactoryInterface $factory = null)
+ {
+ parent::__construct($factory);
+ }
+
+ public function execute(Arguments $args, ConsoleIo $io)
+ {
+ $valid = $this->users->check('all');
+ }
+
+}
+
+// In src/Application.php
+public function services(ContainerInterface $container): void
+{
+ $container
+ ->add(CheckUsersCommand::class)
+ ->addArgument(UsersService::class)
+ ->addArgument(CommandFactoryInterface::class);
+ $container->add(UsersService::class);
+}
+```
+
+The injection process is a bit different here. Instead of adding the
+`UsersService` to the container we first have to add the Command as
+a whole to the Container and add the `UsersService` as an argument.
+With that you can then access that service inside the constructor
+of the command.
+
+## Component Example
+
+``` php
+// In src/Controller/Component/SearchComponent.php
+class SearchComponent extends Component
+{
+ public function __construct(
+ ComponentRegistry $registry,
+ private UserService $users,
+ array $config = []
+ ) {
+ parent::__construct($registry, $config);
+ }
+
+ public function something()
+ {
+ $valid = $this->users->check('all');
+ }
+}
+
+// In src/Application.php
+public function services(ContainerInterface $container): void
+{
+ $container->add(SearchComponent::class)
+ ->addArgument(ComponentRegistry::class)
+ ->addArgument(UsersService::class);
+ $container->add(UsersService::class);
+}
+```
+
+## Adding Services
+
+In order to have services created by the container, you need to tell it which
+classes it can create and how to build those classes. The
+simplest definition is via a class name:
+
+``` php
+// Add a class by its name.
+$container->add(BillingService::class);
+```
+
+Your application and plugins define the services they have in the
+`services()` hook method:
+
+``` php
+// in src/Application.php
+namespace App;
+
+use App\Service\BillingService;
+use Cake\Core\ContainerInterface;
+use Cake\Http\BaseApplication;
+
+class Application extends BaseApplication
+{
+ public function services(ContainerInterface $container): void
+ {
+ $container->add(BillingService::class);
+ }
+}
+```
+
+You can define implementations for interfaces that your application uses:
+
+``` php
+use App\Service\AuditLogServiceInterface;
+use App\Service\AuditLogService;
+
+// in your Application::services() method.
+
+// Add an implementation for an interface.
+$container->add(AuditLogServiceInterface::class, AuditLogService::class);
+```
+
+The container can leverage factory functions to create objects if necessary:
+
+``` php
+$container->add(AuditLogServiceInterface::class, function (...$args) {
+ return new AuditLogService(...$args);
+});
+```
+
+Factory functions will receive all of the resolved dependencies for the class
+as arguments.
+
+Once you've defined a class, you also need to define the dependencies it
+requires. Those dependencies can be either objects or primitive values:
+
+``` php
+// Add a primitive value like a string, array or number.
+$container->add('apiKey', 'abc123');
+
+$container->add(BillingService::class)
+ ->addArgument('apiKey');
+```
+
+Your services can depend on `ServerRequest` in controller actions as it will
+be added automatically.
+
+### Adding Shared Services
+
+By default services are not shared. Every object (and dependencies) is created
+each time it is fetched from the container. If you want to re-use a single
+instance, often referred to as a singleton, you can mark a service as 'shared':
+
+``` php
+// in your Application::services() method.
+
+$container->addShared(BillingService::class);
+```
+
+### Using ORM Tables as Services
+
+If you want to have ORM Tables injected as a dependency to a service, you can
+add `TableContainer` to your applications's service container:
+
+``` php
+// In your Application::services() method.
+// Allow your Tables to be dependency injected.
+$container->delegate(new \Cake\ORM\Locator\TableContainer());
+```
+
+::: info Added in version 5.3.0
+`TableContainer` was added.
+:::
+
+### Extending Definitions
+
+Once a service is defined you can modify or update the service definition by
+extending them. This allows you to add additional arguments to services defined
+elsewhere:
+
+``` php
+// Add an argument to a partially defined service elsewhere.
+$container->extend(BillingService::class)
+ ->addArgument('logLevel');
+```
+
+### Tagging Services
+
+By tagging services you can get all of those services resolved at the same
+time. This can be used to build services that combine collections of other
+services like in a reporting system:
+
+``` php
+$container->add(BillingReport::class)->addTag('reports');
+$container->add(UsageReport::class)->addTag('reports');
+
+$container->add(ReportAggregate::class, function () use ($container) {
+ return new ReportAggregate($container->get('reports'));
+});
+```
+
+
+
+### Using Configuration Data
+
+Often you'll need configuration data in your services. If you need a specific value,
+you can inject it as a constructor argument using the `Cake\Core\Attribute\Configure`
+attribute:
+
+``` php
+use Cake\Core\Attribute\Configure;
+
+class InjectedService
+{
+ public function __construct(
+ #[Configure('MyService.apiKey')] protected string $apiKey,
+ ) { }
+}
+```
+
+::: info Added in version 5.3.0
+:::
+
+If you want to inject a copy of all configuration data, CakePHP includes
+an injectable configuration reader:
+
+``` php
+use Cake\Core\ServiceConfig;
+
+// Use a shared instance
+$container->addShared(ServiceConfig::class);
+```
+
+The `ServiceConfig` class provides a read-only view of all the data available
+in `Configure` so you don't have to worry about accidentally changing
+configuration.
+
+## Service Providers
+
+Service providers allow you to group related services together helping you
+organize your services. Service providers can help increase your application's
+performance as defined services are lazily registered after
+their first use.
+
+### Creating Service Providers
+
+An example ServiceProvider would look like:
+
+``` php
+namespace App\ServiceProvider;
+
+use Cake\Core\ContainerInterface;
+use Cake\Core\ServiceProvider;
+// Other imports here.
+
+class BillingServiceProvider extends ServiceProvider
+{
+ protected $provides = [
+ StripeService::class,
+ 'configKey',
+ ];
+
+ public function services(ContainerInterface $container): void
+ {
+ $container->add(StripeService::class);
+ $container->add('configKey', 'some value');
+ }
+}
+```
+
+Service providers use their `services()` method to define all the services they
+will provide. Additionally those services **must be** defined in the `$provides`
+property. Failing to include a service in the `$provides` property will result
+in it not be loadable from the container.
+
+### Using Service Providers
+
+To load a service provider add it into the container using the
+`addServiceProvider()` method:
+
+``` php
+// in your Application::services() method.
+$container->addServiceProvider(new BillingServiceProvider());
+```
+
+### Bootable ServiceProviders
+
+If your service provider needs to run logic when it is added to the container,
+you can implement the `bootstrap()` method. This situation can come up when your
+service provider needs to load additional configuration files, load additional
+service providers or modify a service defined elsewhere in your application. An
+example of a bootable service would be:
+
+``` php
+namespace App\ServiceProvider;
+
+use Cake\Core\ServiceProvider;
+// Other imports here.
+
+class BillingServiceProvider extends ServiceProvider
+{
+ protected $provides = [
+ StripeService::class,
+ 'configKey',
+ ];
+
+ public function bootstrap($container)
+ {
+ $container->addServiceProvider(new InvoicingServiceProvider());
+ }
+}
+```
+
+
+
+## Mocking Services in Tests
+
+In tests that use `ConsoleIntegrationTestTrait` or `IntegrationTestTrait`
+you can replace services that are injected via the container with mocks or
+stubs:
+
+``` php
+// In a test method or setup().
+$this->mockService(StripeService::class, function () {
+ return new FakeStripe();
+});
+
+// If you need to remove a mock
+$this->removeMockService(StripeService::class);
+```
+
+Any defined mocks will be replaced in your application's container during
+testing, and automatically injected into your controllers and commands. Mocks
+are cleaned up at the end of each test.
+
+## Auto Wiring
+
+Auto Wiring is turned off by default. To enable it:
+
+``` php
+// In src/Application.php
+public function services(ContainerInterface $container): void
+{
+ $container->delegate(
+ new \League\Container\ReflectionContainer()
+ );
+}
+```
+
+While your dependencies will now be resolved automatically, this approach will
+not cache resolutions which can be detrimental to performance. To enable
+caching:
+
+``` php
+$container->delegate(
+ // or consider using the value of Configure::read('debug')
+ new \League\Container\ReflectionContainer(true)
+);
+```
+
+Read more about auto wiring in the [PHP League Container documentation](https://container.thephpleague.com/4.x/auto-wiring/).
diff --git a/docs/en/development/errors.md b/docs/en/development/errors.md
new file mode 100644
index 0000000000..237e562e02
--- /dev/null
+++ b/docs/en/development/errors.md
@@ -0,0 +1,690 @@
+# Error & Exception Handling
+
+CakePHP applications come with error and exception handling setup for you. PHP
+errors are trapped and displayed or logged. Uncaught exceptions are rendered
+into error pages automatically.
+
+
+
+## Configuration
+
+Error configuration is done in your application's **config/app.php** file. By
+default CakePHP uses `Cake\Error\ErrorTrap` and `Cake\Error\ExceptionTrap`
+to handle both PHP errors and exceptions respectively. The error configuration
+allows you to customize error handling for your application. The following
+options are supported:
+
+- `errorLevel` - int - The level of errors you are interested in capturing.
+ Use the built-in PHP error constants, and bitmasks to select the level of
+ error you are interested in. See [Deprecation Warnings](#deprecation-warnings) to disable
+ deprecation warnings.
+- `trace` - bool - Include stack traces for errors in log files. Stack
+ traces will be included in the log after each error. This is helpful for
+ finding where/when errors are being raised.
+- `exceptionRenderer` - string - The class responsible for rendering uncaught
+ exceptions. If you choose a custom class you should place the file for that
+ class in **src/Error**. This class needs to implement a `render()` method.
+- `log` - bool - When `true`, exceptions + their stack traces will be
+ logged to `Cake\Log\Log`.
+- `skipLog` - array - An array of exception classnames that should not be
+ logged. This is useful to remove NotFoundExceptions or other common, but
+ uninteresting log messages.
+- `extraFatalErrorMemory` - int - Set to the number of megabytes to increase
+ the memory limit by when a fatal error is encountered. This allows breathing
+ room to complete logging or error handling.
+- `logger` (prior to 4.4.0 use `errorLogger`) -`Cake\Error\ErrorLoggerInterface` - The class responsible for logging
+ errors and unhandled exceptions. Defaults to `Cake\Error\ErrorLogger`.
+- `errorRenderer` - `Cake\Error\ErrorRendererInterface` - The class responsible
+ for rendering errors. Default is chosen based on PHP SAPI.
+- `ignoredDeprecationPaths` - array - A list of glob compatible paths that
+ deprecation errors should be ignored in. Added in 4.2.0
+
+By default, PHP errors are displayed when `debug` is `true`, and logged
+when debug is `false`. The fatal error handler will be called independent
+of `debug` level or `errorLevel` configuration, but the result will be
+different based on `debug` level. The default behavior for fatal errors is
+show a page to internal server error (`debug` disabled) or a page with the
+message, file and line (`debug` enabled).
+
+> [!NOTE]
+> If you use a custom error handler, the supported options will
+> depend on your handler.
+
+
+
+## Deprecation Warnings
+
+CakePHP uses deprecation warnings to indicate when features have been
+deprecated. We also recommend this system for use in your plugins and
+application code when useful. You can trigger deprecation warnings with
+`deprecationWarning()`:
+
+``` text
+deprecationWarning('5.0', 'The example() method is deprecated. Use getExample() instead.');
+```
+
+When upgrading CakePHP or plugins you may encounter new deprecation warnings.
+You can temporarily disable deprecation warnings in one of a few ways:
+
+1. Using the `Error.errorLevel` setting to `E_ALL ^ E_USER_DEPRECATED` to
+ ignore *all* deprecation warnings.
+
+2. Using the `Error.ignoredDeprecationPaths` configuration option to ignore
+ deprecations with glob compatible expressions. For example:
+
+ ``` text
+ 'Error' => [
+ 'ignoredDeprecationPaths' => [
+ 'vendors/company/contacts/*',
+ 'src/Models/*',
+ ],
+ ],
+ ```
+
+ Would ignore all deprecations from your `Models` directory and the
+ `Contacts` plugin in your application.
+
+## Changing Exception Handling
+
+Exception handling in CakePHP offers several ways to tailor how exceptions are
+handled. Each approach gives you different amounts of control over the
+exception handling process.
+
+1. *Listen to events* This allows you to be notified through CakePHP events when
+ errors and exceptions have been handled.
+2. *Custom templates* This allows you to change the rendered view
+ templates as you would any other template in your application.
+3. *Custom Controller* This allows you to control how exception
+ pages are rendered.
+4. *Custom ExceptionRenderer* This allows you to control how exception
+ pages and logging are performed.
+5. *Create & register your own traps* This gives you complete
+ control over how errors & exceptions are handled, logged and rendered. Use
+ `Cake\Error\ExceptionTrap` and `Cake\Error\ErrorTrap` as reference when
+ implementing your traps.
+
+## Listen to Events
+
+The `ErrorTrap` and `ExceptionTrap` handlers will trigger CakePHP events
+when they handle errors. You can listen to the `Error.beforeRender` event to be
+notified of PHP errors. The `Exception.beforeRender` event is dispatched when an
+exception is handled:
+
+``` php
+$errorTrap = new ErrorTrap(Configure::read('Error'));
+$errorTrap->getEventManager()->on(
+ 'Error.beforeRender',
+ function (EventInterface $event, PhpError $error) {
+ // do your thing
+ }
+);
+```
+
+Within an `Error.beforeRender` handler you have a few options:
+
+- Stop the event to prevent rendering.
+- Return a string to skip rendering and use the provided string instead
+
+Within an `Exception.beforeRender` handler you have a few options:
+
+- Stop the event to prevent rendering.
+- Set the `exception` data attribute with `setData('exception', $err)`
+ to replace the exception that is being rendered.
+- Return a response from the event listener to skip rendering and use
+ the provided response instead.
+
+
+
+## Custom Templates
+
+The default exception trap renders all uncaught exceptions your application
+raises with the help of `Cake\Error\Renderer\WebExceptionRenderer`, and your application's
+`ErrorController`.
+
+The error page views are located at **templates/Error/**. All 4xx errors use
+the **error400.php** template, and 5xx errors use the **error500.php**. Your
+error templates will have the following variables available:
+
+- `message` The exception message.
+- `code` The exception code.
+- `url` The request URL.
+- `error` The exception object.
+
+In debug mode if your error extends `Cake\Core\Exception\CakeException` the
+data returned by `getAttributes()` will be exposed as view variables as well.
+
+> [!NOTE]
+> You will need to set `debug` to false, to see your **error404** and
+> **error500** templates. In debug mode, you'll see CakePHP's development
+> error page.
+
+### Custom Error Page Layout
+
+By default error templates use **templates/layout/error.php** for a layout.
+You can use the `layout` property to pick a different layout:
+
+``` php
+// inside templates/Error/error400.php
+$this->layout = 'my_error';
+```
+
+The above would use **templates/layout/my_error.php** as the layout for your
+error pages.
+
+Many exceptions raised by CakePHP will render specific view templates in debug
+mode. With debug turned off all exceptions raised by CakePHP will use either
+**error400.php** or **error500.php** based on their status code.
+
+## Custom Controller
+
+The `App\Controller\ErrorController` class is used by CakePHP's exception
+rendering to render the error page view and receives all the standard request
+life-cycle events. By modifying this class you can control which components are
+used and which templates are rendered.
+
+If your application uses [Prefix Routing](../development/routing#prefix-routing) you can create custom error
+controllers for each routing prefix. For example, if you had an `Admin`
+prefix. You could create the following class:
+
+``` php
+namespace App\Controller\Admin;
+
+use App\Controller\AppController;
+use Cake\Event\EventInterface;
+
+class ErrorController extends AppController
+{
+ /**
+ * beforeRender callback.
+ *
+ * @param \Cake\Event\EventInterface $event Event.
+ * @return void
+ */
+ public function beforeRender(EventInterface $event): void
+ {
+ $this->viewBuilder()->setTemplatePath('Error');
+ }
+}
+```
+
+This controller would only be used when an error is encountered in a prefixed
+controller, and allows you to define prefix specific logic/templates as needed.
+
+### Exception specific logic
+
+Within your controller you can define public methods to handle custom
+application errors. For example a `MissingWidgetException` would be handled by
+a `missingWidget()` controller method, and CakePHP would use
+`templates/Error/missing_widget.php` as the template. For example:
+
+``` php
+namespace App\Controller\Admin;
+
+use App\Controller\AppController;
+use Cake\Event\EventInterface;
+
+class ErrorController extends AppController
+{
+ protected function missingWidget(MissingWidgetException $exception)
+ {
+ // You can prepare additional template context or trap errors.
+ }
+}
+```
+
+::: info Added in version 5.2.0
+Exception specific controller methods and templates were added.
+:::
+
+
+
+## Custom ExceptionRenderer
+
+If you want to control the entire exception rendering and logging process you
+can use the `Error.exceptionRenderer` option in **config/app.php** to choose
+a class that will render exception pages. Changing the ExceptionRenderer is
+useful when you want to change the logic used to create an error controller,
+choose the template, or control the overall rendering process.
+
+Your custom exception renderer class should be placed in **src/Error**. Let's
+assume our application uses `App\Exception\MissingWidgetException` to indicate
+a missing widget. We could create an exception renderer that renders specific
+error pages when this error is handled:
+
+``` php
+// In src/Error/AppExceptionRenderer.php
+namespace App\Error;
+
+use Cake\Error\Renderer\WebExceptionRenderer;
+
+class AppExceptionRenderer extends WebExceptionRenderer
+{
+ public function missingWidget($error)
+ {
+ $response = $this->controller->getResponse();
+
+ return $response->withStringBody('Oops that widget is missing.');
+ }
+}
+
+// In Application::middleware()
+$middlewareQueue->add(new ErrorHandlerMiddleware(
+ ['exceptionRenderer' => AppExceptionRenderer::class] + Configure::read('Error'),
+ $this,
+));
+// ...
+```
+
+The above would handle our `MissingWidgetException`,
+and allow us to provide custom display/handling logic for those application
+exceptions.
+
+Exception rendering methods receive the handled exception as an argument, and
+should return a `Response` object. You can also implement methods to add
+additional logic when handling CakePHP errors:
+
+``` php
+// In src/Error/AppExceptionRenderer.php
+namespace App\Error;
+
+use Cake\Error\Renderer\WebExceptionRenderer;
+
+class AppExceptionRenderer extends WebExceptionRenderer
+{
+ public function notFound($error)
+ {
+ // Do something with NotFoundException objects.
+ }
+}
+```
+
+### Changing the ErrorController Class
+
+The exception renderer dictates which controller is used for exception
+rendering. If you want to change which controller is used to render exceptions,
+override the `_getController()` method in your exception renderer:
+
+``` php
+// in src/Error/AppExceptionRenderer
+namespace App\Error;
+
+use App\Controller\SuperCustomErrorController;
+use Cake\Controller\Controller;
+use Cake\Error\Renderer\WebExceptionRenderer;
+
+class AppExceptionRenderer extends WebExceptionRenderer
+{
+ protected function _getController(): Controller
+ {
+ return new SuperCustomErrorController();
+ }
+}
+
+// In Application::middleware()
+$middlewareQueue->add(new ErrorHandlerMiddleware(
+ ['exceptionRenderer' => AppExceptionRenderer::class] + Configure::read('Error'),
+ $this,
+));
+// ...
+```
+
+
+
+application exceptions
+
+
+
+## Creating your own Application Exceptions
+
+You can create your own application exceptions using any of the built in [SPL
+exceptions](https://php.net/manual/en/spl.exceptions.php), `Exception`
+itself, or `Cake\Core\Exception\Exception`.
+If your application contained the following exception:
+
+``` php
+use Cake\Core\Exception\CakeException;
+
+class MissingWidgetException extends CakeException
+{
+}
+```
+
+You could provide nice development errors, by creating
+**templates/Error/missing_widget.php**. When in production mode, the above
+error would be treated as a 500 error and use the **error500** template.
+
+Exceptions that subclass `Cake\Http\Exception\HttpException`, will have their
+error code used as an HTTP status code if the error code is between `400` and
+`506`.
+
+The constructor for `Cake\Core\Exception\CakeException` allows you to
+pass in additional data. This additional data is interpolated into the the
+`_messageTemplate`. This allows you to create data rich exceptions, that
+provide more context around your errors:
+
+``` php
+use Cake\Core\Exception\CakeException;
+
+class MissingWidgetException extends CakeException
+{
+ // Context data is interpolated into this format string.
+ protected $_messageTemplate = 'Seems that %s is missing.';
+
+ // You can set a default exception code as well.
+ protected $_defaultCode = 404;
+}
+
+throw new MissingWidgetException(['widget' => 'Pointy']);
+```
+
+When rendered, this your view template would have a `$widget` variable set. If
+you cast the exception as a string or use its `getMessage()` method you will
+get `Seems that Pointy is missing.`.
+
+> [!NOTE]
+> Prior to CakePHP 4.2.0 use class `Cake\Core\Exception\Exception` instead
+> of `Cake\Core\Exception\CakeException`
+
+### Logging Exceptions
+
+Using the built-in exception handling, you can log all the exceptions that are
+dealt with by ErrorTrap by setting the `log` option to `true` in your
+**config/app.php**. Enabling this will log every exception to
+`Cake\Log\Log` and the configured loggers.
+
+> [!NOTE]
+> If you are using a custom exception handler this setting will have
+> no effect. Unless you reference it inside your implementation.
+
+
+
+## Built in Exceptions for CakePHP
+
+### HTTP Exceptions
+
+There are several built-in exceptions inside CakePHP, outside of the
+internal framework exceptions, there are several
+exceptions for HTTP methods
+
+> nocontentsentry
+>
+> > Used for doing 400 Bad Request error.
+>
+> nocontentsentry
+>
+> > Used for doing a 401 Unauthorized error.
+>
+> nocontentsentry
+>
+> > Used for doing a 403 Forbidden error.
+>
+> nocontentsentry
+>
+> > Used for doing a 403 error caused by an invalid CSRF token.
+>
+> nocontentsentry
+>
+> > Used for doing a 404 Not found error.
+>
+> nocontentsentry
+>
+> > Used for doing a 405 Method Not Allowed error.
+>
+> nocontentsentry
+>
+> > Used for doing a 406 Not Acceptable error.
+>
+> nocontentsentry
+>
+> > Used for doing a 409 Conflict error.
+>
+> nocontentsentry
+>
+> > Used for doing a 410 Gone error.
+
+For more details on HTTP 4xx error status codes see `2616#section-10.4`.
+
+> nocontentsentry
+>
+> > Used for doing a 500 Internal Server Error.
+>
+> nocontentsentry
+>
+> > Used for doing a 501 Not Implemented Errors.
+>
+> nocontentsentry
+>
+> > Used for doing a 503 Service Unavailable error.
+
+For more details on HTTP 5xx error status codes see `2616#section-10.5`.
+
+You can throw these exceptions from your controllers to indicate failure states,
+or HTTP errors. An example use of the HTTP exceptions could be rendering 404
+pages for items that have not been found:
+
+``` php
+use Cake\Http\Exception\NotFoundException;
+
+public function view($id = null)
+{
+ $article = $this->Articles->findById($id)->first();
+ if (empty($article)) {
+ throw new NotFoundException(__('Article not found'));
+ }
+ $this->set('article', $article);
+ $this->viewBuilder()->setOption('serialize', ['article']);
+}
+```
+
+By using exceptions for HTTP errors, you can keep your code both clean, and give
+RESTful responses to client applications and users.
+
+### Using HTTP Exceptions in your Controllers
+
+You can throw any of the HTTP related exceptions from your controller actions
+to indicate failure states. For example:
+
+``` php
+use Cake\Network\Exception\NotFoundException;
+
+public function view($id = null)
+{
+ $article = $this->Articles->findById($id)->first();
+ if (empty($article)) {
+ throw new NotFoundException(__('Article not found'));
+ }
+ $this->set('article', 'article');
+ $this->viewBuilder()->setOption('serialize', ['article']);
+}
+```
+
+The above would cause the configured exception handler to catch and
+process the `NotFoundException`. By default this will create an error
+page, and log the exception.
+
+### Other Built In Exceptions
+
+In addition, CakePHP uses the following exceptions:
+
+> nocontentsentry
+>
+> > The chosen view class could not be found.
+>
+> nocontentsentry
+>
+> > The chosen template file could not be found.
+>
+> nocontentsentry
+>
+> > The chosen layout could not be found.
+>
+> nocontentsentry
+>
+> > The chosen helper could not be found.
+>
+> nocontentsentry
+>
+> > The chosen element file could not be found.
+>
+> nocontentsentry
+>
+> > The chosen cell class could not be found.
+>
+> nocontentsentry
+>
+> > The chosen cell view file could not be found.
+>
+> nocontentsentry
+>
+> > A configured component could not be found.
+>
+> nocontentsentry
+>
+> > The requested controller action could not be found.
+>
+> nocontentsentry
+>
+> > Accessing private/protected/\_ prefixed actions.
+>
+> nocontentsentry
+>
+> > A console library class encounter an error.
+>
+> nocontentsentry
+>
+> > A model's connection is missing.
+>
+> nocontentsentry
+>
+> > A database driver could not be found.
+>
+> nocontentsentry
+>
+> > A PHP extension is missing for the database driver.
+>
+> nocontentsentry
+>
+> > A model's table could not be found.
+>
+> nocontentsentry
+>
+> > A model's entity could not be found.
+>
+> nocontentsentry
+>
+> > A model's behavior could not be found.
+>
+> nocontentsentry
+>
+> > An entity couldn't be saved/deleted while using `Cake\ORM\Table::saveOrFail()` or
+> > `Cake\ORM\Table::deleteOrFail()`.
+>
+> nocontentsentry
+>
+> The requested record could not be found. This will also set HTTP response
+> headers to 404.
+>
+> nocontentsentry
+>
+> > The requested controller could not be found.
+>
+> nocontentsentry
+>
+> > The requested URL cannot be reverse routed or cannot be parsed.
+>
+> nocontentsentry
+>
+> > Base exception class in CakePHP. All framework layer exceptions thrown by
+> > CakePHP will extend this class.
+
+These exception classes all extend `Exception`.
+By extending Exception, you can create your own 'framework' errors.
+
+`method` Class::**responseHeader**($header = null, $value = null)
+
+All Http and Cake exceptions extend the Exception class, which has a method
+to add headers to the response. For instance when throwing a 405
+MethodNotAllowedException the rfc2616 says:
+
+ "The response MUST include an Allow header containing a list of valid
+ methods for the requested resource."
+
+## Customizing PHP Error Handling
+
+By default PHP errors are rendered to console or HTML output, and also logged.
+If necessary, you can swap out CakePHP's error handling logic with your own.
+
+### Custom Error Logging
+
+Error handlers use instances of `Cake\Error\ErrorLoggingInterface` to create
+log messages and log them to the appropriate place. You can replace the error
+logger using the `Error.logger` configure value. An example error
+logger:
+
+``` php
+namespace App\Error;
+
+use Cake\Error\ErrorLoggerInterface;
+use Cake\Error\PhpError;
+use Psr\Http\Message\ServerRequestInterface;
+use Throwable;
+
+/**
+ * Log errors and unhandled exceptions to `Cake\Log\Log`
+ */
+class ErrorLogger implements ErrorLoggerInterface
+{
+ /**
+ * @inheritDoc
+ */
+ public function logError(
+ PhpError $error,
+ ?ServerRequestInterface $request,
+ bool $includeTrace = false
+ ): void {
+ // Log PHP Errors
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function logException(
+ ?ServerRequestInterface $request,
+ bool $includeTrace = false
+ ): void {
+ // Log exceptions.
+ }
+}
+```
+
+### Custom Error Rendering
+
+CakePHP includes error renderers for both web and console environments. If
+however, you would like to replace the logic that renders errors you can create
+a class:
+
+``` php
+// src/Error/CustomErrorRenderer.php
+namespace App\Error;
+
+use Cake\Error\ErrorRendererInterface;
+use Cake\Error\PhpError;
+
+class CustomErrorRenderer implements ErrorRendererInterface
+{
+ public function write(string $out): void
+ {
+ // output the rendered error to the appropriate output stream
+ }
+
+ public function render(PhpError $error, bool $debug): string
+ {
+ // Convert the error into the output string.
+ }
+}
+```
+
+The constructor of your renderer will be passed an array of all the Error
+configuration. You connect your custom error renderer to CakePHP via the
+`Error.errorRenderer` config value. When replacing error handling you will
+need to account for both web and command line environments.
diff --git a/docs/en/development/rest.md b/docs/en/development/rest.md
new file mode 100644
index 0000000000..ad54e9dd8e
--- /dev/null
+++ b/docs/en/development/rest.md
@@ -0,0 +1,149 @@
+# REST
+
+REST is a foundational concept to the open web. CakePHP provides functionality
+to build applications that expose REST APIs with low complexity abstractions and
+interfaces.
+
+CakePHP provides methods for exposing your controller actions via HTTP methods,
+and serializing view variables based on content-type negotiation. Content-Type
+negotiation allows clients of your application to send requests with serialize
+data and receive responses with serialized data via the `Accept` and
+`Content-Type` headers, or URL extensions.
+
+## Getting Started
+
+To get started with adding a REST API to your application, we'll first need
+a controller containing actions that we want to expose as an API. A basic
+controller might look something like this:
+
+``` php
+// src/Controller/RecipesController.php
+use Cake\View\JsonView;
+
+class RecipesController extends AppController
+{
+ public function viewClasses(): array
+ {
+ return [JsonView::class];
+ }
+
+ public function index()
+ {
+ $recipes = $this->Recipes->find('all')->all();
+ $this->set('recipes', $recipes);
+ $this->viewBuilder()->setOption('serialize', ['recipes']);
+ }
+
+ public function view($id)
+ {
+ $recipe = $this->Recipes->get($id);
+ $this->set('recipe', $recipe);
+ $this->viewBuilder()->setOption('serialize', ['recipe']);
+ }
+
+ public function add()
+ {
+ $this->request->allowMethod(['post', 'put']);
+ $recipe = $this->Recipes->newEntity($this->request->getData());
+ if ($this->Recipes->save($recipe)) {
+ $message = 'Saved';
+ } else {
+ $message = 'Error';
+ }
+ $this->set([
+ 'message' => $message,
+ 'recipe' => $recipe,
+ ]);
+ $this->viewBuilder()->setOption('serialize', ['recipe', 'message']);
+ }
+
+ public function edit($id)
+ {
+ $this->request->allowMethod(['patch', 'post', 'put']);
+ $recipe = $this->Recipes->get($id);
+ $recipe = $this->Recipes->patchEntity($recipe, $this->request->getData());
+ if ($this->Recipes->save($recipe)) {
+ $message = 'Saved';
+ } else {
+ $message = 'Error';
+ }
+ $this->set([
+ 'message' => $message,
+ 'recipe' => $recipe,
+ ]);
+ $this->viewBuilder()->setOption('serialize', ['recipe', 'message']);
+ }
+
+ public function delete($id)
+ {
+ $this->request->allowMethod(['delete']);
+ $recipe = $this->Recipes->get($id);
+ $message = 'Deleted';
+ if (!$this->Recipes->delete($recipe)) {
+ $message = 'Error';
+ }
+ $this->set('message', $message);
+ $this->viewBuilder()->setOption('serialize', ['message']);
+ }
+}
+```
+
+In our `RecipesController`, we have several actions that define the logic
+to create, edit, view and delete recipes. In each of our actions we're using
+the `serialize` option to tell CakePHP which view variables should be
+serialized when making API responses. We'll connect our controller to the
+application URLs with [Resource Routes](../development/routing#resource-routes):
+
+``` php
+// in config/routes.php
+$routes->scope('/', function (RouteBuilder $routes): void {
+ $routes->setExtensions(['json']);
+ $routes->resources('Recipes');
+});
+```
+
+These routes will enable URLs like `/recipes.json` to return a JSON encoded
+response. Clients could also make a request to `/recipes` with the
+`Content-Type: application/json` header as well.
+
+## Encoding Response Data
+
+In the above controller, we're defining a `viewClasses()` method. This method
+defines which views your controller has available for content-negotitation.
+We're including CakePHP's `JsonView` which enables JSON based responses. To
+learn more about it and Xml based views see [JSON and XML views](../views/json-and-xml-views). is
+used by CakePHP to select a view class to render a REST response with.
+
+Next, we have several methods that expose basic logic to create, edit, view and
+delete recipes. In each of our actions we're using the `serialize` option to
+tell CakePHP which view variables should be serialized when making API
+responses.
+
+If we wanted to modify the data before it is converted into JSON we should not
+define the `serialize` option, and instead use template files. We would place
+the REST templates for our RecipesController inside **templates/Recipes/json**.
+
+See the [Controller Viewclasses](../controllers#controller-viewclasses) for more information on how CakePHP's
+response negotiation functionality.
+
+## Parsing Request Bodies
+
+Creating the logic for the edit action requires another step. Because our
+resources are serialized as JSON it would be ergonomic if our requests also
+contained the JSON representation.
+
+In our `Application` class ensure the following is present:
+
+``` php
+$middlewareQueue->add(new BodyParserMiddleware());
+```
+
+This middleware will use the `content-type` header to detect the format of
+request data and parse enabled formats. By default only `JSON` parsing is
+enabled by default. You can enable XML support by enabling the `xml`
+constructor option. When a request is made with a `Content-Type` of
+`application/json`, CakePHP will decode the request data and update the
+request so that `$request->getData()` contains the parsed body.
+
+You can also wire in additional deserializers for alternate formats if you
+need them, using `BodyParserMiddleware::addParser()`.
diff --git a/docs/en/development/routing.md b/docs/en/development/routing.md
new file mode 100644
index 0000000000..31c9afb10e
--- /dev/null
+++ b/docs/en/development/routing.md
@@ -0,0 +1,1943 @@
+# Routing
+
+`class` Cake\\Routing\\**RouteBuilder**
+
+Routing provides you tools that map URLs to controller actions. By defining
+routes, you can separate how your application is implemented from how its URLs
+are structured.
+
+Routing in CakePHP also encompasses the idea of reverse routing, where an array
+of parameters can be transformed into a URL string. By using reverse routing,
+you can re-factor your application's URL structure without having to update all
+your code.
+
+
+
+routes.php
+
+
+
+## Quick Tour
+
+This section will teach you by example the most common uses of the CakePHP
+Router. Typically you want to display something as a landing page, so you add
+this to your **config/routes.php** file:
+
+``` php
+/** @var \Cake\Routing\RouteBuilder $routes */
+$routes->connect('/', ['controller' => 'Articles', 'action' => 'index']);
+```
+
+This will execute the index method in the `ArticlesController` when the
+homepage of your site is visited. Sometimes you need dynamic routes that will
+accept multiple parameters, this would be the case, for example of a route for
+viewing an article's content:
+
+``` php
+$routes->connect('/articles/*', ['controller' => 'Articles', 'action' => 'view']);
+```
+
+The above route will accept any URL looking like `/articles/15` and invoke the
+method `view(15)` in the `ArticlesController`. This will not, though,
+prevent people from trying to access URLs looking like `/articles/foobar`. If
+you wish, you can restrict some parameters to conform to a regular expression:
+
+``` php
+// Using fluent interface
+$routes->connect(
+ '/articles/{id}',
+ ['controller' => 'Articles', 'action' => 'view'],
+)
+->setPatterns(['id' => '\d+'])
+->setPass(['id']);
+
+// Using options array
+$routes->connect(
+ '/articles/{id}',
+ ['controller' => 'Articles', 'action' => 'view'],
+ ['id' => '\d+', 'pass' => ['id']]
+);
+```
+
+The previous example changed the star matcher by a new placeholder `{id}`.
+Using placeholders allows us to validate parts of the URL, in this case we used
+the `\d+` regular expression so that only digits are matched. Finally, we told
+the Router to treat the `id` placeholder as a function argument to the
+`view()` function by specifying the `pass` option. More on using this
+option later.
+
+The CakePHP Router can also reverse match routes. That means that from an
+array containing matching parameters, it is capable of generating a URL string:
+
+``` php
+use Cake\Routing\Router;
+
+echo Router::url(['controller' => 'Articles', 'action' => 'view', 'id' => 15]);
+// Will output
+/articles/15
+```
+
+Routes can also be labelled with a unique name, this allows you to quickly
+reference them when building links instead of specifying each of the routing
+parameters:
+
+``` php
+// In routes.php
+$routes->connect(
+ '/upgrade',
+ ['controller' => 'Subscriptions', 'action' => 'create'],
+ ['_name' => 'upgrade']
+);
+
+use Cake\Routing\Router;
+
+echo Router::url(['_name' => 'upgrade']);
+// Will output
+/upgrade
+```
+
+To help keep your routing code DRY, the Router has the concept of 'scopes'.
+A scope defines a common path segment, and optionally route defaults. Any routes
+connected inside a scope will inherit the path/defaults from their wrapping
+scopes:
+
+``` php
+$routes->scope('/blog', ['plugin' => 'Blog'], function (RouteBuilder $routes) {
+ $routes->connect('/', ['controller' => 'Articles']);
+});
+```
+
+The above route would match `/blog/` and send it to
+`Blog\Controller\ArticlesController::index()`.
+
+The application skeleton comes with a few routes to get you started. Once you've
+added your own routes, you can remove the default routes if you don't need them.
+
+
+
+{controller}, {action}, {plugin}
+
+
+
+
+
+greedy star, trailing star
+
+
+
+
+
+## Connecting Routes
+
+To keep your code `DRY` you should use 'routing scopes'. Routing
+scopes not only let you keep your code DRY, they also help Router optimize its
+operation. This method defaults to the `/` scope. To create a scope and connect
+some routes we'll use the `scope()` method:
+
+``` php
+// In config/routes.php
+use Cake\Routing\RouteBuilder;
+use Cake\Routing\Route\DashedRoute;
+
+$routes->scope('/', function (RouteBuilder $routes) {
+ // Connect the generic fallback routes.
+ $routes->fallbacks(DashedRoute::class);
+});
+```
+
+The `connect()` method takes up to three parameters: the URL template you wish
+to match, the default values for your route elements, and the options for the
+route. Options frequently include regular expression rules to help the router
+match elements in the URL.
+
+The basic format for a route definition is:
+
+``` php
+$routes->connect(
+ '/url/template',
+ ['targetKey' => 'targetValue'],
+ ['option' => 'matchingRegex']
+);
+```
+
+The first parameter is used to tell the router what sort of URL you're trying to
+control. The URL is a normal slash delimited string, but can also contain
+a wildcard (\*) or [Route Elements](#route-elements). Using a wildcard tells the router
+that you are willing to accept any additional arguments supplied. Routes without
+a \* only match the exact template pattern supplied.
+
+Once you've specified a URL, you use the last two parameters of `connect()` to
+tell CakePHP what to do with a request once it has been matched. The second
+parameter defines the route 'target'. This can be defined either as an array, or
+as a destination string. A few examples of route targets are:
+
+``` php
+// Array target to an application controller
+$routes->connect(
+ '/users/view/*',
+ ['controller' => 'Users', 'action' => 'view']
+);
+$routes->connect('/users/view/*', 'Users::view');
+
+// Array target to a prefixed plugin controller
+$routes->connect(
+ '/admin/cms/articles',
+ ['prefix' => 'Admin', 'plugin' => 'Cms', 'controller' => 'Articles', 'action' => 'index']
+);
+$routes->connect('/admin/cms/articles', 'Cms.Admin/Articles::index');
+```
+
+The first route we connect matches URLs starting with `/users/view` and maps
+those requests to the `UsersController->view()`. The trailing `/*` tells the
+router to pass any additional segments as method arguments. For example,
+`/users/view/123` would map to `UsersController->view(123)`.
+
+The above example also illustrates string targets. String targets provide
+a compact way to define a route's destination. String targets have the following
+syntax:
+
+``` text
+[Plugin].[Prefix]/[Controller]::[action]
+```
+
+Some example string targets are:
+
+``` text
+// Application controller
+'Articles::view'
+
+// Application controller with prefix
+Admin/Articles::view
+
+// Plugin controller
+Cms.Articles::edit
+
+// Prefixed plugin controller
+Vendor/Cms.Management/Admin/Articles::view
+```
+
+Earlier we used the greedy star (`/*`) to capture additional path segments,
+there is also the trailing star (`/**`). Using a trailing double star,
+will capture the remainder of a URL as a single passed argument. This is useful
+when you want to use an argument that included a `/` in it:
+
+``` php
+$routes->connect(
+ '/pages/**',
+ ['controller' => 'Pages', 'action' => 'show']
+);
+```
+
+The incoming URL of `/pages/the-example-/-and-proof` would result in a single
+passed argument of `the-example-/-and-proof`.
+
+The second parameter of `connect()` can define any parameters that
+compose the default route parameters:
+
+``` php
+$routes->connect(
+ '/government',
+ ['controller' => 'Pages', 'action' => 'display', 5]
+);
+```
+
+This example uses the second parameter of `connect()` to
+define default parameters. If you built an application that features products for
+different categories of customers, you might consider creating a route. This
+allows you to link to `/government` rather than `/pages/display/5`.
+
+A common use for routing is to rename controllers and their actions. Instead of
+accessing our users controller at `/users/some-action/5`, we'd like to be able
+to access it through `/cooks/some-action/5`. The following route takes care of
+that:
+
+``` php
+$routes->connect(
+ '/cooks/{action}/*', ['controller' => 'Users']
+);
+```
+
+This is telling the Router that any URL beginning with `/cooks/` should be
+sent to the `UsersController`. The action called will depend on the value of
+the `{action}` parameter. By using [Route Elements](#route-elements), you can create
+variable routes, that accept user input or variables. The above route also uses
+the greedy star. The greedy star indicates that this route should accept any
+additional positional arguments given. These arguments will be made available in
+the [Passed Arguments](#passed-arguments) array.
+
+When generating URLs, routes are used too. Using
+`['controller' => 'Users', 'action' => 'some-action', 5]` as
+a URL will output `/cooks/some-action/5` if the above route is the
+first match found.
+
+The routes we've connected so far will match any HTTP verb. If you are building
+a REST API you'll often want to map HTTP actions to different controller methods.
+The `RouteBuilder` provides helper methods that make defining routes for
+specific HTTP verbs simpler:
+
+``` php
+// Create a route that only responds to GET requests.
+$routes->get(
+ '/cooks/{id}',
+ ['controller' => 'Users', 'action' => 'view'],
+ 'users:view'
+);
+
+// Create a route that only responds to PUT requests
+$routes->put(
+ '/cooks/{id}',
+ ['controller' => 'Users', 'action' => 'update'],
+ 'users:update'
+);
+```
+
+The above routes map the same URL to different controller actions based on the
+HTTP verb used. GET requests will go to the 'view' action, while PUT requests
+will go to the 'update' action. There are HTTP helper methods for:
+
+- GET
+- POST
+- PUT
+- PATCH
+- DELETE
+- OPTIONS
+- HEAD
+
+All of these methods return the route instance allowing you to leverage the
+[fluent setters](#route-fluent-methods) to further configure your route.
+
+
+
+### Route Elements
+
+You can specify your own route elements and doing so gives you the
+power to define places in the URL where parameters for controller
+actions should lie. When a request is made, the values for these
+route elements are found in `$this->request->getParam()` in the controller.
+When you define a custom route element, you can optionally specify a regular
+expression - this tells CakePHP how to know if the URL is correctly formed or
+not. If you choose to not provide a regular expression, any non `/` character
+will be treated as part of the parameter:
+
+``` php
+$routes->connect(
+ '/{controller}/{id}',
+ ['action' => 'view']
+)->setPatterns(['id' => '[0-9]+']);
+
+$routes->connect(
+ '/{controller}/{id}',
+ ['action' => 'view'],
+ ['id' => '[0-9]+']
+);
+```
+
+The above example illustrates how to create a quick way to view
+models from any controller by crafting a URL that looks like
+`/controller-name/{id}`. The URL provided to `connect()` specifies two
+route elements: `{controller}` and `{id}`. The `{controller}` element
+is a CakePHP default route element, so the router knows how to match and
+identify controller names in URLs. The `{id}` element is a custom
+route element, and must be further clarified by specifying a
+matching regular expression in the third parameter of `connect()`.
+
+CakePHP does not automatically produce lowercased and dashed URLs when using the
+`{controller}` parameter. If you need this, the above example could be
+rewritten like so:
+
+``` php
+use Cake\Routing\Route\DashedRoute;
+
+// Create a builder with a different route class.
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->setRouteClass(DashedRoute::class);
+ $routes->connect('/{controller}/{id}', ['action' => 'view'])
+ ->setPatterns(['id' => '[0-9]+']);
+
+ $routes->connect(
+ '/{controller}/{id}',
+ ['action' => 'view'],
+ ['id' => '[0-9]+']
+ );
+});
+```
+
+The `DashedRoute` class will make sure that the `{controller}` and
+`{plugin}` parameters are correctly lowercased and dashed.
+
+> [!NOTE]
+> Patterns used for route elements must not contain any capturing
+> groups. If they do, Router will not function correctly.
+
+Once this route has been defined, requesting `/apples/5` would call the `view()`
+method of the ApplesController. Inside the `view()` method, you would need to
+access the passed ID at `$this->request->getParam('id')`.
+
+If you have a single controller in your application and you do not want the
+controller name to appear in the URL, you can map all URLs to actions in your
+controller. For example, to map all URLs to actions of the `home` controller,
+e.g have URLs like `/demo` instead of `/home/demo`, you can do the
+following:
+
+``` php
+$routes->connect('/{action}', ['controller' => 'Home']);
+```
+
+If you would like to provide a case insensitive URL, you can use regular
+expression inline modifiers:
+
+``` php
+$routes->connect(
+ '/{userShortcut}',
+ ['controller' => 'Teachers', 'action' => 'profile', 1],
+)->setPatterns(['userShortcut' => '(?i:principal)']);
+```
+
+One more example, and you'll be a routing pro:
+
+``` php
+$routes->connect(
+ '/{controller}/{year}/{month}/{day}',
+ ['action' => 'index']
+)->setPatterns([
+ 'year' => '[12][0-9]{3}',
+ 'month' => '0[1-9]|1[012]',
+ 'day' => '0[1-9]|[12][0-9]|3[01]'
+]);
+```
+
+This is rather involved, but shows how powerful routes can be. The URL supplied
+has four route elements. The first is familiar to us: it's a default route
+element that tells CakePHP to expect a controller name.
+
+Next, we specify some default values. Regardless of the controller,
+we want the `index()` action to be called.
+
+Finally, we specify some regular expressions that will match years,
+months and days in numerical form. Note that parenthesis (capturing groups)
+are not supported in the regular expressions. You can still specify
+alternates, as above, but not grouped with parenthesis.
+
+Once defined, this route will match `/articles/2007/02/01`,
+`/articles/2004/11/16`, handing the requests to
+the `index()` actions of their respective controllers, with the date
+parameters in `$this->request->getParam()`.
+
+### Reserved Route Elements
+
+There are several route elements that have special meaning in
+CakePHP, and should not be used unless you want the special meaning
+
+- `controller` Used to name the controller for a route.
+- `action` Used to name the controller action for a route.
+- `plugin` Used to name the plugin a controller is located in.
+- `prefix` Used for [Prefix Routing](#prefix-routing)
+- `_ext` Used for [File extentions routing](#file-extensions).
+- `_base` Set to `false` to remove the base path from the generated URL. If
+ your application is not in the root directory, this can be used to generate
+ URLs that are 'cake relative'.
+- `_scheme` Set to create links on different schemes like webcal or ftp.
+ Defaults to the current scheme.
+- `_host` Set the host to use for the link. Defaults to the current host.
+- `_port` Set the port if you need to create links on non-standard ports.
+- `_full` If `true` the value of `App.fullBaseUrl` mentioned in
+ [General Configuration](../development/configuration#general-configuration) will be prepended to generated URLs.
+- `#` Allows you to set URL hash fragments.
+- `_https` Set to `true` to convert the generated URL to https or `false`
+ to force http. Prior to 4.5.0 use `_ssl`.
+- `_method` Define the HTTP verb/method to use. Useful when working with
+ [Resource Routes](#resource-routes).
+- `_name` Name of route. If you have setup named routes, you can use this key
+ to specify it.
+
+
+
+### Configuring Route Options
+
+There are a number of route options that can be set on each route. After
+connecting a route you can use its fluent builder methods to further configure
+the route. These methods replace many of the keys in the `$options` parameter
+of `connect()`:
+
+``` php
+$routes->connect(
+ '/{lang}/articles/{slug}',
+ ['controller' => 'Articles', 'action' => 'view'],
+)
+// Allow GET and POST requests.
+->setMethods(['GET', 'POST'])
+
+// Only match on the blog subdomain.
+->setHost('blog.example.com')
+
+// Set the route elements that should be converted to passed arguments
+->setPass(['slug'])
+
+// Set the matching patterns for route elements
+->setPatterns([
+ 'slug' => '[a-z0-9-_]+',
+ 'lang' => 'en|fr|es',
+])
+
+// Also allow JSON file extensions
+->setExtensions(['json'])
+
+// Set lang to be a persistent parameter
+->setPersist(['lang']);
+```
+
+### Setting Default Options for Routes in a Scope
+
+You can set default options that will be applied to all routes within a scope
+using the `setOptions()` method. This is useful when you want to apply the
+same options (like `_host`, `_https`, or `_port`) to multiple routes
+without repeating them:
+
+``` php
+$routes->scope('/api', function (RouteBuilder $routes) {
+ // Set default options for all routes in this scope
+ $routes->setOptions([
+ '_host' => 'api.example.com',
+ '_https' => true
+ ]);
+
+ // These routes will automatically have _host and _https set
+ $routes->get('/users', ['controller' => 'Users', 'action' => 'index']);
+ $routes->get('/posts', ['controller' => 'Posts', 'action' => 'index']);
+});
+```
+
+Options set via `setOptions()` are:
+
+- **Inherited by nested scopes** - Child scopes automatically receive the parent's default options
+- **Overridable on individual routes** - Options passed to `connect()` or HTTP verb methods take precedence over defaults
+- **Merged with route-specific options** - Default options are combined with any options you specify on individual routes
+
+Example with nested scopes and overrides:
+
+``` php
+$routes->scope('/api', function (RouteBuilder $routes) {
+ $routes->setOptions(['_host' => 'api.example.com']);
+
+ // This route uses the default host
+ $routes->get('/public', ['controller' => 'Public', 'action' => 'index']);
+
+ // This route overrides the default host
+ $routes->get('/internal', [
+ 'controller' => 'Internal',
+ 'action' => 'index',
+ '_host' => 'internal.example.com'
+ ]);
+
+ // Nested scope inherits the default host
+ $routes->scope('/v2', function (RouteBuilder $routes) {
+ // This also uses api.example.com
+ $routes->get('/users', ['controller' => 'Users', 'action' => 'index']);
+ });
+});
+```
+
+The `setOptions()` method is particularly useful for:
+
+- Setting the same `_host` for an entire API scope
+- Enforcing `_https => true` for secure sections of your application
+- Configuring `_port` for routes that should use non-standard ports
+
+### Passing Parameters to Action
+
+When connecting routes using [Route Elements](#route-elements) you may want to have routed
+elements be passed arguments instead. The `pass` option indicates which route
+elements should also be made available as arguments passed into the controller
+functions:
+
+``` php
+// src/Controller/BlogsController.php
+public function view($articleId = null, $slug = null)
+{
+ // Some code here...
+}
+
+// routes.php
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->connect(
+ '/blog/{id}-{slug}', // For example, /blog/3-CakePHP_Rocks
+ ['controller' => 'Blogs', 'action' => 'view']
+ )
+ // Define the route elements in the route template
+ // to prepend as function arguments. Order matters as this
+ // will pass the `$id` and `$slug` elements as the first and
+ // second parameters. Any additional passed parameters in your
+ // route will be added after the setPass() arguments.
+ ->setPass(['id', 'slug'])
+ // Define a pattern that `id` must match.
+ ->setPatterns([
+ 'id' => '[0-9]+',
+ ]);
+});
+```
+
+Now thanks to the reverse routing capabilities, you can pass in the URL array
+like below and CakePHP will know how to form the URL as defined in the routes:
+
+``` php
+// view.php
+// This will return a link to /blog/3-CakePHP_Rocks
+echo $this->Html->link('CakePHP Rocks', [
+ 'controller' => 'Blog',
+ 'action' => 'view',
+ 'id' => 3,
+ 'slug' => 'CakePHP_Rocks'
+]);
+
+// You can also used numerically indexed parameters.
+echo $this->Html->link('CakePHP Rocks', [
+ 'controller' => 'Blog',
+ 'action' => 'view',
+ 3,
+ 'CakePHP_Rocks'
+]);
+```
+
+
+
+### Using Path Routing
+
+We talked about string targets above. The same also works for URL generation using
+`Router::pathUrl()`:
+
+``` php
+echo Router::pathUrl('Articles::index');
+// outputs: /articles
+
+echo Router::pathUrl('MyBackend.Admin/Articles::view', [3]);
+// outputs: /admin/my-backend/articles/view/3
+```
+
+> [!TIP]
+> IDE support for Path Routing autocomplete can be enabled with [CakePHP IdeHelper Plugin](https://github.com/dereuromark/cakephp-ide-helper).
+
+
+
+### Using Named Routes
+
+Sometimes you'll find typing out all the URL parameters for a route too verbose,
+or you'd like to take advantage of the performance improvements that named
+routes have. When connecting routes you can specify a `_name` option, this
+option can be used in reverse routing to identify the route you want to use:
+
+``` php
+// Connect a route with a name.
+$routes->connect(
+ '/login',
+ ['controller' => 'Users', 'action' => 'login'],
+ ['_name' => 'login']
+);
+
+// Name a verb specific route
+$routes->post(
+ '/logout',
+ ['controller' => 'Users', 'action' => 'logout'],
+ 'logout'
+);
+
+// Generate a URL using a named route.
+$url = Router::url(['_name' => 'logout']);
+
+// Generate a URL using a named route,
+// with some query string args.
+$url = Router::url(['_name' => 'login', 'username' => 'jimmy']);
+```
+
+If your route template contains any route elements like `{controller}` you'll
+need to supply those as part of the options to `Router::url()`.
+
+> [!NOTE]
+> Route names must be unique across your entire application. The same
+> `_name` cannot be used twice, even if the names occur inside a different
+> routing scope.
+
+When building named routes, you will probably want to stick to some conventions
+for the route names. CakePHP makes building up route names easier by allowing
+you to define name prefixes in each scope:
+
+``` php
+$routes->scope('/api', ['_namePrefix' => 'api:'], function (RouteBuilder $routes) {
+ // This route's name will be `api:ping`
+ $routes->get('/ping', ['controller' => 'Pings'], 'ping');
+});
+// Generate a URL for the ping route
+Router::url(['_name' => 'api:ping']);
+
+// Use namePrefix with plugin()
+$routes->plugin('Contacts', ['_namePrefix' => 'contacts:'], function (RouteBuilder $routes) {
+ // Connect routes.
+});
+
+// Or with prefix()
+$routes->prefix('Admin', ['_namePrefix' => 'admin:'], function (RouteBuilder $routes) {
+ // Connect routes.
+});
+```
+
+You can also use the `_namePrefix` option inside nested scopes and it works as
+you'd expect:
+
+``` php
+$routes->plugin('Contacts', ['_namePrefix' => 'contacts:'], function (RouteBuilder $routes) {
+ $routes->scope('/api', ['_namePrefix' => 'api:'], function (RouteBuilder $routes) {
+ // This route's name will be `contacts:api:ping`
+ $routes->get('/ping', ['controller' => 'Pings'], 'ping');
+ });
+});
+
+// Generate a URL for the ping route
+Router::url(['_name' => 'contacts:api:ping']);
+```
+
+Routes connected in named scopes will only have names added if the route is also
+named. Nameless routes will not have the `_namePrefix` applied to them.
+
+
+
+admin routing, prefix routing
+
+
+
+
+
+### Prefix Routing
+
+`static` Cake\\Routing\\RouteBuilder::**prefix**($name, $callback)
+
+Many applications require an administration section where
+privileged users can make changes. This is often done through a
+special URL such as `/admin/users/edit/5`. In CakePHP, prefix routing
+can be enabled by using the `prefix` scope method:
+
+``` php
+use Cake\Routing\Route\DashedRoute;
+
+$routes->prefix('Admin', function (RouteBuilder $routes) {
+ // All routes here will be prefixed with `/admin`, and
+ // have the `'prefix' => 'Admin'` route element added that
+ // will be required when generating URLs for these routes
+ $routes->fallbacks(DashedRoute::class);
+});
+```
+
+Prefixes are mapped to sub-namespaces in your application's `Controller`
+namespace. By having prefixes as separate controllers you can create smaller and
+simpler controllers. Behavior that is common to the prefixed and non-prefixed
+controllers can be encapsulated using inheritance,
+[Components](../controllers/components), or traits. Using our users example, accessing
+the URL `/admin/users/edit/5` would call the `edit()` method of our
+**src/Controller/Admin/UsersController.php** passing 5 as the first parameter.
+The view file used would be **templates/Admin/Users/edit.php**
+
+You can map the URL /admin to your `index()` action of pages controller using
+following route:
+
+``` php
+$routes->prefix('Admin', function (RouteBuilder $routes) {
+ // Because you are in the admin scope,
+ // you do not need to include the /admin prefix
+ // or the Admin route element.
+ $routes->connect('/', ['controller' => 'Pages', 'action' => 'index']);
+});
+```
+
+When creating prefix routes, you can set additional route parameters using
+the `$options` argument:
+
+``` php
+$routes->prefix('Admin', ['param' => 'value'], function (RouteBuilder $routes) {
+ // Routes connected here are prefixed with '/admin' and
+ // have the 'param' routing key set.
+ $routes->connect('/{controller}');
+});
+```
+
+Note the additional route parameters will be added to all the connected routes defined
+inside the prefix block. You will need to use all the parameters in the url array to
+build the route later, if you don't use them you'll get a `MissingRouteException`.
+
+Multi word prefixes are by default converted using dasherize inflection, ie `MyPrefix`
+would be mapped to `my-prefix` in the URL. Make sure to set a path for such prefixes
+if you want to use a different format like for example underscoring:
+
+``` php
+$routes->prefix('MyPrefix', ['path' => '/my_prefix'], function (RouteBuilder $routes) {
+ // Routes connected here are prefixed with '/my_prefix'
+ $routes->connect('/{controller}');
+});
+```
+
+You can define prefixes inside plugin scopes as well:
+
+``` php
+$routes->plugin('DebugKit', function (RouteBuilder $routes) {
+ $routes->prefix('Admin', function (RouteBuilder $routes) {
+ $routes->connect('/{controller}');
+ });
+});
+```
+
+The above would create a route template like `/debug-kit/admin/{controller}`.
+The connected route would have the `plugin` and `prefix` route elements set.
+
+When defining prefixes, you can nest multiple prefixes if necessary:
+
+``` php
+$routes->prefix('Manager', function (RouteBuilder $routes) {
+ $routes->prefix('Admin', function (RouteBuilder $routes) {
+ $routes->connect('/{controller}/{action}');
+ });
+});
+```
+
+The above would create a route template like `/manager/admin/{controller}/{action}`.
+The connected route would have the `prefix` route element set to
+`Manager/Admin`.
+
+The current prefix will be available from the controller methods through
+`$this->request->getParam('prefix')`
+
+When using prefix routes it's important to set the `prefix` option, and to
+use the same CamelCased format that is used in the `prefix()` method. Here's
+how to build this link using the HTML helper:
+
+``` php
+// Go into a prefixed route.
+echo $this->Html->link(
+ 'Manage articles',
+ ['prefix' => 'Manager/Admin', 'controller' => 'Articles', 'action' => 'add']
+);
+
+// Leave a prefix
+echo $this->Html->link(
+ 'View Post',
+ ['prefix' => false, 'controller' => 'Articles', 'action' => 'view', 5]
+);
+```
+
+
+
+plugin routing
+
+
+
+### Creating Links to Prefix Routes
+
+You can create links that point to a prefix, by adding the prefix key to your
+URL array:
+
+``` php
+echo $this->Html->link(
+ 'New admin todo',
+ ['prefix' => 'Admin', 'controller' => 'TodoItems', 'action' => 'create']
+);
+```
+
+When using nesting, you need to chain them together:
+
+``` php
+echo $this->Html->link(
+ 'New todo',
+ ['prefix' => 'Admin/MyPrefix', 'controller' => 'TodoItems', 'action' => 'create']
+);
+```
+
+This would link to a controller with the namespace `App\Controller\Admin\MyPrefix` and the file path
+`src/Controller/Admin/MyPrefix/TodoItemsController.php`.
+
+> [!NOTE]
+> The prefix is always CamelCased here, even if the routing result is dashed.
+> The route itself will do the inflection if necessary.
+
+### Plugin Routing
+
+`static` Cake\\Routing\\RouteBuilder::**plugin**($name, $options = [], $callback)
+
+Routes for [Plugins](../plugins) should be created using the `plugin()`
+method. This method creates a new routing scope for the plugin's routes:
+
+``` php
+$routes->plugin('DebugKit', function (RouteBuilder $routes) {
+ // Routes connected here are prefixed with '/debug-kit' and
+ // have the plugin route element set to 'DebugKit'.
+ $routes->connect('/{controller}');
+});
+```
+
+When creating plugin scopes, you can customize the path element used with the
+`path` option:
+
+``` php
+$routes->plugin('DebugKit', ['path' => '/debugger'], function (RouteBuilder $routes) {
+ // Routes connected here are prefixed with '/debugger' and
+ // have the plugin route element set to 'DebugKit'.
+ $routes->connect('/{controller}');
+});
+```
+
+When using scopes you can nest plugin scopes within prefix scopes:
+
+``` php
+$routes->prefix('Admin', function (RouteBuilder $routes) {
+ $routes->plugin('DebugKit', function (RouteBuilder $routes) {
+ $routes->connect('/{controller}');
+ });
+});
+```
+
+The above would create a route that looks like `/admin/debug-kit/{controller}`.
+It would have the `prefix`, and `plugin` route elements set. The
+[Plugin Routes](../plugins#plugin-routes) section has more information on building plugin routes.
+
+### Creating Links to Plugin Routes
+
+You can create links that point to a plugin, by adding the plugin key to your
+URL array:
+
+``` php
+echo $this->Html->link(
+ 'New todo',
+ ['plugin' => 'Todo', 'controller' => 'TodoItems', 'action' => 'create']
+);
+```
+
+Conversely if the active request is a plugin request and you want to create
+a link that has no plugin you can do the following:
+
+``` php
+echo $this->Html->link(
+ 'New todo',
+ ['plugin' => null, 'controller' => 'Users', 'action' => 'profile']
+);
+```
+
+By setting `'plugin' => null` you tell the Router that you want to
+create a link that is not part of a plugin.
+
+### SEO-Friendly Routing
+
+Some developers prefer to use dashes in URLs, as it's perceived to give
+better search engine rankings. The `DashedRoute` class can be used in your
+application with the ability to route plugin, controller, and camelized action
+names to a dashed URL.
+
+For example, if we had a `ToDo` plugin, with a `TodoItems` controller, and a
+`showItems()` action, it could be accessed at `/to-do/todo-items/show-items`
+with the following router connection:
+
+``` php
+use Cake\Routing\Route\DashedRoute;
+
+$routes->plugin('ToDo', ['path' => 'to-do'], function (RouteBuilder $routes) {
+ $routes->fallbacks(DashedRoute::class);
+});
+```
+
+### Matching Specific HTTP Methods
+
+Routes can match specific HTTP methods using the HTTP verb helper methods:
+
+``` php
+$routes->scope('/', function (RouteBuilder $routes) {
+ // This route only matches on POST requests.
+ $routes->post(
+ '/reviews/start',
+ ['controller' => 'Reviews', 'action' => 'start']
+ );
+
+ // Match multiple verbs
+ $routes->connect(
+ '/reviews/start',
+ [
+ 'controller' => 'Reviews',
+ 'action' => 'start',
+ ]
+ )->setMethods(['POST', 'PUT']);
+});
+```
+
+You can match multiple HTTP methods by using an array. Because the `_method`
+parameter is a routing key, it participates in both URL parsing and URL
+generation. To generate URLs for method specific routes you'll need to include
+the `_method` key when generating the URL:
+
+``` php
+$url = Router::url([
+ 'controller' => 'Reviews',
+ 'action' => 'start',
+ '_method' => 'POST',
+]);
+```
+
+### Matching Specific Hostnames
+
+Routes can use the `_host` option to only match specific hosts. You can use
+the `*.` wildcard to match any subdomain:
+
+``` php
+$routes->scope('/', function (RouteBuilder $routes) {
+ // This route only matches on http://images.example.com
+ $routes->connect(
+ '/images/default-logo.png',
+ ['controller' => 'Images', 'action' => 'default']
+ )->setHost('images.example.com');
+
+ // This route only matches on http://*.example.com
+ $routes->connect(
+ '/images/old-logo.png',
+ ['controller' => 'Images', 'action' => 'oldLogo']
+ )->setHost('*.example.com');
+});
+```
+
+The `_host` option is also used in URL generation. If your `_host` option
+specifies an exact domain, that domain will be included in the generated URL.
+However, if you use a wildcard, then you will need to provide the `_host`
+parameter when generating URLs:
+
+``` php
+// If you have this route
+$routes->connect(
+ '/images/old-logo.png',
+ ['controller' => 'Images', 'action' => 'oldLogo']
+)->setHost('images.example.com');
+
+// You need this to generate a url
+echo Router::url([
+ 'controller' => 'Images',
+ 'action' => 'oldLogo',
+ '_host' => 'images.example.com',
+]);
+```
+
+
+
+file extensions
+
+
+
+
+
+### Routing File Extensions
+
+`method` Cake\\Routing\\RouteBuilder::**setExtensions**(array|string $extensions)
+
+To handle different file extensions in your URLs, you can define the extensions
+using the `Cake\Routing\RouteBuilder::setExtensions()` method:
+
+``` php
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->setExtensions(['json', 'xml']);
+});
+```
+
+This will enable the named extensions for all routes that are being connected in
+that scope **after** the `setExtensions()` call, including those that are being
+connected in nested scopes.
+
+> [!NOTE]
+> Setting the extensions should be the first thing you do in a scope, as the
+> extensions will only be applied to routes connected **after** the extensions
+> are set.
+>
+> Also be aware that re-opened scopes will **not** inherit extensions defined in
+> previously opened scopes.
+
+By using extensions, you tell the router to remove any matching file extensions
+from the URL, and then parse what remains. If you want to create a URL such as
+/page/title-of-page.html you would create your route using:
+
+``` php
+$routes->scope('/page', function (RouteBuilder $routes) {
+ $routes->setExtensions(['json', 'xml', 'html']);
+ $routes->connect(
+ '/{title}',
+ ['controller' => 'Pages', 'action' => 'view']
+ )->setPass(['title']);
+});
+```
+
+Then to create links which map back to the routes simply use:
+
+``` php
+$this->Html->link(
+ 'Link title',
+ ['controller' => 'Pages', 'action' => 'view', 'title' => 'super-article', '_ext' => 'html']
+);
+```
+
+
+
+## Route Scoped Middleware
+
+While Middleware can be applied to your entire application, applying middleware
+to specific routing scopes offers more flexibility, as you can apply middleware
+only where it is needed allowing your middleware to not concern itself with
+how/where it is being applied.
+
+> [!NOTE]
+> Applied scoped middleware will be run by [RoutingMiddleware](../controllers/middleware#routing-middleware),
+> normally at the end of your application's middleware queue.
+
+Before middleware can be applied to a scope, it needs to be
+registered into the route collection:
+
+``` php
+// in config/routes.php
+use Cake\Http\Middleware\CsrfProtectionMiddleware;
+use Cake\Http\Middleware\EncryptedCookieMiddleware;
+
+$routes->registerMiddleware('csrf', new CsrfProtectionMiddleware());
+$routes->registerMiddleware('cookies', new EncryptedCookieMiddleware());
+```
+
+Once registered, scoped middleware can be applied to specific
+scopes:
+
+``` php
+$routes->scope('/cms', function (RouteBuilder $routes) {
+ // Enable CSRF & cookies middleware
+ $routes->applyMiddleware('csrf', 'cookies');
+ $routes->get('/articles/{action}/*', ['controller' => 'Articles']);
+});
+```
+
+In situations where you have nested scopes, inner scopes will inherit the
+middleware applied in the containing scope:
+
+``` php
+$routes->scope('/api', function (RouteBuilder $routes) {
+ $routes->applyMiddleware('ratelimit', 'auth.api');
+ $routes->scope('/v1', function (RouteBuilder $routes) {
+ $routes->applyMiddleware('v1compat');
+ // Define routes here.
+ });
+});
+```
+
+In the above example, the routes defined in `/v1` will have 'ratelimit',
+'auth.api', and 'v1compat' middleware applied. If you re-open a scope, the
+middleware applied to routes in each scope will be isolated:
+
+``` php
+$routes->scope('/blog', function (RouteBuilder $routes) {
+ $routes->applyMiddleware('auth');
+ // Connect the authenticated actions for the blog here.
+});
+$routes->scope('/blog', function (RouteBuilder $routes) {
+ // Connect the public actions for the blog here.
+});
+```
+
+In the above example, the two uses of the `/blog` scope do not share
+middleware. However, both of these scopes will inherit middleware defined in
+their enclosing scopes.
+
+### Grouping Middleware
+
+To help keep your route code `DRY (Do not Repeat Yourself)` middleware can
+be combined into groups. Once combined groups can be applied like middleware
+can:
+
+``` php
+$routes->registerMiddleware('cookie', new EncryptedCookieMiddleware());
+$routes->registerMiddleware('auth', new AuthenticationMiddleware());
+$routes->registerMiddleware('csrf', new CsrfProtectionMiddleware());
+$routes->middlewareGroup('web', ['cookie', 'auth', 'csrf']);
+
+// Apply the group
+$routes->applyMiddleware('web');
+```
+
+
+
+## RESTful Routing
+
+Router helps generate RESTful routes for your controllers. RESTful routes are
+helpful when you are creating API endpoints for your application. If we wanted
+to allow REST access to a recipe controller, we'd do something like this:
+
+``` php
+// In config/routes.php...
+
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->setExtensions(['json']);
+ $routes->resources('Recipes');
+});
+```
+
+The first line sets up a number of default routes for REST
+access where method specifies the desired result format, for example, xml,
+json and rss. These routes are HTTP Request Method sensitive.
+
+| HTTP format | URL.format | Controller action invoked |
+|-------------|---------------------|--------------------------------|
+| GET | /recipes.format | RecipesController::index() |
+| GET | /recipes/123.format | RecipesController::view(123) |
+| POST | /recipes.format | RecipesController::add() |
+| PUT | /recipes/123.format | RecipesController::edit(123) |
+| PATCH | /recipes/123.format | RecipesController::edit(123) |
+| DELETE | /recipes/123.format | RecipesController::delete(123) |
+
+> [!NOTE]
+> The default for pattern for resource IDs only matches integers or UUIDs.
+> If your IDs are different you will have to supply a regular expression pattern
+> via the `id` option, for example, `$builder->resources('Recipes', ['id' => '.*'])`.
+
+The HTTP method being used is detected from a few different sources.
+The sources in order of preference are:
+
+1. The `_method` POST variable
+2. The `X_HTTP_METHOD_OVERRIDE` header.
+3. The `REQUEST_METHOD` header
+
+The `_method` POST variable is helpful in using a browser as a
+REST client (or anything else that can do POST). Just set
+the value of `_method` to the name of the HTTP request method you
+wish to emulate.
+
+### Creating Nested Resource Routes
+
+Once you have connected resources in a scope, you can connect routes for
+sub-resources as well. Sub-resource routes will be prepended by the original
+resource name and a id parameter. For example:
+
+``` php
+$routes->scope('/api', function (RouteBuilder $routes) {
+ $routes->resources('Articles', function (RouteBuilder $routes) {
+ $routes->resources('Comments');
+ });
+});
+```
+
+Will generate resource routes for both `articles` and `comments`. The
+comments routes will look like:
+
+``` text
+/api/articles/{article_id}/comments
+/api/articles/{article_id}/comments/{id}
+```
+
+You can get the `article_id` in `CommentsController` by:
+
+``` php
+$this->request->getParam('article_id');
+```
+
+By default resource routes map to the same prefix as the containing scope. If
+you have both nested and non-nested resource controllers you can use a different
+controller in each context by using prefixes:
+
+``` php
+$routes->scope('/api', function (RouteBuilder $routes) {
+ $routes->resources('Articles', function (RouteBuilder $routes) {
+ $routes->resources('Comments', ['prefix' => 'Articles']);
+ });
+});
+```
+
+The above would map the 'Comments' resource to the
+`App\Controller\Articles\CommentsController`. Having separate controllers lets
+you keep your controller logic simpler. The prefixes created this way are
+compatible with [Prefix Routing](#prefix-routing).
+
+> [!NOTE]
+> While you can nest resources as deeply as you require, it is not recommended
+> to nest more than 2 resources together.
+
+### Limiting the Routes Created
+
+By default CakePHP will connect 6 routes for each resource. If you'd like to
+only connect specific resource routes you can use the `only` option:
+
+``` php
+$routes->resources('Articles', [
+ 'only' => ['index', 'view']
+]);
+```
+
+Would create read only resource routes. The route names are `create`,
+`update`, `view`, `index`, and `delete`.
+
+The default **route name and controller action used** are as follows:
+
+| Route name | Controller action used |
+|------------|------------------------|
+| create | add |
+| update | edit |
+| view | view |
+| index | index |
+| delete | delete |
+
+### Changing the Controller Actions Used
+
+You may need to change the controller action names that are used when connecting
+routes. For example, if your `edit()` action is called `put()` you can
+use the `actions` key to rename the actions used:
+
+``` php
+$routes->resources('Articles', [
+ 'actions' => ['update' => 'put', 'create' => 'add']
+]);
+```
+
+The above would use `put()` for the `edit()` action, and `add()`
+instead of `create()`.
+
+### Mapping Additional Resource Routes
+
+You can map additional resource methods using the `map` option:
+
+``` php
+$routes->resources('Articles', [
+ 'map' => [
+ 'deleteAll' => [
+ 'action' => 'deleteAll',
+ 'method' => 'DELETE'
+ ]
+ ]
+]);
+// This would connect /articles/deleteAll
+```
+
+In addition to the default routes, this would also connect a route for
+/articles/delete-all. By default the path segment will match the key name. You
+can use the 'path' key inside the resource definition to customize the path
+name:
+
+``` php
+$routes->resources('Articles', [
+ 'map' => [
+ 'updateAll' => [
+ 'action' => 'updateAll',
+ 'method' => 'PUT',
+ 'path' => '/update-many',
+ ],
+ ],
+]);
+// This would connect /articles/update-many
+```
+
+If you define 'only' and 'map', make sure that your mapped methods are also in
+the 'only' list.
+
+### Prefixed Resource Routing
+
+Resource routes can be connected to controllers in routing prefixes by
+connecting routes within a prefixed scope or by using the `prefix` option:
+
+``` php
+$routes->resources('Articles', [
+ 'prefix' => 'Api',
+]);
+```
+
+
+
+### Custom Route Classes for Resource Routes
+
+You can provide `connectOptions` key in the `$options` array for
+`resources()` to provide custom setting used by `connect()`:
+
+``` php
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->resources('Books', [
+ 'connectOptions' => [
+ 'routeClass' => 'ApiRoute',
+ ]
+ ];
+});
+```
+
+### URL Inflection for Resource Routes
+
+By default, multi-worded controllers' URL fragments are the dashed
+form of the controller's name. For example, `BlogPostsController`'s URL fragment
+would be **/blog-posts**.
+
+You can specify an alternative inflection type using the `inflect` option:
+
+``` php
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->resources('BlogPosts', [
+ 'inflect' => 'underscore' // Will use ``Inflector::underscore()``
+ ]);
+});
+```
+
+The above will generate URLs styled like: **/blog_posts**.
+
+### Changing the Path Element
+
+By default resource routes use an inflected form of the resource name for the
+URL segment. You can set a custom URL segment with the `path` option:
+
+``` php
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->resources('BlogPosts', ['path' => 'posts']);
+});
+```
+
+
+
+passed arguments
+
+
+
+
+
+## Passed Arguments
+
+Passed arguments are additional arguments or path segments that are
+used when making a request. They are often used to pass parameters
+to your controller methods. :
+
+ http://localhost/calendars/view/recent/mark
+
+In the above example, both `recent` and `mark` are passed arguments to
+`CalendarsController::view()`. Passed arguments are given to your controllers
+in three ways. First as arguments to the action method called, and secondly they
+are available in `$this->request->getParam('pass')` as a numerically indexed
+array. When using custom routes you can force particular parameters to go into
+the passed arguments as well.
+
+If you were to visit the previously mentioned URL, and you
+had a controller action that looked like:
+
+``` php
+class CalendarsController extends AppController
+{
+ public function view($arg1, $arg2)
+ {
+ debug(func_get_args());
+ }
+}
+```
+
+You would get the following output:
+
+``` text
+Array
+(
+ [0] => recent
+ [1] => mark
+)
+```
+
+This same data is also available at `$this->request->getParam('pass')` in your
+controllers, views, and helpers. The values in the pass array are numerically
+indexed based on the order they appear in the called URL:
+
+``` php
+debug($this->request->getParam('pass'));
+```
+
+Either of the above would output:
+
+``` text
+Array
+(
+ [0] => recent
+ [1] => mark
+)
+```
+
+When generating URLs, using a `routing array` you add passed
+arguments as values without string keys in the array:
+
+``` php
+['controller' => 'Articles', 'action' => 'view', 5]
+```
+
+Since `5` has a numeric key, it is treated as a passed argument.
+
+## Generating URLs
+
+`static` Cake\\Routing\\RouteBuilder::**url**($url = null, $full = false)
+
+`static` Cake\\Routing\\RouteBuilder::**reverse**($params, $full = false)
+
+Generating URLs or Reverse routing is a feature in CakePHP that is used to
+allow you to change your URL structure without having to modify all your code.
+
+If you create URLs using strings like:
+
+``` php
+$this->Html->link('View', '/articles/view/' . $id);
+```
+
+And then later decide that `/articles` should really be called
+'posts' instead, you would have to go through your entire
+application renaming URLs. However, if you defined your link like:
+
+``` php
+//`link()` uses Router::url() internally and accepts a routing array
+
+$this->Html->link(
+ 'View',
+ ['controller' => 'Articles', 'action' => 'view', $id]
+);
+```
+
+or:
+
+``` php
+//'Router::reverse()' operates on the request parameters array
+//and will produce a url string, valid input for `link()`
+
+$requestParams = Router::getRequest()->getAttribute('params');
+$this->Html->link('View', Router::reverse($requestParams));
+```
+
+Then when you decided to change your URLs, you could do so by defining a
+route. This would change both the incoming URL mapping, as well as the
+generated URLs.
+
+The choice of technique is determined by how well you can predict the routing
+array elements.
+
+### Using `Router::url()`
+
+`Router::url()` allows you to use `routing arrays ` in
+situations where the array elements required are fixed or easily deduced.
+
+It will provide reverse routing when the destination url is well defined:
+
+``` php
+$this->Html->link(
+ 'View',
+ ['controller' => 'Articles', 'action' => 'view', $id]
+);
+```
+
+It is also useful when the destination is unknown but follows a well
+defined pattern:
+
+``` php
+$this->Html->link(
+ 'View',
+ ['controller' => $controller, 'action' => 'view', $id]
+);
+```
+
+Elements with numeric keys are treated as [Passed Arguments](#passed-arguments).
+
+When using routing arrays, you can define both query string parameters and
+document fragments using special keys:
+
+``` php
+$routes->url([
+ 'controller' => 'Articles',
+ 'action' => 'index',
+ '?' => ['page' => 1],
+ '#' => 'top'
+]);
+
+// Will generate a URL like.
+/articles/index?page=1#top
+```
+
+You can also use any of the special route elements when generating URLs:
+
+- `_ext` Used for [File Extensions](#file-extensions) routing.
+- `_base` Set to `false` to remove the base path from the generated URL. If
+ your application is not in the root directory, this can be used to generate
+ URLs that are 'cake relative'.
+- `_scheme` Set to create links on different schemes like `webcal` or
+ `ftp`. Defaults to the current scheme.
+- `_host` Set the host to use for the link. Defaults to the current host.
+- `_port` Set the port if you need to create links on non-standard ports.
+- `_method` Define the HTTP verb the URL is for.
+- `_full` If `true` the value of `App.fullBaseUrl` mentioned in
+ [General Configuration](../development/configuration#general-configuration) will be prepended to generated URLs.
+- `_https` Set to `true` to convert the generated URL to https or `false`
+ to force http. Prior to 4.5.0 use `_ssl`
+- `_name` Name of route. If you have setup named routes, you can use this key
+ to specify it.
+
+### Using `Router::reverse()`
+
+`Router::reverse()` allows you to use the [Request Parameters](../controllers/request-response#request-parameters) in cases
+where the current URL with some modification is the basis for the destination
+and the elements of the current URL are unpredictable.
+
+As an example, imagine a blog that allowed users to create **Articles** and
+**Comments**, and to mark both as either *published* or *draft*. Both the index
+page URLs might include the user id. The **Comments** URL might also include
+an article id to identify what article the comment refers to.
+
+Here are urls for this scenario:
+
+ /articles/index/42
+ /comments/index/42/18
+
+When the author uses these pages, it would be convenient to include links
+that allow the page to be displayed with all results, published only,
+or draft only.
+
+To keep the code DRY, it would be best to include the links through
+an element:
+
+``` php
+// element/filter_published.php
+
+$params = $this->getRequest()->getAttribute('params');
+
+/* prepare url for Draft */
+$params = Hash::insert($params, '?.published', 0);
+echo $this->Html->link(__('Draft'), Router::reverse($params));
+
+/* Prepare url for Published */
+$params = Hash::insert($params, '?.published', 1);
+echo $this->Html->link(__('Published'), Router::reverse($params));
+
+/* Prepare url for All */
+$params = Hash::remove($params, '?.published');
+echo $this->Html->link(__('All'), Router::reverse($params));
+```
+
+The links generated by these method calls would include one or two pass
+parameters depending on the structure of the current URL. And the code
+would work for any future URL, for example, if you started using
+pathPrefixes or if you added more pass parameters.
+
+### Routing Arrays vs Request Parameters
+
+The significant difference between the two arrays and their use in these
+reverse routing methods is in the way they include pass parameters.
+
+Routing arrays include pass parameters as un-keyed values in the array:
+
+``` php
+$url = [
+ 'controller' => 'Articles',
+ 'action' => 'View',
+ $id, //a pass parameter
+ 'page' => 3, //a query argument
+];
+```
+
+Request parameters include pass parameters on the 'pass' key of the array:
+
+``` php
+$url = [
+ 'controller' => 'Articles',
+ 'action' => 'View',
+ 'pass' => [$id], //the pass parameters
+ '?' => ['page' => 3], //the query arguments
+];
+```
+
+So it is possible, if you wish, to convert the request parameters into
+a routing array or vice versa.
+
+
+
+## Generating Asset URLs
+
+The `Asset` class provides methods for generating URLs to your application's
+css, javascript, images and other static asset files:
+
+``` php
+use Cake\Routing\Asset;
+
+// Generate a URL to APP/webroot/js/app.js
+$js = Asset::scriptUrl('app.js');
+
+// Generate a URL to APP/webroot/css/app.css
+$css = Asset::cssUrl('app.css');
+
+// Generate a URL to APP/webroot/image/logo.png
+$img = Asset::imageUrl('logo.png');
+
+// Generate a URL to APP/webroot/files/upload/photo.png
+$file = Asset::url('files/upload/photo.png');
+```
+
+The above methods also accept an array of options as their second parameter:
+
+- `fullBase` Append the full URL with domain name.
+- `pathPrefix` Path prefix for relative URLs.
+- `plugin` You can provide `false` to prevent paths from being treated as
+ a plugin asset.
+- `timestamp` Overrides the value of `Asset.timestamp` in Configure. Set to
+ `false` to skip timestamp generation. Set to `true` to apply timestamps
+ when debug is true. Set to `'force'` to always enable timestamping
+ regardless of debug value.
+
+``` php
+// Generates http://example.org/img/logo.png
+$img = Asset::url('logo.png', ['fullBase' => true]);
+
+// Generates /img/logo.png?1568563625
+// Where the timestamp is the last modified time of the file.
+$img = Asset::url('logo.png', ['timestamp' => true]);
+```
+
+To generate asset URLs for files in plugins use `plugin syntax`:
+
+``` php
+// Generates `/debug_kit/img/cake.png`
+$img = Asset::imageUrl('DebugKit.cake.png');
+```
+
+
+
+## Redirect Routing
+
+Redirect routing allows you to issue HTTP status 30x redirects for
+incoming routes, and point them at different URLs. This is useful
+when you want to inform client applications that a resource has moved
+and you don't want to expose two URLs for the same content.
+
+Redirection routes are different from normal routes as they perform an actual
+header redirection if a match is found. The redirection can occur to
+a destination within your application or an outside location:
+
+``` php
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->redirect(
+ '/home/*',
+ ['controller' => 'Articles', 'action' => 'view'],
+ ['persist' => true]
+ // Or ['persist'=>['id']] for default routing where the
+ // view action expects $id as an argument.
+ );
+})
+```
+
+Redirects `/home/*` to `/articles/view` and passes the parameters to
+`/articles/view`. Using an array as the redirect destination allows
+you to use other routes to define where a URL string should be
+redirected to. You can redirect to external locations using
+string URLs as the destination:
+
+``` php
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->redirect('/articles/*', 'http://google.com', ['status' => 302]);
+});
+```
+
+This would redirect `/articles/*` to `http://google.com` with a
+HTTP status of 302.
+
+
+
+## Entity Routing
+
+Entity routing allows you to use an entity, an array or object implement
+`ArrayAccess` as the source of routing parameters. This allows you to refactor
+routes more easily, and generate URLs with less code. For example, if you start
+off with a route that looks like:
+
+``` php
+$routes->get(
+ '/view/{id}',
+ ['controller' => 'Articles', 'action' => 'view'],
+ 'articles:view'
+);
+```
+
+You can generate URLs to this route using:
+
+``` php
+// $article is an entity in the local scope.
+Router::url(['_name' => 'articles:view', 'id' => $article->id]);
+```
+
+Later on, you may want to expose the article slug in the URL for SEO purposes.
+In order to do this you would need to update everywhere you generate a URL to
+the `articles:view` route, which could take some time. If we use entity routes
+we pass the entire article entity into URL generation allowing us to skip any
+rework when URLs require more parameters:
+
+``` php
+use Cake\Routing\Route\EntityRoute;
+
+// Create entity routes for the rest of this scope.
+$routes->setRouteClass(EntityRoute::class);
+
+// Create the route just like before.
+$routes->get(
+ '/view/{id}/{slug}',
+ ['controller' => 'Articles', 'action' => 'view'],
+ 'articles:view'
+);
+```
+
+Now we can generate URLs using the `_entity` key:
+
+``` php
+Router::url(['_name' => 'articles:view', '_entity' => $article]);
+```
+
+This will extract both the `id` property and the `slug` property out of the
+provided entity.
+
+
+
+## Custom Route Classes
+
+Custom route classes allow you to extend and change how individual routes parse
+requests and handle reverse routing. Route classes have a few conventions:
+
+- Route classes are expected to be found in the `Routing\Route` namespace of
+ your application or plugin.
+- Route classes should extend `Cake\Routing\Route\Route`.
+- Route classes should implement one or both of `match()` and/or `parse()`.
+
+The `parse()` method is used to parse an incoming URL. It should generate an
+array of request parameters that can be resolved into a controller & action.
+Return `null` from this method to indicate a match failure.
+
+The `match()` method is used to match an array of URL parameters and create a
+string URL. If the URL parameters do not match the route `false` should be
+returned.
+
+You can use a custom route class when making a route by using the `routeClass`
+option:
+
+``` php
+$routes->connect(
+ '/{slug}',
+ ['controller' => 'Articles', 'action' => 'view'],
+ ['routeClass' => 'SlugRoute']
+);
+
+// Or by setting the routeClass in your scope.
+$routes->scope('/', function (RouteBuilder $routes) {
+ $routes->setRouteClass('SlugRoute');
+ $routes->connect(
+ '/{slug}',
+ ['controller' => 'Articles', 'action' => 'view']
+ );
+});
+```
+
+This route would create an instance of `SlugRoute` and allow you
+to implement custom parameter handling. You can use plugin route classes using
+standard `plugin syntax`.
+
+### Default Route Class
+
+`static` Cake\\Routing\\RouteBuilder::**setRouteClass**($routeClass = null)
+
+If you want to use an alternate route class for your routes besides the
+default `Route`, you can do so by calling `RouteBuilder::setRouteClass()`
+before setting up any routes and avoid having to specify the `routeClass`
+option for each route. For example using:
+
+``` php
+use Cake\Routing\Route\DashedRoute;
+
+$routes->setRouteClass(DashedRoute::class);
+```
+
+will cause all routes connected after this to use the `DashedRoute` route class.
+Calling the method without an argument will return current default route class.
+
+### Fallbacks Method
+
+`method` Cake\\Routing\\RouteBuilder::**fallbacks**($routeClass = null)
+
+The fallbacks method is a simple shortcut for defining default routes. The
+method uses the passed routing class for the defined rules or if no class is
+provided the class returned by `RouteBuilder::setRouteClass()` is used.
+
+Calling fallbacks like so:
+
+``` php
+use Cake\Routing\Route\DashedRoute;
+
+$routes->fallbacks(DashedRoute::class);
+```
+
+Is equivalent to the following explicit calls:
+
+``` php
+use Cake\Routing\Route\DashedRoute;
+
+$routes->connect('/{controller}', ['action' => 'index'], ['routeClass' => DashedRoute::class]);
+$routes->connect('/{controller}/{action}/*', [], ['routeClass' => DashedRoute::class]);
+```
+
+> [!NOTE]
+> Using the default route class (`Route`) with fallbacks, or any route
+> with `{plugin}` and/or `{controller}` route elements will result in
+> inconsistent URL case.
+
+> [!WARNING]
+> Fallback route templates are very generic and allow URLs to be generated
+> and parsed for controllers & actions that do not exist. Fallback URLs can
+> also introduce ambiguity and duplication in your URLs.
+>
+> As your application grows, it is recommended to move away from fallback URLs
+> and explicitly define the routes in your application.
+
+## Creating Persistent URL Parameters
+
+You can hook into the URL generation process using URL filter functions. Filter
+functions are called *before* the URLs are matched against the routes, this
+allows you to prepare URLs before routing.
+
+Callback filter functions should expect the following parameters:
+
+- `$params` The URL parameter array being processed.
+- `$request` The current request (`Cake\Http\ServerRequest` instance).
+
+The URL filter function should *always* return the parameters even if unmodified.
+
+URL filters allow you to implement features like persistent parameters:
+
+``` php
+Router::addUrlFilter(function (array $params, ServerRequest $request) {
+ if ($request->getParam('lang') && !isset($params['lang'])) {
+ $params['lang'] = $request->getParam('lang');
+ }
+
+ return $params;
+});
+```
+
+Filter functions are applied in the order they are connected.
+
+Another use case is changing a certain route on runtime (plugin routes for
+example):
+
+``` php
+Router::addUrlFilter(function (array $params, ServerRequest $request) {
+ if (empty($params['plugin']) || $params['plugin'] !== 'MyPlugin' || empty($params['controller'])) {
+ return $params;
+ }
+ if ($params['controller'] === 'Languages' && $params['action'] === 'view') {
+ $params['controller'] = 'Locations';
+ $params['action'] = 'index';
+ $params['language'] = $params[0];
+ unset($params[0]);
+ }
+
+ return $params;
+});
+```
+
+This will alter the following route:
+
+``` php
+Router::url(['plugin' => 'MyPlugin', 'controller' => 'Languages', 'action' => 'view', 'es']);
+```
+
+into this:
+
+``` php
+Router::url(['plugin' => 'MyPlugin', 'controller' => 'Locations', 'action' => 'index', 'language' => 'es']);
+```
+
+> [!WARNING]
+> If you are using the caching features of [Routing Middleware](../controllers/middleware#routing-middleware) you must
+> define the URL filters in your application `bootstrap()` as filters are
+> not part of the cached data.
diff --git a/docs/en/development/sessions.md b/docs/en/development/sessions.md
new file mode 100644
index 0000000000..a462517bef
--- /dev/null
+++ b/docs/en/development/sessions.md
@@ -0,0 +1,496 @@
+# Sessions
+
+CakePHP provides a wrapper and suite of utility features on top of PHP's native
+`session` extension. Sessions allow you to identify unique users across
+requests and store persistent data for specific users. Unlike Cookies, session
+data is not available on the client side. Usage of `$_SESSION` is generally
+avoided in CakePHP, and instead usage of the Session classes is preferred.
+
+
+
+## Session Configuration
+
+Session configuration is generally defined in **/config/app.php**. The available
+options are:
+
+- `Session.timeout` - The number of *minutes* a session can remain 'idle'. If
+ no request is received for `timeout` minutes, CakePHP's session
+ handler will expire the session. You can set this option to `0` to disable
+ server side idle timeouts.
+- `Session.defaults` - Allows you to use the built-in default session
+ configurations as a base for your session configuration. See below for the
+ built-in defaults.
+- `Session.handler` - Allows you to define a custom session handler. The core
+ database and cache session handlers use this. See below for additional
+ information on Session handlers.
+- `Session.ini` - Allows you to set additional session ini settings for your
+ config. This combined with `Session.handler` replace the custom session
+ handling features of previous versions
+- `Session.cookie` - The name of the cookie to use. Defaults to value set for
+ `session.name` php.ini config.
+- `Session.cookiePath` - The url path for which session cookie is set. Maps to
+ the `session.cookie_path` php.ini config. Defaults to base path of app.
+
+CakePHP's defaults `session.cookie_secure` to `true`, when your application
+is on an SSL protocol. If your application serves from both SSL and non-SSL
+protocols, then you might have problems with sessions being lost. If you need
+access to the session on both SSL and non-SSL domains you will want to disable
+this:
+
+``` php
+Configure::write('Session', [
+ 'defaults' => 'php',
+ 'ini' => [
+ 'session.cookie_secure' => false
+ ]
+]);
+```
+
+CakePHP also sets the [SameSite](https://owasp.org/www-community/SameSite) attribute to `Lax`
+by default for session cookies, which helps protect against CSRF attacks.
+You can change the default value by setting `session.cookie_samesite` php.ini config:
+
+``` php
+Configure::write('Session', [
+ 'defaults' => 'php',
+ 'ini' => [
+ 'session.cookie_samesite' => 'Strict',
+ ],
+]);
+```
+
+The session cookie path defaults to app's base path. To change this you can use
+the `session.cookie_path` ini value. For example if you want your session to
+persist across all subdomains you can do:
+
+``` php
+Configure::write('Session', [
+ 'defaults' => 'php',
+ 'ini' => [
+ 'session.cookie_path' => '/',
+ 'session.cookie_domain' => '.yourdomain.com',
+ ],
+]);
+```
+
+By default PHP sets the session cookie to expire as soon as the browser is
+closed, regardless of the configured `Session.timeout` value. The cookie
+timeout is controlled by the `session.cookie_lifetime` ini value and can be
+configured using:
+
+``` php
+Configure::write('Session', [
+ 'defaults' => 'php',
+ 'ini' => [
+ // Invalidate the cookie after 30 minutes
+ 'session.cookie_lifetime' => 1800
+ ]
+]);
+```
+
+The difference between `Session.timeout` and the `session.cookie_lifetime`
+value is that the latter relies on the client telling the truth about the
+cookie. If you require stricter timeout checking, without relying on what the
+client reports, you should use `Session.timeout`.
+
+Please note that `Session.timeout` corresponds to the total time of
+inactivity for a user (i.e. the time without visiting any page where the session
+is used), and does not limit the total amount of minutes a user can stay active
+on the site.
+
+## Built-in Session Handlers & Configuration
+
+CakePHP comes with several built-in session configurations. You can either use
+these as the basis for your session configuration, or you can create a fully
+custom solution. To use defaults, simply set the 'defaults' key to the name of
+the default you want to use. You can then override any sub setting by declaring
+it in your Session config:
+
+``` php
+Configure::write('Session', [
+ 'defaults' => 'php'
+]);
+```
+
+The above will use the built-in 'php' session configuration. You could augment
+part or all of it by doing the following:
+
+``` php
+Configure::write('Session', [
+ 'defaults' => 'php',
+ 'cookie' => 'my_app',
+ 'timeout' => 4320 // 3 days
+]);
+```
+
+The above overrides the timeout and cookie name for the 'php' session
+configuration. The built-in configurations are:
+
+- `php` - Saves sessions with the standard settings in your php.ini file.
+- `cake` - Saves sessions as files inside `tmp/sessions/`. This is a
+ good option when on hosts that don't allow you to write outside your own home
+ dir.
+- `database` - Use the built-in database sessions. See below for more
+ information.
+- `cache` - Use the built-in cache sessions. See below for more information.
+
+### Session Handlers
+
+Session handlers can also be defined in the session config array. By defining
+the 'handler.engine' config key, you can name the class name, or provide
+a handler instance. The class/object must implement the
+native PHP `SessionHandlerInterface`. Implementing this interface will allow
+`Session` to automatically map the methods for the handler. Both the core
+Cache and Database session handlers use this method for saving sessions.
+Additional settings for the handler should be placed inside the handler array.
+You can then read those values out from inside your handler:
+
+``` text
+'Session' => [
+ 'handler' => [
+ 'engine' => 'DatabaseSession',
+ 'model' => 'CustomSessions',
+ ],
+]
+```
+
+The above shows how you could setup the Database session handler with an
+application model. When using class names as your handler.engine, CakePHP will
+expect to find your class in the `Http\Session` namespace. For example, if
+you had an `AppSessionHandler` class, the file should be
+**src/Http/Session/AppSessionHandler.php**, and the class name should be
+`App\Http\Session\AppSessionHandler`. You can also use session handlers
+from inside plugins. By setting the engine to `MyPlugin.PluginSessionHandler`.
+
+### Database Sessions
+
+If you need to use a database to store your session data, configure as follows:
+
+``` text
+'Session' => [
+ 'defaults' => 'database'
+]
+```
+
+This configuration requires a database table, having this schema:
+
+``` sql
+CREATE TABLE `sessions` (
+ `id` char(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
+ `created` datetime DEFAULT CURRENT_TIMESTAMP, -- Optional
+ `modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Optional
+ `data` blob DEFAULT NULL, -- for PostgreSQL use bytea instead of blob
+ `expires` int(10) unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+```
+
+You can find a copy of the schema for the sessions table in the [application skeleton](https://github.com/cakephp/app) in **config/schema/sessions.sql**.
+
+You can also use your own `Table` class to handle the saving of the sessions:
+
+``` text
+'Session' => [
+ 'defaults' => 'database',
+ 'handler' => [
+ 'engine' => 'DatabaseSession',
+ 'model' => 'CustomSessions',
+ ],
+]
+```
+
+The above will tell Session to use the built-in 'database' defaults, and
+specify that a Table called `CustomSessions` will be the delegate for saving
+session information to the database.
+
+
+
+### Cache Sessions
+
+The Cache class can be used to store sessions as well. This allows you to store
+sessions in a cache like APCu, or Memcached. There are some caveats to
+using cache sessions, in that if you exhaust the cache space, sessions will
+start to expire as records are evicted.
+
+To use Cache based sessions you can configure you Session config like:
+
+``` php
+Configure::write('Session', [
+ 'defaults' => 'cache',
+ 'handler' => [
+ 'config' => 'session',
+ ],
+]);
+```
+
+This will configure Session to use the `CacheSession` class as the
+delegate for saving the sessions. You can use the 'config' key which cache
+configuration to use. The default cache configuration is `'default'`.
+
+### Session Locking
+
+The app skeleton comes preconfigured with a session config like this:
+
+``` text
+'Session' => [
+ 'defaults' => 'php',
+],
+```
+
+This means CakePHP will handle sessions via what is configured in your `php.ini`.
+In most cases this will be the default configuration so PHP will save any newly
+created session as a file in e.g. `/var/lib/php/session`
+
+But this also means any computationally heavy task like querying a large dataset
+combined with an active session will **lock that session file** - therefore
+blocking users to e.g. open a second tab of your app to do something else
+in the meantime.
+
+To prevent this behavior you will have to change the way how sessions are being
+handled in CakePHP by using a different session handler like [Sessions Cache Sessions](#sessions-cache-sessions)
+combined with the [Redis Engine](../core-libraries/caching#caching-redisengine) or another cache engine.
+
+> [!TIP]
+> If you want to read more about Session Locking see [here](https://ma.ttias.be/php-session-locking-prevent-sessions-blocking-in-requests/)
+
+## Setting ini directives
+
+The built-in defaults attempt to provide a common base for session
+configuration. You may need to tweak specific ini flags as well. CakePHP
+exposes the ability to customize the ini settings for both default
+configurations, as well as custom ones. The `ini` key in the session settings,
+allows you to specify individual configuration values. For example you can use
+it to control settings like `session.gc_divisor`:
+
+``` php
+Configure::write('Session', [
+ 'defaults' => 'php',
+ 'ini' => [
+ 'session.cookie_name' => 'MyCookie',
+ 'session.cookie_lifetime' => 1800, // Valid for 30 minutes
+ 'session.gc_divisor' => 1000,
+ 'session.cookie_httponly' => true
+ ]
+]);
+```
+
+## Creating a Custom Session Handler
+
+Creating a custom session handler is straightforward in CakePHP. In this
+example we'll create a session handler that stores sessions both in the Cache
+(APC) and the database. This gives us the best of fast IO of APC,
+without having to worry about sessions evaporating when the cache fills up.
+
+First we'll need to create our custom class and put it in
+**src/Http/Session/ComboSession.php**. The class should look
+something like:
+
+``` php
+namespace App\Http\Session;
+
+use Cake\Cache\Cache;
+use Cake\Core\Configure;
+use Cake\Http\Session\DatabaseSession;
+
+class ComboSession extends DatabaseSession
+{
+ protected $cacheKey;
+
+ public function __construct()
+ {
+ $this->cacheKey = Configure::read('Session.handler.cache');
+ parent::__construct();
+ }
+
+ // Read data from the session.
+ public function read($id): string
+ {
+ $result = Cache::read($id, $this->cacheKey);
+ if ($result) {
+ return $result;
+ }
+
+ return parent::read($id);
+ }
+
+ // Write data into the session.
+ public function write($id, $data): bool
+ {
+ Cache::write($id, $data, $this->cacheKey);
+
+ return parent::write($id, $data);
+ }
+
+ // Destroy a session.
+ public function destroy($id): bool
+ {
+ Cache::delete($id, $this->cacheKey);
+
+ return parent::destroy($id);
+ }
+
+ // Removes expired sessions.
+ public function gc($expires = null): bool
+ {
+ return parent::gc($expires);
+ }
+}
+```
+
+Our class extends the built-in `DatabaseSession` so we don't have to duplicate
+all of its logic and behavior. We wrap each operation with
+a `Cake\Cache\Cache` operation. This lets us fetch sessions from
+the fast cache, and not have to worry about what happens when we fill the cache.
+In **config/app.php** make the session block look like:
+
+``` text
+'Session' => [
+ 'defaults' => 'database',
+ 'handler' => [
+ 'engine' => 'ComboSession',
+ 'model' => 'Session',
+ 'cache' => 'apc',
+ ],
+],
+// Make sure to add a apc cache config
+'Cache' => [
+ 'apc' => ['engine' => 'Apc']
+]
+```
+
+Now our application will start using our custom session handler for reading and
+writing session data.
+
+`class` **Session**
+
+
+
+## Accessing the Session Object
+
+You can access the session data any place you have access to a request object.
+This means the session is accessible from:
+
+- Controllers
+- Views
+- Helpers
+- Cells
+- Components
+
+A basic example of session usage in controllers, views and cells would be:
+
+``` php
+$name = $this->request->getSession()->read('User.name');
+
+// If you are accessing the session multiple times,
+// you will probably want a local variable.
+$session = $this->request->getSession();
+$name = $session->read('User.name');
+```
+
+In helpers, use `$this->getView()->getRequest()` to get the request object;
+In components, use `$this->getController()->getRequest()`.
+
+## Reading & Writing Session Data
+
+`method` Session::**read**($key, $default = null): mixed
+
+You can read values from the session using `Hash::extract()`
+compatible syntax:
+
+``` php
+$session->read('Config.language', 'en');
+```
+
+`method` Session::**readOrFail**($key): mixed
+
+The same as convenience wrapper around non-nullable return value:
+
+``` php
+$session->readOrFail('Config.language');
+```
+
+This is useful, when you know this key has to be set and you don't want to have to check
+for the existence in code itself.
+
+`method` Session::**write**($key, $value): void
+
+`$key` should be the dot separated path you wish to write `$value` to:
+
+``` php
+$session->write('Config.language', 'en');
+```
+
+You may also specify one or multiple hashes like so:
+
+``` php
+$session->write([
+ 'Config.theme' => 'blue',
+ 'Config.language' => 'en',
+]);
+```
+
+`method` Session::**delete**($key): void
+
+When you need to delete data from the session, you can use `delete()`:
+
+``` php
+$session->delete('Some.value');
+```
+
+
+
+
+
+
+
+When you need to read and delete data from the session, you can use
+`consume()`:
+
+``` php
+$session->consume('Some.value');
+```
+
+`method` Session::**check**($key): bool
+
+If you want to see if data exists in the session, you can use `check()`:
+
+``` php
+if ($session->check('Config.language')) {
+ // Config.language exists and is not null.
+}
+```
+
+## Destroying the Session
+
+`method` Session::**destroy**(): void
+
+Destroying the session is useful when users log out. To destroy a session, use
+the `destroy()` method:
+
+``` php
+$session->destroy();
+```
+
+Destroying a session will remove all serverside data in the session, but will
+**not** remove the session cookie.
+
+## Rotating Session Identifiers
+
+`method` Session::**renew**(): void
+
+While the `Authentication Plugin` automatically renews the session id when users login and
+logout, you may need to rotate the session id's manually. To do this use the
+`renew()` method:
+
+``` php
+$session->renew();
+```
+
+## Flash Messages
+
+Flash messages are small messages displayed to end users once. They are often
+used to present error messages, or confirm that actions took place successfully.
+
+To set and display flash messages you should use
+[FlashComponent](../controllers/components/flash) and
+[FlashHelper](../views/helpers/flash)
diff --git a/docs/en/development/testing.md b/docs/en/development/testing.md
new file mode 100644
index 0000000000..c25ea6daee
--- /dev/null
+++ b/docs/en/development/testing.md
@@ -0,0 +1,2128 @@
+# Testing
+
+CakePHP comes with comprehensive testing support built-in. CakePHP comes with
+integration for [PHPUnit](https://phpunit.de). In addition to the features
+offered by PHPUnit, CakePHP offers some additional features to make testing
+easier. This section will cover installing PHPUnit, and getting started with
+Unit Testing, and how you can use the extensions that CakePHP offers.
+
+## Installing PHPUnit
+
+CakePHP uses PHPUnit as its underlying test framework. PHPUnit is the de-facto
+standard for unit testing in PHP. It offers a deep and powerful set of features
+for making sure your code does what you think it does. PHPUnit can be installed
+through using either a [PHAR package](https://phpunit.de/#download) or
+[Composer](https://getcomposer.org).
+
+### Install PHPUnit with Composer
+
+To install PHPUnit with Composer:
+
+``` bash
+$ php composer.phar require --dev phpunit/phpunit:"^10.1"
+```
+
+This will add the dependency to the `require-dev` section of your
+`composer.json`, and then install PHPUnit along with any dependencies.
+
+You can now run PHPUnit using:
+
+``` bash
+$ vendor/bin/phpunit
+```
+
+### Using the PHAR File
+
+After you have downloaded the **phpunit.phar** file, you can use it to run your
+tests:
+
+``` bash
+php phpunit.phar
+```
+
+> [!TIP]
+> As a convenience you can make phpunit.phar available globally
+> on Unix or Linux with the following:
+>
+> ``` bash
+> chmod +x phpunit.phar
+> sudo mv phpunit.phar /usr/local/bin/phpunit
+> phpunit --version
+> ```
+>
+> Please refer to the PHPUnit documentation for instructions regarding
+> [Globally installing the PHPUnit PHAR on Windows](https://phpunit.de/manual/current/en/installation.html#installation.phar.windows).
+
+## Test Database Setup
+
+Remember to have debug enabled in your **config/app_local.php** file before running
+any tests. Before running any tests you should be sure to add a `test`
+datasource configuration to **config/app_local.php**. This configuration is used by
+CakePHP for fixture tables and data:
+
+``` php
+'Datasources' => [
+ 'test' => [
+ 'datasource' => 'Cake\Database\Driver\Mysql',
+ 'persistent' => false,
+ 'host' => 'dbhost',
+ 'username' => 'dblogin',
+ 'password' => 'dbpassword',
+ 'database' => 'test_database',
+ ],
+],
+```
+
+> [!NOTE]
+> It's a good idea to make the test database and your actual database
+> different databases. This will prevent embarrassing mistakes later.
+
+## Checking the Test Setup
+
+After installing PHPUnit and setting up your `test` datasource configuration
+you can make sure you're ready to write and run your own tests by running your
+application's tests:
+
+``` bash
+# For phpunit.phar
+$ php phpunit.phar
+
+# For Composer installed phpunit
+$ vendor/bin/phpunit
+```
+
+The above should run any tests you have, or let you know that no tests were run.
+To run a specific test you can supply the path to the test as a parameter to
+PHPUnit. For example, if you had a test case for ArticlesTable class you could
+run it with:
+
+``` bash
+$ vendor/bin/phpunit tests/TestCase/Model/Table/ArticlesTableTest
+```
+
+You should see a green bar with some additional information about the tests run,
+and number passed.
+
+> [!NOTE]
+> If you are on a Windows system you probably won't see any colors.
+
+## Test Case Conventions
+
+Like most things in CakePHP, test cases have some conventions. Concerning
+tests:
+
+1. PHP files containing tests should be in your
+ `tests/TestCase/[Type]` directories.
+2. The filenames of these files should end in **Test.php** instead
+ of just .php.
+3. The classes containing tests should extend `Cake\TestSuite\TestCase`,
+ `Cake\TestSuite\IntegrationTestCase` or `\PHPUnit\Framework\TestCase`.
+4. Like other classnames, the test case classnames should match the filename.
+ **RouterTest.php** should contain `class RouterTest extends TestCase`.
+5. The name of any method containing a test (i.e. containing an
+ assertion) should begin with `test`, as in `testPublished()`.
+ You can also use the `@test` annotation to mark methods as test methods.
+
+## Creating Your First Test Case
+
+In the following example, we'll create a test case for a very simple helper
+method. The helper we're going to test will be formatting progress bar HTML.
+Our helper looks like:
+
+``` php
+namespace App\View\Helper;
+
+use Cake\View\Helper;
+
+class ProgressHelper extends Helper
+{
+ public function bar($value)
+ {
+ $width = round($value / 100, 2) * 100;
+
+ return sprintf(
+ '
+
+
', $width);
+ }
+}
+```
+
+This is a very simple example, but it will be useful to show how you can create
+a simple test case. After creating and saving our helper, we'll create the test
+case file in **tests/TestCase/View/Helper/ProgressHelperTest.php**. In that file
+we'll start with the following:
+
+``` php
+namespace App\Test\TestCase\View\Helper;
+
+use App\View\Helper\ProgressHelper;
+use Cake\TestSuite\TestCase;
+use Cake\View\View;
+
+class ProgressHelperTest extends TestCase
+{
+ public function setUp(): void
+ {
+ }
+
+ public function testBar(): void
+ {
+ }
+}
+```
+
+We'll flesh out this skeleton in a minute. We've added two methods to start
+with. First is `setUp()`. This method is called before every *test* method
+in a test case class. Setup methods should initialize the objects needed for the
+test, and do any configuration needed. In our setup method we'll add the
+following:
+
+``` php
+public function setUp(): void
+{
+ parent::setUp();
+ $View = new View();
+ $this->Progress = new ProgressHelper($View);
+}
+```
+
+Calling the parent method is important in test cases, as `TestCase::setUp()`
+does a number things like backing up the values in
+`Cake\Core\Configure` and, storing the paths in
+`Cake\Core\App`.
+
+Next, we'll fill out the test method. We'll use some assertions to ensure that
+our code creates the output we expect:
+
+``` php
+public function testBar(): void
+{
+ $result = $this->Progress->bar(90);
+ $this->assertStringContainsString('width: 90%', $result);
+ $this->assertStringContainsString('progress-bar', $result);
+
+ $result = $this->Progress->bar(33.3333333);
+ $this->assertStringContainsString('width: 33%', $result);
+}
+```
+
+The above test is a simple one but shows the potential benefit of using test
+cases. We use `assertStringContainsString()` to ensure that our helper is returning a
+string that contains the content we expect. If the result did not contain the
+expected content the test would fail, and we would know that our code is
+incorrect.
+
+By using test cases you can describe the relationship between a set of
+known inputs and their expected output. This helps you be more confident of the
+code you're writing as you can ensure that the code you wrote fulfills the
+expectations and assertions your tests make. Additionally because tests are
+code, they can be re-run whenever you make a change. This helps prevent
+the creation of new bugs.
+
+> [!NOTE]
+> EventManager is refreshed for each test method. This means that when running
+> multiple tests at once, you will lose your event listeners that were
+> registered in config/bootstrap.php as the bootstrap is only executed once.
+
+
+
+## Running Tests
+
+Once you have PHPUnit installed and some test cases written, you'll want to run
+the test cases very frequently. It's a good idea to run tests before committing
+any changes to help ensure you haven't broken anything.
+
+By using `phpunit` you can run your application tests. To run your
+application's tests you can simply run:
+
+``` bash
+vendor/bin/phpunit
+
+php phpunit.phar
+```
+
+If you have cloned the [CakePHP source from GitHub](https://github.com/cakephp/cakephp)
+and wish to run CakePHP's unit-tests don't forget to execute the following `Composer`
+command prior to running `phpunit` so that any dependencies are installed:
+
+``` bash
+composer install
+```
+
+From your application's root directory. To run tests for a plugin that is part
+of your application source, first `cd` into the plugin directory, then use
+`phpunit` command that matches how you installed phpunit:
+
+``` bash
+cd plugins
+
+../vendor/bin/phpunit
+
+php ../phpunit.phar
+```
+
+To run tests on a standalone plugin, you should first install the project in
+a separate directory and install its dependencies:
+
+``` bash
+git clone git://github.com/cakephp/debug_kit.git
+cd debug_kit
+php ~/composer.phar install
+vendor/bin/phpunit
+```
+
+### Filtering Test Cases
+
+When you have larger test cases, you will often want to run a subset of the test
+methods when you are trying to work on a single failing case. With the
+CLI runner you can use an option to filter test methods:
+
+``` bash
+$ phpunit --filter testSave tests/TestCase/Model/Table/ArticlesTableTest
+```
+
+The filter parameter is used as a case-sensitive regular expression for
+filtering which test methods to run.
+
+### Generating Code Coverage
+
+You can generate code coverage reports from the command line using PHPUnit's
+built-in code coverage tools. PHPUnit will generate a set of static HTML files
+containing the coverage results. You can generate coverage for a test case by
+doing the following:
+
+``` bash
+$ phpunit --coverage-html webroot/coverage tests/TestCase/Model/Table/ArticlesTableTest
+```
+
+This will put the coverage results in your application's webroot directory. You
+should be able to view the results by going to
+`http://localhost/your_app/coverage`.
+
+You can also use `phpdbg` to generate coverage instead of xdebug.
+`phpdbg` is generally faster at generating coverage:
+
+``` bash
+$ phpdbg -qrr phpunit --coverage-html webroot/coverage tests/TestCase/Model/Table/ArticlesTableTest
+```
+
+### Combining Test Suites for Plugins
+
+Often times your application will be composed of several plugins. In these
+situations it can be pretty tedious to run tests for each plugin. You can make
+running tests for each of the plugins that compose your application by adding
+additional `` sections to your application's **phpunit.xml.dist**
+file:
+
+``` xml
+
+
+ tests/TestCase/
+
+
+
+
+ plugins/Forum/tests/TestCase/
+
+
+```
+
+Any additional test suites added to the `` element will
+automatically be run when you use `phpunit`.
+
+If you are using `` to use fixtures from plugins that you have
+installed with composer, the plugin's `composer.json` file should add the
+fixture namespace to the autoload section. Example:
+
+``` json
+"autoload-dev": {
+ "psr-4": {
+ "PluginName\\Test\\Fixture\\": "tests/Fixture/"
+ }
+},
+```
+
+## Test Case Lifecycle Callbacks
+
+Test cases have a number of lifecycle callbacks you can use when doing testing:
+
+- `setUp` is called before every test method. Should be used to create the
+ objects that are going to be tested, and initialize any data for the test.
+ Always remember to call `parent::setUp()`
+- `tearDown` is called after every test method. Should be used to cleanup after
+ the test is complete. Always remember to call `parent::tearDown()`.
+- `setupBeforeClass` is called once before test methods in a case are started.
+ This method must be *static*.
+- `tearDownAfterClass` is called once after test methods in a case are started.
+ This method must be *static*.
+
+
+
+## Fixtures
+
+When testing code that depends on models and the database, one can use
+**fixtures** as a way to create initial state for your application's tests.
+By using fixture data you can reduce repetitive setup steps in your tests.
+Fixtures are well suited to data that is common or shared amongst many or all of
+your tests. Data that is only needed in a subset of tests should be created in
+tests as needed.
+
+CakePHP uses the connection named `test` in your **config/app.php**
+configuration file. If this connection is not usable, an exception will be
+raised and you will not be able to use database fixtures.
+
+CakePHP performs the following during the course of a test run:
+
+1. Creates tables for each of the fixtures needed.
+2. Populates tables with data.
+3. Runs test methods.
+4. Empties the fixture tables.
+
+The schema for fixtures is created at the beginning of a test run via migrations
+or a SQL dump file.
+
+### Test Connections
+
+By default CakePHP will alias each connection in your application. Each
+connection defined in your application's bootstrap that does not start with
+`test_` will have a `test_` prefixed alias created. Aliasing connections
+ensures, you don't accidentally use the wrong connection in test cases.
+Connection aliasing is transparent to the rest of your application. For example
+if you use the 'default' connection, instead you will get the `test`
+connection in test cases. If you use the 'replica' connection, the test suite
+will attempt to use 'test_replica'.
+
+
+
+### PHPUnit Configuration
+
+Before you can use fixtures you should double check that your `phpunit.xml`
+contains the fixture extension:
+
+``` xml
+
+
+
+
+
+```
+
+The extension is included in your application and plugins generated by `bake`
+by default.
+
+
+
+### Creating Schema in Tests
+
+You can generate test database schema either via CakePHP's migrations, loading
+a SQL dump file or using another external schema management tool. You should
+create your schema in your application's `tests/bootstrap.php` file.
+
+### Creating Schema with Migrations
+
+If you use CakePHP's [migrations plugin](https://book.cakephp.org/migrations) to manage your
+application's schema, you can reuse those migrations to generate your test
+database schema as well:
+
+``` php
+// in tests/bootstrap.php
+use Migrations\TestSuite\Migrator;
+
+$migrator = new Migrator();
+
+// Simple setup for with no plugins
+$migrator->run();
+
+// Run migrations for a plugin
+$migrator->run(['plugin' => 'Contacts']);
+
+// Run the Documents migrations on the test_docs connection.
+$migrator->run(['plugin' => 'Documents', 'connection' => 'test_docs']);
+```
+
+If you need to run multiple sets of migrations, those can be run as follows:
+
+``` php
+$migrator->runMany([
+ // Run app migrations on test connection.
+ ['connection' => 'test'],
+ // Run Contacts migrations on test connection.
+ ['plugin' => 'Contacts'],
+ // Run Documents migrations on test_docs connection.
+ ['plugin' => 'Documents', 'connection' => 'test_docs']
+]);
+```
+
+Using `runMany()` will ensure that plugins that share a database don't drop
+tables as each set of migrations is run.
+
+The migrations plugin will only run unapplied migrations, and will reset
+migrations if your current migration head differs from the applied migrations.
+
+You can also configure how migrations should be run in tests in your datasources
+configuration. See the [migrations docs](../migrations) for more information.
+
+### Creating Schema with Abstract Schema
+
+For plugins that need to define schema in tests, but don't need or want to have
+dependencies on migrations, you can define schema as a structured array of
+tables. This format is not recommended for application development as it can be
+time-consuming to maintain.
+
+Each table can define `columns`, `constraints`, and `indexes`.
+An example table would be:
+
+``` text
+return [
+ 'articles' => [
+ 'columns' => [
+ 'id' => [
+ 'type' => 'integer',
+ ],
+ 'author_id' => [
+ 'type' => 'integer',
+ 'null' => true,
+ ],
+ 'title' => [
+ 'type' => 'string',
+ 'null' => true,
+ ],
+ 'body' => 'text',
+ 'published' => [
+ 'type' => 'string',
+ 'length' => 1,
+ 'default' => 'N',
+ ],
+ ],
+ 'constraints' => [
+ 'primary' => [
+ 'type' => 'primary',
+ 'columns' => [
+ 'id',
+ ],
+ ],
+ ],
+ ],
+ // More tables
+];
+```
+
+The options available to `columns`, `indexes` and `constraints` match the
+attributes that are available in CakePHP's schema reflection APIs. Tables are
+created incrementally and you must take care to ensure that tables are created
+before foreign key references are made. Once you have created your schema file
+you can load it in your `tests/bootstrap.php` with:
+
+``` php
+$loader = new SchemaLoader();
+$loader->loadInternalFile($pathToSchemaFile);
+```
+
+### Creating Schema with SQL Dump Files
+
+To load a SQL dump file you can use the following:
+
+``` php
+// in tests/bootstrap.php
+use Cake\TestSuite\Fixture\SchemaLoader;
+
+// Load one or more SQL files.
+(new SchemaLoader())->loadSqlFiles('path/to/schema.sql', 'test');
+```
+
+At the beginning of each test run `SchemaLoader` will drop all tables in the
+connection and rebuild tables based on the provided schema file.
+
+### Creating Fixtures
+
+Fixtures defines the records that will be inserted into the test database at the
+beginning of each test. Let's create our first fixture, that will be
+used to test our own Article model. Create a file named **ArticlesFixture.php**
+in your **tests/Fixture** directory, with the following content:
+
+``` php
+namespace App\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+class ArticlesFixture extends TestFixture
+{
+ // Optional. Set this property to load fixtures
+ // to a different test datasource
+ public $connection = 'test';
+
+ // Optional. Lets you define which table alias is used when
+ // reflecting schema and inserting rows. Inferred from the
+ // class name by default. Added in 5.3.0
+ public $tableAlias = 'Articles';
+
+ // Optional. Lets you define the table name for a fixture.
+ // If defined, this table name will be camelized to create
+ // $tableAlias.
+ public $table = 'articles';
+
+ public $records = [
+ [
+ 'title' => 'First Article',
+ 'body' => 'First Article Body',
+ 'published' => '1',
+ 'created' => '2007-03-18 10:39:23',
+ 'modified' => '2007-03-18 10:41:31'
+ ],
+ [
+ 'title' => 'Second Article',
+ 'body' => 'Second Article Body',
+ 'published' => '1',
+ 'created' => '2007-03-18 10:41:23',
+ 'modified' => '2007-03-18 10:43:31'
+ ],
+ [
+ 'title' => 'Third Article',
+ 'body' => 'Third Article Body',
+ 'published' => '1',
+ 'created' => '2007-03-18 10:43:23',
+ 'modified' => '2007-03-18 10:45:31'
+ ]
+ ];
+ }
+```
+
+> [!NOTE]
+> It is recommended to not manually add values to auto incremental columns,
+> as it interferes with the sequence generation in PostgreSQL and SQLServer.
+
+The `$connection` property defines the datasource of which the fixture will
+use. If your application uses multiple datasources, you should make the
+fixtures match the model's datasources but prefixed with `test_`.
+For example if your model uses the `mydb` datasource, your fixture should use
+the `test_mydb` datasource. If the `test_mydb` connection doesn't exist,
+your models will use the default `test` datasource. Fixture datasources must
+be prefixed with `test` to reduce the possibility of accidentally truncating
+all your application's data when running tests.
+
+We can define a set of records that will be populated after the fixture table is
+created. The format is fairly straight forward, `$records` is an array of
+records. Each item in `$records` should be a single row. Inside each row,
+should be an associative array of the columns and values for the row. Just keep
+in mind that each record in the `$records` array must have the same keys as
+rows are bulk inserted.
+
+As you evolve your schema your fixture records may accumulate unused or
+unsupported fields. You can enable `strictFields` on a fixture to have errors
+raised when a record contains fields that are not defined in the schema:
+
+``` php
+namespace App\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+class ArticlesFixture extends TestFixture
+{
+ protected $strictFields = true;
+
+ // rest of fixture
+}
+```
+
+The `strictFields` mode can be useful in catching typos or when you want to
+enforce stricter maintenance of test data.
+
+::: info Added in version 5.2.0
+`TestFixture::$strictFields` was added.
+:::
+
+### Dynamic Data
+
+To use functions or other dynamic data in your fixture records you can define
+your records in the fixture's `init()` method:
+
+``` php
+namespace App\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+class ArticlesFixture extends TestFixture
+{
+ public function init(): void
+ {
+ $this->records = [
+ [
+ 'title' => 'First Article',
+ 'body' => 'First Article Body',
+ 'published' => '1',
+ 'created' => date('Y-m-d H:i:s'),
+ 'modified' => date('Y-m-d H:i:s'),
+ ],
+ ];
+ parent::init();
+ }
+}
+```
+
+> [!NOTE]
+> When overriding `init()` remember to always call `parent::init()`.
+
+### Loading Fixtures in your Test Cases
+
+After you've created your fixtures, you'll want to use them in your test cases.
+In each test case you should load the fixtures you will need. You should load a
+fixture for every model that will have a query run against it. To load fixtures
+you define the `$fixtures` property in your model:
+
+``` php
+class ArticlesTest extends TestCase
+{
+ protected $fixtures = ['app.Articles', 'app.Comments'];
+}
+```
+
+As of 4.1.0 you can use `getFixtures()` to define your fixture list with
+a method:
+
+``` php
+public function getFixtures(): array
+{
+ return [
+ 'app.Articles',
+ 'app.Comments',
+ ];
+}
+```
+
+The above will load the Article and Comment fixtures from the application's
+Fixture directory. You can also load fixtures from CakePHP core, or plugins:
+
+``` php
+class ArticlesTest extends TestCase
+{
+ protected $fixtures = [
+ 'plugin.DebugKit.Articles',
+ 'plugin.MyVendorName/MyPlugin.Messages',
+ 'core.Comments',
+ ];
+}
+```
+
+Using the `core` prefix will load fixtures from CakePHP, and using a plugin
+name as the prefix, will load the fixture from the named plugin.
+
+You can load fixtures in subdirectories. Using multiple directories can make it
+easier to organize your fixtures if you have a larger application. To load
+fixtures in subdirectories, simply include the subdirectory name in the fixture
+name:
+
+``` php
+class ArticlesTest extends CakeTestCase
+{
+ protected $fixtures = ['app.Blog/Articles', 'app.Blog/Comments'];
+}
+```
+
+In the above example, both fixtures would be loaded from
+`tests/Fixture/Blog/`.
+
+You can also directly include fixtures by FQCN:
+
+``` php
+public function getFixtures(): array
+{
+ return [
+ UsersFixture::class,
+ ArticlesFixture::class,
+ ];
+}
+```
+
+
+
+### Fixture State Managers
+
+By default CakePHP resets fixture state at the end of each test by truncating
+all the tables in the database. This operation can become expensive as your
+application grows. By using `TransactionStrategy` each test method will be run
+inside a transaction that is rolled back at the end of the test. This can yield
+improved performance but requires your tests not heavily rely on static fixture
+data, as auto-increment values are not reset before each test.
+
+The fixture state management strategy can be defined within the test case:
+
+``` php
+use Cake\TestSuite\TestCase;
+use Cake\TestSuite\Fixture\FixtureStrategyInterface;
+use Cake\TestSuite\Fixture\TransactionStrategy;
+
+class ArticlesTableTest extends TestCase
+{
+ /**
+ * Create the fixtures strategy used for this test case.
+ * You can use a base class/trait to change multiple classes.
+ */
+ protected function getFixtureStrategy(): FixtureStrategyInterface
+ {
+ return new TransactionStrategy();
+ }
+}
+```
+
+To switch out the general default strategy, use Configure key `TestSuite.fixtureStrategy` in your `app.php`:
+
+``` php
+'TestSuite' => [
+ 'fixtureStrategy' => \Cake\TestSuite\Fixture\TransactionStrategy::class,
+],
+```
+
+The recommended strategy for medium and large applications is the `TransactionStrategy`, as using rollbacks to undo changes from tests is simpler to maintain, and reduces the chances of cross-contamination and side-effects between tests.
+
+### Fixture Factories
+
+As your application grows, so does the number and the size of your test
+fixtures. You might find it difficult to maintain them and to keep track of
+their content. The [fixture factories plugin](https://github.com/vierge-noire/cakephp-fixture-factories) proposes an
+alternative for large sized applications.
+
+The plugin uses the [test suite light plugin](https://github.com/vierge-noire/cakephp-test-suite-light)
+in order to truncate all dirty tables before each test.
+
+The following command will help you bake your factories:
+
+ bin/cake bake fixture_factory -h
+
+Once your factories are
+[tuned](https://github.com/vierge-noire/cakephp-fixture-factories/blob/main/docs/factories.md),
+you are ready to create test fixtures in no time.
+
+Unnecessary interaction with the database will slow down your tests as well as
+your application. You can create test fixtures without persisting them which can
+be useful for testing methods without DB interaction:
+
+``` php
+$article = ArticleFactory::make()->getEntity();
+```
+
+In order to persist:
+
+``` php
+$article = ArticleFactory::make()->persist();
+```
+
+The factories help creating associated fixtures too.
+Assuming that articles belongs to many authors, we can now, for example,
+create 5 articles each with 2 authors:
+
+``` php
+$articles = ArticleFactory::make(5)->with('Authors', 2)->getEntities();
+```
+
+Note that the fixture factories do not require any fixture creation or
+declaration. Still, they are fully compatible with the fixtures that come with
+cakephp. You will find additional insights and documentation [here](https://github.com/vierge-noire/cakephp-fixture-factories).
+
+## Loading Routes in Tests
+
+If you are testing mailers, controller components or other classes that require
+routes and resolving URLs, you will need to load routes. During
+the `setUp()` of a class or during individual test methods you can use
+`loadRoutes()` to ensure your application routes are loaded:
+
+``` php
+public function setUp(): void
+{
+ parent::setUp();
+ $this->loadRoutes();
+}
+```
+
+This method will build an instance of your `Application` and call the
+`routes()` method on it. If your `Application` class requires specialized
+constructor parameters you can provide those to `loadRoutes($constructorArgs)`.
+
+### Creating Routes in Tests
+
+Sometimes it may be be necessary to dynamically add routes in tests, for example
+when developing plugins, or applications that are extensible.
+
+Just like loading existing application routes, this can be done during `setup()`
+of a test method, and/or in the individual test methods themselves:
+
+``` php
+use Cake\Routing\Route\DashedRoute;
+use Cake\Routing\RouteBuilder;
+use Cake\Routing\Router;
+use Cake\TestSuite\TestCase;
+
+class PluginHelperTest extends TestCase
+{
+ protected RouteBuilder $routeBuilder;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->routeBuilder = Router::createRouteBuilder('/');
+ $this->routeBuilder->scope('/', function (RouteBuilder $routes) {
+ $routes->setRouteClass(DashedRoute::class);
+ $routes->get(
+ '/test/view/{id}',
+ ['controller' => 'Tests', 'action' => 'view']
+ );
+ // ...
+ });
+
+ // ...
+ }
+}
+```
+
+This will create a new route builder instance that will merge connected routes
+into the same route collection used by all other route builder instances that
+may already exist, or are yet to be created in the environment.
+
+### Loading Plugins in Tests
+
+If your application would dynamically load plugins, you can use
+`loadPlugins()` to load one or more plugins during tests:
+
+``` php
+public function testMethodUsingPluginResources()
+{
+ $this->loadPlugins(['Company/Cms']);
+ // Test logic that requires Company/Cms to be loaded.
+}
+```
+
+## Testing Table Classes
+
+Let's say we already have our Articles Table class defined in
+**src/Model/Table/ArticlesTable.php**, and it looks like:
+
+``` php
+namespace App\Model\Table;
+
+use Cake\ORM\Table;
+use Cake\ORM\Query\SelectQuery;
+
+class ArticlesTable extends Table
+{
+ public function findPublished(SelectQuery $query): SelectQuery
+ {
+ $query->where([
+ $this->getAlias() . '.published' => 1
+ ]);
+
+ return $query;
+ }
+}
+```
+
+We now want to set up a test that will test this table class. Let's now create
+a file named **ArticlesTableTest.php** in your **tests/TestCase/Model/Table** directory,
+with the following contents:
+
+``` php
+namespace App\Test\TestCase\Model\Table;
+
+use App\Model\Table\ArticlesTable;
+use Cake\TestSuite\TestCase;
+
+class ArticlesTableTest extends TestCase
+{
+ protected $fixtures = ['app.Articles'];
+}
+```
+
+In our test cases' variable `$fixtures` we define the set of fixtures that
+we'll use. You should remember to include all the fixtures that will have
+queries run against them.
+
+### Creating a Test Method
+
+Let's now add a method to test the function `published()` in the Articles
+table. Edit the file **tests/TestCase/Model/Table/ArticlesTableTest.php** so it
+now looks like this:
+
+``` php
+namespace App\Test\TestCase\Model\Table;
+
+use App\Model\Table\ArticlesTable;
+use Cake\TestSuite\TestCase;
+
+class ArticlesTableTest extends TestCase
+{
+ protected $fixtures = ['app.Articles'];
+
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->Articles = $this->getTableLocator()->get('Articles');
+ }
+
+ public function testFindPublished(): void
+ {
+ $query = $this->Articles->find('published')->select(['id', 'title']);
+ $this->assertInstanceOf('Cake\ORM\Query\SelectQuery', $query);
+ $result = $query->enableHydration(false)->toArray();
+ $expected = [
+ ['id' => 1, 'title' => 'First Article'],
+ ['id' => 2, 'title' => 'Second Article'],
+ ['id' => 3, 'title' => 'Third Article']
+ ];
+
+ $this->assertEquals($expected, $result);
+ }
+}
+```
+
+You can see we have added a method called `testFindPublished()`. We start by
+creating an instance of our `ArticlesTable` class, and then run our
+`find('published')` method. In `$expected` we set what we expect should be
+the proper result (that we know since we have defined which records are
+initially populated to the article table.) We test that the result equals our
+expectation by using the `assertEquals()` method. See the [Running Tests](#running-tests)
+section for more information on how to run your test case.
+
+Using the fixture factories, the test would now look like this:
+
+``` php
+namespace App\Test\TestCase\Model\Table;
+
+use App\Test\Factory\ArticleFactory;
+use Cake\TestSuite\TestCase;
+
+class ArticlesTableTest extends TestCase
+{
+ public function testFindPublished(): void
+ {
+ // Persist 3 published articles
+ $articles = ArticleFactory::make(['published' => 1], 3)->persist();
+ // Persist 2 unpublished articles
+ ArticleFactory::make(['published' => 0], 2)->persist();
+
+ $result = ArticleFactory::find('published')->find('list')->toArray();
+
+ $expected = [
+ $articles[0]->id => $articles[0]->title,
+ $articles[1]->id => $articles[1]->title,
+ $articles[2]->id => $articles[2]->title,
+ ];
+
+ $this->assertEquals($expected, $result);
+ }
+}
+```
+
+No fixtures need to be loaded. The 5 articles created will exist only in this test. The
+static method `::find()` will query the database without using the table `ArticlesTable`
+and it's events.
+
+### Mocking Model Methods
+
+There will be times you'll want to mock methods on models when testing them. You
+should use `getMockForModel` to create testing mocks of table classes. It
+avoids issues with reflected properties that normal mocks have:
+
+``` php
+public function testSendingEmails(): void
+{
+ $model = $this->getMockForModel('EmailVerification', ['send']);
+ $model->expects($this->once())
+ ->method('send')
+ ->will($this->returnValue(true));
+
+ $model->verifyEmail('test@example.com');
+}
+```
+
+In your `tearDown()` method be sure to remove the mock with:
+
+``` php
+$this->getTableLocator()->clear();
+```
+
+
+
+## Controller Integration Testing
+
+While you can test controller classes in a similar fashion to Helpers, Models,
+and Components, CakePHP offers a specialized `IntegrationTestTrait` trait.
+Using this trait in your controller test cases allows you to
+test controllers from a high level.
+
+If you are unfamiliar with integration testing, it is a testing approach that
+allows you to test multiple units in concert. The integration testing
+features in CakePHP simulate an HTTP request being handled by your application.
+For example, testing your controller will also exercise any components, models
+and helpers that would be involved in handling a given request. This gives you a
+more high level test of your application and all its working parts.
+
+Say you have a typical ArticlesController, and its corresponding model. The
+controller code looks like:
+
+``` php
+namespace App\Controller;
+
+use App\Controller\AppController;
+
+class ArticlesController extends AppController
+{
+ public function index($short = null)
+ {
+ if ($this->request->is('post')) {
+ $article = $this->Articles->newEntity($this->request->getData());
+ if ($this->Articles->save($article)) {
+ // Redirect as per PRG pattern
+ return $this->redirect(['action' => 'index']);
+ }
+ }
+ if (!empty($short)) {
+ $result = $this->Articles->find('all', fields: ['id', 'title'])->all();
+ } else {
+ $result = $this->Articles->find()->all();
+ }
+
+ $this->set([
+ 'title' => 'Articles',
+ 'articles' => $result
+ ]);
+ }
+}
+```
+
+Create a file named **ArticlesControllerTest.php** in your
+**tests/TestCase/Controller** directory and put the following inside:
+
+``` php
+namespace App\Test\TestCase\Controller;
+
+use Cake\TestSuite\IntegrationTestTrait;
+use Cake\TestSuite\TestCase;
+
+class ArticlesControllerTest extends TestCase
+{
+ use IntegrationTestTrait;
+
+ protected $fixtures = ['app.Articles'];
+
+ public function testIndex(): void
+ {
+ $this->get('/articles');
+
+ $this->assertResponseOk();
+ // More asserts.
+ }
+
+ public function testIndexQueryData(): void
+ {
+ $this->get('/articles?page=1');
+
+ $this->assertResponseOk();
+ // More asserts.
+ }
+
+ public function testIndexShort(): void
+ {
+ $this->get('/articles/index/short');
+
+ $this->assertResponseOk();
+ $this->assertResponseContains('Articles');
+ // More asserts.
+ }
+
+ public function testIndexPostData(): void
+ {
+ $data = [
+ 'user_id' => 1,
+ 'published' => 1,
+ 'slug' => 'new-article',
+ 'title' => 'New Article',
+ 'body' => 'New Body'
+ ];
+ $this->post('/articles', $data);
+
+ $this->assertResponseSuccess();
+ $articles = $this->getTableLocator()->get('Articles');
+ $query = $articles->find()->where(['title' => $data['title']]);
+ $this->assertEquals(1, $query->count());
+ }
+}
+```
+
+This example shows a few of the request sending methods and a few of the
+assertions that `IntegrationTestTrait` provides. Before you can do any
+assertions you'll need to dispatch a request. You can use one of the following
+methods to send a request:
+
+- `get()` Sends a GET request.
+- `post()` Sends a POST request.
+- `put()` Sends a PUT request.
+- `delete()` Sends a DELETE request.
+- `patch()` Sends a PATCH request.
+- `options()` Sends an OPTIONS request.
+- `head()` Sends a HEAD request.
+
+All of the methods except `get()` and `delete()` accept a second parameter
+that allows you to send a request body. After dispatching a request you can use
+the various assertions provided by `IntegrationTestTrait` or PHPUnit to
+ensure your request had the correct side-effects.
+
+### Setting up the Request
+
+The `IntegrationTestTrait` trait comes with a number of helpers to
+to configure the requests you will send to your application under test:
+
+``` php
+// Set cookies
+$this->cookie('name', 'Uncle Bob');
+
+// Set session data
+$this->session(['Auth.User.id' => 1]);
+
+// Configure headers and merge with the existing request
+$this->configRequest([
+ 'headers' => ['Accept' => 'application/json']
+]);
+
+// Replace the existing request. Added in 5.1.0
+$this->replaceRequest([
+ 'headers' => ['Accept' => 'application/json']
+]);
+```
+
+The state set by these helper methods is reset in the `tearDown()` method.
+
+::: info Added in version 5.1.0
+`replaceRequest()` was added.
+:::
+
+### Testing Actions Protected by CsrfProtectionMiddleware or FormProtectionComponent
+
+When testing actions protected by either `CsrfProtectionMiddleware` or `FormProtectionComponent` you
+can enable automatic token generation to ensure your tests won't fail due to
+token mismatches:
+
+``` php
+public function testAdd(): void
+{
+ $this->enableCsrfToken();
+ $this->enableSecurityToken();
+ $this->post('/posts/add', ['title' => 'Exciting news!']);
+}
+```
+
+It is also important to enable debug in tests that use tokens to prevent the
+`FormProtectionComponent` from thinking the debug token is being used in a non-debug
+environment. When testing with other methods like `requireSecure()` you
+can use `configRequest()` to set the correct environment variables:
+
+``` php
+// Fake out SSL connections.
+$this->configRequest([
+ 'environment' => ['HTTPS' => 'on']
+]);
+```
+
+If your action requires unlocked fields you can declare them with
+`setUnlockedFields()`:
+
+``` php
+$this->setUnlockedFields(['dynamic_field']);
+```
+
+### Integration Testing PSR-7 Middleware
+
+Integration testing can also be used to test your entire PSR-7 application and
+[Middleware](../controllers/middleware). By default `IntegrationTestTrait` will
+auto-detect the presence of an `App\Application` class and automatically
+enable integration testing of your Application.
+
+You can customize the application class name used, and the constructor
+arguments, by using the `configApplication()` method:
+
+``` php
+public function setUp(): void
+{
+ $this->configApplication('App\App', [CONFIG]);
+}
+```
+
+You should also take care to try and use [Application Bootstrap](../development/application#application-bootstrap) to load
+any plugins containing events/routes. Doing so will ensure that your
+events/routes are connected for each test case. Alternatively if you wish to
+load plugins manually in a test you can use the `loadPlugins()` method.
+
+### Testing with Encrypted Cookies
+
+If you use the [Encrypted Cookie Middleware](../controllers/middleware#encrypted-cookie-middleware) in your
+application, there are helper methods for setting encrypted cookies in your
+test cases:
+
+``` php
+// Set a cookie using AES and the default key.
+$this->cookieEncrypted('my_cookie', 'Some secret values');
+
+// Assume this action modifies the cookie.
+$this->get('/articles/index');
+
+$this->assertCookieEncrypted('An updated value', 'my_cookie');
+```
+
+### Testing Flash Messages
+
+If you want to assert the presence of flash messages in the session and not the
+rendered HTML, you can use `enableRetainFlashMessages()` in your tests to
+retain flash messages in the session so you can write assertions:
+
+``` php
+// Enable retention of flash messages instead of consuming them.
+$this->enableRetainFlashMessages();
+$this->get('/articles/delete/9999');
+
+$this->assertSession('That article does not exist', 'Flash.flash.0.message');
+
+// Assert a flash message in the 'flash' key.
+$this->assertFlashMessage('Article deleted', 'flash');
+
+// Assert the second flash message, also in the 'flash' key.
+$this->assertFlashMessageAt(1, 'Article really deleted');
+
+// Assert a flash message in the 'auth' key at the first position
+$this->assertFlashMessageAt(0, 'You are not allowed to enter this dungeon!', 'auth');
+
+// Assert a flash messages uses the error element
+$this->assertFlashElement('Flash/error');
+
+// Assert the second flash message element
+$this->assertFlashElementAt(1, 'Flash/error');
+
+// Assert a flash message contains a substring (Added in 5.3.0)
+$this->assertFlashMessageContains('deleted', 'flash');
+
+// Assert the second flash message contains a substring (Added in 5.3.0)
+$this->assertFlashMessageContainsAt(1, 'really deleted');
+```
+
+::: info Added in version 5.3.0
+`assertFlashMessageContains()` and `assertFlashMessageContainsAt()` were added.
+:::
+
+### Testing a JSON Responding Controller
+
+JSON is a friendly and common format to use when building a web service.
+Testing the endpoints of your web service is very simple with CakePHP. Let us
+begin with a simple example controller that responds in JSON:
+
+``` php
+use Cake\View\JsonView;
+
+class MarkersController extends AppController
+{
+ public function viewClasses(): array
+ {
+ return [JsonView::class];
+ }
+
+ public function view($id)
+ {
+ $marker = $this->Markers->get($id);
+ $this->set('marker', $marker);
+ $this->viewBuilder()->setOption('serialize', ['marker']);
+ }
+}
+```
+
+Now we create the file **tests/TestCase/Controller/MarkersControllerTest.php**
+and make sure our web service is returning the proper response:
+
+``` php
+class MarkersControllerTest extends IntegrationTestCase
+{
+ use IntegrationTestTrait;
+
+ public function testGet(): void
+ {
+ $this->configRequest([
+ 'headers' => ['Accept' => 'application/json']
+ ]);
+ $this->get('/markers/view/1.json');
+
+ // Check that the response was a 200
+ $this->assertResponseOk();
+
+ $expected = [
+ ['id' => 1, 'lng' => 66, 'lat' => 45],
+ ];
+ $expected = json_encode($expected, JSON_PRETTY_PRINT);
+ $this->assertEquals($expected, (string)$this->_response->getBody());
+ }
+}
+```
+
+We use the `JSON_PRETTY_PRINT` option as CakePHP's built in JsonView will use
+that option when `debug` is enabled.
+
+### Testing with file uploads
+
+Simulating file uploads is straightforward when you use the default
+"[uploaded files as objects](../controllers/request-response#request-file-uploads)" mode. You can simply
+create instances that implement
+[\Psr\Http\Message\UploadedFileInterface](https://www.php-fig.org/psr/psr-7/#16-uploaded-files)
+(the default implementation currently used by CakePHP is
+`\Laminas\Diactoros\UploadedFile`), and pass them in your test request data.
+In the CLI environment such objects will by default pass validation checks that
+test whether the file was uploaded via HTTP. The same is not true for array style
+data as found in `$_FILES`, it would fail that check.
+
+In order to simulate exactly how the uploaded file objects would be present on
+a regular request, you not only need to pass them in the request data, but you also
+need to pass them to the test request configuration via the `files` option. It's
+not technically necessary though unless your code accesses uploaded files via the
+`Cake\Http\ServerRequest::getUploadedFile()` or
+`Cake\Http\ServerRequest::getUploadedFiles()` methods.
+
+Let's assume articles have a teaser image, and a `Articles hasMany Attachments`
+association, the form would look like something like this accordingly, where one
+image file, and multiple attachments/files would be accepted:
+
+``` php
+= $this->Form->create($article, ['type' => 'file']) ?>
+= $this->Form->control('title') ?>
+= $this->Form->control('teaser_image', ['type' => 'file']) ?>
+= $this->Form->control('attachments.0.attachment', ['type' => 'file']) ?>
+= $this->Form->control('attachments.0.description']) ?>
+= $this->Form->control('attachments.1.attachment', ['type' => 'file']) ?>
+= $this->Form->control('attachments.1.description']) ?>
+= $this->Form->button('Submit') ?>
+= $this->Form->end() ?>
+```
+
+The test that would simulate the corresponding request could look like this:
+
+``` php
+public function testAddWithUploads(): void
+{
+ $teaserImage = new \Laminas\Diactoros\UploadedFile(
+ '/path/to/test/file.jpg', // stream or path to file representing the temp file
+ 12345, // the filesize in bytes
+ \UPLOAD_ERR_OK, // the upload/error status
+ 'teaser.jpg', // the filename as sent by the client
+ 'image/jpeg' // the mimetype as sent by the client
+ );
+
+ $textAttachment = new \Laminas\Diactoros\UploadedFile(
+ '/path/to/test/file.txt',
+ 12345,
+ \UPLOAD_ERR_OK,
+ 'attachment.txt',
+ 'text/plain'
+ );
+
+ $pdfAttachment = new \Laminas\Diactoros\UploadedFile(
+ '/path/to/test/file.pdf',
+ 12345,
+ \UPLOAD_ERR_OK,
+ 'attachment.pdf',
+ 'application/pdf'
+ );
+
+ // This is the data accessible via `$this->request->getUploadedFile()`
+ // and `$this->request->getUploadedFiles()`.
+ $this->configRequest([
+ 'files' => [
+ 'teaser_image' => $teaserImage,
+ 'attachments' => [
+ 0 => [
+ 'attachment' => $textAttachment,
+ ],
+ 1 => [
+ 'attachment' => $pdfAttachment,
+ ],
+ ],
+ ],
+ ]);
+
+ // This is the data accessible via `$this->request->getData()`.
+ $postData = [
+ 'title' => 'New Article',
+ 'teaser_image' => $teaserImage,
+ 'attachments' => [
+ 0 => [
+ 'attachment' => $textAttachment,
+ 'description' => 'Text attachment',
+ ],
+ 1 => [
+ 'attachment' => $pdfAttachment,
+ 'description' => 'PDF attachment',
+ ],
+ ],
+ ];
+ $this->post('/articles/add', $postData);
+
+ $this->assertResponseOk();
+ $this->assertFlashMessage('The article was saved successfully');
+ $this->assertFileExists('/path/to/uploads/teaser.jpg');
+ $this->assertFileExists('/path/to/uploads/attachment.txt');
+ $this->assertFileExists('/path/to/uploads/attachment.pdf');
+}
+```
+
+> [!TIP]
+> If you configure the test request with files, then it *must* match the
+> structure of your POST data (but only include the uploaded file objects)!
+
+Likewise you can simulate [upload errors](https://www.php.net/manual/en/features.file-upload.errors.php)
+or otherwise invalid files that do not pass validation:
+
+``` php
+public function testAddWithInvalidUploads(): void
+{
+ $missingTeaserImageUpload = new \Laminas\Diactoros\UploadedFile(
+ '',
+ 0,
+ \UPLOAD_ERR_NO_FILE,
+ '',
+ ''
+ );
+
+ $uploadFailureAttachment = new \Laminas\Diactoros\UploadedFile(
+ '/path/to/test/file.txt',
+ 1234567890,
+ \UPLOAD_ERR_INI_SIZE,
+ 'attachment.txt',
+ 'text/plain'
+ );
+
+ $invalidTypeAttachment = new \Laminas\Diactoros\UploadedFile(
+ '/path/to/test/file.exe',
+ 12345,
+ \UPLOAD_ERR_OK,
+ 'attachment.exe',
+ 'application/vnd.microsoft.portable-executable'
+ );
+
+ $this->configRequest([
+ 'files' => [
+ 'teaser_image' => $missingTeaserImageUpload,
+ 'attachments' => [
+ 0 => [
+ 'file' => $uploadFailureAttachment,
+ ],
+ 1 => [
+ 'file' => $invalidTypeAttachment,
+ ],
+ ],
+ ],
+ ]);
+
+ $postData = [
+ 'title' => 'New Article',
+ 'teaser_image' => $missingTeaserImageUpload,
+ 'attachments' => [
+ 0 => [
+ 'file' => $uploadFailureAttachment,
+ 'description' => 'Upload failure attachment',
+ ],
+ 1 => [
+ 'file' => $invalidTypeAttachment,
+ 'description' => 'Invalid type attachment',
+ ],
+ ],
+ ];
+ $this->post('/articles/add', $postData);
+
+ $this->assertResponseOk();
+ $this->assertFlashMessage('The article could not be saved');
+ $this->assertResponseContains('A teaser image is required');
+ $this->assertResponseContains('Max allowed filesize exceeded');
+ $this->assertResponseContains('Unsupported file type');
+ $this->assertFileNotExists('/path/to/uploads/teaser.jpg');
+ $this->assertFileNotExists('/path/to/uploads/attachment.txt');
+ $this->assertFileNotExists('/path/to/uploads/attachment.exe');
+}
+```
+
+### Disabling Error Handling Middleware in Tests
+
+When debugging tests that are failing because your application is encountering
+errors it can be helpful to temporarily disable the error handling middleware to
+allow the underlying error to bubble up. You can use
+`disableErrorHandlerMiddleware()` to do this:
+
+``` php
+public function testGetMissing(): void
+{
+ $this->disableErrorHandlerMiddleware();
+ $this->get('/markers/not-there');
+ $this->assertResponseCode(404);
+}
+```
+
+In the above example, the test would fail and the underlying exception message
+and stack trace would be displayed instead of the rendered error page being
+checked.
+
+### Assertion methods
+
+The `IntegrationTestTrait` trait provides a number of assertion methods that
+make testing responses much simpler. Some examples are:
+
+``` php
+// Check for a 2xx response code
+$this->assertResponseOk();
+
+// Check for a 2xx/3xx response code
+$this->assertResponseSuccess();
+
+// Check for a 4xx response code
+$this->assertResponseError();
+
+// Check for a 5xx response code
+$this->assertResponseFailure();
+
+// Check for a specific response code, for example, 200
+$this->assertResponseCode(200);
+
+// Check the Location header
+$this->assertRedirect(['controller' => 'Articles', 'action' => 'index']);
+
+// Check the Location header matches the same previous URL
+$this->assertRedirectBack();
+
+// Check the Location header matches the referer URL
+$this->assertRedirectBackToReferer();
+
+// Check that no Location header has been set
+$this->assertNoRedirect();
+
+// Check a part of the Location header
+$this->assertRedirectContains('/articles/edit/');
+
+// Assert location header does not contain
+$this->assertRedirectNotContains('/articles/edit/');
+
+// Assert not empty response content
+$this->assertResponseNotEmpty();
+
+// Assert empty response content
+$this->assertResponseEmpty();
+
+// Assert response content
+$this->assertResponseEquals('Yeah!');
+
+// Assert response content doesn't equal
+$this->assertResponseNotEquals('No!');
+
+// Assert partial response content
+$this->assertResponseContains('You won!');
+$this->assertResponseNotContains('You lost!');
+
+// Assert file sent back
+$this->assertFileResponse('/absolute/path/to/file.ext');
+
+// Assert layout
+$this->assertLayout('default');
+
+// Assert which template was rendered (if any)
+$this->assertTemplate('index');
+
+// Assert data in the session
+$this->assertSession(1, 'Auth.User.id');
+
+// Assert response header.
+$this->assertHeader('Content-Type', 'application/json');
+$this->assertHeaderContains('Content-Type', 'html');
+
+// Assert content-type header doesn't contain xml
+$this->assertHeaderNotContains('Content-Type', 'xml');
+
+// Assert view variables
+$user = $this->viewVariable('user');
+$this->assertEquals('jose', $user->username);
+
+// Assert cookie values in the response
+$this->assertCookie('1', 'thingid');
+
+// Assert a cookie is or is not present
+$this->assertCookieIsSet('remember_me');
+$this->assertCookieNotSet('remember_me');
+
+// Check the content type
+$this->assertContentType('application/json');
+```
+
+In addition to the above assertion methods, you can also use all of the
+assertions in [TestSuite](https://api.cakephp.org/5.x/class-Cake.TestSuite.TestCase.html) and those
+found in [PHPUnit](https://phpunit.de/manual/current/en/appendixes.assertions.html).
+
+### Comparing test results to a file
+
+For some types of test, it may be easier to compare the result of a test to the
+contents of a file - for example, when testing the rendered output of a view.
+The `StringCompareTrait` adds a simple assert method for this purpose.
+
+Usage involves using the trait, setting the comparison base path and calling
+`assertSameAsFile`:
+
+``` php
+use Cake\TestSuite\StringCompareTrait;
+use Cake\TestSuite\TestCase;
+
+class SomeTest extends TestCase
+{
+ use StringCompareTrait;
+
+ public function setUp(): void
+ {
+ $this->_compareBasePath = APP . 'tests' . DS . 'comparisons' . DS;
+ parent::setUp();
+ }
+
+ public function testExample(): void
+ {
+ $result = ...;
+ $this->assertSameAsFile('example.php', $result);
+ }
+}
+```
+
+The above example will compare `$result` to the contents of the file
+`APP/tests/comparisons/example.php`.
+
+A mechanism is provided to write/update test files, by setting the environment
+variable `UPDATE_TEST_COMPARISON_FILES`, which will create and/or update test
+comparison files as they are referenced:
+
+``` bash
+phpunit
+...
+FAILURES!
+Tests: 6, Assertions: 7, Failures: 1
+
+UPDATE_TEST_COMPARISON_FILES=1 phpunit
+...
+OK (6 tests, 7 assertions)
+
+git status
+...
+# Changes not staged for commit:
+# (use "git add ..." to update what will be committed)
+# (use "git checkout -- ..." to discard changes in working directory)
+#
+# modified: tests/comparisons/example.php
+```
+
+## Console Integration Testing
+
+See [Console Integration Testing](../console-commands/commands#console-integration-testing) for how to test console commands.
+
+## Mocking Injected Dependencies
+
+See [Mocking Services In Tests](../development/dependency-injection#mocking-services-in-tests) for how to replace services injected with
+the dependency injection container in your integration tests.
+
+## Mocking HTTP Client Responses
+
+See [Httpclient Testing](../core-libraries/httpclient#httpclient-testing) to know how to create mock responses to external APIs.
+
+## Testing Views
+
+Generally most applications will not directly test their HTML code. Doing so is
+often results in fragile, difficult to maintain test suites that are prone to
+breaking. When writing functional tests using `IntegrationTestTrait`
+you can inspect the rendered view content by setting the `return` option to
+'view'. While it is possible to test view content using `IntegrationTestTrait`,
+a more robust and maintainable integration/view testing can be accomplished
+using tools like [Selenium webdriver](https://www.selenium.dev/).
+
+## Testing Components
+
+Let's pretend we have a component called PagematronComponent in our application.
+This component helps us set the pagination limit value across all the
+controllers that use it. Here is our example component located in
+**src/Controller/Component/PagematronComponent.php**:
+
+``` php
+class PagematronComponent extends Component
+{
+ public $controller = null;
+
+ public function setController($controller)
+ {
+ $this->controller = $controller;
+ // Make sure the controller is using pagination
+ if (!isset($this->controller->paginate)) {
+ $this->controller->paginate = [];
+ }
+ }
+
+ public function startup(EventInterface $event): void
+ {
+ $this->setController($event->getSubject());
+ }
+
+ public function adjust(string $length = 'short'): void
+ {
+ switch ($length) {
+ case 'long':
+ $this->controller->paginate['limit'] = 100;
+ break;
+ case 'medium':
+ $this->controller->paginate['limit'] = 50;
+ break;
+ default:
+ $this->controller->paginate['limit'] = 20;
+ break;
+ }
+ }
+}
+```
+
+Now we can write tests to ensure our paginate `limit` parameter is being set
+correctly by the `adjust()` method in our component. We create the file
+**tests/TestCase/Controller/Component/PagematronComponentTest.php**:
+
+``` php
+namespace App\Test\TestCase\Controller\Component;
+
+use App\Controller\Component\PagematronComponent;
+use Cake\Controller\Controller;
+use Cake\Controller\ComponentRegistry;
+use Cake\Event\Event;
+use Cake\Http\ServerRequest;
+use Cake\Http\Response;
+use Cake\TestSuite\TestCase;
+
+class PagematronComponentTest extends TestCase
+{
+ protected $component;
+ protected $controller;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+ // Setup our component and provide it a basic controller.
+ // If your component relies on Application features, use AppController.
+ $request = new ServerRequest();
+ $response = new Response();
+ $this->controller = new Controller($request);
+ $registry = new ComponentRegistry($this->controller);
+
+ $this->component = new PagematronComponent($registry);
+ $event = new Event('Controller.startup', $this->controller);
+ $this->component->startup($event);
+ }
+
+ public function testAdjust(): void
+ {
+ // Test our adjust method with different parameter settings
+ $this->component->adjust();
+ $this->assertEquals(20, $this->controller->paginate['limit']);
+
+ $this->component->adjust('medium');
+ $this->assertEquals(50, $this->controller->paginate['limit']);
+
+ $this->component->adjust('long');
+ $this->assertEquals(100, $this->controller->paginate['limit']);
+ }
+
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ // Clean up after we're done
+ unset($this->component, $this->controller);
+ }
+}
+```
+
+## Testing Helpers
+
+Since a decent amount of logic resides in Helper classes, it's
+important to make sure those classes are covered by test cases.
+
+First we create an example helper to test. The `CurrencyRendererHelper` will
+help us display currencies in our views and for simplicity only has one method
+`usd()`:
+
+``` php
+// src/View/Helper/CurrencyRendererHelper.php
+namespace App\View\Helper;
+
+use Cake\View\Helper;
+
+class CurrencyRendererHelper extends Helper
+{
+ public function usd($amount): string
+ {
+ return 'USD ' . number_format($amount, 2, '.', ',');
+ }
+}
+```
+
+Here we set the decimal places to 2, decimal separator to dot, thousands
+separator to comma, and prefix the formatted number with 'USD' string.
+
+Now we create our tests:
+
+``` php
+// tests/TestCase/View/Helper/CurrencyRendererHelperTest.php
+
+namespace App\Test\TestCase\View\Helper;
+
+use App\View\Helper\CurrencyRendererHelper;
+use Cake\TestSuite\TestCase;
+use Cake\View\View;
+
+class CurrencyRendererHelperTest extends TestCase
+{
+ public $helper = null;
+
+ // Here we instantiate our helper
+ public function setUp(): void
+ {
+ parent::setUp();
+ $View = new View();
+ $this->helper = new CurrencyRendererHelper($View);
+ }
+
+ // Testing the usd() function
+ public function testUsd(): void
+ {
+ $this->assertEquals('USD 5.30', $this->helper->usd(5.30));
+
+ // We should always have 2 decimal digits
+ $this->assertEquals('USD 1.00', $this->helper->usd(1));
+ $this->assertEquals('USD 2.05', $this->helper->usd(2.05));
+
+ // Testing the thousands separator
+ $this->assertEquals(
+ 'USD 12,000.70',
+ $this->helper->usd(12000.70)
+ );
+ }
+}
+```
+
+Here, we call `usd()` with different parameters and tell the test suite to
+check if the returned values are equal to what is expected.
+
+Save this and execute the test. You should see a green bar and messaging
+indicating 1 pass and 4 assertions.
+
+When you are testing a Helper which uses other helpers, be sure to mock the
+View clases `loadHelpers` method.
+
+
+
+## Testing Events
+
+The [Events System](../core-libraries/events) is a great way to decouple your application
+code, but sometimes when testing, you tend to test the results of events in the
+test cases that execute those events. This is an additional form of coupling
+that can be removed by using `assertEventFired` and `assertEventFiredWith`
+instead.
+
+Expanding on the Orders example, say we have the following tables:
+
+``` php
+class OrdersTable extends Table
+{
+ public function place($order): bool
+ {
+ if ($this->save($order)) {
+ // moved cart removal to CartsTable
+ $event = new Event('Model.Order.afterPlace', $this, [
+ 'order' => $order
+ ]);
+ $this->getEventManager()->dispatch($event);
+
+ return true;
+ }
+
+ return false;
+ }
+}
+
+class CartsTable extends Table
+{
+ public function initialize()
+ {
+ // Models don't share the same event manager instance,
+ // so we need to use the global instance to listen to
+ // events from other models
+ \Cake\Event\EventManager::instance()->on(
+ 'Model.Order.afterPlace',
+ callable: [$this, 'removeFromCart']
+ );
+ }
+
+ public function removeFromCart(EventInterface $event): void
+ {
+ $order = $event->getData('order');
+ $this->delete($order->cart_id);
+ }
+}
+```
+
+> [!NOTE]
+> To assert that events are fired, you must first enable
+> [Tracking Events](../core-libraries/events#tracking-events) on the event manager you wish to assert against.
+
+To test the `OrdersTable` above, we enable tracking in `setUp()` then assert
+that the event was fired, and assert that the `$order` entity was passed in
+the event data:
+
+``` php
+namespace App\Test\TestCase\Model\Table;
+
+use App\Model\Table\OrdersTable;
+use Cake\Event\EventList;
+use Cake\TestSuite\TestCase;
+
+class OrdersTableTest extends TestCase
+{
+ protected $fixtures = ['app.Orders'];
+
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->Orders = $this->getTableLocator()->get('Orders');
+ // enable event tracking
+ $this->Orders->getEventManager()->setEventList(new EventList());
+ }
+
+ public function testPlace(): void
+ {
+ $order = new Order([
+ 'user_id' => 1,
+ 'item' => 'Cake',
+ 'quantity' => 42,
+ ]);
+
+ $this->assertTrue($this->Orders->place($order));
+
+ $this->assertEventFired('Model.Order.afterPlace', $this->Orders->getEventManager());
+ $this->assertEventFiredWith('Model.Order.afterPlace', 'order', $order, $this->Orders->getEventManager());
+ }
+}
+```
+
+By default, the global `EventManager` is used for assertions, so testing
+global events does not require passing the event manager:
+
+``` php
+$this->assertEventFired('My.Global.Event');
+$this->assertEventFiredWith('My.Global.Event', 'user', 1);
+```
+
+## Testing Email
+
+See [Email Testing](../core-libraries/email#email-testing) for information on testing email.
+
+## Testing Logging
+
+See [Log Testing](../core-libraries/logging#log-testing) for information on testing log messages.
+
+## Creating Test Suites
+
+If you want several of your tests to run at the same time, you can create a test
+suite. A test suite is composed of several test cases. You can either create
+test suites in your application's **phpunit.xml** file. A simple example
+would be:
+
+``` xml
+
+
+ src/Model
+ src/Service/UserServiceTest.php
+ src/Model/Cloud/ImagesTest.php
+
+
+```
+
+## Creating Tests for Plugins
+
+Tests for plugins are created in their own directory inside the plugins
+folder. :
+
+ /src
+ /plugins
+ /Blog
+ /tests
+ /TestCase
+ /Fixture
+
+They work just like normal tests but you have to remember to use the naming
+conventions for plugins when importing classes. This is an example of a testcase
+for the `BlogPost` model from the plugins chapter of this manual. A difference
+from other tests is in the first line where 'Blog.BlogPost' is imported. You
+also need to prefix your plugin fixtures with `plugin.Blog.BlogPosts`:
+
+``` php
+namespace Blog\Test\TestCase\Model\Table;
+
+use Blog\Model\Table\BlogPostsTable;
+use Cake\TestSuite\TestCase;
+
+class BlogPostsTableTest extends TestCase
+{
+ // Plugin fixtures located in /plugins/Blog/tests/Fixture/
+ protected $fixtures = ['plugin.Blog.BlogPosts'];
+
+ public function testSomething(): void
+ {
+ // Test something.
+ }
+}
+```
+
+If you want to use plugin fixtures in the app tests you can
+reference them using `plugin.pluginName.fixtureName` syntax in the
+`$fixtures` array. Additionally if you use vendor plugin name or fixture
+directories you can use the following: `plugin.vendorName/pluginName.folderName/fixtureName`.
+
+Before you can use fixtures you should ensure you have the [fixture
+listener](#fixture-phpunit-configuration) configured in your `phpunit.xml`
+file. You should also ensure that your fixtures are loadable. Ensure the
+following is present in your **composer.json** file:
+
+``` json
+"autoload-dev": {
+ "psr-4": {
+ "MyPlugin\\Test\\": "plugins/MyPlugin/tests/"
+ }
+}
+```
+
+> [!NOTE]
+> Remember to run `composer.phar dumpautoload` when adding new autoload
+> mappings.
+
+## Generating Tests with Bake
+
+If you use [bake](../bake/usage) to
+generate scaffolding, it will also generate test stubs. If you need to
+re-generate test case skeletons, or if you want to generate test skeletons for
+code you wrote, you can use `bake`:
+
+``` bash
+bin/cake bake test
+```
+
+`` should be one of:
+
+1. Entity
+2. Table
+3. Controller
+4. Component
+5. Behavior
+6. Helper
+7. Shell
+8. Task
+9. ShellHelper
+10. Cell
+11. Form
+12. Mailer
+13. Command
+
+While `` should be the name of the object you want to bake a test
+skeleton for.
diff --git a/docs/en/elasticsearch.md b/docs/en/elasticsearch.md
new file mode 100644
index 0000000000..51a601bca0
--- /dev/null
+++ b/docs/en/elasticsearch.md
@@ -0,0 +1,3 @@
+# ElasticSearch
+
+This page has [moved](https://book.cakephp.org/elasticsearch/3/en/).
diff --git a/docs/en/epub-contents.md b/docs/en/epub-contents.md
new file mode 100644
index 0000000000..a3c19e4265
--- /dev/null
+++ b/docs/en/epub-contents.md
@@ -0,0 +1,54 @@
+orphan
+
+# Contents
+
+- [CakePHP at a Glance](intro)
+- [Quick Start Guide](quickstart)
+- [Migration Guides](appendices/migration-guides)
+- [Tutorials & Examples](tutorials-and-examples)
+- [Contributing](contributing)
+- [Installation](installation)
+- [Configuration](development/configuration)
+- [Routing](development/routing)
+- [Request & Response Objects](controllers/request-response)
+- [Controllers](controllers)
+- [Views](views)
+- [Database Access & ORM](orm)
+- [Caching](core-libraries/caching)
+- [Bake Console](bake)
+- [Console Commands](console-commands)
+- [Debugging](development/debugging)
+- [Deployment](deployment)
+- [Mailer](core-libraries/email)
+- [Error & Exception Handling](development/errors)
+- [Events System](core-libraries/events)
+- [Internationalization & Localization](core-libraries/internationalization-and-localization)
+- [Logging](core-libraries/logging)
+- [Modelless Forms](core-libraries/form)
+- [Pagination](controllers/pagination)
+- [Plugins](plugins)
+- [REST](development/rest)
+- [Security](security)
+- [Sessions](development/sessions)
+- [Testing](development/testing)
+- [Validation](core-libraries/validation)
+- [App Class](core-libraries/app)
+- [Collections](core-libraries/collections)
+- [Hash](core-libraries/hash)
+- [Http Client](core-libraries/httpclient)
+- [Inflector](core-libraries/inflector)
+- [Number](core-libraries/number)
+- [Registry Objects](core-libraries/registry-objects)
+- [Text](core-libraries/text)
+- [Date & Time](core-libraries/time)
+- [Xml](core-libraries/xml)
+- [Constants & Functions](core-libraries/global-constants-and-functions)
+- [Chronos](chronos)
+- [Debug Kit](debug-kit)
+- [Migrations](migrations)
+- [ElasticSearch](elasticsearch)
+- [Appendices](appendices)
+
+
+
+
diff --git a/docs/en/index.md b/docs/en/index.md
new file mode 100644
index 0000000000..94a533f505
--- /dev/null
+++ b/docs/en/index.md
@@ -0,0 +1,648 @@
+---
+title: CakePHP 5 Documentation - Build Better Web Applications Faster
+description: Official CakePHP 5 documentation. Learn how to build modern PHP web applications with convention over configuration, powerful ORM, built-in security, and rapid development tools.
+outline: 2
+---
+# Build Better Web Applications, Faster
+
+**CakePHP 5** is a modern PHP framework running on PHP |phpversion| (min. PHP |minphpversion|) that helps you write clean, maintainable code without the complexity. Whether you're building a simple blog or a complex enterprise application, CakePHP gives you the tools to get it done right.
+
+::: tip Perfect for
+✅ Developers who value **convention over configuration**
+✅ Teams building **secure, scalable applications**
+✅ Projects that need to **ship quickly** without sacrificing quality
+✅ Applications requiring **modern PHP standards** (PSR-7, PSR-15, PSR-17)
+:::
+
+## Why CakePHP?
+
+
+
+🚀 **Rapid Development**
+Scaffold applications in minutes with powerful code generation tools.
+
+🔒 **Security First**
+Built-in protection against SQL injection, XSS, CSRF, and more.
+
+📦 **Batteries Included**
+ORM, validation, caching, authentication — everything you need out of the box.
+
+🎯 **Convention over Configuration**
+Sensible defaults mean less setup, more coding.
+
+
+
+
+## Quick Start
+
+Get a CakePHP application running in under 5 minutes:
+
+::: code-group
+
+```bash [Composer]
+# Create new project
+composer create-project --prefer-dist cakephp/app:~5.0 my_app
+
+# Start development server
+cd my_app
+bin/cake server
+
+# Open http://localhost:8765
+```
+
+```bash [DDEV]
+# Setup with DDEV
+mkdir my-cakephp-app && cd my-cakephp-app
+ddev config --project-type=cakephp --docroot=webroot
+ddev composer create --prefer-dist cakephp/app:~5.0
+ddev launch
+```
+
+```bash [Docker]
+# Using official PHP image
+docker run -it --rm -v $(pwd):/app composer create-project \
+ --prefer-dist cakephp/app:~5.0 my_app
+
+cd my_app
+docker run -it --rm -p 8765:8765 -v $(pwd):/app \
+ -w /app php:8.2-cli php bin/cake server -H 0.0.0.0
+```
+
+:::
+
+> [!TIP]
+> You should see a welcome page with green checkmarks at **http://localhost:8765**
+
+::: details System Requirements
+Make sure your system meets these requirements before getting started:
+
+| Component | Version |
+|-----------|------|
+| PHP | 8.2 - 8.5 |
+| Database | MySQL 5.7+, PostgreSQL 9.6+, SQLite 3, SQL Server 2012+ |
+| Extensions | `mbstring`, `intl`, `pdo`, `simplexml` |
+| Composer | Latest |
+:::
+
+## Your First Application
+
+Let's build a simple blog in 10 minutes! Follow along with this hands-on tutorial.
+
+> [!NOTE]
+> This tutorial assumes you already have a database created. Use your database management tool to create a database named `blog` before proceeding.
+
+### Step 1: Configure Database Connection
+
+Update your database credentials in **config/app_local.php**:
+
+```php{7-11} [config/app_local.php]
+ [
+ 'default' => [
+ 'host' => 'localhost',
+ 'username' => 'my_user',
+ 'password' => 'secret',
+ 'database' => 'blog',
+ 'encoding' => 'utf8mb4',
+ ],
+ ],
+];
+```
+
+::: tip Configuration Files
+- **config/app.php** - Default configuration (committed to git)
+- **config/app_local.php** - Local overrides (gitignored)
+:::
+
+### Step 2: Create Database Tables
+
+Choose your preferred approach for creating the database schema:
+
+> [!NOTE]
+> **Migrations** are recommended for team projects and production - they're version-controlled and database-agnostic.
+> **Raw SQL** is fine for quick prototyping or if you prefer direct database control.
+
+#### Option A: Using Migrations
+
+::: code-group
+
+```bash [Commands]
+# Step 1: Generate a migration file
+bin/cake bake migration CreateArticles
+# This creates: config/Migrations/YYYYMMDDHHMMSS_CreateArticles.php
+
+# Step 2: Edit the generated file to define your table structure
+# See the "Migration File" tab for the complete example
+# Open: config/Migrations/YYYYMMDDHHMMSS_CreateArticles.php
+
+# Step 3: Run the migration to create the table
+bin/cake migrations migrate
+
+# Step 4: (Optional) Generate and run seed data
+bin/cake bake seed Articles
+bin/cake migrations seed
+```
+
+```php [Migration File]
+table('articles');
+ $table->addColumn('title', 'string', ['limit' => 255])
+ ->addColumn('slug', 'string', ['limit' => 191])
+ ->addColumn('body', 'text', ['null' => true])
+ ->addColumn('published', 'boolean', ['default' => false])
+ ->addColumn('created', 'datetime')
+ ->addColumn('modified', 'datetime')
+ ->addIndex(['slug'], ['unique' => true])
+ ->create();
+ }
+}
+```
+
+```php [Seed File]
+ 'First Post',
+ 'slug' => 'first-post',
+ 'body' => 'This is my first blog post!',
+ 'published' => true,
+ 'created' => date('Y-m-d H:i:s'),
+ 'modified' => date('Y-m-d H:i:s'),
+ ],
+ [
+ 'title' => 'Second Post',
+ 'slug' => 'second-post',
+ 'body' => 'Another great article.',
+ 'published' => true,
+ 'created' => date('Y-m-d H:i:s'),
+ 'modified' => date('Y-m-d H:i:s'),
+ ],
+ ];
+
+ $table = $this->table('articles');
+ $table->insert($data)->save();
+ }
+}
+```
+
+:::
+
+#### Option B: Using Raw SQL
+
+::: code-group
+
+```sql [MySQL]
+USE blog;
+
+CREATE TABLE articles (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ title VARCHAR(255) NOT NULL,
+ slug VARCHAR(191) UNIQUE,
+ body TEXT,
+ published BOOLEAN DEFAULT FALSE,
+ created DATETIME,
+ modified DATETIME
+) ENGINE=InnoDB;
+
+INSERT INTO articles (title, slug, body, published, created, modified)
+VALUES ('First Post', 'first-post', 'This is my first blog post!', TRUE, NOW(), NOW());
+```
+
+```sql [PostgreSQL]
+\c blog
+
+CREATE TABLE articles (
+ id SERIAL PRIMARY KEY,
+ title VARCHAR(255) NOT NULL,
+ slug VARCHAR(191) UNIQUE,
+ body TEXT,
+ published BOOLEAN DEFAULT FALSE,
+ created TIMESTAMP,
+ modified TIMESTAMP
+);
+
+INSERT INTO articles (title, slug, body, published, created, modified)
+VALUES ('First Post', 'first-post', 'This is my first blog post!', TRUE, NOW(), NOW());
+```
+
+```sql [SQLite]
+CREATE TABLE articles (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title VARCHAR(255) NOT NULL,
+ slug VARCHAR(191) UNIQUE,
+ body TEXT,
+ published BOOLEAN DEFAULT 0,
+ created DATETIME,
+ modified DATETIME
+);
+
+INSERT INTO articles (title, slug, body, published, created, modified)
+VALUES ('First Post', 'first-post', 'This is my first blog post!', 1, datetime('now'), datetime('now'));
+```
+
+:::
+
+### Step 3: Generate Your First Model
+
+Use CakePHP's code generation tool:
+
+```bash
+# Generate model classes
+bin/cake bake model Articles
+
+# Output:
+# ✓ Created: src/Model/Table/ArticlesTable.php
+# ✓ Created: src/Model/Entity/Article.php
+# ✓ Created: tests/TestCase/Model/Table/ArticlesTableTest.php
+```
+
+This creates:
+
+::: code-group
+
+```php [ArticlesTable.php]
+setTable('articles');
+ $this->setDisplayField('title');
+ $this->setPrimaryKey('id');
+
+ $this->addBehavior('Timestamp');
+ }
+}
+```
+
+```php [Article.php]
+ true,
+ 'slug' => true,
+ 'body' => true,
+ 'published' => true,
+ 'created' => true,
+ 'modified' => true,
+ ];
+}
+```
+
+:::
+
+### Step 4: Create Your Controller
+
+Generate a controller with views:
+
+```bash
+bin/cake bake controller Articles
+```
+
+```php [src/Controller/ArticlesController.php]
+Articles->find('all')
+ ->where(['published' => true])
+ ->orderBy(['created' => 'DESC']);
+
+ $this->set(compact('articles'));
+ }
+
+ public function view(?string $slug = null): void
+ {
+ $article = $this->Articles
+ ->findBySlug($slug)
+ ->firstOrFail();
+
+ $this->set(compact('article'));
+ }
+
+ public function add(): void
+ {
+ $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 save article.');
+ }
+
+ $this->set(compact('article'));
+ }
+}
+```
+
+### Step 5: Create Your Views
+
+Create a simple list view in **templates/Articles/index.php**:
+
+```php [templates/Articles/index.php]
+
+
+
+
+= $this->Html->link('New Article', ['action' => 'add'], ['class' => 'button']) ?>
+```
+
+> [!TIP]
+> Visit **http://localhost:8765/articles** to see your blog in action!
+
+## Next Steps
+
+Now that you've built your first CakePHP application, here are some great places to continue your journey:
+
+**Learn the Fundamentals:**
+- [CakePHP Conventions](intro/conventions) - Understanding the naming conventions that power CakePHP
+- [MVC Pattern](intro) - How CakePHP structures applications
+- [Configuration](development/configuration) - Customizing your application
+
+**Build Real Applications:**
+- [Tutorials & Examples](tutorials-and-examples) - Step-by-step guides
+- [CMS Tutorial](tutorials-and-examples/cms/installation) - Build a complete content management system
+
+**Master Core Features:**
+- [Database & ORM](orm) - Advanced queries, associations, and data modeling
+- [Controllers](controllers) - Request handling, components, and middleware
+- [Views](views) - Templates, helpers, and rendering
+- [Security](security) - Best practices for secure applications
+
+## Get Help
+
+Join our community and get the support you need:
+
+
+
+**Additional Resources:**
+- [API Documentation](https://api.cakephp.org/5.0/) - Complete API reference
+- [Stack Overflow](https://stackoverflow.com/questions/tagged/cakephp) - Find answers to common questions
+
+## How CakePHP Works
+
+Understanding the request flow helps you build better applications. Here's what happens when a user visits your CakePHP application:
+
+
+
+
+
+**A typical request follows these steps:**
+
+1. 🌐 **Request arrives** → Webserver routes to `webroot/index.php`
+2. ⚙️ **Application boots** → Your app is loaded and middleware initializes
+3. 🛣️ **Routing** → Request is matched to a controller and action
+4. 🎮 **Controller** → Action is called, interacts with Models
+5. 📊 **Model Layer** → Fetches and processes data from the database
+6. 🎨 **View Layer** → Renders the response (HTML, JSON, XML, etc.)
+7. 📤 **Response sent** → Back through middleware to the client
+
+::: tip Understanding MVC
+The **Model** handles your data and business logic, the **Controller** coordinates the request, and the **View** presents the data. This separation keeps your code organized and testable.
+
+[Learn more about MVC in CakePHP →](intro)
+:::
+
+## Everything You Need, Out of the Box
+
+CakePHP comes with powerful features that save you time and help you build better applications:
+
+::: details 🚀 Code Generation (Bake)
+Generate complete CRUD applications in seconds:
+```bash
+bin/cake bake all Articles
+# Creates: Model, Controller, Views, Tests
+```
+Bake can scaffold entire features, saving hours of repetitive coding. Perfect for prototyping or generating boilerplate.
+
+[Learn more about Bake →](https://book.cakephp.org/bake/2/en/)
+:::
+
+::: details 💾 Caching Framework
+Integrated caching with multiple backends:
+```php
+// Cache expensive operations
+$results = Cache::remember('expensive_query', function () {
+ return $this->Articles->find('complex')->toArray();
+});
+```
+**Supported backends:** Redis, Memcached, APCu, File, Database
+
+[Learn more about Caching →](core-libraries/caching)
+:::
+
+::: details 🧪 Built-in Testing
+Write tests with confidence using the integrated testing framework:
+```php
+public function testAddArticle(): void
+{
+ $this->post('/articles/add', ['title' => 'Test']);
+ $this->assertResponseOk();
+ $this->assertFlashMessage('Article saved!');
+}
+```
+Supports unit tests, integration tests, and browser tests out of the box.
+
+[Learn more about Testing →](development/testing)
+:::
+
+::: details 🔐 Authentication & Authorization
+Drop-in user management with flexible policies:
+```bash
+composer require cakephp/authentication cakephp/authorization
+```
+Handle login, permissions, and access control with minimal configuration.
+
+[Authentication Guide →](https://book.cakephp.org/authentication/) • [Authorization Guide →](https://book.cakephp.org/authorization/)
+:::
+
+::: details 🌐 REST API Support
+Build APIs with automatic content type negotiation:
+```php
+// Automatically serves JSON/XML based on Accept header
+public function index()
+{
+ $articles = $this->Articles->find('all');
+ $this->set('articles', $articles);
+ $this->viewBuilder()->setOption('serialize', ['articles']);
+}
+```
+Supports JSON, XML, and custom formats with minimal code.
+
+[Learn more about REST →](development/rest)
+:::
+
+::: details 📦 Database Migrations
+Version control your database schema:
+```bash
+bin/cake bake migration CreateArticles
+bin/cake migrations migrate
+```
+Keep your database changes in sync across development, staging, and production.
+
+[Learn more about Migrations →](https://book.cakephp.org/migrations/)
+:::
+
+## What Makes CakePHP Special?
+
+::: code-group
+
+```php [Convention Over Configuration]
+Articles->find('all');
+ $this->set(compact('articles'));
+ }
+}
+```
+
+```php [Powerful ORM]
+Articles->find()
+ ->contain(['Users', 'Comments'])
+ ->matching('Tags', function ($q) {
+ return $q->where(['Tags.name IN' => ['PHP', 'CakePHP']]);
+ })
+ ->where(['Articles.published' => true])
+ ->orderBy(['Articles.view_count' => 'DESC'])
+ ->limit(10);
+```
+
+```php [Built-in Security]
+Articles->newEntity($data);
+// Only $_accessible fields can be set
+```
+
+```bash [Code Generation]
+# Generate complete CRUD in seconds
+bin/cake bake all Articles
+
+# Creates:
+# ✓ Model (Table + Entity)
+# ✓ Controller (with all actions)
+# ✓ Templates (index, view, add, edit)
+# ✓ Tests (full coverage)
+```
+
+:::
+
+
+
+
+
+
+
+
+
Ready to Build Something Amazing?
+
+**Start your CakePHP journey today and join thousands of developers building modern web applications.**
+
+
diff --git a/docs/en/installation.md b/docs/en/installation.md
new file mode 100644
index 0000000000..52cf3698f8
--- /dev/null
+++ b/docs/en/installation.md
@@ -0,0 +1,502 @@
+---
+title: Installation Guide
+description: Learn how to install CakePHP using Composer, Docker, or DDEV. Complete setup guide for PHP 8.2+ with all system requirements and web server configurations.
+---
+
+# Installation
+
+CakePHP is designed to be easy to install and configure. This guide will walk you through getting CakePHP up and running in just a few minutes.
+
+## System Requirements
+
+::: tip Quick Check
+Verify your PHP version meets the requirements:
+```bash
+php -v
+```
+:::
+
+**Minimum Requirements:**
+
+| Component | Version |
+|-----------|---------|
+| PHP | |minphpversion| (|phpversion| supported) |
+| Extensions | `mbstring`, `intl`, `pdo`, `simplexml` |
+| Composer | Latest stable |
+
+**Supported Databases:**
+
+- MySQL 5.7+
+- MariaDB 10.1+
+- PostgreSQL 9.6+
+- Microsoft SQL Server 2012+
+- SQLite 3.8.9+
+
+::: warning Web Server Requirements
+Your web server's PHP version must match your CLI PHP version (|minphpversion|+). All database drivers require the appropriate PDO extension.
+:::
+
+## Installation Methods
+
+Choose the method that best fits your workflow:
+
+### Method 1: Composer (Recommended)
+
+The standard way to install CakePHP:
+
+::: code-group
+
+```bash [Install Composer]
+# Linux/macOS
+curl -sS https://getcomposer.org/installer | php
+sudo mv composer.phar /usr/local/bin/composer
+
+# Verify installation
+composer --version
+```
+
+```powershell [Windows]
+# Download and run the Windows installer
+# https://getcomposer.org/Composer-Setup.exe
+
+# Verify installation
+composer --version
+```
+
+```bash [Create Project]
+# Create a new CakePHP 5 application
+composer create-project --prefer-dist cakephp/app:~|cakeversion| my_app_name
+
+# Navigate to your app
+cd my_app_name
+
+# Start development server
+bin/cake server
+
+# Or if you have frankenphp available
+bin/cake server --frankenphp
+```
+
+:::
+
+::: tip Version Constraints
+Your `composer.json` version constraint controls updates:
+- `"cakephp/cakephp": "|cakeversion|.*"` - Patch releases only (recommended)
+- `"cakephp/cakephp": "^|cakeversion|"` - Minor + patch releases (may require config changes)
+:::
+
+### Method 2: DDEV (Fast Setup)
+
+Perfect for local development environments:
+
+::: code-group
+
+```bash [New Project]
+# Create and configure project
+mkdir my-cakephp-app && cd my-cakephp-app
+ddev config --project-type=cakephp --docroot=webroot
+ddev composer create --prefer-dist cakephp/app:~|cakeversion|
+
+# Launch in browser
+ddev launch
+```
+
+```bash [Existing Project]
+# Clone your repository
+git clone
+cd
+
+# Configure DDEV
+ddev config --project-type=cakephp --docroot=webroot
+ddev composer install
+
+# Launch in browser
+ddev launch
+```
+
+:::
+
+::: info Learn More
+Check [DDEV Documentation](https://ddev.readthedocs.io/) for installation and advanced configuration.
+:::
+
+### Method 3: Docker
+
+For containerized development:
+
+```bash
+# Create project using Composer in Docker
+docker run --rm -v $(pwd):/app composer create-project \
+ --prefer-dist cakephp/app:~|cakeversion| my_app
+
+# Start PHP development server (install required extensions first)
+cd my_app
+docker run -it --rm -p 8765:8765 -v $(pwd):/app \
+ -w /app php:8.2-cli bash -c "apt-get update && apt-get install -y libicu-dev && docker-php-ext-install intl mbstring && php bin/cake server -H 0.0.0.0"
+```
+
+::: warning Development Only
+The built-in server is for development only. Never use it in production environments.
+:::
+
+## File Permissions
+
+CakePHP uses the **tmp** and **logs** directories for various operations like caching, sessions, and logging.
+
+::: warning Permission Setup Required
+Ensure **logs** and **tmp** (including all subdirectories) are writable by your web server user.
+:::
+
+### Quick Setup (Unix/Linux/macOS)
+
+If your web server and CLI users differ, set permissions for the directories:
+
+::: code-group
+
+```bash [Linux with ACL]
+# Auto-detect web server user and set permissions using ACL
+HTTPDUSER=`ps aux | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\ -f1`
+setfacl -R -m u:${HTTPDUSER}:rwx tmp logs
+setfacl -R -d -m u:${HTTPDUSER}:rwx tmp logs
+```
+
+```bash [macOS / Without ACL]
+# Auto-detect web server user and set permissions using chmod
+HTTPDUSER=`ps aux | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\ -f1`
+sudo chown -R $(whoami):${HTTPDUSER} tmp logs
+sudo chmod -R 775 tmp logs
+```
+
+```bash [Simple Alternative]
+# If auto-detection doesn't work, use broader permissions
+chmod -R 777 tmp logs
+```
+
+:::
+
+::: warning macOS Note
+macOS does not include `setfacl` by default. Use the chmod method or install ACL tools via Homebrew: `brew install acl`
+:::
+
+### Make Console Executable
+
+::: code-group
+
+```bash [Unix/Linux/macOS]
+chmod +x bin/cake
+```
+
+```bash [Windows]
+# .bat file is already executable
+# For WSL or shared directories, ensure execute permissions are shared
+```
+
+```bash [Alternative]
+# If you cannot change permissions
+php bin/cake.php
+```
+
+:::
+
+## Development Server
+
+The fastest way to get started. CakePHP includes a development server built on PHP's built-in web server:
+
+::: code-group
+
+```bash [Default]
+# Starts at http://localhost:8765
+bin/cake server
+```
+
+```bash [Custom Host/Port]
+# Useful for network access or port conflicts
+bin/cake server -H 192.168.1.100 -p 5000
+```
+
+```bash [Network Access]
+# Allow access from other devices on your network
+bin/cake server -H 0.0.0.0
+```
+
+:::
+
+::: tip Success!
+Visit **http://localhost:8765** and you should see the CakePHP welcome page with green checkmarks.
+:::
+
+::: danger Production Warning
+**Never** use the development server in production. It's designed only for local development and lacks security hardening, performance optimizations, and proper process management.
+:::
+
+## Production Deployment
+
+For production environments, configure your web server to serve from the **webroot** directory.
+
+### Directory Structure
+
+After installation, your structure should look like this:
+
+```text
+my_app/
+├── bin/
+├── config/
+├── logs/
+├── plugins/
+├── src/
+├── templates/
+├── tests/
+├── tmp/
+├── vendor/
+├── webroot/ # ← Web server document root
+│ ├── css/
+│ ├── img/
+│ ├── js/
+│ ├── .htaccess
+│ └── index.php
+├── .gitignore
+├── .htaccess
+├── composer.json
+└── README.md
+```
+
+::: tip Configuration Required
+Point your web server's DocumentRoot to `/path/to/my_app/webroot/`
+:::
+
+## Web Server Configuration
+
+::: info Configuration Examples
+The following examples are illustrative starting points. You should fine-tune these configurations to match your application's specific requirements, security policies, and performance needs.
+:::
+
+Choose your web server and follow the appropriate configuration:
+
+### Apache
+
+Apache works out of the box with CakePHP's included `.htaccess` files.
+
+::: code-group
+
+```apache [Virtual Host]
+
+ ServerName myapp.local
+ DocumentRoot /var/www/myapp/webroot
+
+
+ Options FollowSymLinks
+ AllowOverride All
+ Require all granted
+
+
+ ErrorLog ${APACHE_LOG_DIR}/myapp_error.log
+ CustomLog ${APACHE_LOG_DIR}/myapp_access.log combined
+
+```
+
+```apache [Enable mod_rewrite]
+# Ensure mod_rewrite is enabled
+LoadModule rewrite_module modules/mod_rewrite.so
+
+# Verify with:
+apache2ctl -M | grep rewrite
+```
+
+```apache [Subdirectory Install]
+# If installing in a subdirectory like /~username/myapp/
+
+ RewriteEngine On
+ RewriteBase /~username/myapp
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^ index.php [L]
+
+```
+
+:::
+
+::: details Troubleshooting Apache
+**If rewrites aren't working:**
+
+1. Verify `AllowOverride All` is set in your DocumentRoot directive
+2. Check that `.htaccess` files exist in root and webroot directories
+3. Ensure `mod_rewrite` is loaded
+4. Restart Apache after configuration changes
+
+**Performance optimization:**
+```apache
+# Add to webroot/.htaccess to prevent CakePHP from handling static assets
+RewriteCond %{REQUEST_URI} !^/(webroot/)?(img|css|js)/(.*)$
+```
+:::
+
+### nginx
+
+nginx requires manual rewrite configuration:
+
+```nginx
+server {
+ listen 80;
+ server_name myapp.local;
+
+ root /var/www/myapp/webroot;
+ index index.php;
+
+ access_log /var/log/nginx/myapp_access.log;
+ error_log /var/log/nginx/myapp_error.log;
+
+ location / {
+ try_files $uri $uri/ /index.php?$args;
+ }
+
+ location ~ \.php$ {
+ try_files $uri =404;
+ include fastcgi_params;
+ fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
+ fastcgi_index index.php;
+ fastcgi_intercept_errors on;
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+ }
+}
+```
+
+::: warning PHP-FPM Socket
+Modern PHP-FPM uses Unix sockets instead of TCP. Update `fastcgi_pass` to match your setup:
+- Unix socket: `unix:/var/run/php/php8.2-fpm.sock`
+- TCP: `127.0.0.1:9000`
+:::
+
+### Caddy / FrankenPHP
+
+Modern web server with automatic HTTPS. FrankenPHP extends Caddy with a built-in PHP runtime:
+
+::: code-group
+
+```text [Caddy + PHP-FPM]
+myapp.local {
+ root * /var/www/myapp/webroot
+ php_fastcgi unix//var/run/php/php8.2-fpm.sock
+ encode gzip
+ file_server
+
+ try_files {path} {path}/ /index.php?{query}
+}
+```
+
+```dockerfile [FrankenPHP Docker]
+# Dockerfile in your project root
+FROM dunglas/frankenphp
+
+# Copy your CakePHP application
+COPY . /app
+
+# FrankenPHP defaults to /app/public as document root.
+# CakePHP uses webroot/ instead, so override it:
+ENV SERVER_ROOT=/app/webroot
+
+# Install dependencies (composer.json lives in /app)
+RUN composer install --no-dev --optimize-autoloader
+
+# Build and run:
+# docker build -t myapp .
+# docker run -p 80:80 -p 443:443 myapp
+```
+
+```bash [FrankenPHP Binary]
+# Download FrankenPHP
+curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 -o frankenphp
+chmod +x frankenphp
+
+# Run with your CakePHP app
+./frankenphp php-server --root /var/www/myapp/webroot
+```
+
+```text [FrankenPHP Caddyfile]
+# Caddyfile in your project root
+{
+ frankenphp
+}
+
+myapp.local {
+ root * /var/www/myapp/webroot
+ php_server
+ encode zstd gzip
+ file_server
+}
+```
+
+:::
+
+::: tip Local Development
+For local development, you can use the built-in CakePHP development server with FrankenPHP support:
+```bash
+bin/cake server --frankenphp
+```
+This requires the `frankenphp` binary to be available in your `PATH`.
+:::
+
+::: info Why FrankenPHP?
+FrankenPHP combines PHP with Caddy, providing automatic HTTPS, HTTP/3, and modern compression without needing PHP-FPM. Particularly efficient for containerized deployments.
+:::
+
+### IIS (Windows)
+
+1. Install [URL Rewrite Module 2.0](https://www.iis.net/downloads/microsoft/url-rewrite)
+2. Create `web.config` in your application root:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Without URL Rewriting
+
+If you cannot enable URL rewriting, you can use CakePHP's built-in non-rewritten `index.php` URLs.
+
+In **config/app.php**, uncomment:
+
+```php
+'App' => [
+ // ...
+ 'baseUrl' => env('SCRIPT_NAME'),
+]
+```
+
+Remove these `.htaccess` files:
+- `/.htaccess`
+- `/webroot/.htaccess`
+
+Your URLs will include `index.php`:
+- **With rewriting:** `https://myapp.com/articles/view/1`
+- **Without rewriting:** `https://myapp.com/index.php/articles/view/1`
+
+## Next Steps
+
+Your CakePHP installation is complete! Here's what to do next:
+
+::: tip Ready to Build?
+Follow the [Quick Start Guide](quickstart) to create your first CakePHP application in minutes.
+:::
+
+**Learn More:**
+- [Configuration](development/configuration) - Customize your application
+- [Database Setup](orm/database-basics) - Connect to your database
+- [Tutorials](tutorials-and-examples) - Step-by-step guides
diff --git a/docs/en/intro.md b/docs/en/intro.md
new file mode 100644
index 0000000000..e2ad374b63
--- /dev/null
+++ b/docs/en/intro.md
@@ -0,0 +1,164 @@
+# CakePHP at a Glance
+
+CakePHP is designed to make common web-development tasks simple, and easy. By
+providing an all-in-one toolbox to get you started the various parts of CakePHP
+work well together or separately.
+
+The goal of this overview is to introduce the general concepts in CakePHP, and
+give you a quick overview of how those concepts are implemented in CakePHP. If
+you are itching to get started on a project, you can [start with the
+tutorial](tutorials-and-examples/cms/installation), or [dive into the docs](topics).
+
+## Conventions Over Configuration
+
+CakePHP provides a basic organizational structure that covers class names,
+filenames, database table names, and other conventions. While the conventions
+take some time to learn, by following the conventions CakePHP provides you can
+avoid needless configuration and make a uniform application structure that makes
+working with various projects simple. The [conventions chapter](intro/conventions) covers the various conventions that CakePHP uses.
+
+## The Model Layer
+
+The Model layer represents the part of your application that implements the
+business logic. It is responsible for retrieving data and converting it into the
+primary meaningful concepts in your application. This includes processing,
+validating, associating or other tasks related to handling data.
+
+In the case of a social network, the Model layer would take care of
+tasks such as saving the user data, saving friends' associations, storing
+and retrieving user photos, finding suggestions for new friends, etc.
+The model objects can be thought of as "Friend", "User", "Comment", or
+"Photo". If we wanted to load some data from our `users` table we could do:
+
+``` php
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+$users = $this->fetchTable('Users');
+$resultset = $users->find()->all();
+foreach ($resultset as $row) {
+ echo $row->username;
+}
+```
+
+You may notice that we didn't have to write any code before we could start
+working with our data. By using conventions, CakePHP will use standard classes
+for table and entity classes that have not yet been defined.
+
+If we wanted to make a new user and save it (with validation) we would do
+something like:
+
+``` php
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+$users = $this->fetchTable('Users');
+$user = $users->newEntity(['email' => 'mark@example.com']);
+$users->save($user);
+```
+
+## The View Layer
+
+The View layer renders a presentation of modeled data. Being separate from the
+Model objects, it is responsible for using the information it has available
+to produce any presentational interface your application might need.
+
+For example, the view could use model data to render an HTML view template containing it,
+or a XML formatted result for others to consume:
+
+``` php
+// In a view template file, we'll render an 'element' for each user.
+
+
+
+```
+
+The View layer provides a number of extension points like [View Templates](views#view-templates), [View Elements](views#view-elements)
+and [View Cells](views/cells) to let you re-use your presentation logic.
+
+The View layer is not only limited to HTML or text representation of the data.
+It can be used to deliver common data formats like JSON, XML, and through
+a pluggable architecture any other format you may need, such as CSV.
+
+## The Controller Layer
+
+The Controller layer handles requests from users. It is responsible for
+rendering a response with the aid of both the Model and the View layers.
+
+A controller can be seen as a manager that ensures that all resources needed for
+completing a task are delegated to the correct workers. It waits for petitions
+from clients, checks their validity according to authentication or authorization
+rules, delegates data fetching or processing to the model, selects the type of
+presentational data that the clients are accepting, and finally delegates the
+rendering process to the View layer. An example of a user registration
+controller would be:
+
+``` php
+public function add()
+{
+ $user = $this->Users->newEmptyEntity();
+ if ($this->request->is('post')) {
+ $user = $this->Users->patchEntity($user, $this->request->getData());
+ if ($this->Users->save($user, ['validate' => 'registration'])) {
+ $this->Flash->success(__('You are now registered.'));
+ } else {
+ $this->Flash->error(__('There were some problems.'));
+ }
+ }
+ $this->set('user', $user);
+}
+```
+
+You may notice that we never explicitly rendered a view. CakePHP's conventions
+will take care of selecting the right view and rendering it with the view data
+we prepared with `set()`.
+
+
+
+## CakePHP Request Cycle
+
+Now that you are familiar with the different layers in CakePHP, lets review how
+a request cycle works in CakePHP:
+
+
+
+
+
+The typical CakePHP request cycle starts with a user requesting a page or
+resource in your application. At a high level each request goes through the
+following steps:
+
+1. The webserver rewrite rules direct the request to **webroot/index.php**.
+2. Your Application is loaded and bound to an `HttpServer`.
+3. Your application's middleware is initialized.
+4. A request and response is dispatched through the PSR-7 Middleware that your
+ application uses. Typically this includes error trapping and routing.
+5. If no response is returned from the middleware and the request contains
+ routing information, a controller & action are selected.
+6. The controller's action is called and the controller interacts with the
+ required Models and Components.
+7. The controller delegates response creation to the View to generate the output
+ resulting from the model data.
+8. The view uses Helpers and Cells to generate the response body and headers.
+9. The response is sent back out through the [Middleware](controllers/middleware).
+10. The `HttpServer` emits the response to the webserver.
+
+## Just the Start
+
+Hopefully this quick overview has piqued your interest. Some other great
+features in CakePHP are:
+
+- A [caching](core-libraries/caching) framework that integrates with
+ Memcached, Redis and other backends.
+- Powerful [code generation tools](bake/usage) so you can start immediately.
+- [Integrated testing framework](development/testing) so you can ensure
+ your code works perfectly.
+
+The next obvious steps are to [download CakePHP](installation), read the
+[tutorial and build something awesome](tutorials-and-examples/cms/installation).
+
+## Additional Reading
+
+- [Where to Get Help](intro/where-to-get-help)
+- [CakePHP Conventions](intro/conventions)
+- [CakePHP Folder Structure](intro/cakephp-folder-structure)
diff --git a/docs/en/intro/cakephp-folder-structure.md b/docs/en/intro/cakephp-folder-structure.md
new file mode 100644
index 0000000000..d8d3b3bf23
--- /dev/null
+++ b/docs/en/intro/cakephp-folder-structure.md
@@ -0,0 +1,69 @@
+# CakePHP Folder Structure
+
+After you've downloaded the CakePHP application skeleton, there are a few top
+level folders you should see:
+
+- `bin/` holds the Cake console executables so you can execute e.g. `bin/cake bake all`.
+
+- `config/` holds the [Configuration](../development/configuration) files.
+ Database connection details, bootstrapping, core configuration files
+ and more should be stored here.
+
+- `plugins/` is where the [Plugins](../plugins) your application uses are stored.
+
+- `logs/` contains your log files, can be adjusted via [Log Configuration](../core-libraries/logging.md#logging-configuration).
+
+- `src/` will be where your application's source files like Controllers, Models, Commands etc. will be placed.
+
+- `templates/` has presentational files placed here:
+ elements, error pages, layouts, and view template files.
+
+- `resources/` is primarily used for the `locales/` subfolder storing language files for static internationalization.
+
+- `tests/` will be where you put the test cases for your application.
+
+- `tmp/` is where CakePHP stores temporary data. The actual data it
+ stores depends on how you have CakePHP configured, but this folder
+ is usually used to store translation messages, model descriptions and sometimes
+ session information.
+
+- `vendor/` is where CakePHP and other application dependencies will
+ be installed by [Composer](https://getcomposer.org). **Editing these files is not
+ advised, as Composer will overwrite your changes next time you update.**
+
+- `webroot/` is the public document root of your application. It
+ contains all the files you want to be publicly reachable.
+
+Make sure that the `tmp/` and `logs/` folders exist and are writable,
+otherwise the performance of your application will be severely
+impacted. In debug mode, CakePHP will warn you if these directories are not
+writable.
+
+## The src Folder
+
+CakePHP's `src/` folder is where you will do most of your application
+development. Let's look a little closer at the folders inside.
+
+### Command
+Contains your application's console commands. See
+[Command Objects](../console-commands/commands) to learn more.
+
+> [!NOTE]
+> The folder `Command/` is not present by default.
+> It will be auto generated when you create your first command using bake.
+
+### Console
+Contains the installation script executed by Composer.
+
+### Controller
+Contains your application's [Controllers](../controllers) and their components.
+
+### Middleware
+Stores any [Middleware](../controllers/middleware) for your application.
+
+### Model
+Contains your application's [Tables](../orm/table-objects.md), [Entities](../orm/entities.md) and [Behaviors](../orm/behaviors.md).
+
+### View
+Presentational classes are placed here: [Views](../views.md), [Cells](../views/cells.md), [Helpers](../views/helpers.md).
+
diff --git a/docs/en/intro/conventions.md b/docs/en/intro/conventions.md
new file mode 100644
index 0000000000..03eaf0974f
--- /dev/null
+++ b/docs/en/intro/conventions.md
@@ -0,0 +1,296 @@
+# CakePHP Conventions
+
+We are big fans of convention over configuration. While it takes a bit of time
+to learn CakePHP's conventions, you save time in the long run. By following
+conventions, you get free functionality, and you liberate yourself from the
+maintenance nightmare of tracking config files. Conventions also make for a very
+uniform development experience, allowing other developers to jump in and help.
+
+## Controller Conventions
+
+Controller class names are plural, CamelCased, and end in `Controller`.
+`UsersController` and `MenuLinksController` are both examples of
+conventional controller names.
+
+Public methods on Controllers are often exposed as 'actions' accessible through
+a web browser. They are camelBacked. For example the `/users/view-me` maps to the `viewMe()` method
+of the `UsersController` out of the box (if one uses default dashed inflection in routing).
+Protected or private methods cannot be accessed with routing.
+
+For inflection of acronyms it is useful to treat them as words, so `CMS` would be `Cms`.
+
+### URL Considerations for Controller Names
+
+As you've just seen, single word controllers map to a simple lower case URL
+path. For example, `UsersController` (which would be defined in the file name
+**UsersController.php**) is accessed from `http://example.com/users`.
+
+While you can route multiple word controllers in any way you like, the
+convention is that your URLs are lowercase and dashed using the `DashedRoute`
+class, therefore `/menu-links/view-all` is the correct form to access
+the `MenuLinksController::viewAll()` action.
+
+When you create links using `this->Html->link()`, you can use the following
+conventions for the url array:
+
+``` php
+$this->Html->link('link-title', [
+ 'prefix' => 'MyPrefix' // CamelCased
+ 'plugin' => 'MyPlugin', // CamelCased
+ 'controller' => 'ControllerName', // CamelCased
+ 'action' => 'actionName' // camelBacked
+]
+```
+
+For more information on CakePHP URLs and parameter handling, see
+[Routes Configuration](../development/routing#routes-configuration).
+
+
+
+## File and Class Name Conventions
+
+In general, filenames match the class names, and follow the PSR-4 standard for
+autoloading. The following are some examples of class names and their filenames:
+
+- The Controller class `LatestArticlesController` would be found in a file
+ named **LatestArticlesController.php**
+- The Component class `MyHandyComponent` would be found in a file named
+ **MyHandyComponent.php**
+- The Table class `OptionValuesTable` would be found in a file named
+ **OptionValuesTable.php**.
+- The Entity class `OptionValue` would be found in a file named
+ **OptionValue.php**.
+- The Behavior class `EspeciallyFunkableBehavior` would be found in a file
+ named **EspeciallyFunkableBehavior.php**
+- The View class `SuperSimpleView` would be found in a file named
+ **SuperSimpleView.php**
+- The Helper class `BestEverHelper` would be found in a file named
+ **BestEverHelper.php**
+
+Each file would be located in the appropriate folder/namespace in your app
+folder.
+
+
+
+## Database Conventions
+
+Table names corresponding to CakePHP models are plural and underscored. For
+example `users`, `menu_links`, and `user_favorite_pages`
+respectively. Table name whose name contains multiple words should only
+pluralize the last word, for example, `menu_links`.
+
+Column names with two or more words are underscored, for example, `first_name`.
+
+Foreign keys in hasMany, belongsTo/hasOne relationships are recognized by
+default as the (singular) name of the related table followed by `_id`. So if
+Users hasMany Articles, the `articles` table will refer to the `users`
+table via a `user_id` foreign key. For a table like `menu_links`
+whose name contains multiple words, the foreign key would be
+`menu_link_id`.
+
+Join (or "junction") tables are used in BelongsToMany relationships between
+models. These should be named for the tables they connect. The names should be
+pluralized and sorted alphabetically: `articles_tags`, not `tags_articles`
+or `article_tags`. *The bake command will not work if this convention is not
+followed.* If the junction table holds any data other than the linking foreign
+keys, you should create a concrete entity/table class for the table.
+
+In addition to using an auto-incrementing integer as primary keys, you can also
+use UUID columns. CakePHP will create UUID values automatically using
+(`Cake\Utility\Text::uuid()`) whenever you save new records using
+the `Table::save()` method.
+
+## Model Conventions
+
+Table class names are plural, CamelCased and end in `Table`. `UsersTable`,
+`MenuLinksTable`, and `UserFavoritePagesTable` are all examples of
+table class names matching the `users`, `menu_links` and
+`user_favorite_pages` tables respectively.
+
+Entity class names are singular CamelCased and have no suffix. `User`,
+`MenuLink`, and `UserFavoritePage` are all examples of entity names
+matching the `users`, `menu_links` and `user_favorite_pages`
+tables respectively.
+
+Enum class names should use a `{Entity}{Column}` convention, and enum cases
+should use CamelCased names.
+
+## View Conventions
+
+View template files are named after the controller functions they display, in an
+underscored form. The `viewAll()` function of the `ArticlesController` class
+will look for a view template in **templates/Articles/view_all.php**.
+
+The basic pattern is
+**templates/Controller/underscored_function_name.php**.
+
+> [!NOTE]
+> By default CakePHP uses English inflections. If you have database
+> tables/columns that use another language, you will need to add inflection
+> rules (from singular to plural and vice-versa). You can use
+> `Cake\Utility\Inflector` to define your custom inflection
+> rules. See the documentation about [Inflector](../core-libraries/inflector) for more
+> information.
+
+## Plugins Conventions
+
+It is useful to prefix a CakePHP plugin with "cakephp-" in the package name.
+This makes the name semantically related on the framework it depends on.
+
+Do **not** use the CakePHP namespace (cakephp) as vendor name as this is
+reserved to CakePHP owned plugins. The convention is to use lowercase letters
+and dashes as separator:
+
+``` text
+// Bad
+cakephp/foo-bar
+
+// Good
+your-name/cakephp-foo-bar
+```
+
+See [awesome list recommendations](https://github.com/FriendsOfCake/awesome-cakephp/blob/master/CONTRIBUTING.md#tips-for-creating-cakephp-plugins)
+for details.
+
+## Summarized
+
+By naming the pieces of your application using CakePHP conventions, you gain
+functionality without the hassle and maintenance tethers of configuration.
+Here's a final example that ties the conventions together:
+
+- Database table: "articles", "menu_links"
+- Table class: `ArticlesTable`, found at **src/Model/Table/ArticlesTable.php**
+- Entity class: `Article`, found at **src/Model/Entity/Article.php**
+- Controller class: `ArticlesController`, found at
+ **src/Controller/ArticlesController.php**
+- View template, found at **templates/Articles/index.php**
+
+Using these conventions, CakePHP knows that a request to
+`http://example.com/articles` maps to a call on the `index()` method of the
+`ArticlesController`, where the `Articles` model is automatically available.
+None of these relationships have been configured by any means other than by
+creating classes and files that you'd need to create anyway.
+
+
+
+
+
+
+
+
+
+
+
Example
+
articles
+
menu_links
+
+
+
+
Database Table
+
articles
+
menu_links
+
Table names corresponding to CakePHP models are plural and underscored.
+
+
+
File
+
ArticlesController.php
+
MenuLinksController.php
+
+
+
+
Table
+
ArticlesTable.php
+
MenuLinksTable.php
+
Table class names are plural, CamelCased and end in Table
+
+
+
Entity
+
Article.php
+
MenuLink.php
+
Entity class names are singular, CamelCased: Article and MenuLink
View template files are named after the controller functions they display, in an underscored form
+
+
+
Behavior
+
ArticlesBehavior.php
+
MenuLinksBehavior.php
+
+
+
+
View
+
ArticlesView.php
+
MenuLinksView.php
+
+
+
+
Helper
+
ArticlesHelper.php
+
MenuLinksHelper.php
+
+
+
+
Component
+
ArticlesComponent.php
+
MenuLinksComponent.php
+
+
+
+
Plugin
+
Bad: cakephp/articles Good: you/cakephp-articles
+
cakephp/menu-links you/cakephp-menu-links
+
Useful to prefix a CakePHP plugin with "cakephp-" in the package name. Do not use the CakePHP namespace (cakephp) as vendor name as this is reserved to CakePHP owned plugins. The convention is to use lowercase letters and dashes as separator.
+
+
+
Each file would be located in the appropriate folder/namespace in your app folder.
+
+
+
+
+## Database Convention Summary
+
+
+
+
+
+
+
+
+
Foreign keys
+
hasMany belongsTo/ hasOne BelongsToMany
+
Relationships are recognized by default as the (singular) name of the related table followed by _id. Users hasMany Articles, articles table will refer to the users table via a user_id foreign key.
+
+
+
Multiple Words
+
menu_links whose name contains multiple words, the foreign key would be menu_link_id.
+
+
+
Auto Increment
+
In addition to using an auto-incrementing integer as primary keys, you can also use UUID columns. CakePHP will create UUID values automatically using (Cake\Utility\Text::uuid()) whenever you save new records using the Table::save() method.
+
+
+
Join tables
+
Should be named after the model tables they will join or the bake command won't work, arranged in alphabetical order (articles_tags rather than tags_articles). Additional columns on the junction table you should create a separate entity/table class for that table.
+
+
+
+
+Now that you've been introduced to CakePHP's fundamentals, you might try a run
+through the [Content Management Tutorial](../tutorials-and-examples/cms/installation) to see how things fit
+together.
diff --git a/docs/en/intro/where-to-get-help.md b/docs/en/intro/where-to-get-help.md
new file mode 100644
index 0000000000..2b0d01a1da
--- /dev/null
+++ b/docs/en/intro/where-to-get-help.md
@@ -0,0 +1,105 @@
+# Where to Get Help
+
+## The Official CakePHP website
+
+
+
+The Official CakePHP website is always a great place to visit. It features links
+to oft-used developer tools, screencasts, donation opportunities, and downloads.
+
+## The Cookbook
+
+
+
+This manual should probably be the first place you go to get answers. As with
+many other open source projects, we get new folks regularly. Try your best to
+answer your questions on your own first. Answers may come slower, but will
+remain longer – and you'll also be lightening our support load. Both the manual
+and the API have an online component.
+
+## The Bakery
+
+
+
+The CakePHP Bakery is a clearing house for all things regarding CakePHP. Check
+it out for tutorials, case studies, and code examples. Once you're acquainted
+with CakePHP, log on and share your knowledge with the community and gain
+instant fame and fortune.
+
+## The API
+
+
+
+Straight to the point and straight from the core developers, the CakePHP API
+(Application Programming Interface) is the most comprehensive documentation
+around for all the nitty gritty details of the internal workings of the
+framework. It's a straight forward code reference, so bring your propeller hat.
+
+## The Test Cases
+
+If you ever feel the information provided in the API is not sufficient, check
+out the code of the test cases provided with CakePHP in `tests/TestCase/`.
+They can serve as practical examples for function and data member usage for a class.
+
+## Slack
+
+[CakePHP Slack Support Channel](https://cakesf.slack.com/messages/german/)
+
+If you're stumped, give us a holler in the CakePHP Slack support channel.
+We'd love to hear from you, whether you need some help, want to
+find users in your area, or would like to donate your brand new sports car.
+
+## Discord
+
+[CakePHP Discord](https://discord.com/invite/k4trEMPebj)
+
+You can also join us on Discord.
+
+
+
+## Official CakePHP Forum
+
+[CakePHP Official Forum](https://discourse.cakephp.org)
+
+Our official forum where you can ask for help, suggest ideas and have a talk
+about CakePHP. It's a perfect place for quickly finding answers and help others.
+Join the CakePHP family by signing up.
+
+## Stackoverflow
+
+[https://stackoverflow.com/](https://stackoverflow.com/questions/tagged/cakephp/)
+
+Tag your questions with `cakephp` and the specific version you are using to
+enable existing users of stackoverflow to find your questions.
+
+## Where to get Help in your Language
+
+### Danish
+
+- [Danish CakePHP Slack Channel](https://cakesf.slack.com/messages/denmark/)
+
+### French
+
+- [French CakePHP Slack Channel](https://cakesf.slack.com/messages/french/)
+
+### German
+
+- [German CakePHP Slack Channel](https://cakesf.slack.com/messages/german/)
+- [German CakePHP Facebook Group](https://www.facebook.com/groups/146324018754907/)
+
+### Dutch
+
+- [Dutch CakePHP Slack Channel](https://cakesf.slack.com/messages/netherlands/)
+
+### Japanese
+
+- [Japanese CakePHP Slack Channel](https://cakesf.slack.com/messages/japanese/)
+- [Japanese CakePHP Facebook Group](https://www.facebook.com/groups/304490963004377/)
+
+### Portuguese
+
+- [Portuguese CakePHP Slack Channel](https://cakesf.slack.com/messages/portuguese/)
+
+### Spanish
+
+- [Spanish CakePHP Slack Channel](https://cakesf.slack.com/messages/spanish/)
diff --git a/docs/en/migrations.md b/docs/en/migrations.md
new file mode 100644
index 0000000000..fce5ff7ec6
--- /dev/null
+++ b/docs/en/migrations.md
@@ -0,0 +1,3 @@
+# Migrations
+
+This page has [moved](https://book.cakephp.org/migrations/4/).
diff --git a/docs/en/orm.md b/docs/en/orm.md
new file mode 100644
index 0000000000..d80c153105
--- /dev/null
+++ b/docs/en/orm.md
@@ -0,0 +1,127 @@
+# Database Access & ORM
+
+In CakePHP, working with data through the database is done with two primary object types:
+
+- **Repositories** or **table objects** provide access to collections of data.
+ They allow you to save new records, modify/delete existing ones, define
+ relations, and perform bulk operations.
+- **Entities** represent individual records and allow you to define row/record
+ level behavior & functionality.
+
+These two classes are usually responsible for managing almost everything
+that happens regarding your data, its validity, interactions and evolution
+of the information workflow in your domain of work.
+
+CakePHP's built-in ORM specializes in relational databases, but can be extended
+to support alternative datasources.
+
+The CakePHP ORM borrows ideas and concepts from both ActiveRecord and Datamapper
+patterns. It aims to create a hybrid implementation that combines aspects of
+both patterns to create a fast, simple to use ORM.
+
+Before we get started exploring the ORM, make sure you [configure your
+database connections](orm/database-basics#database-configuration).
+
+## Quick Example
+
+To get started you don't have to write any code. If you've followed the
+[CakePHP conventions for your database tables](intro/conventions#model-and-database-conventions) you can just start using the ORM. For example
+if we wanted to load some data from our `articles` table we would start off
+creating our `Articles` table class. Create
+**src/Model/Table/ArticlesTable.php** with the following code:
+
+``` php
+fetchTable('Articles')->find()->all();
+
+ foreach ($resultset as $row) {
+ echo $row->title;
+ }
+}
+```
+
+In other contexts, you can use the `LocatorAwareTrait` which add accessor methods for ORM tables:
+
+``` php
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+public function someMethod()
+{
+ $articles = $this->fetchTable('Articles');
+ // more code.
+}
+```
+
+Within a static method you can use the `Cake\Datasource\FactoryLocator`
+to get the table locator:
+
+``` php
+$articles = TableRegistry::getTableLocator()->get('Articles');
+```
+
+Table classes represent **collections** of **entities**. Next, lets create an
+entity class for our Articles. Entity classes let you define accessor and
+mutator methods, define custom logic for individual records and much more. We'll
+start off by adding the following to **src/Model/Entity/Article.php** after the
+`fetchTable('Articles');
+$resultset = $articles->find()->all();
+
+foreach ($resultset as $row) {
+ // Each row is now an instance of our Article class.
+ echo $row->title;
+}
+```
+
+CakePHP uses naming conventions to link the Table and Entity class together. If
+you need to customize which entity a table uses you can use the
+`entityClass()` method to set a specific classname.
+
+See the chapters on [Table Objects](orm/table-objects) and [Entities](orm/entities) for more
+information on how to use table objects and entities in your application.
+
+## More Information
+
+- [Database Basics](orm/database-basics)
+- [Query Builder](orm/query-builder)
+- [Table Objects](orm/table-objects)
+- [Entities](orm/entities)
+- [Associations - Linking Tables Together](orm/associations)
+- [Retrieving Data & Results Sets](orm/retrieving-data-and-resultsets)
+- [Validating Data](orm/validation)
+- [Saving Data](orm/saving-data)
+- [Deleting Data](orm/deleting-data)
+- [Behaviors](orm/behaviors)
+- [Schema System](orm/schema-system)
diff --git a/docs/en/orm/associations.md b/docs/en/orm/associations.md
new file mode 100644
index 0000000000..c4b712a038
--- /dev/null
+++ b/docs/en/orm/associations.md
@@ -0,0 +1,786 @@
+# Associations - Linking Tables Together
+
+Defining relations between different objects in your application should be
+a natural process. For example, an article may have many comments, and belong to
+an author. Authors may have many articles and comments. The four association
+types in CakePHP are: hasOne, hasMany, belongsTo, and belongsToMany.
+
+| Relationship | Association Type | Example |
+|--------------|------------------|------------------------------------|
+| one to one | hasOne | A user has one profile. |
+| one to many | hasMany | A user can have multiple articles. |
+| many to one | belongsTo | Many articles belong to a user. |
+| many to many | belongsToMany | Tags belong to many articles. |
+
+Associations are defined during the `initialize()` method of your table
+object. Methods matching the association type allow you to define the
+associations in your application. For example if we wanted to define a belongsTo
+association in our ArticlesTable:
+
+``` php
+namespace App\Model\Table;
+
+use Cake\ORM\Table;
+
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsTo('Authors');
+ }
+}
+```
+
+The simplest form of any association setup takes the table alias you want to
+associate with. By default all of the details of an association will use the
+CakePHP conventions. If you want to customize how your associations are handled
+you can modify them with setters:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsTo('Authors', [
+ 'className' => 'Publishing.Authors'
+ ])
+ ->setForeignKey('author_id')
+ ->setProperty('author');
+ }
+}
+```
+
+The property name will be the property key (of the associated entity) on the entity object, in this case:
+
+``` php
+$authorEntity = $articleEntity->author;
+```
+
+You can also use arrays to customize your associations:
+
+``` php
+$this->belongsTo('Authors', [
+ 'className' => 'Publishing.Authors',
+ 'foreignKey' => 'author_id',
+ 'propertyName' => 'author'
+]);
+```
+
+However, arrays do not offer the typehinting and autocomplete benefits that the fluent interface does.
+
+The same table can be used multiple times to define different types of
+associations. For example consider a case where you want to separate
+approved comments and those that have not been moderated yet:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->hasMany('Comments')
+ ->setFinder('approved');
+
+ $this->hasMany('UnapprovedComments', [
+ 'className' => 'Comments'
+ ])
+ ->setFinder('unapproved')
+ ->setProperty('unapproved_comments');
+ }
+}
+```
+
+As you can see, by specifying the `className` key, it is possible to use the
+same table as different associations for the same table. You can even create
+self-associated tables to create parent-child relationships:
+
+``` php
+class CategoriesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->hasMany('SubCategories', [
+ 'className' => 'Categories',
+ ]);
+
+ $this->belongsTo('ParentCategories', [
+ 'className' => 'Categories',
+ ]);
+ }
+}
+```
+
+You can also setup associations in mass by making a single call to
+`Table::addAssociations()` which accepts an array containing a set of
+table names indexed by association type as an argument:
+
+``` php
+class PostsTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addAssociations([
+ 'belongsTo' => [
+ 'Users' => ['className' => 'App\Model\Table\UsersTable'],
+ ],
+ 'hasMany' => ['Comments'],
+ 'belongsToMany' => ['Tags'],
+ ]);
+ }
+}
+```
+
+Each association type accepts multiple associations where the keys are the
+aliases, and the values are association config data. If numeric keys are used
+the values will be treated as association aliases.
+
+
+
+## HasOne Associations
+
+Let's set up a Users table with a hasOne relationship to the Addresses table.
+
+First, your database tables need to be keyed correctly. For a hasOne
+relationship to work, one table has to contain a foreign key that points to a
+record in the other table. In this case, the Addresses table will contain a field
+called 'user_id'. The basic pattern is:
+
+**hasOne:** the *other* model contains the foreign key.
+
+| Relation | Schema |
+|------------------------|-------------------|
+| Users hasOne Addresses | addresses.user_id |
+| Doctors hasOne Mentors | mentors.doctor_id |
+
+> [!NOTE]
+> It is not mandatory to follow CakePHP conventions, you can override the name
+> of any `foreignKey` in your associations definitions. Nevertheless, sticking
+> to conventions will make your code less repetitive, easier to read and to
+> maintain.
+
+Once you create the `UsersTable` and `AddressesTable` classes, you can make
+the association with the following code:
+
+``` php
+class UsersTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->hasOne('Addresses');
+ }
+}
+```
+
+If you need more control, you can define your associations using the setters.
+For example, you might want to limit the association to include only certain
+records:
+
+``` php
+class UsersTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->hasOne('Addresses')
+ ->setName('Addresses')
+ ->setFinder('primary')
+ ->setDependent(true);
+ }
+}
+```
+
+If you want to break different addresses into multiple associations, you can do something like:
+
+``` php
+class UsersTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->hasOne('HomeAddresses', [
+ 'className' => 'Addresses'
+ ])
+ ->setProperty('home_address')
+ ->setConditions(['HomeAddresses.label' => 'Home'])
+ ->setDependent(true);
+
+ $this->hasOne('WorkAddresses', [
+ 'className' => 'Addresses'
+ ])
+ ->setProperty('work_address')
+ ->setConditions(['WorkAddresses.label' => 'Work'])
+ ->setDependent(true);
+ }
+}
+```
+
+> [!NOTE]
+> If a column is shared by multiple hasOne associations, you must qualify it with the association alias.
+> In the above example, the 'label' column is qualified with the 'HomeAddresses' and 'WorkAddresses' aliases.
+
+Possible keys for hasOne association arrays include:
+
+- **className**: The class name of the other table. This is the same name used
+ when getting an instance of the table. In the 'Users hasOne Addresses' example,
+ it should be 'Addresses'. The default value is the name of the association.
+- **foreignKey**: The name of the foreign key column in the other table. The
+ default value is the underscored, singular name of the current model,
+ suffixed with '\_id' such as 'user_id' in the above example.
+- **bindingKey**: The name of the column in the current table used to match the
+ `foreignKey`. The default value is the primary key of the current table
+ such as 'id' of Users in the above example.
+- **conditions**: An array of find() compatible conditions such as
+ `['Addresses.primary' => true]`
+- **joinType**: The type of the join used in the SQL query. Accepted values are
+ 'LEFT' and 'INNER'. You can use 'INNER' to get results only where the
+ association is set. The default value is 'LEFT'.
+- **dependent**: When the dependent key is set to `true`, and an entity is
+ deleted, the associated model records are also deleted. In this case we set it
+ to `true` so that deleting a User will also delete her associated Address.
+- **cascadeCallbacks**: When this and **dependent** are `true`, cascaded
+ deletes will load and delete entities so that callbacks are properly
+ triggered. When `false`, `deleteAll()` is used to remove associated data
+ and no callbacks are triggered.
+- **propertyName**: The property name that should be filled with data from the
+ associated table into the source table results. By default this is the
+ underscored & singular name of the association so `address` in our example.
+- **strategy**: The query strategy used to load matching record from the other table.
+ Accepted values are `'join'` and `'select'`. Using `'select'` will generate a separate query
+ and can be useful when the other table is in different database. The default is `'join'`.
+- **finder**: The finder method to use when loading associated records.
+
+Once this association has been defined, find operations on the Users table can
+contain the Address record if it exists:
+
+``` php
+// In a controller or table method.
+$query = $users->find('all')->contain(['Addresses'])->all();
+foreach ($query as $user) {
+ echo $user->address->street;
+}
+```
+
+The above would emit SQL that is similar to:
+
+``` sql
+SELECT * FROM users INNER JOIN addresses ON addresses.user_id = users.id;
+```
+
+
+
+## BelongsTo Associations
+
+Now that we have Address data access from the User table, let's define
+a belongsTo association in the Addresses table in order to get access to related
+User data. The belongsTo association is a natural complement to the hasOne and
+hasMany associations - it allows us to see related data from the other
+direction.
+
+When keying your database tables for a belongsTo relationship, follow this
+convention:
+
+**belongsTo:** the *current* model contains the foreign key.
+
+| Relation | Schema |
+|---------------------------|-------------------|
+| Addresses belongsTo Users | addresses.user_id |
+| Mentors belongsTo Doctors | mentors.doctor_id |
+
+> [!TIP]
+> If a table contains a foreign key, it belongs to the other table.
+
+We can define the belongsTo association in our Addresses table as follows:
+
+``` php
+class AddressesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsTo('Users');
+ }
+}
+```
+
+We can also define a more specific relationship using the setters:
+
+``` php
+class AddressesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsTo('Users')
+ ->setForeignKey('user_id')
+ ->setJoinType('INNER');
+ }
+}
+```
+
+Possible keys for belongsTo association arrays include:
+
+- **className**: The class name of the other table. This is the same name used
+ when getting an instance of the table. In the 'Addresses belongsTo Users' example,
+ it should be 'Users'. The default value is the name of the association.
+- **foreignKey**: The name of the foreign key column in the current table. The
+ default value is the underscored, singular name of the other model,
+ suffixed with '\_id' such as 'user_id' in the above example.
+- **bindingKey**: The name of the column in the other table used to match the
+ `foreignKey`. The default value is the primary key of the other table
+ such as 'id' of Users in the above example.
+- **conditions**: An array of find() compatible conditions or SQL strings such
+ as `['Users.active' => true]`
+- **joinType**: The type of the join used in the SQL query. Accepted values are
+ 'LEFT' and 'INNER'. You can use 'INNER' to get results only where the
+ association is set. The default value is 'LEFT'.
+- **propertyName**: The property name that should be filled with data from the
+ associated table into the source table results. By default this is the
+ underscored & singular name of the association so `user` in our example.
+- **strategy**: The query strategy used to load matching record from the other table.
+ Accepted values are `'join'` and `'select'`. Using `'select'` will generate a separate query
+ and can be useful when the other table is in different database. The default is `'join'`.
+- **finder**: The finder method to use when loading associated records.
+
+Once this association has been defined, find operations on the Addresses table can
+contain the User record if it exists:
+
+``` php
+// In a controller or table method.
+$query = $addresses->find('all')->contain(['Users'])->all();
+foreach ($query as $address) {
+ echo $address->user->username;
+}
+```
+
+The above would output SQL similar to:
+
+``` sql
+SELECT * FROM addresses LEFT JOIN users ON addresses.user_id = users.id;
+```
+
+
+
+## HasMany Associations
+
+An example of a hasMany association is "Articles hasMany Comments". Defining this
+association will allow us to fetch an article's comments when the article is
+loaded.
+
+When creating your database tables for a hasMany relationship, follow this
+convention:
+
+**hasMany:** the *other* model contains the foreign key.
+
+| Relation | Schema |
+|---------------------------|---------------------|
+| Articles hasMany Comments | Comments.article_id |
+| Products hasMany Options | Options.product_id |
+| Doctors hasMany Patients | Patients.doctor_id |
+
+We can define the hasMany association in our Articles model as follows:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->hasMany('Comments');
+ }
+}
+```
+
+We can also define a more specific relationship using the setters:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->hasMany('Comments')
+ ->setForeignKey('article_id')
+ ->setDependent(true);
+ }
+}
+```
+
+Sometimes you may want to configure composite keys in your associations:
+
+``` php
+// Within ArticlesTable::initialize() call
+$this->hasMany('Comments')
+ ->setForeignKey([
+ 'article_id',
+ 'article_hash',
+ ]);
+```
+
+Relying on the example above, we have passed an array containing the desired
+composite keys to `setForeignKey()`. By default the `bindingKey` would be
+automatically defined as `id` and `hash` respectively, but let's assume that
+you need to specify different binding fields than the defaults. You can setup it
+manually with `setBindingKey()`:
+
+``` php
+// Within ArticlesTable::initialize() call
+$this->hasMany('Comments')
+ ->setForeignKey([
+ 'article_id',
+ 'article_hash',
+ ])
+ ->setBindingKey([
+ 'whatever_id',
+ 'whatever_hash',
+ ]);
+```
+
+Like hasOne associations, `foreignKey` is in the other (Comments)
+table and `bindingKey` is in the current (Articles) table.
+
+Possible keys for hasMany association arrays include:
+
+- **className**: The class name of the other table. This is the same name used
+ when getting an instance of the table. In the 'Articles hasMany Comments' example,
+ it should be 'Comments'. The default value is the name of the association.
+- **foreignKey**: The name of the foreign key column in the other table. The
+ default value is the underscored, singular name of the current model,
+ suffixed with '\_id' such as 'article_id' in the above example.
+- **bindingKey**: The name of the column in the current table used to match the
+ `foreignKey`. The default value is the primary key of the current table
+ such as 'id' of Articles in the above example.
+- **conditions**: an array of find() compatible conditions or SQL
+ strings such as `['Comments.visible' => true]`. It is recommended to
+ use the `finder` option instead.
+- **sort**: an array of find() compatible order clauses or SQL
+ strings such as `['Comments.created' => 'ASC']`
+- **dependent**: When dependent is set to `true`, recursive model
+ deletion is possible. In this example, Comment records will be
+ deleted when their associated Article record has been deleted.
+- **cascadeCallbacks**: When this and **dependent** are `true`, cascaded
+ deletes will load and delete entities so that callbacks are properly
+ triggered. When `false`, `deleteAll()` is used to remove associated data
+ and no callbacks are triggered.
+- **propertyName**: The property name that should be filled with data from the
+ associated table into the source table results. By default this is the
+ underscored & plural name of the association so `comments` in our example.
+- **strategy**: Defines the query strategy to use. Defaults to 'select'. The
+ other valid value is 'subquery', which replaces the `IN` list with an
+ equivalent subquery.
+- **saveStrategy**: Either 'append' or 'replace'. Defaults to 'append'. When 'append' the current
+ records are appended to any records in the database. When 'replace' associated
+ records not in the current set will be removed. If the foreign key is a nullable
+ column or if `dependent` is true records will be orphaned.
+- **finder**: The finder method to use when loading associated records. See the
+ [Association Finder](#association-finder) section for more information.
+
+Once this association has been defined, find operations on the Articles table
+can contain the Comment records if they exist:
+
+``` php
+// In a controller or table method.
+$query = $articles->find('all')->contain(['Comments'])->all();
+foreach ($query as $article) {
+ echo $article->comments[0]->text;
+}
+```
+
+The above would output SQL similar to:
+
+``` sql
+SELECT * FROM articles;
+SELECT * FROM comments WHERE article_id IN (1, 2, 3, 4, 5);
+```
+
+When the subquery strategy is used, SQL similar to the following will be
+generated:
+
+``` sql
+SELECT * FROM articles;
+SELECT * FROM comments WHERE article_id IN (SELECT id FROM articles);
+```
+
+You may want to cache the counts for your hasMany associations. This is useful
+when you often need to show the number of associated records, but don't want to
+load all the records just to count them. For example, the comment count on any
+given article is often cached to make generating lists of articles more
+efficient. You can use the [CounterCacheBehavior](../orm/behaviors/counter-cache) to cache counts of associated records.
+
+You should make sure that your database tables do not contain columns that match
+association property names. If for example you have counter fields that conflict
+with association properties, you must either rename the association property, or
+the column name.
+
+
+
+## BelongsToMany Associations
+
+An example of a BelongsToMany association is "Article BelongsToMany Tags", where
+the tags from one article are shared with other articles. BelongsToMany is
+often referred to as "has and belongs to many", and is a classic "many to many"
+association.
+
+The main difference between hasMany and BelongsToMany is that the link between
+the models in a BelongsToMany association is not exclusive. For example, we are
+joining our Articles table with a Tags table. Using 'funny' as a Tag for my
+Article, doesn't "use up" the tag. I can also use it on the next article
+I write.
+
+Three database tables are required for a BelongsToMany association. In the
+example above we would need tables for `articles`, `tags` and
+`articles_tags`. The `articles_tags` table contains the data that links
+tags and articles together. The joining table is named after the two tables
+involved, separated with an underscore by convention. In its simplest form, this
+table consists of `article_id` and `tag_id` and a multi-column
+`PRIMARY KEY` index spanning both columns.
+
+**belongsToMany** requires a separate join table that includes both *model*
+names.
+
+| Relationship | Join Table Fields |
+|----|----|
+| Articles belongsToMany Tags | articles_tags.id, articles_tags.tag_id, articles_tags.article_id |
+| Patients belongsToMany Doctors | doctors_patients.id, doctors_patients.doctor_id, doctors_patients.patient_id. |
+
+We can define the belongsToMany association in both our models as follows:
+
+``` php
+// In src/Model/Table/ArticlesTable.php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsToMany('Tags');
+ }
+}
+
+// In src/Model/Table/TagsTable.php
+class TagsTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsToMany('Articles');
+ }
+}
+```
+
+We can also define a more specific relationship using configuration:
+
+``` php
+// In src/Model/Table/TagsTable.php
+class TagsTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsToMany('Articles', [
+ 'joinTable' => 'articles_tags',
+ ]);
+ }
+}
+```
+
+Possible keys for belongsToMany association arrays include:
+
+- **className**: The class name of the other table. This is the same name used
+ when getting an instance of the table. In the 'Articles belongsToMany Tags'
+ example, it should be 'Tags'. The default value is the name of the association.
+- **joinTable**: The name of the join table used in this
+ association (if the current table doesn't adhere to the naming
+ convention for belongsToMany join tables). By default this table
+ name will be used to load the Table instance for the join table.
+- **foreignKey**: The name of the foreign key that references the current model
+ found on the join table, or list in case of composite foreign keys.
+ This is especially handy if you need to define multiple
+ belongsToMany relationships. The default value for this key is the
+ underscored, singular name of the current model, suffixed with '\_id'.
+- **bindingKey**: The name of the column in the current table, that will be used
+ for matching the `foreignKey`. Defaults to the primary key.
+- **targetForeignKey**: The name of the foreign key that references the target
+ model found on the join model, or list in case of composite foreign keys.
+ The default value for this key is the underscored, singular name of
+ the target model, suffixed with '\_id'.
+- **conditions**: An array of `find()` compatible conditions. If you have
+ conditions on an associated table, you should use a 'through' model, and
+ define the necessary belongsTo associations on it. It is recommended to
+ use the `finder` option instead.
+- **sort**: an array of find() compatible order clauses.
+- **dependent**: When the dependent key is set to `false`, and an entity is
+ deleted, the data of the join table will not be deleted.
+- **through**: Allows you to provide either the alias of the Table instance you
+ want used on the join table, or the instance itself. This makes customizing
+ the join table keys possible, and allows you to customize the behavior of the
+ pivot table.
+- **cascadeCallbacks**: When this is `true`, cascaded deletes will load and
+ delete entities so that callbacks are properly triggered on join table
+ records. When `false`, `deleteAll()` is used to remove associated data and
+ no callbacks are triggered. This defaults to `false` to help reduce
+ overhead.
+- **propertyName**: The property name that should be filled with data from the
+ associated table into the source table results. By default this is the
+ underscored & plural name of the association, so `tags` in our example.
+- **strategy**: Defines the query strategy to use. Defaults to 'select'. The
+ other valid value is 'subquery', which replaces the `IN` list with an
+ equivalent subquery.
+- **saveStrategy**: Either 'append' or 'replace'. Defaults to 'replace'.
+ Indicates the mode to be used for saving associated entities. The former will
+ only create new links between both side of the relation and the latter will
+ do a wipe and replace to create the links between the passed entities when
+ saving.
+- **finder**: The finder method to use when loading associated records. See the
+ [Association Finder](#association-finder) section for more information.
+
+Once this association has been defined, find operations on the Articles table can
+contain the Tag records if they exist:
+
+``` php
+// In a controller or table method.
+$query = $articles->find('all')->contain(['Tags'])->all();
+foreach ($query as $article) {
+ echo $article->tags[0]->text;
+}
+```
+
+The above would output SQL similar to:
+
+``` sql
+SELECT * FROM articles;
+SELECT * FROM tags
+INNER JOIN articles_tags ON (
+ tags.id = article_tags.tag_id
+ AND article_id IN (1, 2, 3, 4, 5)
+);
+```
+
+When the subquery strategy is used, SQL similar to the following will be
+generated:
+
+``` sql
+SELECT * FROM articles;
+SELECT * FROM tags
+INNER JOIN articles_tags ON (
+ tags.id = article_tags.tag_id
+ AND article_id IN (SELECT id FROM articles)
+);
+```
+
+
+
+### Using the 'through' Option
+
+If you plan on adding extra information to the join/pivot table, or if you need
+to use join columns outside of the conventions, you will need to define the
+`through` option. The `through` option provides you full control over how
+the belongsToMany association will be created.
+
+It is sometimes desirable to store additional data with a many to many
+association. Consider the following:
+
+ Student BelongsToMany Course
+ Course BelongsToMany Student
+
+A Student can take many Courses and a Course can be taken by many Students. This
+is a simple many to many association. The following table would suffice:
+
+ id | student_id | course_id
+
+Now what if we want to store the number of days that were attended by the
+student on the course and their final grade? The table we'd want would be:
+
+ id | student_id | course_id | days_attended | grade
+
+The way to implement our requirement is to use a **join model**, otherwise known
+as a **hasMany through** association. That is, the association is a model
+itself. So, we can create a new model CoursesMemberships. Take a look at the
+following models:
+
+``` php
+class StudentsTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsToMany('Courses', [
+ 'through' => 'CoursesMemberships',
+ ]);
+ }
+}
+
+class CoursesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsToMany('Students', [
+ 'through' => 'CoursesMemberships',
+ ]);
+ }
+}
+
+class CoursesMembershipsTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->belongsTo('Students');
+ $this->belongsTo('Courses');
+ }
+}
+```
+
+The CoursesMemberships join table uniquely identifies a given Student's
+participation on a Course in addition to extra meta-information.
+
+When using a query object with a BelongsToMany relationship with a `through`
+model, add contain and matching conditions for the association target table into
+your query object. The `through` table can then be referenced in other conditions
+such as a where condition by designating the through table name before the field
+you are filtering on:
+
+``` php
+$query = $this->find(
+ 'list',
+ valueField: 'studentFirstName', order: 'students.id'
+ )
+ ->contain(['Courses'])
+ ->matching('Courses')
+ ->where(['CoursesMemberships.grade' => 'B']);
+```
+
+
+
+### Using Association Finders
+
+By default associations will load records based on the foreign key columns. If
+you want to define additional conditions for associations, you can use
+a `finder`. When an association is loaded the ORM will use your [custom
+finder](../orm/retrieving-data-and-resultsets#custom-find-methods) to load, update, or delete associated records.
+Using finders lets you encapsulate your queries and make them more reusable.
+There are some limitations when using finders to load data in associations that
+are loaded using joins (belongsTo/hasOne). Only the following aspects of the
+query will be applied to the root query:
+
+- Where conditions.
+- Additional joins.
+- Contained associations.
+
+Other aspects of the query, such as selected columns, order, group by, having
+and other sub-statements, will not be applied to the root query. Associations
+that are *not* loaded through joins (hasMany/belongsToMany), do not have the
+above restrictions and can also use result formatters or map/reduce functions.
+
+## Association Conventions
+
+By default, associations should be configured and referenced using the CamelCase style.
+This enables property chains to related tables in the following way:
+
+``` php
+$this->MyTableOne->MyTableTwo->find()->...;
+```
+
+Association properties on entities do not use CamelCase conventions though. Instead for a hasOne/belongsTo relation like "User belongsTo Roles", you would get a role property instead of Role or \`Roles\`:
+
+``` php
+// A single entity (or null if not available)
+$role = $user->role;
+```
+
+Whereas for the other direction "Roles hasMany Users" it would be:
+
+``` php
+// Collection of user entities (or null if not available)
+$users = $role->users;
+```
+
+## Loading Associations
+
+Once you've defined your associations you can [eager load associations](../orm/retrieving-data-and-resultsets#eager-loading-associations) when fetching results.
diff --git a/docs/en/orm/behaviors.md b/docs/en/orm/behaviors.md
new file mode 100644
index 0000000000..c3c140ccaa
--- /dev/null
+++ b/docs/en/orm/behaviors.md
@@ -0,0 +1,352 @@
+# Behaviors
+
+Behaviors are a way to organize and enable horizontal re-use of Model layer
+logic. Conceptually they are similar to traits. However, behaviors are
+implemented as separate classes. This allows them to hook into the
+life-cycle callbacks that models emit, while providing trait-like features.
+
+Behaviors provide a convenient way to package up behavior that is common across
+many models. For example, CakePHP includes a `TimestampBehavior`. Many
+models will want timestamp fields, and the logic to manage these fields is
+not specific to any one model. It is these kinds of scenarios that behaviors are
+a perfect fit for.
+
+## Using Behaviors
+
+
+
+## Core Behaviors
+
+- [CounterCache](../orm/behaviors/counter-cache)
+- [Timestamp](../orm/behaviors/timestamp)
+- [Translate](../orm/behaviors/translate)
+- [Tree](../orm/behaviors/tree)
+
+## Creating a Behavior
+
+In the following examples we will create a very simple `SluggableBehavior`.
+This behavior will allow us to populate a slug field with the results of
+`Text::slug()` based on another field.
+
+Before we create our behavior we should understand the conventions for
+behaviors:
+
+- Behavior files are located in **src/Model/Behavior**, or
+ `MyPlugin\Model\Behavior`.
+- Behavior classes should be in the `App\Model\Behavior` namespace, or
+ `MyPlugin\Model\Behavior` namespace.
+- Behavior class names end in `Behavior`.
+- Behaviors extend `Cake\ORM\Behavior`.
+
+To create our sluggable behavior. Put the following into
+**src/Model/Behavior/SluggableBehavior.php**:
+
+``` php
+namespace App\Model\Behavior;
+
+use Cake\ORM\Behavior;
+
+class SluggableBehavior extends Behavior
+{
+}
+```
+
+Similar to tables, behaviors also have an `initialize()` hook where you can
+put your behavior's initialization code, if required:
+
+``` php
+public function initialize(array $config): void
+{
+ // Some initialization code here
+}
+```
+
+We can now add this behavior to one of our table classes. In this example we'll
+use an `ArticlesTable`, as articles often have slug properties for creating
+friendly URLs:
+
+``` php
+namespace App\Model\Table;
+
+use Cake\ORM\Table;
+
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Sluggable');
+ }
+}
+```
+
+Our new behavior doesn't do much of anything right now. Next, we'll add a mixin
+method and an event listener so that when we save entities we can automatically
+slug a field.
+
+### Calling behavior methods
+
+Public methods on behaviors can be called as normal methods:
+
+``` php
+$articles->getBehavior('Sluggable')->slug($value);
+```
+
+Public methods defined on behaviors are also added as 'mixin' methods on the
+table object they are attached to. If you attach two behaviors that provide the
+same methods an exception will be raised. If a behavior provides the same method
+as a table class, the behavior method will not be callable from the table.
+Behavior mixin methods will receive the exact same arguments that are provided
+to the table. For example, if our SluggableBehavior defined the following
+method:
+
+``` php
+public function slug($value)
+{
+ return Text::slug($value, $this->_config['replacement']);
+}
+```
+
+It could be invoked using:
+
+``` php
+$slug = $articles->slug('My article');
+```
+
+::: info Deprecated in version 5.3.0
+Calling behavior methods as mixin methods is deprecated
+:::
+
+#### Limiting or Renaming Exposed Mixin Methods
+
+When creating behaviors, there may be situations where you don't want to expose
+public methods as mixin methods. In these cases you can use the
+`implementedMethods` configuration key to rename or exclude mixin methods. For
+example if we wanted to prefix our slug() method we could do the following:
+
+``` php
+protected $_defaultConfig = [
+ 'implementedMethods' => [
+ 'superSlug' => 'slug',
+ ]
+];
+```
+
+Applying this configuration will make `slug()` not callable, however it will
+add a `superSlug()` mixin method to the table. Notably if our behavior
+implemented other public methods they would **not** be available as mixin
+methods with the above configuration.
+
+Since the exposed methods are decided by configuration you can also
+rename/remove mixin methods when adding a behavior to a table. For example:
+
+``` php
+// In a table's initialize() method.
+$this->addBehavior('Sluggable', [
+ 'implementedMethods' => [
+ 'superSlug' => 'slug',
+ ]
+]);
+```
+
+### Defining Event Listeners
+
+Now that our behavior has a mixin method to slug fields, we can implement
+a callback listener to automatically slug a field when entities are saved. We'll
+also modify our slug method to accept an entity instead of just a plain value. Our
+behavior should now look like:
+
+``` php
+namespace App\Model\Behavior;
+
+use ArrayObject;
+use Cake\Datasource\EntityInterface;
+use Cake\Event\EventInterface;
+use Cake\ORM\Behavior;
+use Cake\ORM\Entity;
+use Cake\ORM\Query\SelectQuery;
+use Cake\Utility\Text;
+
+class SluggableBehavior extends Behavior
+{
+ protected array $_defaultConfig = [
+ 'field' => 'title',
+ 'slug' => 'slug',
+ 'replacement' => '-',
+ ];
+
+ public function slug(EntityInterface $entity)
+ {
+ $config = $this->getConfig();
+ $value = $entity->get($config['field']);
+ $entity->set($config['slug'], Text::slug($value, $config['replacement']));
+ }
+
+ public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
+ {
+ $this->slug($entity);
+ }
+
+}
+```
+
+The above code shows a few interesting features of behaviors:
+
+- Behaviors can define callback methods by defining methods that follow the
+ [Table Callbacks](../orm/table-objects#table-callbacks) conventions.
+- Behaviors can define a default configuration property. This property is merged
+ with the overrides when a behavior is attached to the table.
+
+To prevent the save from continuing, simply stop event propagation in your callback:
+
+``` php
+public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
+{
+ if (...) {
+ $event->stopPropagation();
+ $event->setResult(false);
+
+ return;
+ }
+ $this->slug($entity);
+}
+```
+
+Alternatively, you can return false from the callback. This has the same effect as stopping event propagation.
+
+### Defining Finders
+
+Now that we are able to save articles with slug values, we should implement
+a finder method so we can fetch articles by their slug. Behavior finder
+methods, use the same conventions as [Custom Find Methods](../orm/retrieving-data-and-resultsets#custom-find-methods) do. Our
+`find('slug')` method would look like:
+
+``` php
+public function findSlug(SelectQuery $query, string $slug): SelectQuery
+{
+ return $query->where(['slug' => $slug]);
+}
+```
+
+Once our behavior has the above method we can call it:
+
+``` php
+$article = $articles->find('slug', slug: $value)->first();
+```
+
+#### Limiting or Renaming Exposed Finder Methods
+
+When creating behaviors, there may be situations where you don't want to expose
+finder methods, or you need to rename finders to avoid duplicated methods. In
+these cases you can use the `implementedFinders` configuration key to rename
+or exclude finder methods. For example if we wanted to rename our `find(slug)`
+method we could do the following:
+
+``` php
+protected array $_defaultConfig = [
+ 'implementedFinders' => [
+ 'slugged' => 'findSlug',
+ ]
+];
+```
+
+Applying this configuration will make `find('slug')` trigger an error. However
+it will make `find('slugged')` available. Notably if our behavior implemented
+other finder methods they would **not** be available, as they are not included
+in the configuration.
+
+Since the exposed methods are decided by configuration you can also
+rename/remove finder methods when adding a behavior to a table. For example:
+
+``` php
+// In a table's initialize() method.
+$this->addBehavior('Sluggable', [
+ 'implementedFinders' => [
+ 'slugged' => 'findSlug',
+ ]
+]);
+```
+
+## Transforming Request Data into Entity Properties
+
+Behaviors can define logic for how the custom fields they provide are
+marshalled by implementing the `Cake\ORM\PropertyMarshalInterface`. This
+interface requires a single method to be implemented:
+
+``` php
+public function buildMarshalMap($marshaller, $map, $options)
+{
+ return [
+ 'custom_behavior_field' => function ($value, $entity) {
+ // Transform the value as necessary
+ return $value . '123';
+ }
+ ];
+}
+```
+
+The `TranslateBehavior` has a non-trivial implementation of this interface
+that you might want to refer to.
+
+## Removing Loaded Behaviors
+
+To remove a behavior from your table you can call the `removeBehavior()` method:
+
+``` php
+// Remove the loaded behavior
+$this->removeBehavior('Sluggable');
+```
+
+## Accessing Loaded Behaviors
+
+Once you've attached behaviors to your Table instance you can introspect the
+loaded behaviors, or access specific behaviors using the `BehaviorRegistry`:
+
+``` php
+// See which behaviors are loaded
+$table->behaviors()->loaded();
+
+// Check if a specific behavior is loaded.
+// Remember to omit plugin prefixes.
+$table->behaviors()->has('CounterCache');
+
+// Get a loaded behavior
+// Remember to omit plugin prefixes
+$table->behaviors()->get('CounterCache');
+```
+
+### Re-configuring Loaded Behaviors
+
+To modify the configuration of an already loaded behavior you can combine the
+`BehaviorRegistry::get` command with `config` command provided by the
+`InstanceConfigTrait` trait.
+
+For example, if a parent class, such as `AppTable`, loaded the `Timestamp`
+behavior you could do the following to add, modify or remove the configurations
+for the behavior. In this case, we will add an event we want Timestamp to
+respond to:
+
+``` php
+namespace App\Model\Table;
+
+use App\Model\Table\AppTable; // similar to AppController
+
+class UsersTable extends AppTable
+{
+ public function initialize(array $options): void
+ {
+ parent::initialize($options);
+
+ // For example, if our parent calls $this->addBehavior('Timestamp')
+ // and we want to add an additional event
+ if ($this->behaviors()->has('Timestamp')) {
+ $this->behaviors()->get('Timestamp')->setConfig([
+ 'events' => [
+ 'Users.login' => [
+ 'last_login' => 'always'
+ ],
+ ],
+ ]);
+ }
+ }
+}
+```
diff --git a/docs/en/orm/behaviors/counter-cache.md b/docs/en/orm/behaviors/counter-cache.md
new file mode 100644
index 0000000000..656e7e6262
--- /dev/null
+++ b/docs/en/orm/behaviors/counter-cache.md
@@ -0,0 +1,193 @@
+# CounterCache
+
+`class` Cake\\ORM\\Behavior\\**CounterCacheBehavior**
+
+Often times web applications need to display counts of related objects. For
+example, when showing a list of articles you may want to display how many
+comments it has. Or when showing a user you might want to show how many
+friends/followers she has. The CounterCache behavior is intended for these
+situations. CounterCache will update a field in the associated models assigned
+in the options when it is invoked. The fields should exist in the database and
+be of the type INT.
+
+## Basic Usage
+
+You enable the CounterCache behavior like any other behavior, but it won't do
+anything until you configure some relations and the field counts that should be
+stored on each of them. Using our example below, we could cache the comment
+count for each article with the following:
+
+``` php
+class CommentsTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('CounterCache', [
+ 'Articles' => ['comment_count']
+ ]);
+ }
+}
+```
+
+> [!NOTE]
+> The column `comment_count` should exist in the `articles` table.
+
+The CounterCache configuration should be a map of relation names and the
+specific configuration for that relation.
+
+As you see you need to add the behavior on the "other side" of the association
+where you actually want the field to be updated. In this example the behavior
+is added to the `CommentsTable` even though it updates the `comment_count`
+field in the `ArticlesTable`.
+
+The counter's value will be updated each time an entity is saved or deleted.
+The counter **will not** be updated when you
+
+- save the entity without changing data or
+- use `updateAll()` or
+- use `deleteAll()` or
+- execute SQL you have written
+
+## Advanced Usage
+
+If you need to keep a cached counter for less than all of the related records,
+you can supply additional conditions or finder methods to generate a
+counter value:
+
+``` php
+// Use a specific find method.
+// In this case find(published)
+$this->addBehavior('CounterCache', [
+ 'Articles' => [
+ 'comment_count' => [
+ 'finder' => 'published',
+ ],
+ ],
+]);
+```
+
+If you don't have a custom finder method you can provide an array of conditions
+to find records instead:
+
+``` php
+$this->addBehavior('CounterCache', [
+ 'Articles' => [
+ 'comment_count' => [
+ 'conditions' => ['Comments.spam' => false],
+ ],
+ ],
+]);
+```
+
+If you want CounterCache to update multiple fields, for example both showing a
+conditional count and a basic count you can add these fields in the array:
+
+``` php
+$this->addBehavior('CounterCache', [
+ 'Articles' => ['comment_count',
+ 'published_comment_count' => [
+ 'finder' => 'published',
+ ],
+ ],
+]);
+```
+
+If you want to calculate the CounterCache field value on your own, you can set
+the `ignoreDirty` option to `true`.
+This will prevent the field from being recalculated if you've set it dirty
+before:
+
+``` php
+$this->addBehavior('CounterCache', [
+ 'Articles' => [
+ 'comment_count' => [
+ 'ignoreDirty' => true,
+ ],
+ ],
+]);
+```
+
+Lastly, if a custom finder and conditions are not suitable you can provide
+a callback function. Your function must return the count value to be stored:
+
+``` php
+$this->addBehavior('CounterCache', [
+ 'Articles' => [
+ 'rating_avg' => function ($event, $entity, $table, $original) {
+ return 4.5;
+ }
+ ],
+]);
+```
+
+Your function can return `false` to skip updating the counter column, or
+a `SelectQuery` object that produced the count value. If you return a `SelectQuery`
+object, your query will be used as a subquery in the update statement. The
+`$table` parameter refers to the table object holding the behavior (not the
+target relation) for convenience. The callback is invoked at least once with
+`$original` set to `false`. If the entity-update changes the association
+then the callback is invoked a *second* time with `true`, the return value
+then updates the counter of the *previously* associated item.
+
+> [!NOTE]
+> The CounterCache behavior works for `belongsTo` associations only. For
+> example for "Comments belongsTo Articles", you need to add the CounterCache
+> behavior to the `CommentsTable` in order to generate `comment_count` for
+> Articles table.
+
+::: info Changed in version 5.1.2
+As of CakePHP 5.1.2, the counter cache values are updated using a single query using sub-queries, instead of separate queries, to fetch the count and update a record. If required you can disable the use of sub-queries by setting useSubQuery key to false in the config ['Articles' => ['comment_count' => ['useSubQuery' => false]]
+:::
+
+## Belongs to many Usage
+
+It is possible to use the CounterCache behavior in a `belongsToMany` association.
+First, you need to add the `through` and `cascadeCallbacks` options to the
+`belongsToMany` association:
+
+``` text
+'through' => 'CommentsArticles',
+'cascadeCallbacks' => true
+```
+
+Also see [Using The Through Option](../../orm/associations#using-the-through-option) how to configure a custom join table.
+
+The `CommentsArticles` is the name of the junction table classname.
+If you don't have it you should create it with the bake CLI tool.
+
+In this `src/Model/Table/CommentsArticlesTable.php` you then need to add the behavior
+with the same code as described above.:
+
+``` php
+$this->addBehavior('CounterCache', [
+ 'Articles' => ['comments_count'],
+]);
+```
+
+Finally clear all caches with `bin/cake cache clear_all` and try it out.
+
+## Manually updating counter caches
+
+`method` Cake\\ORM\\Behavior\\CounterCacheBehavior::**updateCounterCache(?string $assocName = null, int $limit = 100, ?int $page = null): void**()
+
+The `updateCounterCache()` method allows you to update the counter cache values
+for all records of one or all configured associations in batches. This can be useful,
+for example, to update the counter cache after importing data directly into the database.:
+
+``` php
+// Update the counter cache for all configured associations
+$table->getBehavior('CounterCache')->updateCounterCache();
+
+// Update the counter cache for a specific association, 200 records per batch
+$table->getBehavior('CounterCache')->updateCounterCache('Articles', 200);
+
+// Update only the first page of records
+$table->getBehavior('CounterCache')->updateCounterCache('Articles', page: 1);
+```
+
+::: info Added in version 5.2.0
+:::
+
+> [!NOTE]
+> This methods won't update the counter cache values for fields which are
+> configured to use a closure to get the count value.
diff --git a/docs/en/orm/behaviors/timestamp.md b/docs/en/orm/behaviors/timestamp.md
new file mode 100644
index 0000000000..eb00530d5f
--- /dev/null
+++ b/docs/en/orm/behaviors/timestamp.md
@@ -0,0 +1,88 @@
+# Timestamp
+
+`class` Cake\\ORM\\Behavior\\**TimestampBehavior**
+
+The timestamp behavior allows your table objects to update one or more
+timestamps on each model event. This is primarily used to populate data into
+`created` and `modified` fields. However, with some additional
+configuration, you can update any timestamp/datetime column on any event a table
+publishes.
+
+## Basic Usage
+
+You enable the timestamp behavior like any other behavior:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Timestamp');
+ }
+}
+```
+
+The default configuration will do the following:
+
+- When a new entity is saved the `created` and `modified` fields will be set
+ to the current time.
+- When an entity is updated, the `modified` field is set to the current time.
+
+## Using and Configuring the Behavior
+
+If you need to modify fields with different names, or want to update additional
+timestamp fields on custom events you can use some additional configuration:
+
+``` php
+class OrdersTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Timestamp', [
+ 'events' => [
+ 'Model.beforeSave' => [
+ 'created_at' => 'new',
+ 'updated_at' => 'always',
+ ],
+ 'Orders.completed' => [
+ 'completed_at' => 'always'
+ ]
+ ]
+ ]);
+ }
+}
+```
+
+As you can see above, in addition to the standard `Model.beforeSave` event, we
+are also updating the `completed_at` column when orders are completed.
+
+## Updating Timestamps on Entities
+
+Sometimes you'll want to update just the timestamps on an entity without
+changing any other properties. This is sometimes referred to as 'touching'
+a record. In CakePHP you can use the `touch()` method to do exactly this:
+
+``` php
+// Touch based on the Model.beforeSave event.
+$articles->getBehavior('Timestamp')->touch($article);
+
+// Touch based on a specific event.
+$orders->getBehavior('Timestamp')->touch($order, 'Orders.completed');
+```
+
+After you have saved the entity, the field is updated.
+
+Touching records can be useful when you want to signal that a parent resource
+has changed when a child resource is created/updated. For example: updating an
+article when a new comment is added.
+
+## Saving Updates Without Modifying Timestamps
+
+To disable the automatic modification of the `updated` timestamp column when
+saving an entity you can mark the attribute as 'dirty':
+
+``` php
+// Mark the modified column as dirty making
+// the current value be set on update.
+$order->setDirty('modified', true);
+```
diff --git a/docs/en/orm/behaviors/translate.md b/docs/en/orm/behaviors/translate.md
new file mode 100644
index 0000000000..ce3052e945
--- /dev/null
+++ b/docs/en/orm/behaviors/translate.md
@@ -0,0 +1,565 @@
+# Translate
+
+`class` Cake\\ORM\\Behavior\\**TranslateBehavior**
+
+The Translate behavior allows you to create and retrieve translated copies
+of your entities in multiple languages.
+
+> [!WARNING]
+> The TranslateBehavior does not support composite primary keys at this point
+> in time.
+
+## Translation Strategies
+
+The behavior offers two strategies for how the translations are stored.
+
+1. Shadow table Strategy: This strategy uses a separate "shadow table" for each
+ Table object to store translation of all translated fields of that table.
+ This is the default strategy.
+2. Eav Strategy: This strategy uses a `i18n` table where it stores the
+ translation for each of the fields of any given Table object that it's bound to.
+
+## Shadow Table Strategy
+
+Let's assume we have an `articles` table and we want it's `title` and `body`
+fields to be translated. For that we create a shadow table `articles_translations`:
+
+``` sql
+CREATE TABLE `articles_translations` (
+ `id` int(11) NOT NULL,
+ `locale` varchar(5) NOT NULL,
+ `title` varchar(255),
+ `body` text,
+ PRIMARY KEY (`id`,`locale`)
+);
+```
+
+The shadow table needs `id` and `locale` columns which together
+form the primary key and other columns with same name as primary table which
+need to be translated.
+
+A note on language abbreviations: The Translate Behavior doesn't impose any
+restrictions on the language identifier, the possible values are only restricted
+by the `locale` column type/size. `locale` is defined as `varchar(6)` in
+case you want to use abbreviations like `es-419` (Spanish for Latin America,
+language abbreviation with area code [UN M.49](https://en.wikipedia.org/wiki/UN_M.49)).
+
+> [!TIP]
+> It's wise to use the same language abbreviations as required for
+> [Internationalization and Localization](../../core-libraries/internationalization-and-localization). Thus you are
+> consistent and switching the language works identical for both, the
+> `Translate` behavior and `Internationalization and Localization`.
+
+So it's recommended to use either the two letter ISO code of the language like
+`en`, `fr`, `de` or the full locale name such as `fr_FR`, `es_AR`,
+`da_DK` which contains both the language and the country where it is spoken.
+
+## Eav Strategy
+
+In order to use the Eav strategy, you need to create a `i18n` table with the
+correct schema. Currently the only way of loading the `i18n` table is by
+manually running the following SQL script in your database:
+
+``` sql
+CREATE TABLE i18n (
+ id int NOT NULL auto_increment,
+ locale varchar(6) NOT NULL,
+ model varchar(255) NOT NULL,
+ foreign_key int(10) NOT NULL,
+ field varchar(255) NOT NULL,
+ content text,
+ PRIMARY KEY (id),
+ UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field),
+ INDEX I18N_FIELD(model, foreign_key, field)
+);
+```
+
+The schema is also available as sql file in **/config/schema/i18n.sql**.
+
+## Attaching the Translate Behavior to Your Tables
+
+Attaching the behavior can be done in the `initialize()` method in your Table
+class:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ // By default ShadowTable will be used.
+ $this->addBehavior('Translate', ['fields' => ['title', 'body']]);
+ }
+}
+```
+
+For shadow table strategy specifying the `fields` key is optional as the
+behavior can infer the fields from the shadow table columns.
+
+If you want to use the `EavStrategy` then you can configure the behavior
+as:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Translate', [
+ 'strategyClass' => \Cake\ORM\Behavior\Translate\EavStrategy::class,
+ 'fields' => ['title', 'body'],
+ ]);
+ }
+}
+```
+
+For `EavStrategy` you are required to pass the `fields` key in the
+configuration array. This list of fields is needed to tell the behavior what
+columns will be able to store translations.
+
+By default the locale specified in `App.defaultLocale` config is used as default
+locale for the `TranslateBehavior`. You can override that by setting `defaultLocale`
+config of the behavior:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Translate', [
+ 'defaultLocale' => 'en_GB',
+ ]);
+ }
+}
+```
+
+## Quick tour
+
+Regardless of the datastructure strategy you choose the behavior provides the
+same API to manage translations.
+
+Now, select a language to be used for retrieving entities by changing
+the application language, which will affect all translations:
+
+``` php
+// In the Articles controller. Change the locale to Spanish, for example
+I18n::setLocale('es');
+```
+
+Then, get an existing entity:
+
+``` php
+$article = $this->Articles->get(12);
+echo $article->title; // Echoes 'A title', not translated yet
+```
+
+Next, translate your entity:
+
+``` php
+$article->title = 'Un Artículo';
+$this->Articles->save($article);
+```
+
+You can try now getting your entity again:
+
+``` php
+$article = $this->Articles->get(12);
+echo $article->title; // Echoes 'Un Artículo', yay piece of cake!
+```
+
+Working with multiple translations can be done by using a special trait
+in your Entity class:
+
+``` php
+use Cake\ORM\Behavior\Translate\TranslateTrait;
+use Cake\ORM\Entity;
+
+class Article extends Entity
+{
+ use TranslateTrait;
+}
+```
+
+Now you can find all translations for a single entity:
+
+``` php
+$article = $this->Articles->find('translations')->first();
+echo $article->translation('es')->title; // 'Un Artículo'
+
+echo $article->translation('en')->title; // 'An Article';
+```
+
+And save multiple translations at once:
+
+``` php
+$article->translation('es')->title = 'Otro Título';
+$article->translation('fr')->title = 'Un autre Titre';
+$this->Articles->save($article);
+```
+
+If you want to go deeper on how it works or how to tune the
+behavior for your needs, keep on reading the rest of this chapter.
+
+### Using a Separate Translations Table for Eav strategy
+
+If you wish to use a table other than `i18n` for translating a particular
+repository, you can specify the name of the table class name for your custom
+table in the behavior's configuration. This is common when you have multiple
+tables to translate and you want a cleaner separation of the data that is stored
+for each different table:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Translate', [
+ 'fields' => ['title', 'body'],
+ 'translationTable' => 'ArticlesI18n',
+ ]);
+ }
+}
+```
+
+You need to make sure that any custom table you use has the columns `field`,
+`foreign_key`, `locale` and `model`.
+
+## Reading Translated Content
+
+As shown above you can use the `setLocale()` method to choose the active
+translation for entities that are loaded:
+
+``` php
+// Load I18n core functions at the beginning of your Articles Controller:
+use Cake\I18n\I18n;
+
+// Then you can change the language in your action:
+I18n::setLocale('es');
+
+// All entities in results will contain spanish translation
+$results = $this->Articles->find()->all();
+```
+
+This method works with any finder in your tables. For example, you can
+use TranslateBehavior with `find('list')`:
+
+``` php
+I18n::setLocale('es');
+$data = $this->Articles->find('list')->toArray();
+
+// Data will contain
+[1 => 'Mi primer artículo', 2 => 'El segundo artículo', 15 => 'Otro articulo' ...]
+
+// Change the locale to french for a single find call
+$data = $this->Articles->find('list', locale: 'fr')->toArray();
+```
+
+### Retrieve All Translations For An Entity
+
+When building interfaces for updating translated content, it is often helpful to
+show one or more translation(s) at the same time. You can use the
+`translations` finder for this:
+
+``` php
+// Find the first article with all corresponding translations
+$article = $this->Articles->find('translations')->first();
+```
+
+In the example above you will get a list of entities back that have a
+`_translations` property set. This property will contain a list of translation
+data entities. For example the following properties would be accessible:
+
+``` text
+// Outputs 'en'
+echo $article->_translations['en']->locale;
+
+// Outputs 'title'
+echo $article->_translations['en']->field;
+
+// Outputs 'My awesome post!'
+echo $article->_translations['en']->body;
+```
+
+A more elegant way for dealing with this data is by adding a trait to the entity
+class that is used for your table:
+
+``` php
+use Cake\ORM\Behavior\Translate\TranslateTrait;
+use Cake\ORM\Entity;
+
+class Article extends Entity
+{
+ use TranslateTrait;
+}
+```
+
+This trait contains a single method called `translation`, which lets you
+access or create new translation entities on the fly:
+
+``` php
+// Outputs 'title'
+echo $article->translation('en')->title;
+
+// Adds a new translation data entity to the article
+$article->translation('de')->title = 'Wunderbar';
+```
+
+### Limiting the Translations to be Retrieved
+
+You can limit the languages that are fetched from the database for a particular
+set of records:
+
+``` php
+$results = $this->Articles->find('translations', locales: ['en', 'es']);
+$article = $results->first();
+$spanishTranslation = $article->translation('es');
+$englishTranslation = $article->translation('en');
+```
+
+### Preventing Retrieval of Empty Translations
+
+Translation records can contain any string, if a record has been translated
+and stored as an empty string ('') the translate behavior will take and use
+this to overwrite the original field value.
+
+If this is undesired, you can ignore translations which are empty using the
+`allowEmptyTranslations` config key:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Translate', [
+ 'fields' => ['title', 'body'],
+ 'allowEmptyTranslations' => false
+ ]);
+ }
+}
+```
+
+The above would only load translated data that had content.
+
+### Retrieving All Translations For Associations
+
+It is also possible to find translations for any association in a single find
+operation:
+
+``` php
+$article = $this->Articles->find('translations')->contain([
+ 'Categories' => function ($query) {
+ return $query->find('translations');
+ }
+])->first();
+
+// Outputs 'Programación'
+echo $article->categories[0]->translation('es')->name;
+```
+
+This assumes that `Categories` has the TranslateBehavior attached to it. It
+simply uses the query builder function for the `contain` clause to use the
+`translations` custom finder in the association.
+
+
+
+### Retrieving one language without using I18n::setLocale
+
+calling `I18n::setLocale('es');` changes the default locale for all translated
+finds, there may be times you wish to retrieve translated content without
+modifying the application's state. For these scenarios use the behavior's
+`setLocale()` method:
+
+ I18n::setLocale('en'); // reset for illustration
+
+ // specific locale.
+ $this->Articles->getBehavior('Translate')->setLocale('es');
+
+ $article = $this->Articles->get(12);
+ echo $article->title; // Echoes 'Un Artículo', yay piece of cake!
+
+Note that this only changes the locale of the Articles table, it would not
+affect the language of associated data. To affect associated data it's necessary
+to call the method on each table, for example:
+
+``` php
+I18n::setLocale('en'); // reset for illustration
+
+$this->Articles->getBehavior('Translate')->setLocale('es');
+$this->Articles->Categories->getBehavior('Translate')->setLocale('es');
+
+$data = $this->Articles->find('all', contain: ['Categories']);
+```
+
+This example also assumes that `Categories` has the TranslateBehavior attached
+to it.
+
+### Querying Translated Fields
+
+TranslateBehavior does not substitute find conditions by default. You need to use
+`translationField()` method to compose find conditions on translated fields:
+
+``` php
+$this->Articles->getBehavior('Translate')->setLocale('es');
+$query = $this->Articles->find()->where([
+ $this->Articles->getBehavior('Translate')->translationField('title') => 'Otro Título'
+]);
+```
+
+## Saving in Another Language
+
+The philosophy behind the TranslateBehavior is that you have an entity
+representing the default language, and multiple translations that can override
+certain fields in such entity. Keeping this in mind, you can intuitively save
+translations for any given entity. For example, given the following setup:
+
+``` php
+// in src/Model/Table/ArticlesTable.php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Translate', ['fields' => ['title', 'body']]);
+ }
+}
+
+// in src/Model/Entity/Article.php
+class Article extends Entity
+{
+ use TranslateTrait;
+}
+
+// In the Articles Controller
+$article = new Article([
+ 'title' => 'My First Article',
+ 'body' => 'This is the content',
+ 'footnote' => 'Some afterwords'
+]);
+
+$this->Articles->save($article);
+```
+
+So, after you save your first article, you can now save a translation for it,
+there are a couple ways to do it. The first one is setting the language directly
+into the entity:
+
+``` php
+$article->_locale = 'es';
+$article->title = 'Mi primer Artículo';
+
+$this->Articles->save($article);
+```
+
+After the entity has been saved, the translated field will be persisted as well,
+one thing to note is that values from the default language that were not
+overridden will be preserved:
+
+``` text
+// Outputs 'This is the content'
+echo $article->body;
+
+// Outputs 'Mi primer Artículo'
+echo $article->title;
+```
+
+Once you override the value, the translation for that field will be saved and
+can be retrieved as usual:
+
+``` php
+$article->body = 'El contendio';
+$this->Articles->save($article);
+```
+
+The second way to use for saving entities in another language is to set the
+default language directly to the table:
+
+``` php
+$article->title = 'Mi Primer Artículo';
+
+$this->Articles->getBehavior('Translate')->setLocale('es');
+$this->Articles->save($article);
+```
+
+Setting the language directly in the table is useful when you need to both
+retrieve and save entities for the same language or when you need to save
+multiple entities at once.
+
+
+
+## Saving Multiple Translations
+
+It is a common requirement to be able to add or edit multiple translations to
+any database record at the same time. This can be done using the
+`TranslateTrait`:
+
+``` php
+use Cake\ORM\Behavior\Translate\TranslateTrait;
+use Cake\ORM\Entity;
+
+class Article extends Entity
+{
+ use TranslateTrait;
+}
+```
+
+Now, You can populate translations before saving them:
+
+``` php
+$translations = [
+ 'fr' => ['title' => "Un article"],
+ 'es' => ['title' => 'Un artículo'],
+];
+
+foreach ($translations as $lang => $data) {
+ $article->translation($lang)->set($data, ['guard' => false]);
+}
+
+$this->Articles->save($article);
+```
+
+And create form controls for your translated fields:
+
+``` php
+// In a view template.
+= $this->Form->create($article); ?>
+
+
+```
+
+In your controller, you can marshal the data as normal:
+
+``` php
+$article = $this->Articles->newEntity($this->request->getData());
+$this->Articles->save($article);
+```
+
+This will result in your article, the french and spanish translations all being
+persisted. You'll need to remember to add `_translations` into the
+`$_accessible` fields of your entity as well.
+
+### Validating Translated Entities
+
+When attaching `TranslateBehavior` to a model, you can define the validator
+that should be used when translation records are created/modified by the
+behavior during `newEntity()` or `patchEntity()`:
+
+``` php
+class ArticlesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Translate', [
+ 'fields' => ['title'],
+ 'validator' => 'translated',
+ ]);
+ }
+}
+```
+
+The above will use the validator created by `validationTranslated` to
+validated translated entities.
diff --git a/docs/en/orm/behaviors/tree.md b/docs/en/orm/behaviors/tree.md
new file mode 100644
index 0000000000..177ec2d962
--- /dev/null
+++ b/docs/en/orm/behaviors/tree.md
@@ -0,0 +1,376 @@
+# Tree
+
+`class` Cake\\ORM\\Behavior\\**TreeBehavior**
+
+It's fairly common to want to store hierarchical data in a database
+table. Examples of such data might be categories with unlimited
+subcategories, data related to a multilevel menu system or a
+literal representation of hierarchy such as departments in a company.
+
+Relational databases are usually not well suited for storing and retrieving this
+type of data, but there are a few known techniques that can make them effective
+for working with multi-level information.
+
+The TreeBehavior helps you maintain a hierarchical data structure in the
+database that can be queried without much overhead and helps reconstruct the
+tree data for finding and displaying processes.
+
+## Requirements
+
+This behavior requires the following columns in your table:
+
+- `parent_id` (nullable) The column holding the ID of the parent row. This column should be indexed.
+- `lft` (integer, signed) Used to maintain the tree structure. This column should be indexed.
+- `rght` (integer, signed) Used to maintain the tree structure.
+
+You can configure the name of those fields should you need to customize them.
+More information on the meaning of the fields and how they are used can be found
+in this article describing the [MPTT logic](https://www.sitepoint.com/hierarchical-data-database-2/)
+
+> [!WARNING]
+> The TreeBehavior is not safe for concurrent write operations.
+> Simultaneous requests that modify tree-structured data
+> (e.g., insertions, deletions, or moves) can lead to corruption of the
+> `lft` and `rght` values.
+>
+> To prevent this, a locking mechanism like a
+> [Semaphore](https://www.php.net/manual/en/book.sem.php) should be used.
+
+> [!WARNING]
+> The TreeBehavior does not support composite primary keys at this point in
+> time.
+
+## A Quick Tour
+
+You enable the Tree behavior by adding it to the Table you want to store
+hierarchical data in:
+
+``` php
+class CategoriesTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Tree');
+ }
+}
+```
+
+Once added, you can let CakePHP build the internal structure if the table is
+already holding some rows:
+
+``` php
+// In a controller
+$categories = $this->getTableLocator()->get('Categories');
+$categories->recover();
+```
+
+You can verify it works by getting any row from the table and asking for the
+count of descendants it has:
+
+``` php
+$node = $categories->get(1);
+echo $categories->getBehavior('Tree')->childCount($node);
+```
+
+### Getting direct descendents
+
+Getting a flat list of the descendants for a node can be done with:
+
+``` php
+$descendants = $categories->find('children', for: 1);
+
+foreach ($descendants as $category) {
+ echo $category->name . "\n";
+}
+```
+
+If you need to pass conditions you do so as per normal:
+
+``` php
+$descendants = $categories
+ ->find('children', for: 1)
+ ->where(['name LIKE' => '%Foo%'])
+ ->all();
+
+foreach ($descendants as $category) {
+ echo $category->name . "\n";
+}
+```
+
+If you instead need a threaded list, where children for each node are nested
+in a hierarchy, you can stack the 'threaded' finder:
+
+``` php
+$children = $categories
+ ->find('children', for: 1)
+ ->find('threaded')
+ ->toArray();
+
+foreach ($children as $child) {
+ echo "{$child->name} has " . count($child->children) . " direct children";
+}
+```
+
+While, if you're using custom `parent_id` you need to pass it in the
+'threaded' finder option (i.e. `parentField`) .
+
+> [!NOTE]
+> For more information on 'threaded' finder options see [Finding Threaded Data logic](../../orm/retrieving-data-and-resultsets#finding-threaded-data)
+
+### Getting formatted tree lists
+
+Traversing threaded results usually requires recursive functions in, but if you
+only require a result set containing a single field from each level so you can
+display a list, in an HTML select for example, it is better to use the
+`treeList` finder:
+
+``` php
+$list = $categories->find('treeList')->toArray();
+
+// In a CakePHP template file:
+echo $this->Form->control('categories', ['options' => $list]);
+
+// Or you can output it in plain text, for example in a CLI script
+foreach ($list as $categoryName) {
+ echo $categoryName . "\n";
+}
+```
+
+The output will be similar to:
+
+ My Categories
+ _Fun
+ __Sport
+ ___Surfing
+ ___Skating
+ _Trips
+ __National
+ __International
+
+The `treeList` finder takes a number of options:
+
+- `keyPath`: A dot separated path to fetch the field to use for the array key,
+ or a closure to return the key out of the provided row.
+- `valuePath`: A dot separated path to fetch the field to use for the array
+ value, or a closure to return the value out of the provided row.
+- `spacer`: A string to be used as prefix for denoting the depth in the tree
+ for each item
+
+An example of all options in use is:
+
+``` php
+$query = $categories->find('treeList',
+ keyPath: 'url',
+ valuePath: 'id',
+ spacer: ' '
+);
+```
+
+An example using closure:
+
+``` php
+$query = $categories->find('treeList',
+ valuePath: function($entity){
+ return $entity->url . ' ' . $entity->id
+ }
+);
+```
+
+### Finding a path or branch in the tree
+
+One very common task is to find the tree path from a particular node to the root
+of the tree. This is useful, for example, for adding the breadcrumbs list for
+a menu structure:
+
+``` php
+$nodeId = 5;
+$crumbs = $categories->find('path', for: $nodeId)->all();
+
+foreach ($crumbs as $crumb) {
+ echo $crumb->name . ' > ';
+}
+```
+
+Trees constructed with the TreeBehavior cannot be sorted by any column other
+than `lft`, this is because the internal representation of the tree depends on
+this sorting. Luckily, you can reorder the nodes inside the same level without
+having to change their parent:
+
+``` php
+$node = $categories->get(5);
+
+// Move the node so it shows up one position up when listing children.
+$categories->getBehavior('Tree')->moveUp($node);
+
+// Move the node to the top of the list inside the same level.
+$categories->getBehavior('Tree')->moveUp($node, true);
+
+// Move the node to the bottom.
+$categories->getBehavior('Tree')->moveDown($node, true);
+```
+
+## Configuration
+
+If the default column names that are used by this behavior don't match your own
+schema, you can provide aliases for them:
+
+``` php
+public function initialize(array $config): void
+{
+ $this->addBehavior('Tree', [
+ 'parent' => 'ancestor_id', // Use this instead of parent_id
+ 'left' => 'tree_left', // Use this instead of lft
+ 'right' => 'tree_right' // Use this instead of rght
+ ]);
+}
+```
+
+### Node Level (Depth)
+
+Knowing the depth of tree nodes can be useful when you want to retrieve nodes
+only up to a certain level, for example, when generating menus. You can use the
+`level` option to specify the field that will save level of each node:
+
+``` php
+$this->addBehavior('Tree', [
+ 'level' => 'level', // Defaults to null, i.e. no level saving
+]);
+```
+
+If you don't want to cache the level using a db field you can use
+`TreeBehavior::getLevel()` method to get level of a node.
+
+### Scoping and Multi Trees
+
+Sometimes you want to persist more than one tree structure inside the same
+table, you can achieve that by using the 'scope' configuration. For example, in
+a locations table you may want to create one tree per country:
+
+``` php
+class LocationsTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ $this->addBehavior('Tree', [
+ 'scope' => ['country_name' => 'Brazil']
+ ]);
+ }
+}
+```
+
+In the previous example, all tree operations will be scoped to only the rows
+having the column `country_name` set to 'Brazil'. You can change the scoping
+on the fly by using the 'config' function:
+
+``` php
+$this->getBehavior('Tree')->setConfig('scope', ['country_name' => 'France']);
+```
+
+Optionally, you can have a finer grain control of the scope by passing a closure
+as the scope:
+
+``` php
+$this->getBehavior('Tree')->setConfig('scope', function ($query) {
+ $country = $this->getConfigureContry(); // A made-up function
+ return $query->where(['country_name' => $country]);
+});
+```
+
+### Deletion Behavior
+
+By enabling the `cascadeCallbacks` option, `TreeBehavior` will load all of
+the entities that are going to be deleted. Once loaded, these entities will be
+deleted individually using `Table::delete()`. This enables ORM callbacks to be
+fired when tree nodes are deleted:
+
+``` php
+$this->addBehavior('Tree', [
+ 'cascadeCallbacks' => true,
+]);
+```
+
+## Recovering with custom sort field
+
+By default, `recover()` sorts the items using the primary key. This works great
+if this is a numeric (auto increment) column, but can lead to weird results if you
+use UUIDs.
+
+If you need custom sorting for the recovery, you can set a
+custom order clause in your config:
+
+``` php
+$this->addBehavior('Tree', [
+ 'recoverOrder' => ['country_name' => 'DESC'],
+]);
+```
+
+## Saving Hierarchical Data
+
+When using the Tree behavior, you usually don't need to worry about the
+internal representation of the hierarchical structure. The positions where nodes
+are placed in the tree are deduced from the `parent_id` column in each of your
+entities:
+
+``` php
+$aCategory = $categoriesTable->get(10);
+$aCategory->parent_id = 5;
+$categoriesTable->save($aCategory);
+```
+
+Providing inexistent parent ids when saving or attempting to create a loop in
+the tree (making a node child of itself) will throw an exception.
+
+You can make a node into a root in the tree by setting the `parent_id` column to
+null:
+
+``` php
+$aCategory = $categoriesTable->get(10);
+$aCategory->parent_id = null;
+$categoriesTable->save($aCategory);
+```
+
+Children for the new root node will be preserved.
+
+## Deleting Nodes
+
+Deleting a node and all its sub-tree (any children it may have at any depth in
+the tree) is trivial:
+
+``` php
+$aCategory = $categoriesTable->get(10);
+$categoriesTable->delete($aCategory);
+```
+
+The TreeBehavior will take care of all internal deleting operations for you. It
+is also possible to only delete one node and re-assign all children to the
+immediately superior parent node in the tree:
+
+``` php
+$aCategory = $categoriesTable->get(10);
+$categoriesTable->getBehavior('Tree')->removeFromTree($aCategory);
+$categoriesTable->delete($aCategory);
+```
+
+All children nodes will be kept and a new parent will be assigned to them.
+
+The deletion of a node is based off of the `lft` and `rght` values of the entity. This
+is important to note when looping through the various children of a node for
+conditional deletes:
+
+``` php
+$descendants = $teams->find('children', for: 1)->all();
+
+foreach ($descendants as $descendant) {
+ $team = $teams->get($descendant->id); // search for the up-to-date entity object
+ if ($team->expired) {
+ $teams->delete($team); // deletion reorders the lft and rght of database entries
+ }
+}
+```
+
+TreeBehavior will reorder the `lft` and `rght` values of records in the
+table when a node is deleted.
+
+In our example above, the `lft` and `rght` values of the entities inside
+`$descendants` will be inaccurate. You will need to reload existing entity
+objects if you need an accurate shape of the tree.
diff --git a/docs/en/orm/database-basics.md b/docs/en/orm/database-basics.md
new file mode 100644
index 0000000000..852267223b
--- /dev/null
+++ b/docs/en/orm/database-basics.md
@@ -0,0 +1,1252 @@
+# Database Basics
+
+The CakePHP database access layer abstracts and provides help with most aspects
+of dealing with relational databases such as, keeping connections to the server,
+building queries, preventing SQL injections, inspecting and altering schemas,
+and with debugging and profiling queries sent to the database.
+
+## Quick Tour
+
+The functions described in this chapter illustrate what is possible to do with
+the lower-level database access API. If instead you want to learn more about the
+complete ORM, you can read the [Query Builder](../orm/query-builder) and
+[Table Objects](../orm/table-objects) sections.
+
+The easiest way to create a database connection is using a `DSN` string:
+
+``` php
+use Cake\Datasource\ConnectionManager;
+
+$dsn = 'mysql://root:password@localhost/my_database';
+ConnectionManager::setConfig('default', ['url' => $dsn]);
+```
+
+Once created, you can access the connection object to start using it:
+
+``` php
+$connection = ConnectionManager::get('default');
+```
+
+> [!NOTE]
+> For supported databases, see [installation notes](../installation).
+
+
+
+### Running Select Statements
+
+Running raw SQL queries is a breeze:
+
+``` php
+use Cake\Datasource\ConnectionManager;
+
+$connection = ConnectionManager::get('default');
+$results = $connection->execute('SELECT * FROM articles')->fetchAll('assoc');
+```
+
+You can use prepared statements to insert parameters:
+
+``` php
+$results = $connection
+ ->execute('SELECT * FROM articles WHERE id = :id', ['id' => 1])
+ ->fetchAll('assoc');
+```
+
+It is also possible to use complex data types as arguments:
+
+``` php
+use Cake\Datasource\ConnectionManager;
+use DateTime;
+
+$connection = ConnectionManager::get('default');
+$results = $connection
+ ->execute(
+ 'SELECT * FROM articles WHERE created >= :created',
+ ['created' => new DateTime('1 day ago')],
+ ['created' => 'datetime']
+ )
+ ->fetchAll('assoc');
+```
+
+Instead of writing the SQL manually, you can use the query builder:
+
+``` php
+// Prior to 4.5 use $connection->query() instead.
+$results = $connection
+ ->selectQuery('*', 'articles')
+ ->where(['created >' => new DateTime('1 day ago')], ['created' => 'datetime'])
+ ->orderBy(['title' => 'DESC'])
+ ->execute()
+ ->fetchAll('assoc');
+```
+
+### Running Insert Statements
+
+Inserting rows in the database is usually a matter of a couple lines:
+
+``` php
+use Cake\Datasource\ConnectionManager;
+use DateTime;
+
+$connection = ConnectionManager::get('default');
+$connection->insert('articles', [
+ 'title' => 'A New Article',
+ 'created' => new DateTime('now')
+], ['created' => 'datetime']);
+```
+
+### Running Update Statements
+
+Updating rows in the database is equally intuitive, the following example will
+update the article with **id** 10:
+
+``` php
+use Cake\Datasource\ConnectionManager;
+$connection = ConnectionManager::get('default');
+$connection->update('articles', ['title' => 'New title'], ['id' => 10]);
+```
+
+### Running Delete Statements
+
+Similarly, the `delete()` method is used to delete rows from the database, the
+following example deletes the article with **id** 10:
+
+``` php
+use Cake\Datasource\ConnectionManager;
+$connection = ConnectionManager::get('default');
+$connection->delete('articles', ['id' => 10]);
+```
+
+
+
+## Configuration
+
+By convention database connections are configured in **config/app.php**. The
+connection information defined in this file is fed into
+`Cake\Datasource\ConnectionManager` creating the connection configuration
+your application will be using. Sample connection information can be found in
+**config/app.default.php**. A sample connection configuration would look
+like:
+
+``` php
+'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,
+ ],
+],
+```
+
+The above will create a 'default' connection, with the provided parameters. You
+can define as many connections as you want in your configuration file. You can
+also define additional connections at runtime using
+`Cake\Datasource\ConnectionManager::setConfig()`. An example of that
+would be:
+
+``` php
+use Cake\Datasource\ConnectionManager;
+
+ConnectionManager::setConfig('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,
+]);
+```
+
+Configuration options can also be provided as a `DSN` string. This is
+useful when working with environment variables or `PaaS` providers:
+
+``` css
+ConnectionManager::setConfig('default', [
+ 'url' => 'mysql://my_app:sekret@localhost/my_app?encoding=utf8&timezone=UTC&cacheMetadata=true',
+]);
+```
+
+When using a DSN string you can define any additional parameters/options as
+query string arguments.
+
+By default, all Table objects will use the `default` connection. To
+use a non-default connection, see [Configuring Table Connections](../orm/table-objects#configuring-table-connections).
+
+There are a number of keys supported in database configuration. A full list is
+as follows:
+
+className
+The fully namespaced class name of the class that represents the connection to a database server.
+This class is responsible for loading the database driver, providing SQL
+transaction mechanisms and preparing SQL statements among other things.
+
+driver
+The class name of the driver used to implement all specificities for
+a database engine. This can either be a short classname using `plugin syntax`,
+a fully namespaced name, or a constructed driver instance.
+Examples of short classnames are Mysql, Sqlite, Postgres, and Sqlserver.
+
+persistent
+Whether or not to use a persistent connection to the database. This option
+is not supported by SqlServer. An exception is thrown if you attempt to set
+`persistent` to `true` with SqlServer.
+
+host
+The database server's hostname (or IP address).
+
+username
+The username for the account.
+
+password
+The password for the account.
+
+database
+The name of the database for this connection to use. Avoid using `.` in
+your database name. Because of how it complicates identifier quoting CakePHP
+does not support `.` in database names. The path to your SQLite database
+should be an absolute path (for example, `ROOT . DS . 'my_app.db'`) to avoid
+incorrect paths caused by relative paths.
+
+port (*optional*)
+The TCP port or Unix socket used to connect to the server.
+
+encoding
+Indicates the character set to use when sending SQL statements to
+the server. This defaults to the database's default encoding for
+all databases other than DB2.
+
+timezone
+Server timezone to set.
+
+schema
+Used in PostgreSQL database setups to specify which schema to use.
+
+unix_socket
+Used by drivers that support it to connect via Unix socket files. If you are
+using PostgreSQL and want to use Unix sockets, leave the host key blank.
+
+ssl_key
+The file path to the SSL key file. (Only supported by MySQL).
+
+ssl_cert
+The file path to the SSL certificate file. (Only supported by MySQL).
+
+ssl_ca
+The file path to the SSL certificate authority. (Only supported by MySQL).
+
+init
+A list of queries that should be sent to the database server as
+when the connection is created.
+
+log
+Set to `true` to enable query logging. When enabled queries will be logged
+at a `debug` level with the `queriesLog` scope.
+
+quoteIdentifiers
+Set to `true` if you are using reserved words or special characters in
+your table or column names. Enabling this setting will result in queries
+built using the [Query Builder](../orm/query-builder) having identifiers quoted when
+creating SQL. It should be noted that this decreases performance because
+each query needs to be traversed and manipulated before being executed.
+
+flags
+An associative array of PDO constants that should be passed to the
+underlying PDO instance. See the PDO documentation for the flags supported
+by the driver you are using.
+
+cacheMetadata
+Either boolean `true`, or a string containing the cache configuration to
+store meta data in. Having metadata caching disabled by setting it to `false`
+is not advised and can result in very poor performance. See the
+[Database Metadata Cache](#database-metadata-cache) section for more information.
+
+mask
+Set the permissions on the generated database file. (Only supported by SQLite)
+
+cache
+The `cache` flag to send to SQLite.
+
+mode
+The `mode` flag value to send to SQLite.
+
+At this point, you might want to take a look at the
+[CakePHP Conventions](../intro/conventions). The correct naming for your tables (and the addition
+of some columns) can score you some free functionality and help you avoid
+configuration. For example, if you name your database table big_boxes, your
+table BigBoxesTable, and your controller BigBoxesController, everything will
+work together automatically. By convention, use underscores, lower case, and
+plural forms for your database table names - for example: bakers,
+pastry_stores, and savory_cakes.
+
+> [!NOTE]
+> If your MySQL server is configured with `skip-character-set-client-handshake`
+> then you MUST use the `flags` config to set your charset encoding. For example:
+>
+> ``` php
+> 'flags' => [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']
+> ```
+
+
+
+## Read and Write Connections
+
+Connections can have separate read and write roles. Read
+roles are expected to represent read-only replicas and write roles are expected
+to be the default connection and support write operations.
+
+Read roles are configured by providing a `read` key in the connection config.
+Write roles are configured by providing a `write` key.
+
+Role configurations override the values in the shared connection config. If the read
+and write role configurations are the same, a single connection to the database is used
+for both:
+
+``` text
+'default' => [
+ 'driver' => 'mysql',
+ 'username' => '...',
+ 'password' => '...',
+ 'database' => '...',
+ 'read' => [
+ 'host' => 'read-db.example.com',
+ ],
+ 'write' => [
+ 'host' => 'write-db.example.com',
+ ]
+];
+```
+
+You can specify the same value for both `read` and `write` key without creating
+multiple connections to the database.
+
+## Managing Connections
+
+`class` Cake\\Datasource\\**ConnectionManager**
+
+The `ConnectionManager` class acts as a registry to access database
+connections your application has. It provides a place that other objects can get
+references to existing connections.
+
+### Accessing Connections
+
+`static` Cake\\Datasource\\ConnectionManager::**get**($name): ConnectionInterface
+
+Once configured connections can be fetched using
+`Cake\Datasource\ConnectionManager::get()`. This method will
+construct and load a connection if it has not been built before, or return the
+existing known connection:
+
+``` php
+use Cake\Datasource\ConnectionManager;
+
+$connection = ConnectionManager::get('default');
+```
+
+Attempting to load connections that do not exist will throw an exception.
+
+### Creating Connections at Runtime
+
+Using `setConfig()` and `get()` you can create new connections that are not
+defined in your configuration files at runtime:
+
+``` php
+ConnectionManager::setConfig('my_connection', $config);
+$connection = ConnectionManager::get('my_connection');
+```
+
+See the [Database Configuration](#database-configuration) for more information on the configuration
+data used when creating connections.
+
+## Data Types
+
+`class` Cake\\Database\\**TypeFactory**
+
+Since not every database vendor includes the same set of data types, or
+the same names for similar data types, CakePHP provides a set of abstracted
+data types for use with the database layer. The types CakePHP supports are:
+
+string
+Maps to `VARCHAR` type. In SQL Server the `NVARCHAR` types are used.
+
+char
+Maps to `CHAR` type. In SQL Server the `NCHAR` type is used.
+
+text
+Maps to `TEXT` types.
+
+uuid
+Maps to the UUID type if a database provides one, otherwise this will
+generate a `CHAR(36)` field.
+
+binaryuuid
+Maps to the UUID type if the database provides one, otherwise this will
+generate a `BINARY(16)` column. Binary UUIDs provide more efficient storage
+compared to string UUIDs by storing the UUID as 16 bytes of binary data rather
+than a 36-character string. This type automatically handles conversion between
+string UUID format (with dashes) and binary format.
+
+nativeuuid
+Maps to the UUID type in MySQL with MariaDb. In all other databases,
+`nativeuuid` is an alias for `uuid`.
+
+integer
+Maps to the `INTEGER` type provided by the database. BIT is not yet supported
+at this moment.
+
+smallinteger
+Maps to the `SMALLINT` type provided by the database.
+
+tinyinteger
+Maps to the `TINYINT` or `SMALLINT` type provided by the database. In MySQL
+`TINYINT(1)` is treated as a boolean.
+
+biginteger
+Maps to the `BIGINT` type provided by the database.
+
+float
+Maps to either `DOUBLE` or `FLOAT` depending on the database. The `precision`
+option can be used to define the precision used.
+
+decimal
+Maps to the `DECIMAL` type. Supports the `length` and `precision`
+options. Values for decimal type ares be represented as strings (not as float
+as some might expect). This is because decimal types are used to represent
+exact numeric values in databases and using float type for them in PHP can
+potentially lead to precision loss.
+
+If you want the values to be float in your PHP code then consider using
+FLOAT or DOUBLE type columns in your database. Also, depending on your use
+case you can explicitly map your decimal columns to float type in your table
+schema.
+
+boolean
+Maps to `BOOLEAN` except in MySQL, where `TINYINT(1)` is used to represent
+booleans. `BIT(1)` is not yet supported at this moment.
+
+binary
+Maps to the `BLOB` or `BYTEA` type provided by the database.
+
+date
+Maps to a native `DATE` column type. The return value of this column
+type is `Cake\I18n\Date` which emulates the date related
+methods of PHP's `DateTime` class.
+
+datetime
+See [Datetime Type](#datetime-type).
+
+datetimefractional
+See [Datetime Type](#datetime-type).
+
+timestamp
+Maps to the `TIMESTAMP` type.
+
+timestampfractional
+Maps to the `TIMESTAMP(N)` type.
+
+time
+Maps to a `TIME` type in all databases.
+
+year
+Maps to the `YEAR` type. Only supported in MySQL.
+
+json
+Maps to a `JSON` type if it's available, otherwise it maps to `TEXT`.
+
+enum
+See [Enum Type](#enum-type).
+
+geometry
+Maps to a generic geometry storage type.
+
+point
+Maps to a single point in geospatial storage.
+
+linestring
+Maps to a single line in geospatial storage.
+
+polygon
+Maps to a single polygon in geospatial storage.
+
+inet
+Maps to the `INET` type. Only implemented in postgres.
+
+cidr
+Maps to the `CIDR` type. Only implemented in postgres.
+
+macaddr
+Maps to the `MACADDR` type. Only implemented in postgres.
+
+These types are used in both the schema reflection features that CakePHP
+provides, and schema generation features CakePHP uses when using test fixtures.
+
+Each type can also provide translation functions between PHP and SQL
+representations. These methods are invoked based on the type hints provided when
+doing queries. For example a column that is marked as 'datetime' will
+automatically convert input parameters from `DateTime` instances into a
+timestamp or formatted datestrings. Likewise, 'binary' columns will accept file
+handles, and generate file handles when reading data.
+
+::: info Changed in version 5.1.0
+The `geometry`, `point`, `linestring`, and `polygon` types were added.
+:::
+
+::: info Changed in version 5.2.0
+The `nativeuuid` type was added.
+:::
+
+::: info Added in version 5.3.0
+The `inet`, `cidr`, `macaddr`, and `year` types were added.
+:::
+
+
+
+### DateTime Type
+
+`class` Cake\\Database\\**DateTimeType**
+
+Maps to a native `DATETIME` column type. In PostgreSQL and SQL Server this
+turns into a `TIMESTAMP` type. The default return value of this column type is
+`Cake\I18n\DateTime` which extends [Chronos](https://github.com/cakephp/chronos) and the native `DateTimeImmutable`.
+
+`method` Cake\\Database\\DateTimeType::**setTimezone**(string|\\DateTimeZone|null $timezone)
+
+If your database server's timezone does not match your application's PHP timezone
+then you can use this method to specify your database's timezone. This timezone
+will then used when converting PHP objects to database's datetime string and
+vice versa.
+
+`class` Cake\\Database\\**DateTimeFractionalType**
+
+Can be used to map datetime columns that contain microseconds such as
+`DATETIME(6)` in MySQL. To use this type you need to add it as a mapped type:
+
+``` php
+// in config/bootstrap.php
+use Cake\Database\TypeFactory;
+use Cake\Database\Type\DateTimeFractionalType;
+
+// Overwrite the default datetime type with a more precise one.
+TypeFactory::map('datetime', DateTimeFractionalType::class);
+```
+
+`class` Cake\\Database\\**DateTimeTimezoneType**
+
+Can be used to map datetime columns that contain time zones such as
+`TIMESTAMPTZ` in PostgreSQL. To use this type you need to add it as a mapped type:
+
+``` php
+// in config/bootstrap.php
+use Cake\Database\TypeFactory;
+use Cake\Database\Type\DateTimeTimezoneType;
+
+// Overwrite the default datetime type with a more precise one.
+TypeFactory::map('datetime', DateTimeTimezoneType::class);
+```
+
+
+
+### Enum Type
+
+`class` Cake\\Database\\**EnumType**
+
+Maps a [BackedEnum](https://www.php.net/manual/en/language.enumerations.backed.php) to a string or integer column.
+To use this type you need to specify which column is associated to which BackedEnum inside the table class:
+
+``` php
+use App\Model\Enum\ArticleStatus;
+use Cake\Database\Type\EnumType;
+
+// in src/Model/Table/ArticlesTable.php
+public function initialize(array $config): void
+{
+ parent::initialize($config);
+
+ $this->getSchema()->setColumnType('status', EnumType::from(ArticleStatus::class));
+}
+```
+
+A simple `ArticleStatus` could look like:
+
+``` php
+namespace App\Model\Enum;
+
+enum ArticleStatus: string
+{
+ case Published = 'Y';
+ case Unpublished = 'N';
+}
+```
+
+CakePHP also provides the `EnumLabelInterface` which can be implemented by
+Enums that want to provide a map of human-readable labels:
+
+``` php
+namespace App\Model\Enum;
+
+use Cake\Database\Type\EnumLabelInterface;
+
+enum ArticleStatus: string implements EnumLabelInterface
+{
+ case Published = 'Y';
+ case Unpublished = 'N';
+
+ public static function label(): string
+ {
+ return match ($this) {
+ self::Published => __('Published'),
+ self::Unpublished => __('Unpublished'),
+ };
+ }
+}
+```
+
+This can be useful if you want to use your enums in `FormHelper` select
+inputs. You can use [bake](../bake) to generate an enum class:
+
+``` text
+# generate an enum class with two cases and stored as an integer
+bin/cake bake enum UserStatus inactive:0,active:1 -i
+
+# generate an enum class with two cases as a string
+bin/cake bake enum UserStatus published:Y,unpublished:N
+```
+
+CakePHP recommends a few conventions for enums:
+
+- Enum classnames should follow `{Entity}{ColumnName}` style to enable
+ detection while running bake and to aid with project consistency.
+- Enum cases should use CamelCase style.
+- Enums should implement the `Cake\Database\Type\EnumLabelInterface` to
+ improve compatibility with bake, and `FormHelper`.
+
+### Geospatial Types
+
+The `geometry`, `point`, `linestring`, and `polygon` types are also known
+as the "geospatial types". CakePHP offers limited support for geospatial
+columns. Currently they can be defined in migrations, read in schema reflection,
+and have values set as text.
+
+::: info Added in version 5.1.0
+Geospatial schema types were added.
+:::
+
+
+
+### Adding Custom Types
+
+`class` Cake\\Database\\**TypeFactory**
+
+`static` Cake\\Database\\TypeFactory::**map**($name, $class): void
+
+If you need to use vendor specific types that are not built into CakePHP you can
+add additional new types to CakePHP's type system. Type classes are expected to
+implement the following methods:
+
+- `toPHP`: Casts given value from a database type to a PHP equivalent.
+- `toDatabase`: Casts given value from a PHP type to one acceptable by a database.
+- `toStatement`: Casts given value to its Statement equivalent.
+- `marshal`: Marshals flat data into PHP objects.
+
+To fulfill the basic interface, extend `Cake\Database\Type`.
+For example if we wanted to add a PointMutation type, we could make the following type
+class:
+
+``` php
+// in src/Database/Type/PointMutationType.php
+
+namespace App\Database\Type;
+
+use Cake\Database\Driver;
+use Cake\Database\Type\BaseType;
+use PDO;
+
+class PointMutationType extends BaseType
+{
+ public function toPHP(mixed $value, Driver $driver): mixed
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ return $this->pmDecode($valu
+
+ public function marshal(mixed $value): mixed
+ {
+ if (is_array($value) || $value === null) {
+ return $value;
+ }
+
+ return $this->pmDecode($value);
+ }
+
+ public function toDatabase(mixed $value, Driver $driver): mixed
+ {
+ return sprintf('%d%s>%s', $value['position'], $value['from'], $value['to']);
+ }
+
+ public function toStatement(mixed $value, Driver $driver): int
+ {
+ if ($value === null) {
+ return PDO::PARAM_NULL;
+ }
+
+ return PDO::PARAM_STR;
+ }
+
+ protected function pmDecode(mixed $value): mixed
+ {
+ if (preg_match('/^(\d+)([a-zA-Z])>([a-zA-Z])$/', $value, $matches)) {
+ return [
+ 'position' => (int) $matches[1],
+ 'from' => $matches[2],
+ 'to' => $matches[3]
+ ];
+ }
+
+ return null;
+ }
+}
+```
+
+By default the `toStatement()` method will treat values as strings which will
+work for our new type.
+
+### Connecting Custom Datatypes to Schema Reflection and Generation
+
+Once we've created our new type, we need to add it into
+the type mapping. During our application bootstrap we should do the following:
+
+``` php
+use Cake\Database\TypeFactory;
+
+TypeFactory::map('point_mutation', \App\Database\Type\PointMutationType:class);
+```
+
+We then have two ways to use our datatype in our models.
+
+1. The first path is to overwrite the reflected schema data to use our new type.
+2. The second is to implement `Cake\Database\Type\ColumnSchemaAwareInterface`
+ and define the SQL column type and reflection logic.
+
+Overwriting the reflected schema with our custom type will enable CakePHP's
+database layer to automatically convert PointMutation data when creating queries. In your
+Table's [getSchema() method](../orm/saving-data#saving-complex-types) add the
+
+following:
+
+``` php
+class WidgetsTable extends Table
+{
+ public function initialize(array $config): void
+ {
+ return parent::getSchema()->setColumnType('mutation', 'point_mutation');
+
+ }
+}
+```
+
+Implementing `ColumnSchemaAwareInterface` gives you more control over
+custom datatypes. This avoids overwriting schema definitions if your
+datatype has an unambiguous SQL column definition. For example, we could have
+our PointMutation type be used anytime a `TEXT` column with a specific comment is
+used:
+
+``` php
+// in src/Database/Type/PointMutationType.php
+
+namespace App\Database\Type;
+
+use Cake\Database\Driver;
+use Cake\Database\Type\BaseType;
+use Cake\Database\Type\ColumnSchemaAwareInterface;
+use Cake\Database\Schema\TableSchemaInterface;
+use PDO;
+
+class PointMutationType extends BaseType
+ implements ColumnSchemaAwareInterface
+{
+ // other methods from earlier
+
+ /**
+ * Convert abstract schema definition into a driver specific
+ * SQL snippet that can be used in a CREATE TABLE statement.
+ *
+ * Returning null will fall through to CakePHP's built-in types.
+ */
+ public function getColumnSql(
+ TableSchemaInterface $schema,
+ string $column,
+ Driver $driver
+ ): ?string {
+ $data = $schema->getColumn($column);
+ $sql = $driver->quoteIdentifier($column);
+ $sql .= ' JSON';
+ if (isset($data['null']) && $data['null'] === false) {
+ $sql .= ' NOT NULL';
+ }
+
+ return $sql;
+ }
+
+ /**
+ * Convert the column data returned from schema reflection
+ * into the abstract schema data.
+ *
+ * Returning null will fall through to CakePHP's built-in types.
+ */
+ public function convertColumnDefinition(
+ array $definition,
+ Driver $driver
+ ): ?array {
+ return [
+ 'type' => $this->_name,
+ 'length' => null,
+ ];
+ }
+```
+
+The `$definition` data passed to `convertColumnDefinition()` will contain
+the following keys. All keys will exist but may contain `null` if the key has
+no value for the current database driver:
+
+- `length` The length of a column if available..
+- `precision` The precision of the column if available.
+- `scale` Can be included for SQLServer connections.
+
+
+
+### Mapping Custom Datatypes to SQL Expressions
+
+The previous example maps a custom datatype for a 'point_mutation' column type
+which is easily represented as a string in a SQL statement. Complex SQL data
+types cannot be represented as strings/integers in SQL queries. When working
+with these datatypes your Type class needs to implement the
+`Cake\Database\Type\ExpressionTypeInterface` interface. This interface lets
+your custom type represent a value as a SQL expression. As an example, we'll
+build a simple Type class for handling `POINT` type data out of MySQL. First
+we'll define a 'value' object that we can use to represent `POINT` data in
+PHP:
+
+``` php
+// in src/Database/Point.php
+namespace App\Database;
+
+// Our value object is immutable.
+class Point
+{
+ protected $_lat;
+ protected $_long;
+
+ // Factory method.
+ public static function parse($value)
+ {
+ // Parse the WKB data from MySQL.
+ $unpacked = unpack('x4/corder/Ltype/dlat/dlong', $value);
+
+ return new static($unpacked['lat'], $unpacked['long']);
+ }
+
+ public function __construct($lat, $long)
+ {
+ $this->_lat = $lat;
+ $this->_long = $long;
+ }
+
+ public function lat()
+ {
+ return $this->_lat;
+ }
+
+ public function long()
+ {
+ return $this->_long;
+ }
+}
+```
+
+With our value object created, we'll need a Type class to map data into this
+value object and into SQL expressions:
+
+``` php
+namespace App\Database\Type;
+
+use App\Database\Point;
+use Cake\Database\Driver;
+use Cake\Database\Expression\FunctionExpression;
+use Cake\Database\ExpressionInterface;
+use Cake\Database\Type\BaseType;
+use Cake\Database\Type\ExpressionTypeInterface;
+
+class PointType extends BaseType implements ExpressionTypeInterface
+{
+ public function toPHP($value, Driver $d): mixed
+ {
+ return $value === null ? null : Point::parse($value);
+ }
+
+ public function marshal($value): mixed
+ {
+ if (is_string($value)) {
+ $value = explode(',', $value);
+ }
+ if (is_array($value)) {
+ return new Point($value[0], $value[1]);
+ }
+
+ return null;
+ }
+
+ public function toExpression($value): ExpressionInterface
+ {
+ if ($value instanceof Point) {
+ return new FunctionExpression(
+ 'POINT',
+ [
+ $value->lat(),
+ $value->long()
+ ]
+ );
+ }
+ if (is_array($value)) {
+ return new FunctionExpression('POINT', [$value[0], $value[1]]);
+ }
+ // Handle other cases.
+ }
+
+ public function toDatabase($value, Driver $driver): mixed
+ {
+ return $value;
+ }
+}
+```
+
+The above class does a few interesting things:
+
+- The `toPHP` method handles parsing the SQL query results into a value
+ object.
+- The `marshal` method handles converting, data such as given request data, into our value object.
+ We're going to accept string values like `'10.24,12.34` and arrays for now.
+- The `toExpression` method handles converting our value object into the
+ equivalent SQL expressions. In our example the resulting SQL would be
+ something like `POINT(10.24, 12.34)`.
+
+Once we've built our custom type, we'll need to [connect our type
+to our table class](../orm/saving-data#saving-complex-types).
+
+## Connection Classes
+
+`class` Cake\\Database\\**Connection**
+
+Connection classes provide a simple interface to interact with database
+connections in a consistent way. They are intended as a more abstract interface to
+the driver layer and provide features for executing queries, logging queries, and doing
+transactional operations.
+
+
+
+### Executing Queries
+
+`method` Cake\\Database\\Connection::**execute**(string $sql, array $params = [], array $types = []): StatementInterface
+
+Once you've gotten a connection object, you'll probably want to issue some
+queries with it. CakePHP's database abstraction layer provides wrapper features
+on top of PDO and native drivers. These wrappers provide a similar interface to
+PDO. There are a few different ways you can run queries depending on the type of
+query you need to run and what kind of results you need back. The most basic
+method is `execute()` which allows you to run complet SQL queries:
+
+``` php
+$statement = $connection->execute('UPDATE articles SET published = 1 WHERE id = 2');
+```
+
+For parameterized queries use the 2nd argument:
+
+``` php
+$statement = $connection->execute(
+ 'UPDATE articles SET published = ? WHERE id = ?',
+ [1, 2]
+);
+```
+
+Without any type hinting information, `execute` will assume all placeholders
+are string values. If you need to bind specific types of data, you can use their
+abstract type names when creating a query:
+
+``` php
+$statement = $connection->execute(
+ 'UPDATE articles SET published_date = ? WHERE id = ?',
+ [new DateTime('now'), 2],
+ ['date', 'integer']
+);
+```
+
+`method` Cake\\Database\\Connection::**selectQuery**(): SelectQuery
+
+These methods allow you to use rich data types in your applications and properly convert
+them into SQL statements. The last and most flexible way of creating queries is
+to use the [Query Builder](../orm/query-builder). This approach allows you to build complex and
+expressive queries without having to use platform specific SQL. When using the
+query builder, no SQL will be sent to the database server until the `execute()`
+method is called, or the query is iterated. Iterating a query will first execute
+it and then start iterating over the result set:
+
+``` php
+$query = $connection->selectQuery();
+$query->select('*')
+ ->from('articles')
+ ->where(['published' => true]);
+
+foreach ($query as $row) {
+ // Do something with the row.
+}
+```
+
+> [!NOTE]
+> Instead of iterating the `$query` you can also call it's `all()` method
+> to get the results.
+
+`method` Cake\\Database\\Connection::**updateQuery**(): UpdateQuery
+
+This method provides you a builder for `UPDATE` queries:
+
+``` php
+$query = $connection->updateQuery('articles')
+ ->set(['published' => true])
+ ->where(['id' => 2]);
+$statement = $query->execute();
+```
+
+`method` Cake\\Database\\Connection::**insertQuery**(): InsertQuery
+
+This method provides you a builder for `INSERT` queries:
+
+``` php
+$query = $connection->insertQuery();
+$query->into('articles')
+ ->columns(['title'])
+ ->values(['1st article']);
+$statement = $query->execute();
+```
+
+`method` Cake\\Database\\Connection::**deleteQuery**(): DeleteQuery
+
+This method provides you a builder for `DELETE` queries:
+
+``` php
+$query = $connection->deleteQuery();
+$query->delete('articles')
+ ->where(['id' => 2]);
+$statement = $query->execute();
+```
+
+### Using Transactions
+
+The connection objects provide you a few simple ways you do database
+transactions. The most basic way of doing transactions is through the `begin()`,
+`commit()` and `rollback()` methods, which map to their SQL equivalents:
+
+``` php
+$connection->begin();
+$connection->execute('UPDATE articles SET published = ? WHERE id = ?', [true, 2]);
+$connection->execute('UPDATE articles SET published = ? WHERE id = ?', [false, 4]);
+$connection->commit();
+```
+
+`method` Cake\\Database\\Connection::**transactional**(callable $callback): mixed
+
+In addition to this interface connection instances also provide the
+`transactional()` method which makes handling the begin/commit/rollback calls
+much simpler:
+
+``` php
+$connection->transactional(function ($connection) {
+ $connection->execute('UPDATE articles SET published = ? WHERE id = ?', [true, 2]);
+ $connection->execute('UPDATE articles SET published = ? WHERE id = ?', [false, 4]);
+});
+```
+
+In addition to basic queries, you can execute more complex queries using either
+the [Query Builder](../orm/query-builder) or [Table Objects](../orm/table-objects). The transactional method will
+do the following:
+
+- Call `begin`.
+- Call the provided closure.
+- If the closure raises an exception, a rollback will be issued. The original
+ exception will be re-thrown.
+- If the closure returns `false`, a rollback will be issued.
+- If the closure executes successfully, the transaction will be committed.
+
+## Interacting with Statements
+
+When using the lower level database API, you will often encounter statement
+objects. These objects allow you to manipulate the underlying prepared statement
+from the driver. After creating and executing a query object, or using
+`execute()` you will have a `StatementInterface` instance.
+
+### Executing & Fetching Rows
+
+Once a query is executed using `execute()`, results can be fetched using
+`fetch()`, `fetchAll()`:
+
+``` php
+$statement->execute();
+
+// Read one row.
+$row = $statement->fetch('assoc');
+
+// Read all rows.
+$rows = $statement->fetchAll('assoc');
+```
+
+### Getting affected Row Counts
+
+After executing a statement, you can fetch the number of affected rows:
+
+``` php
+$rowCount = $statement->rowCount();
+```
+
+### Checking Error Codes
+
+If your query was not successful, you can get related error information
+using the `errorCode()` and `errorInfo()` methods. These methods work the
+same way as the ones provided by PDO:
+
+``` php
+$code = $statement->errorCode();
+$info = $statement->errorInfo();
+```
+
+
+
+## Query Logging
+
+Query logging can be enabled when configuring your connection by setting the
+`log` option to `true`.
+
+When query logging is enabled, queries will be logged to
+`Cake\Log\Log` using the 'debug' level, and the 'queriesLog' scope.
+You will need to have a logger configured to capture this level & scope. Logging
+to `stderr` can be useful when working on unit tests, and logging to
+files/syslog can be useful when working with web requests:
+
+``` php
+use Cake\Log\Log;
+
+// Console logging
+Log::setConfig('queries', [
+ 'className' => 'Console',
+ 'stream' => 'php://stderr',
+ 'scopes' => ['queriesLog']
+]);
+
+// File logging
+Log::setConfig('queries', [
+ 'className' => 'File',
+ 'path' => LOGS,
+ 'file' => 'queries.log',
+ 'scopes' => ['queriesLog']
+]);
+```
+
+> [!NOTE]
+> Query logging is only intended for debugging/development uses. You should
+> never leave query logging on in production as it will negatively impact the
+> performance of your application.
+
+
+
+## Identifier Quoting
+
+By default CakePHP does **not** quote identifiers in generated SQL queries. The
+reason for this is identifier quoting has a few drawbacks:
+
+- Performance overhead - Quoting identifiers is much slower and complex than not doing it.
+- Not necessary in most cases - In non-legacy databases that follow CakePHP's
+ conventions there is no reason to quote identifiers.
+
+If you are using a legacy schema that requires identifier quoting you can enable
+it using the `quoteIdentifiers` setting in your
+[Database Configuration](#database-configuration). You can also enable this feature at runtime:
+
+``` php
+$connection->getDriver()->enableAutoQuoting();
+```
+
+When enabled, identifier quoting will cause additional query traversal that
+converts all identifiers into `IdentifierExpression` objects.
+
+> [!NOTE]
+> SQL snippets contained in QueryExpression objects will not be modified.
+
+
+
+## Metadata Caching
+
+CakePHP's ORM uses database reflection to determine the schema, indexes and
+foreign keys your application contains. Because this metadata changes
+infrequently and can be expensive to access, it is typically cached. By default,
+metadata is stored in the `_cake_model_` cache configuration. You can define
+a custom cache configuration using the `cacheMetatdata` option in your
+datasource configuration:
+
+``` text
+'Datasources' => [
+ 'default' => [
+ // Other keys go here.
+
+ // Use the 'orm_metadata' cache config for metadata.
+ 'cacheMetadata' => 'orm_metadata',
+ ]
+],
+```
+
+You can also configure the metadata caching at runtime with the
+`cacheMetadata()` method:
+
+``` php
+// Disable the cache
+$connection->cacheMetadata(false);
+
+// Enable the cache
+$connection->cacheMetadata(true);
+
+// Use a custom cache config
+$connection->cacheMetadata('orm_metadata');
+```
+
+CakePHP also includes a CLI tool for managing metadata caches. See the
+[Schema Cache Tool](../console-commands/schema-cache) chapter for more information.
+
+## Creating Databases
+
+If you want to create a connection without selecting a database you can omit
+the database name:
+
+``` php
+$dsn = 'mysql://root:password@localhost/';
+ConnectionManager::setConfig('setup', ['url' => $dsn]);
+```
+
+You can now use your connection object to execute queries that create/modify
+databases. For example to create a database:
+
+``` php
+$connection = ConnectionManager::get('setup');
+$connection->execute("CREATE DATABASE IF NOT EXISTS my_database");
+```
+
+> [!NOTE]
+> When creating a database it is a good idea to set the character set and
+> collation parameters (e.g. `DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci`).
+> If these values are missing, the database will set whatever system default values it uses.
diff --git a/docs/en/orm/deleting-data.md b/docs/en/orm/deleting-data.md
new file mode 100644
index 0000000000..81bb2577fe
--- /dev/null
+++ b/docs/en/orm/deleting-data.md
@@ -0,0 +1,133 @@
+# Deleting Data
+
+`class` Cake\\ORM\\**Table**
+
+## Deleting a Single Entity
+
+`method` Cake\\ORM\\Table::**delete**(EntityInterface $entity, array $options = []): bool
+
+Once you've loaded an entity you can delete it by calling the originating
+table's delete method:
+
+``` php
+// In a controller.
+$entity = $this->Articles->get(2);
+$result = $this->Articles->delete($entity);
+```
+
+When deleting entities a few things happen:
+
+1. The [delete rules](../orm/validation#application-rules) will be applied. If the rules
+ fail, deletion will be prevented.
+2. The `Model.beforeDelete` event is triggered. If this event is stopped, the
+ delete will be aborted and the event's result will be returned.
+3. The entity will be deleted.
+4. All dependent associations will be deleted. If associations are being deleted
+ as entities, additional events will be dispatched.
+5. Any junction table records for BelongsToMany associations will be removed.
+6. The `Model.afterDelete` event will be triggered.
+
+By default all deletes happen within a transaction. You can disable the
+transaction with the atomic option:
+
+``` php
+$result = $this->Articles->delete($entity, ['atomic' => false]);
+```
+
+The `$options` parameter supports the following options:
+
+- `atomic` Defaults to true. When true the deletion happens within
+ a transaction.
+- `checkRules` Defaults to true. Check deletion rules before deleting
+ records.
+
+## Cascading Deletes
+
+When deleting entities, associated data can also be deleted. If your HasOne and
+HasMany associations are configured as `dependent`, delete operations will
+'cascade' to those entities as well. By default entities in associated tables
+are removed using `Cake\ORM\Table::deleteAll()`. You can elect to
+have the ORM load related entities, and delete them individually by setting the
+`cascadeCallbacks` option to `true`. A sample HasMany association with both
+these options enabled would be:
+
+``` php
+// In a Table's initialize method.
+$this->hasMany('Comments', [
+ 'dependent' => true,
+ 'cascadeCallbacks' => true,
+]);
+```
+
+> [!NOTE]
+> Setting `cascadeCallbacks` to `true`, results in considerably slower deletes
+> when compared to bulk deletes. The cascadeCallbacks option should only be
+> enabled when your application has important work handled by event listeners.
+
+## Bulk Deletes
+
+`method` Cake\\ORM\\Table::**deleteMany**(iterable $entities, array $options = []): iterable|false
+
+If you have an array of entities you want to delete you can use `deleteMany()`
+to delete them in a single transaction:
+
+``` php
+// Get a boolean indicating success
+$success = $this->Articles->deleteMany($entities);
+
+// Will throw a PersistenceFailedException if any entity cannot be deleted.
+$this->Articles->deleteManyOrFail($entities);
+```
+
+The `$options` for these methods are the same as `delete()`. Deleting
+records with these method **will** trigger events.
+
+### deleteAll()
+
+`method` Cake\\ORM\\Table::**deleteAll**($conditions): int
+
+There may be times when deleting rows one by one is not efficient or useful.
+In these cases it is more performant to use a bulk-delete to remove many rows at
+once:
+
+``` php
+// Delete all the spam
+public function destroySpam()
+{
+ return $this->deleteAll(['is_spam' => true]);
+}
+```
+
+A bulk-delete will be considered successful if 1 or more rows are deleted. The
+function returns the number of deleted records as an integer.
+
+> [!WARNING]
+> deleteAll will *not* trigger beforeDelete/afterDelete events.
+> If you need callbacks triggered, first load the entities with `find()`
+> and delete them in a loop.
+
+## Strict Deletes
+
+`method` Cake\\ORM\\Table::**deleteOrFail**(EntityInterface $entity, array $options = []): bool
+
+Using this method will throw an
+`Cake\ORM\Exception\PersistenceFailedException` if:
+
+- the entity is new
+- the entity has no primary key value
+- application rules checks failed
+- the delete was aborted by a callback.
+
+If you want to track down the entity that failed to delete, you can use the
+`Cake\ORM\Exception\PersistenceFailedException::getEntity()` method:
+
+``` php
+try {
+ $table->deleteOrFail($entity);
+} catch (\Cake\ORM\Exception\PersistenceFailedException $e) {
+ echo $e->getEntity();
+}
+```
+
+As this internally performs a `Cake\ORM\Table::delete()` call, all
+corresponding delete events will be triggered.
diff --git a/docs/en/orm/entities.md b/docs/en/orm/entities.md
new file mode 100644
index 0000000000..c509b89e7a
--- /dev/null
+++ b/docs/en/orm/entities.md
@@ -0,0 +1,657 @@
+# Entities
+
+`class` Cake\\ORM\\**Entity**
+
+While [Table Objects](../orm/table-objects) represent and provide access to a collection of
+objects, entities represent individual rows or domain objects in your
+application. Entities contain methods to manipulate and
+access the data they contain. Fields can also be accessed as properties on the object.
+
+Entities are created for you each time you iterate the query instance returned
+by `find()` of a table object or when you call `all()` or `first()` method
+of the query instance.
+
+## Creating Entity Classes
+
+You don't need to create entity classes to get started with the ORM in CakePHP.
+However, if you want to have custom logic in your entities you will need to
+create classes. By convention entity classes live in **src/Model/Entity/**. If
+our application had an `articles` table we could create the following entity:
+
+``` php
+// src/Model/Entity/Article.php
+namespace App\Model\Entity;
+
+use Cake\ORM\Entity;
+
+class Article extends Entity
+{
+}
+```
+
+Right now this entity doesn't do very much. However, when we load data from our
+articles table, we'll get instances of this class.
+
+> [!NOTE]
+> If you don't define an entity class CakePHP will use the basic Entity class.
+
+## Creating Entities
+
+Entities can be directly instantiated:
+
+``` php
+use App\Model\Entity\Article;
+
+$article = new Article();
+```
+
+When instantiating an entity you can pass the fields with the data you want
+to store in them:
+
+``` php
+use App\Model\Entity\Article;
+
+$article = new Article([
+ 'id' => 1,
+ 'title' => 'New Article',
+ 'created' => new DateTime('now')
+]);
+```
+
+The preferred way of getting new entities is using the `newEmptyEntity()` method from the
+`Table` objects:
+
+``` php
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+$article = $this->fetchTable('Articles')->newEmptyEntity();
+
+$article = $this->fetchTable('Articles')->newEntity([
+ 'id' => 1,
+ 'title' => 'New Article',
+ 'created' => new DateTime('now')
+]);
+```
+
+`$article` will be an instance of `App\Model\Entity\Article` or fallback to
+`Cake\ORM\Entity` instance if you haven't created the `Article` class.
+
+> [!NOTE]
+> Prior to CakePHP 4.3 you need to use `$this->getTableLocator->get('Articles')`
+> to get the table instance.
+
+## Accessing Entity Data
+
+Entities provide a few ways to access the data they contain. Most commonly you
+will access the data in an entity using object notation:
+
+``` php
+use App\Model\Entity\Article;
+
+$article = new Article;
+$article->title = 'This is my first post';
+echo $article->title;
+```
+
+You can also use the `get()` and `set()` methods.
+
+### set()
+
+`method` Cake\\ORM\\Entity::**set**($field, $value = null, array $options = [])
+
+### get()
+
+`method` Cake\\ORM\\Entity::**get**($field)
+
+For example:
+
+``` php
+$article->set('title', 'This is my first post');
+echo $article->get('title');
+```
+
+### patch()
+
+`method` Cake\\ORM\\Entity::**patch**(array $fields, array $options = [])
+
+Using `patch()` you can mass assign multiple fields at once:
+
+``` php
+$article->patch([
+ 'title' => 'My first post',
+ 'body' => 'It is the best ever!'
+]);
+```
+
+> [!NOTE]
+> `patch()` is available since CakePHP 5.2.0. Prior to that you should use
+> `set()` instead.
+
+> [!WARNING]
+> When updating entities with request data you should configure which fields
+> can be set with mass assignment.
+
+You can check if fields are defined in your entities with `has()`:
+
+``` php
+$article = new Article([
+ 'title' => 'First post',
+ 'user_id' => null
+]);
+$article->has('title'); // true
+$article->has('user_id'); // true
+$article->has('undefined'); // false
+```
+
+The `has()` method will return `true` if a field is defined. You can use
+`hasValue()` to check if a field contains a 'non-empty' value:
+
+``` php
+$article = new Article([
+ 'title' => 'First post',
+ 'user_id' => null,
+ 'text' => '',
+ 'links' => [],
+]);
+$article->has('title'); // true
+$article->hasValue('title'); // true
+
+$article->has('user_id'); // true
+$article->hasValue('user_id'); // false
+
+$article->has('text'); // true
+$article->hasValue('text'); // false
+
+$article->has('links'); // true
+$article->hasValue('links'); // false
+```
+
+If you often partially load entities you should enable strict-property access
+behavior to ensure you're not using properties that haven't been loaded. On
+a per-entity basis you can enable this behavior:
+
+``` php
+$article->requireFieldPresence();
+```
+
+Once enabled, accessing properties that are not defined will raise
+a `Cake\ORM\MissingPropertyException`.
+
+## Accessors & Mutators
+
+In addition to the simple get/set interface, entities allow you to provide
+accessors and mutator methods. These methods let you customize how fields
+are read or set.
+
+### Accessors
+
+Accessors let you customize how fields are read. They use the convention of
+`_get(FieldName)` with `(FieldName)` being the CamelCased version (multiple
+words are joined together to a single word with the first letter of each word
+capitalized) of the field name.
+
+They receive the basic value stored in the `_fields` array as their only
+argument. For example:
+
+``` php
+namespace App\Model\Entity;
+
+use Cake\ORM\Entity;
+
+class Article extends Entity
+{
+ protected function _getTitle($title)
+ {
+ return strtoupper($title);
+ }
+}
+```
+
+The example above converts the value of the `title` field to an uppercase
+version each time it is read. It would be run when getting the field through any
+of these two ways:
+
+``` php
+echo $article->title; // returns FOO instead of foo
+echo $article->get('title'); // returns FOO instead of foo
+```
+
+> [!NOTE]
+> Code in your accessors is executed each time you reference the field. You can
+> use a local variable to cache it if you are performing a resource-intensive
+> operation in your accessor like this: \$myEntityProp = \$entity-\>my_property.
+
+> [!WARNING]
+> Accessors will be used when saving entities, so be careful when defining methods
+> that format data, as the formatted data will be persisted.
+
+### Mutators
+
+You can customize how fields get set by defining a mutator. They use the
+convention of `_set(FieldName)` with `(FieldName)` being the CamelCased version
+of the field name.
+
+Mutators should always return the value that should be stored in the field.
+You can also use mutators to set other fields. When doing this,
+be careful to not introduce any loops, as CakePHP will not prevent infinitely
+looping mutator methods. For example:
+
+``` php
+namespace App\Model\Entity;
+
+use Cake\ORM\Entity;
+use Cake\Utility\Text;
+
+class Article extends Entity
+{
+ protected function _setTitle($title)
+ {
+ $this->slug = Text::slug($title);
+
+ return strtoupper($title);
+ }
+}
+```
+
+The example above is doing two things: It stores a modified version of the
+given value in the `slug` field and stores an uppercase version in the
+`title` field. It would be run when setting the field through
+any of these two ways:
+
+``` php
+$user->title = 'foo'; // sets slug field and stores FOO instead of foo
+$user->set('title', 'foo'); // sets slug field and stores FOO instead of foo
+```
+
+> [!WARNING]
+> Accessors are also run before entities are persisted to the database.
+> If you want to transform fields but not persist that transformation,
+> we recommend using virtual fields as those are not persisted.
+
+
+
+### Creating Virtual Fields
+
+By defining accessors you can provide access to fields that do not
+actually exist. For example if your users table has `first_name` and
+`last_name` you could create a method for the full name:
+
+``` php
+namespace App\Model\Entity;
+
+use Cake\ORM\Entity;
+
+class User extends Entity
+{
+ protected function _getFullName()
+ {
+ return $this->first_name . ' ' . $this->last_name;
+ }
+}
+```
+
+You can access virtual fields as if they existed on the entity. The property
+name will be the lower case and underscored version of the method (`full_name`):
+
+``` php
+echo $user->full_name;
+echo $user->get('full_name');
+```
+
+Do bear in mind that virtual fields cannot be used in finds. If you want
+them to be part of JSON or array representations of your entities,
+see [Exposing Virtual Fields](#exposing-virtual-fields).
+
+## Checking if an Entity Has Been Modified
+
+`method` Cake\\ORM\\Entity::**isDirty**(?string $field = null)
+
+You may want to make code conditional based on whether or not fields have
+changed in an entity. For example, you may only want to validate fields when
+they change:
+
+``` php
+// See if the title has been modified.
+$article->isDirty('title');
+```
+
+You can also flag fields as being modified. This is handy when appending into
+array fields as this wouldn't automatically mark the field as dirty, only
+exchanging completely would.:
+
+``` php
+// Add a comment and mark the field as changed.
+$article->comments[] = $newComment;
+$article->setDirty('comments', true);
+```
+
+In addition you can also base your conditional code on the original field
+values by using the `getOriginal()` method. This method will either return
+the original value of the field if it has been modified or its actual value.
+
+You can also check for changes to any field in the entity:
+
+``` php
+// See if the entity has changed
+$article->isDirty();
+```
+
+To remove the dirty mark from fields in an entity, you can use the `clean()`
+method:
+
+``` php
+$article->clean();
+```
+
+When creating a new entity, you can avoid the fields from being marked as dirty
+by passing an extra option:
+
+``` php
+$article = new Article(['title' => 'New Article'], ['markClean' => true]);
+```
+
+To get a list of all dirty fields of an `Entity` you may call:
+
+``` php
+$dirtyFields = $entity->getDirty();
+```
+
+## Validation Errors
+
+After you [save an entity](../orm/saving-data#saving-entities) any validation errors will be
+stored on the entity itself. You can access any validation errors using the
+`getErrors()`, `getError()` or `hasErrors()` methods:
+
+``` php
+// Get all the errors
+$errors = $user->getErrors();
+
+// Get the errors for a single field.
+$errors = $user->getError('password');
+
+// Does the entity or any nested entity have an error.
+$user->hasErrors();
+
+// Does only the root entity have an error
+$user->hasErrors(false);
+```
+
+The `setErrors()` or `setError()` method can also be used to set the errors
+on an entity, making it easier to test code that works with error messages:
+
+``` php
+$user->setError('password', ['Password is required']);
+$user->setErrors([
+ 'password' => ['Password is required'],
+ 'username' => ['Username is required']
+]);
+```
+
+
+
+## Mass Assignment
+
+While setting fields to entities in bulk is simple and convenient, it can
+create significant security issues. Bulk assigning user data from the request
+into an entity allows the user to modify any and all columns. When using
+anonymous entity classes or creating the entity class with the [Bake Console](../bake)
+CakePHP does not protect against mass-assignment.
+
+The `_accessible` property allows you to provide a map of fields and
+whether or not they can be mass-assigned. The values `true` and `false`
+indicate whether a field can or cannot be mass-assigned:
+
+``` php
+namespace App\Model\Entity;
+
+use Cake\ORM\Entity;
+
+class Article extends Entity
+{
+ protected array $_accessible = [
+ 'title' => true,
+ 'body' => true
+ ];
+}
+```
+
+In addition to concrete fields there is a special `*` field which defines the
+fallback behavior if a field is not specifically named:
+
+``` php
+namespace App\Model\Entity;
+
+use Cake\ORM\Entity;
+
+class Article extends Entity
+{
+ protected array $_accessible = [
+ 'title' => true,
+ 'body' => true,
+ '*' => false,
+ ];
+}
+```
+
+> [!NOTE]
+> If the `*` field is not defined it will default to `false`.
+
+### Avoiding Mass Assignment Protection
+
+When creating a new entity using the `new` keyword you can tell it to not
+protect itself against mass assignment:
+
+``` php
+use App\Model\Entity\Article;
+
+$article = new Article(['id' => 1, 'title' => 'Foo'], ['guard' => false]);
+```
+
+### Modifying the Guarded Fields at Runtime
+
+You can modify the list of guarded fields at runtime using the `setAccess()`
+method:
+
+``` php
+// Make user_id accessible.
+$article->setAccess('user_id', true);
+
+// Make title guarded.
+$article->setAccess('title', false);
+```
+
+> [!NOTE]
+> Modifying accessible fields affects only the instance the method is called
+> on.
+
+When using the `newEntity()` and `patchEntity()` methods in the `Table`
+objects you can customize mass assignment protection with options. Please refer
+to the [Changing Accessible Fields](../orm/saving-data#changing-accessible-fields) section for more information.
+
+### Bypassing Field Guarding
+
+There are some situations when you want to allow mass-assignment to guarded
+fields:
+
+``` php
+$article->patch($fields, ['guard' => false]);
+```
+
+By setting the `guard` option to `false`, you can ignore the accessible
+field list for a single call to `patch()`.
+
+### Checking if an Entity was Persisted
+
+It is often necessary to know if an entity represents a row that is already
+in the database. In those situations use the `isNew()` method:
+
+``` php
+if (!$article->isNew()) {
+ echo 'This article was saved already!';
+}
+```
+
+If you are certain that an entity has already been persisted, you can use
+`setNew()`:
+
+``` php
+$article->setNew(false);
+
+$article->setNew(true);
+```
+
+
+
+## Lazy Loading Associations
+
+While eager loading associations is generally the most efficient way to access
+your associations, there may be times when you need to lazily load associated
+data. Before we get into how to lazy load associations, we should discuss the
+differences between eager loading and lazy loading associations:
+
+Eager loading
+Eager loading uses joins (where possible) to fetch data from the
+database in as *few* queries as possible. When a separate query is required,
+like in the case of a HasMany association, a single query is emitted to
+fetch *all* the associated data for the current set of objects.
+
+Lazy loading
+Lazy loading defers loading association data until it is absolutely
+required. While this can save CPU time because possibly unused data is not
+hydrated into objects, it can result in many more queries being emitted to
+the database. For example looping over a set of articles & their comments
+will frequently emit N queries where N is the number of articles being
+iterated.
+
+While lazy loading is not included by CakePHP's ORM, you can just use one of the
+community plugins to do so. We recommend [the LazyLoad Plugin](https://github.com/jeremyharris/cakephp-lazyload)
+
+After adding the plugin to your entity, you will be able to do the following:
+
+``` php
+$article = $this->Articles->findById($id);
+
+// The comments property was lazy loaded
+foreach ($article->comments as $comment) {
+ echo $comment->body;
+}
+```
+
+## Creating Re-usable Code with Traits
+
+You may find yourself needing the same logic in multiple entity classes. PHP's
+traits are a great fit for this. You can put your application's traits in
+**src/Model/Entity**. By convention traits in CakePHP are suffixed with
+`Trait` so they can be discernible from classes or interfaces. Traits are
+often a good complement to behaviors, allowing you to provide functionality for
+the table and entity objects.
+
+For example if we had SoftDeletable plugin, it could provide a trait. This trait
+could give methods for marking entities as 'deleted', the method `softDelete`
+could be provided by a trait:
+
+``` php
+// SoftDelete/Model/Entity/SoftDeleteTrait.php
+
+namespace SoftDelete\Model\Entity;
+
+trait SoftDeleteTrait
+{
+ public function softDelete()
+ {
+ $this->set('deleted', true);
+ }
+}
+```
+
+You could then use this trait in your entity class by importing it and including
+it:
+
+``` php
+namespace App\Model\Entity;
+
+use Cake\ORM\Entity;
+use SoftDelete\Model\Entity\SoftDeleteTrait;
+
+class Article extends Entity
+{
+ use SoftDeleteTrait;
+}
+```
+
+## Converting to Arrays/JSON
+
+When building APIs, you may often need to convert entities into arrays or JSON
+data. CakePHP makes this simple:
+
+``` php
+// Get an array.
+// Associations will be converted with toArray() as well.
+$array = $user->toArray();
+
+// Convert to JSON
+// Associations will be converted with jsonSerialize hook as well.
+$json = json_encode($user);
+```
+
+When converting an entity to an JSON, the virtual & hidden field lists are
+applied. Entities are recursively converted to JSON as well. This means that if you
+eager loaded entities and their associations CakePHP will correctly handle
+converting the associated data into the correct format.
+
+
+
+### Exposing Virtual Fields
+
+By default virtual fields are not exported when converting entities to
+arrays or JSON. In order to expose virtual fields you need to make them
+visible. When defining your entity class you can provide a list of virtual
+field that should be exposed:
+
+``` php
+namespace App\Model\Entity;
+
+use Cake\ORM\Entity;
+
+class User extends Entity
+{
+ protected array $_virtual = ['full_name'];
+}
+```
+
+This list can be modified at runtime using the `setVirtual()` method:
+
+``` php
+$user->setVirtual(['full_name', 'is_admin']);
+```
+
+### Hiding Fields
+
+There are often fields you do not want exported in JSON or array formats. For
+example it is often unwise to expose password hashes or account recovery
+questions. When defining an entity class, define which fields should be
+hidden:
+
+``` php
+namespace App\Model\Entity;
+
+use Cake\ORM\Entity;
+
+class User extends Entity
+{
+ protected $_hidden = ['password'];
+}
+```
+
+This list can be modified at runtime using the `setHidden()` method:
+
+``` php
+$user->setHidden(['password', 'recovery_question']);
+```
+
+## Storing Complex Types
+
+Accessor & Mutator methods on entities are not intended to contain the logic for
+serializing and unserializing complex data coming from the database. Refer to
+the [Saving Complex Types](../orm/saving-data#saving-complex-types) section to understand how your application can
+store more complex data types like arrays and objects.
diff --git a/docs/en/orm/query-builder.md b/docs/en/orm/query-builder.md
new file mode 100644
index 0000000000..106850d86a
--- /dev/null
+++ b/docs/en/orm/query-builder.md
@@ -0,0 +1,2323 @@
+# Query Builder
+
+`class` Cake\\ORM\\Query\\SelectQuery\\**SelectQuery**
+
+The ORM's query builder provides a simple to use fluent interface for creating
+and running queries. By composing queries together, you can create advanced
+queries using unions and subqueries with ease.
+
+Underneath the covers, the query builder uses PDO prepared statements which
+protect against SQL injection attacks.
+
+## The SelectQuery Object
+
+The easiest way to create a `SelectQuery` object is to use `find()` from a
+`Table` object. This method will return an incomplete query ready to be
+modified. You can also use a table's connection object to access the lower level
+query builder that does not include ORM features, if necessary. See the
+[Database Queries](../orm/database-basics#database-queries) section for more information:
+
+``` php
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+$articles = $this->fetchTable('Articles');
+
+// Start a new query.
+$query = $articles->find();
+```
+
+When inside a controller, you can use the automatic table variable that is
+created using the conventions system:
+
+``` php
+// Inside ArticlesController.php
+
+$query = $this->Articles->find();
+```
+
+### Selecting Rows From A Table
+
+``` php
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+$query = $this->fetchTable('Articles')->find();
+
+foreach ($query->all() as $article) {
+ debug($article->title);
+}
+```
+
+For the remaining examples, assume that `$articles` is a
+`Cake\ORM\Table`. When inside controllers, you can use
+`$this->Articles` instead of `$articles`.
+
+Almost every method in a `SelectQuery` object will return the same query, this means
+that `SelectQuery` objects are lazy, and will not be executed unless you tell them
+to:
+
+``` php
+$query->where(['id' => 1]); // Return the same query object
+$query->orderBy(['title' => 'DESC']); // Still same object, no SQL executed
+```
+
+You can of course chain the methods you call on SelectQuery objects:
+
+``` php
+$query = $articles
+ ->find()
+ ->select(['id', 'name'])
+ ->where(['id !=' => 1])
+ ->orderBy(['created' => 'DESC']);
+
+foreach ($query->all() as $article) {
+ debug($article->created);
+}
+```
+
+If you try to call `debug()` on a SelectQuery object, you will see its internal
+state and the SQL that will be executed in the database:
+
+``` php
+debug($articles->find()->where(['id' => 1]));
+
+// Outputs
+// ...
+// 'sql' => 'SELECT * FROM articles where id = ?'
+// ...
+```
+
+You can execute a query directly without having to use `foreach` on it.
+The easiest way is to either call the `all()` or `toList()` methods:
+
+``` php
+$resultsIteratorObject = $articles
+ ->find()
+ ->where(['id >' => 1])
+ ->all();
+
+foreach ($resultsIteratorObject as $article) {
+ debug($article->id);
+}
+
+$resultsArray = $articles
+ ->find()
+ ->where(['id >' => 1])
+ ->all()
+ ->toList();
+
+foreach ($resultsArray as $article) {
+ debug($article->id);
+}
+
+debug($resultsArray[0]->title);
+```
+
+In the above example, `$resultsIteratorObject` will be an instance of
+`Cake\ORM\ResultSet`, an object you can iterate and apply several extracting
+and traversing methods on.
+
+Often, there is no need to call `all()`, you can simply iterate the
+SelectQuery object to get its results. Query objects can also be used directly as the
+result object; trying to iterate the query, calling `toList()` or `toArray()`, will
+result in the query being executed and results returned to you.
+
+### Selecting A Single Row From A Table
+
+You can use the `first()` method to get the first result in the query:
+
+``` php
+$article = $articles
+ ->find()
+ ->where(['id' => 1])
+ ->first();
+
+debug($article->title);
+```
+
+### Getting A List Of Values From A Column
+
+``` php
+// Use the extract() method from the collections library
+// This executes the query as well
+$allTitles = $articles->find()->all()->extract('title');
+
+foreach ($allTitles as $title) {
+ echo $title;
+}
+```
+
+You can also get a key-value list out of a query result:
+
+``` php
+$list = $articles->find('list')->all();
+foreach ($list as $id => $title) {
+ echo "$id : $title"
+}
+```
+
+For more information on how to customize the fields used for populating the list
+refer to [Table Find List](../orm/retrieving-data-and-resultsets#table-find-list) section.
+
+### ResultSet Is A Collection Object
+
+Once you get familiar with the Query object methods, it is strongly encouraged
+that you visit the [Collection](../core-libraries/collections) section to
+improve your skills in efficiently traversing the results. The ResultSet (returned
+by calling the `SelectQuery`'s `all()` method) implements the collection interface:
+
+``` php
+// Use the combine() method from the collections library
+// This is equivalent to find('list')
+$keyValueList = $articles->find()->all()->combine('id', 'title');
+
+// An advanced example
+$results = $articles->find()
+ ->where(['id >' => 1])
+ ->orderBy(['title' => 'DESC'])
+ ->all()
+ ->map(function ($row) {
+ $row->trimmedTitle = trim($row->title);
+
+ return $row;
+ })
+ ->combine('id', 'trimmedTitle') // combine() is another collection method
+ ->toArray(); // Also a collections library method
+
+foreach ($results as $id => $trimmedTitle) {
+ echo "$id : $trimmedTitle";
+}
+```
+
+### Queries Are Lazily Evaluated
+
+Query objects are lazily evaluated. This means a query is not executed until one
+of the following things occur:
+
+- The query is iterated with `foreach`.
+- The query's `execute()` method is called. This will return the underlying
+ statement object, and is to be used with insert/update/delete queries.
+- The query's `first()` method is called. This will return the first result in the set
+ built by `SELECT` (it adds `LIMIT 1` to the query).
+- The query's `all()` method is called. This will return the result set and
+ can only be used with `SELECT` statements.
+- The query's `toList()` or `toArray()` method is called.
+
+Until one of these conditions are met, the query can be modified without additional
+SQL being sent to the database. It also means that if a Query hasn't been
+evaluated, no SQL is ever sent to the database. Once executed, modifying and
+re-evaluating a query will result in additional SQL being run. Calling the same query without modification multiple times will return same reference.
+
+If you want to take a look at what SQL CakePHP is generating, you can turn
+database [query logging](../orm/database-basics#database-query-logging) on.
+
+## Selecting Data
+
+CakePHP makes building `SELECT` queries simple. To limit the fields fetched,
+you can use the `select()` method:
+
+``` php
+$query = $articles->find();
+$query->select(['id', 'title', 'body']);
+foreach ($query->all() as $row) {
+ debug($row->title);
+}
+```
+
+You can set aliases for fields by providing fields as an associative array:
+
+``` php
+// Results in SELECT id AS pk, title AS aliased_title, body ...
+$query = $articles->find();
+$query->select(['pk' => 'id', 'aliased_title' => 'title', 'body']);
+```
+
+To select distinct fields, you can use the `distinct()` method:
+
+``` php
+// Results in SELECT DISTINCT country FROM ...
+$query = $articles->find();
+$query->select(['country'])
+ ->distinct(['country']);
+```
+
+To set some basic conditions you can use the `where()` method:
+
+``` php
+// Conditions are combined with AND
+$query = $articles->find();
+$query->where(['title' => 'First Post', 'published' => true]);
+
+// You can call where() multiple times
+$query = $articles->find();
+$query->where(['title' => 'First Post'])
+ ->where(['published' => true]);
+```
+
+You can also pass an anonymous function to the `where()` method. The passed
+anonymous function will receive an instance of
+`\Cake\Database\Expression\QueryExpression` as its first argument, and
+`\Cake\ORM\Query\SelectQuery` as its second:
+
+``` php
+$query = $articles->find();
+$query->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->eq('published', true);
+});
+```
+
+See the [Advanced Query Conditions](#advanced-query-conditions) section to find out how to construct
+more complex `WHERE` conditions.
+
+### Selecting Specific Fields
+
+By default a query will select all fields from a table, the exception is when you
+call the `select()` function yourself and pass certain fields:
+
+``` php
+// Only select id and title from the articles table
+$articles->find()->select(['id', 'title']);
+```
+
+If you wish to still select all fields from a table after having called
+`select($fields)`, you can pass the table instance to `select()` for this
+purpose:
+
+``` php
+// Only all fields from the articles table including
+// a calculated slug field.
+$query = $articlesTable->find();
+$query
+ ->select(['slug' => $query->func()->concat(['title' => 'identifier', '-', 'id' => 'identifier'])])
+ ->select($articlesTable); // Select all fields from articles
+```
+
+You can use `selectAlso()` to select all fields on a table and
+*also* select some additional fields:
+
+``` php
+$query = $articlesTable->find();
+$query->selectAlso(['count' => $query->func()->count('*')]);
+```
+
+If you want to select all but a few fields on a table, you can use
+`selectAllExcept()`:
+
+``` php
+$query = $articlesTable->find();
+
+// Get all fields except the published field.
+$query->selectAllExcept($articlesTable, ['published']);
+```
+
+You can also pass an `Association` object when working with contained
+associations.
+
+
+
+### Using SQL Functions
+
+CakePHP's ORM offers abstraction for some commonly used SQL functions. Using the
+abstraction allows the ORM to select the platform specific implementation of the
+function you want. For example, `concat` is implemented differently in MySQL,
+PostgreSQL and SQL Server. Using the abstraction allows your code to be
+portable:
+
+``` php
+// Results in SELECT COUNT(*) count FROM ...
+$query = $articles->find();
+$query->select(['count' => $query->func()->count('*')]);
+```
+
+Note that most of the functions accept an additional argument to specify the types
+to bind to the arguments and/or the return type, for example:
+
+``` php
+$query->select(['minDate' => $query->func()->min('date', ['date']);
+```
+
+For details, see the documentation for `Cake\Database\FunctionsBuilder`.
+
+You can access existing wrappers for several SQL functions through `SelectQuery::func()`:
+
+`rand()`
+Generate a random value between 0 and 1 via SQL.
+
+`sum()`
+Calculate a sum. Assumes arguments are literal values.
+
+`avg()`
+Calculate an average. Assumes arguments are literal values.
+
+`min()`
+Calculate the min of a column. Assumes arguments are literal values.
+
+`max()`
+Calculate the max of a column. Assumes arguments are literal values.
+
+`count()`
+Calculate the count. Assumes arguments are literal values.
+
+`cast()`
+Convert a field or expression from one data type to another.
+
+`concat()`
+Concatenate two values together. Assumes arguments are bound parameters.
+
+`coalesce()`
+Coalesce values. Assumes arguments are bound parameters.
+
+`dateDiff()`
+Get the difference between two dates/times. Assumes arguments are bound parameters.
+
+`now()`
+Defaults to returning date and time, but accepts 'time' or 'date' to return only
+those values.
+
+`extract()`
+Returns the specified data part from the SQL expression.
+
+`dateAdd()`
+Add the time unit to the date expression.
+
+`dayOfWeek()`
+Returns a FunctionExpression representing a call to SQL WEEKDAY function.
+
+#### Window-Only Functions
+
+These window-only functions contain a window expression by default:
+
+`rowNumber()`
+Returns an Aggregate expression for the `ROW_NUMBER()` SQL function.
+
+`lag()`
+Returns an Aggregate expression for the `LAG()` SQL function.
+
+`lead()`
+Returns an Aggregate expression for the `LEAD()` SQL function.
+
+When providing arguments for SQL functions, there are two kinds of parameters
+you can use, literal arguments and bound parameters. Identifier/Literal parameters allow
+you to reference columns or other SQL literals. Bound parameters can be used to
+safely add user data to SQL functions. For example:
+
+``` php
+$query = $articles->find()->innerJoinWith('Categories');
+$concat = $query->func()->concat([
+ 'Articles.title' => 'identifier',
+ ' - CAT: ',
+ 'Categories.name' => 'identifier',
+ ' - Age: ',
+ $query->func()->dateDiff([
+ 'NOW()' => 'literal',
+ 'Articles.created' => 'identifier',
+ ])
+]);
+$query->select(['link_title' => $concat]);
+```
+
+Both `literal` and `identifier` arguments allow you to reference other columns
+and SQL literals while `identifier` will be appropriately quoted if auto-quoting
+is enabled. If not marked as literal or identifier, arguments will be bound
+parameters allowing you to safely pass user data to the function.
+
+The above example generates something like this in MYSQL.
+
+``` sql
+SELECT CONCAT(
+ Articles.title,
+ :c0,
+ Categories.name,
+ :c1,
+ (DATEDIFF(NOW(), Articles.created))
+) FROM articles;
+```
+
+The `:c0` argument will have `' - CAT:'` text bound when the query is
+executed. The `dateDiff` expression was translated to the appropriate SQL.
+
+#### Custom Functions
+
+If `func()` does not already wrap the SQL function you need, you can call
+it directly through `func()` and still safely pass arguments and user data
+as described. Make sure you pass the appropriate argument type for custom
+functions or they will be treated as bound parameters:
+
+``` php
+$query = $articles->find();
+$year = $query->func()->year([
+ 'created' => 'identifier'
+]);
+$time = $query->func()->date_format([
+ 'created' => 'identifier',
+ "'%H:%i'" => 'literal'
+]);
+$query->select([
+ 'yearCreated' => $year,
+ 'timeCreated' => $time
+]);
+```
+
+These custom function would generate something like this in MYSQL:
+
+``` sql
+SELECT YEAR(created) as yearCreated,
+ DATE_FORMAT(created, '%H:%i') as timeCreated
+FROM articles;
+```
+
+> [!NOTE]
+> Use `func()` to pass untrusted user data to any SQL function.
+
+### Ordering Results
+
+To apply ordering, you can use the `orderBy()` method:
+
+``` php
+$query = $articles->find()
+ ->orderBy(['title' => 'ASC', 'id' => 'ASC']);
+```
+
+When calling `orderBy()` multiple times on a query, multiple clauses will be
+appended. However, when using finders you may sometimes need to overwrite the
+`ORDER BY`. Set the second parameter of `orderBy()` (as well as
+`orderByAsc()` or `orderByDesc()`) to `SelectQuery::OVERWRITE` or to `true`:
+
+``` php
+$query = $articles->find()
+ ->orderBy(['title' => 'ASC']);
+// Later, overwrite the ORDER BY clause instead of appending to it.
+$query = $articles->find()
+ ->orderBy(['created' => 'DESC'], SelectQuery::OVERWRITE);
+```
+
+The `orderByAsc` and `orderByDesc` methods can be used when you need to sort on
+complex expressions:
+
+``` php
+$query = $articles->find();
+$concat = $query->func()->concat([
+ 'title' => 'identifier',
+ 'synopsis' => 'identifier'
+]);
+$query->orderByAsc($concat);
+```
+
+To build complex order clauses, use a Closure to build order expressions:
+
+``` php
+$query->orderByAsc(function (QueryExpression $exp, SelectQuery $query) {
+ return $exp->addCase(/* ... */);
+});
+```
+
+### Limiting Results
+
+To limit the number of rows or set the row offset you can use the `limit()`
+and `page()` methods:
+
+``` php
+// Fetch rows 50 to 100
+$query = $articles->find()
+ ->limit(50)
+ ->page(2);
+```
+
+As you can see from the examples above, all the methods that modify the query
+provide a fluent interface, allowing you to build a query through chained method
+calls.
+
+### Aggregates - Group and Having
+
+When using aggregate functions like `count` and `sum` you may want to use
+`group by` and `having` clauses:
+
+``` php
+$query = $articles->find();
+$query->select([
+ 'count' => $query->func()->count('view_count'),
+ 'published_date' => 'DATE(created)'
+])
+->groupBy('published_date')
+->having(['count >' => 3]);
+```
+
+### Case Statements
+
+The ORM also offers the SQL `case` expression. The `case` expression allows
+for implementing `if ... then ... else` logic inside your SQL. This can be useful
+for reporting on data where you need to conditionally sum or count data, or where you
+need to specific data based on a condition.
+
+If we wished to know how many published articles are in our database, we could use the following SQL:
+
+``` sql
+SELECT
+COUNT(CASE WHEN published = 'Y' THEN 1 END) AS number_published,
+COUNT(CASE WHEN published = 'N' THEN 1 END) AS number_unpublished
+FROM articles
+```
+
+To do this with the query builder, we'd use the following code:
+
+``` php
+$query = $articles->find();
+$publishedCase = $query->expr()
+ ->case()
+ ->when(['published' => 'Y'])
+ ->then(1);
+$unpublishedCase = $query->expr()
+ ->case()
+ ->when(['published' => 'N'])
+ ->then(1);
+
+$query->select([
+ 'number_published' => $query->func()->count($publishedCase),
+ 'number_unpublished' => $query->func()->count($unpublishedCase)
+]);
+```
+
+The `when()` method accepts SQL snippets, array conditions, and `Closure`
+for when you need additional logic to build the cases. If we wanted to classify
+cities into SMALL, MEDIUM, or LARGE based on population size, we could do the
+following:
+
+``` php
+$query = $cities->find();
+$sizing = $query->expr()->case()
+ ->when(['population <' => 100000])
+ ->then('SMALL')
+ ->when($query->expr()->between('population', 100000, 999000))
+ ->then('MEDIUM')
+ ->when(['population >=' => 999001])
+ ->then('LARGE');
+$query = $query->select(['size' => $sizing]);
+# SELECT CASE
+# WHEN population < 100000 THEN 'SMALL'
+# WHEN population BETWEEN 100000 AND 999000 THEN 'MEDIUM'
+# WHEN population >= 999001 THEN 'LARGE'
+# END AS size
+```
+
+You need to be careful when including user provided data into case expressions
+as it can create SQL injection vulnerabilities:
+
+``` php
+// Unsafe do *not* use
+$case->when($requestData['published']);
+
+// Instead pass user data as values to array conditions
+$case->when(['published' => $requestData['published']]);
+```
+
+For more complex scenarios you can use `QueryExpression` objects and bound
+values:
+
+``` php
+$userValue = $query->expr()
+ ->case()
+ ->when($query->expr('population >= :userData'))
+ ->then(123, 'integer');
+
+$query->select(['val' => $userValue])
+ ->bind(':userData', $requestData['value'], 'integer');
+```
+
+By using bindings you can safely embed user data into complex raw SQL snippets.
+
+`then()`, `when()` and `else()` will try to infer the
+value type based on the parameter type. If you need to bind a value as
+a different type you can declare the desired type:
+
+``` php
+$case->when(['published' => true])->then('1', 'integer');
+```
+
+You can create `if ... then ... else` conditions by using `else()`:
+
+``` php
+$published = $query->expr()
+ ->case()
+ ->when(['published' => true])
+ ->then('Y')
+ ->else('N');
+
+# CASE WHEN published = true THEN 'Y' ELSE 'N' END;
+```
+
+Also, it's possible to create the simple variant by passing a value to `case()`:
+
+``` php
+$published = $query->expr()
+ ->case($query->identifier('published'))
+ ->when(true)
+ ->then('Y')
+ ->else('N');
+
+# CASE published WHEN true THEN 'Y' ELSE 'N' END;
+```
+
+The `addCase` function can also chain together multiple statements to create
+`if .. then .. [elseif .. then .. ] [ .. else ]` logic inside your SQL.
+
+If we wanted to classify cities into SMALL, MEDIUM, or LARGE based on population
+size, we could do the following:
+
+``` php
+$query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->addCase(
+ [
+ $q->expr()->lt('population', 100000),
+ $q->expr()->between('population', 100000, 999000),
+ $q->expr()->gte('population', 999001),
+ ],
+ ['SMALL', 'MEDIUM', 'LARGE'], # values matching conditions
+ ['string', 'string', 'string'] # type of each value
+ );
+ });
+# WHERE CASE
+# WHEN population < 100000 THEN 'SMALL'
+# WHEN population BETWEEN 100000 AND 999000 THEN 'MEDIUM'
+# WHEN population >= 999001 THEN 'LARGE'
+# END
+```
+
+Any time there are fewer case conditions than values, `addCase` will
+automatically produce an `if .. then .. else` statement:
+
+``` php
+$query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->addCase(
+ [
+ $q->expr()->eq('population', 0),
+ ],
+ ['DESERTED', 'INHABITED'], # values matching conditions
+ ['string', 'string'] # type of each value
+ );
+ });
+# WHERE CASE
+# WHEN population = 0 THEN 'DESERTED' ELSE 'INHABITED' END
+```
+
+### Fetching Arrays Instead of Entities
+
+While ORMs and object result sets are powerful, creating entities is sometimes
+unnecessary. For example, when accessing aggregated data, building an Entity may
+not make sense. The process of converting the database results to entities is
+called hydration. If you wish to disable this process you can do this:
+
+``` php
+$query = $articles->find();
+$query->enableHydration(false); // Results as arrays instead of entities
+$result = $query->toList(); // Execute the query and return the array
+```
+
+After executing those lines, your result should look similar to this:
+
+``` php
+[
+ ['id' => 1, 'title' => 'First Article', 'body' => 'Article 1 body' ...],
+ ['id' => 2, 'title' => 'Second Article', 'body' => 'Article 2 body' ...],
+ ...
+]
+```
+
+
+
+### Projecting Results Into DTOs
+
+In addition to fetching results as Entity objects or arrays, you can project
+query results directly into Data Transfer Objects (DTOs). DTOs offer several
+advantages:
+
+- **Memory efficiency** - DTOs consume approximately 3x less memory than Entity
+ objects, making them ideal for large result sets.
+- **Type safety** - DTOs provide strong typing and IDE autocompletion support,
+ unlike plain arrays.
+- **Decoupled serialization** - DTOs let you separate your API response
+ structure from your database schema, making it easier to version APIs or
+ expose only specific fields.
+- **Read-only data** - Using `readonly` classes ensures data integrity and
+ makes your intent clear.
+
+The `projectAs()` method allows you to specify a DTO class that results will
+be hydrated into:
+
+``` php
+// Define a DTO class
+readonly class ArticleDto
+{
+ public function __construct(
+ public int $id,
+ public string $title,
+ public ?string $body = null,
+ ) {
+ }
+}
+
+// Use projectAs() to hydrate results into DTOs
+$articles = $articlesTable->find()
+ ->select(['id', 'title', 'body'])
+ ->projectAs(ArticleDto::class)
+ ->toArray();
+```
+
+#### DTO Creation Methods
+
+CakePHP supports two approaches for creating DTOs:
+
+**Reflection-based constructor mapping** - CakePHP will use reflection to map
+database columns to constructor parameters:
+
+``` php
+readonly class ArticleDto
+{
+ public function __construct(
+ public int $id,
+ public string $title,
+ public ?AuthorDto $author = null,
+ ) {
+ }
+}
+```
+
+**Factory method pattern** - If your DTO class has a `createFromArray()`
+static method, CakePHP will use that instead:
+
+``` php
+class ArticleDto
+{
+ public int $id;
+ public string $title;
+
+ public static function createFromArray(
+ array $data,
+ bool $ignoreMissing = false
+ ): self {
+ $dto = new self();
+ $dto->id = $data['id'];
+ $dto->title = $data['title'];
+
+ return $dto;
+ }
+}
+```
+
+The factory method approach is approximately 2.5x faster than reflection-based
+hydration.
+
+#### Nested Association DTOs
+
+You can project associated data into nested DTOs. Use the `#[CollectionOf]`
+attribute to specify the type of elements in array properties:
+
+``` php
+use Cake\ORM\Attribute\CollectionOf;
+
+readonly class ArticleDto
+{
+ public function __construct(
+ public int $id,
+ public string $title,
+ public ?AuthorDto $author = null,
+ #[CollectionOf(CommentDto::class)]
+ public array $comments = [],
+ ) {
+ }
+}
+
+readonly class AuthorDto
+{
+ public function __construct(
+ public int $id,
+ public string $name,
+ ) {
+ }
+}
+
+readonly class CommentDto
+{
+ public function __construct(
+ public int $id,
+ public string $body,
+ ) {
+ }
+}
+
+// Fetch articles with associations projected into DTOs
+$articles = $articlesTable->find()
+ ->contain(['Authors', 'Comments'])
+ ->projectAs(ArticleDto::class)
+ ->toArray();
+```
+
+#### Using DTOs for API Responses
+
+DTOs are particularly useful for building API responses where you want to
+control the output structure independently from your database schema. You can
+define a DTO that represents your API contract and include custom serialization
+logic:
+
+``` php
+readonly class ArticleApiResponse
+{
+ public function __construct(
+ public int $id,
+ public string $title,
+ public string $slug,
+ public string $authorName,
+ public string $publishedAt,
+ ) {
+ }
+
+ public static function createFromArray(
+ array $data,
+ bool $ignoreMissing = false
+ ): self {
+ return new self(
+ id: $data['id'],
+ title: $data['title'],
+ slug: Inflector::slug($data['title']),
+ authorName: $data['author']['name'] ?? 'Unknown',
+ publishedAt: $data['created']->format('c'),
+ );
+ }
+}
+
+// In your controller
+$articles = $this->Articles->find()
+ ->contain(['Authors'])
+ ->projectAs(ArticleApiResponse::class)
+ ->toArray();
+
+return $this->response->withType('application/json')
+ ->withStringBody(json_encode(['articles' => $articles]));
+```
+
+This approach keeps your API response format decoupled from your database
+schema, making it easier to evolve your API without changing your data model.
+
+> [!NOTE]
+> DTO projection is applied as the final formatting step, after all other
+> formatters and behaviors have processed the results. This ensures
+> compatibility with existing behavior formatters while still providing the
+> benefits of DTOs.
+
+::: info Added in version 5.3.0
+The `projectAs()` method and `#[CollectionOf]` attribute were added.
+:::
+
+
+
+### Adding Calculated Fields
+
+After your queries, you may need to do some post-processing. If you need to add
+a few calculated fields or derived data, you can use the `formatResults()`
+method. This is a lightweight way to map over the result sets. If you need more
+control over the process, or want to reduce results you should use
+the [Map/Reduce](../orm/retrieving-data-and-resultsets#map-reduce) feature instead. If you were querying a list
+of people, you could calculate their age with a result formatter:
+
+``` php
+// Assuming we have built the fields, conditions and containments.
+$query->formatResults(function (\Cake\Collection\CollectionInterface $results) {
+ return $results->map(function ($row) {
+ $row['age'] = $row['birth_date']->diff(new \DateTime)->y;
+
+ return $row;
+ });
+});
+```
+
+As you can see in the example above, formatting callbacks will get a
+`ResultSetDecorator` as their first argument. The second argument will be
+the Query instance the formatter was attached to. The `$results` argument can
+be traversed and modified as necessary.
+
+Result formatters are required to return an iterator object, which will be used
+as the return value for the query. Formatter functions are applied after all the
+Map/Reduce routines have been executed. Result formatters can be applied from
+within contained associations as well. CakePHP will ensure that your formatters
+are properly scoped. For example, doing the following would work as you may
+expect:
+
+``` php
+// In a method in the Articles table
+$query->contain(['Authors' => function ($q) {
+ return $q->formatResults(function (\Cake\Collection\CollectionInterface $authors) {
+ return $authors->map(function ($author) {
+ $author['age'] = $author['birth_date']->diff(new \DateTime)->y;
+
+ return $author;
+ });
+ });
+}]);
+
+// Get results
+$results = $query->all();
+
+// Outputs 29
+echo $results->first()->author->age;
+```
+
+As seen above, the formatters attached to associated query builders are scoped
+to operate only on the data in the association. CakePHP will ensure that
+computed values are inserted into the correct entity.
+
+If you want to replace the results of an association finder with
+`formatResults` and your replacement data is an associative array, use
+`preserveKeys` to retain keys when results are mapped to the parent query. For
+example:
+
+``` php
+public function findSlugged(SelectQuery $query): SelectQuery
+{
+ return $query->applyOptions(['preserveKeys' => true])
+ ->formatResults(function ($results) {
+ return $results->indexBy(function ($record) {
+ return Text::slug($record->name);
+ });
+ });
+}
+```
+
+The `preserveKeys` option can be set as a contain option as well.
+
+::: info Added in version 5.1.0
+The `preserveKeys` option was added.
+:::
+
+
+
+## Advanced Conditions
+
+The query builder makes it simple to build complex `where` clauses.
+Grouped conditions can be expressed by providing combining `where()` and
+expression objects. For simple queries, you can build conditions using
+an array of conditions:
+
+``` php
+$query = $articles->find()
+ ->where([
+ 'author_id' => 3,
+ 'OR' => [['view_count' => 2], ['view_count' => 3]],
+ ]);
+```
+
+The above would generate SQL like
+
+``` sql
+SELECT * FROM articles WHERE author_id = 3 AND (view_count = 2 OR view_count = 3)
+```
+
+If you'd prefer to avoid deeply nested arrays, you can use the callback form of
+`where()` to build your queries. The callback accepts a QueryExpression which allows
+you to use the expression builder interface to build more complex conditions without arrays.
+For example:
+
+``` php
+$query = $articles->find()->where(function (QueryExpression $exp, SelectQuery $query) {
+ // Use add() to add multiple conditions for the same field.
+ $author = $query->expr()->or(['author_id' => 3])->add(['author_id' => 2]);
+ $published = $query->expr()->and(['published' => true, 'view_count' => 10]);
+
+ return $exp->or([
+ 'promoted' => true,
+ $query->expr()->and([$author, $published])
+ ]);
+});
+```
+
+The above generates SQL similar to:
+
+``` sql
+SELECT *
+FROM articles
+WHERE (
+ (
+ (author_id = 2 OR author_id = 3)
+ AND
+ (published = 1 AND view_count = 10)
+ )
+ OR promoted = 1
+)
+```
+
+The `QueryExpression` passed to the callback allows you to use both
+**combinators** and **conditions** to build the full expression.
+
+Combinators
+These create new `QueryExpression` objects and set how the conditions added
+to that expression are joined together.
+
+- `and()` creates new expression objects that joins all conditions with `AND`.
+- `or()` creates new expression objects that joins all conditions with `OR`.
+
+Conditions
+These are added to the expression and automatically joined together
+depending on which combinator was used.
+
+The `QueryExpression` passed to the callback function defaults to `and()`:
+
+``` php
+$query = $articles->find()
+ ->where(function (QueryExpression $exp) {
+ return $exp
+ ->eq('author_id', 2)
+ ->eq('published', true)
+ ->notEq('spam', true)
+ ->gt('view_count', 10);
+ });
+```
+
+Since we started off using `where()`, we don't need to call `and()`, as
+that happens implicitly. The above shows a few new condition
+methods being combined with `AND`. The resulting SQL would look like:
+
+``` sql
+SELECT *
+FROM articles
+WHERE (
+author_id = 2
+AND published = 1
+AND spam != 1
+AND view_count > 10)
+```
+
+However, if we wanted to use both `AND` & `OR` conditions we could do the
+following:
+
+``` php
+$query = $articles->find()
+ ->where(function (QueryExpression $exp) {
+ $orConditions = $exp->or(['author_id' => 2])
+ ->eq('author_id', 5);
+
+ return $exp
+ ->add($orConditions)
+ ->eq('published', true)
+ ->gte('view_count', 10);
+ });
+```
+
+Which would generate the SQL similar to:
+
+``` sql
+SELECT *
+FROM articles
+WHERE (
+ (author_id = 2 OR author_id = 5)
+ AND published = 1
+ AND view_count >= 10
+)
+```
+
+The **combinators** also allow you pass in a callback which takes
+the new expression object as a parameter if you want to separate
+the method chaining:
+
+``` php
+$query = $articles->find()
+ ->where(function (QueryExpression $exp) {
+ $orConditions = $exp->or(function (QueryExpression $or) {
+ return $or->eq('author_id', 2)
+ ->eq('author_id', 5);
+ });
+
+ return $exp
+ ->not($orConditions)
+ ->lte('view_count', 10);
+ });
+```
+
+You can negate sub-expressions using `not()`:
+
+``` php
+$query = $articles->find()
+ ->where(function (QueryExpression $exp) {
+ $orConditions = $exp->or(['author_id' => 2])
+ ->eq('author_id', 5);
+
+ return $exp
+ ->not($orConditions)
+ ->lte('view_count', 10);
+ });
+```
+
+Which will generate the following SQL looking like:
+
+``` sql
+SELECT *
+FROM articles
+WHERE (
+ NOT (author_id = 2 OR author_id = 5)
+ AND view_count <= 10
+)
+```
+
+It is also possible to build expressions using SQL functions:
+
+``` php
+$query = $articles->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ $year = $q->func()->year([
+ 'created' => 'identifier'
+ ]);
+
+ return $exp
+ ->gte($year, 2014)
+ ->eq('published', true);
+ });
+```
+
+Which will generate the following SQL looking like:
+
+``` sql
+SELECT *
+FROM articles
+WHERE (
+ YEAR(created) >= 2014
+ AND published = 1
+)
+```
+
+When using the expression objects you can use the following methods to create
+conditions:
+
+- `eq()` Creates an equality condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->eq('population', '10000');
+ });
+ # WHERE population = 10000
+ ```
+
+- `notEq()` Creates an inequality condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->notEq('population', '10000');
+ });
+ # WHERE population != 10000
+ ```
+
+- `like()` Creates a condition using the `LIKE` operator:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->like('name', '%A%');
+ });
+ # WHERE name LIKE "%A%"
+ ```
+
+- `notLike()` Creates a negated `LIKE` condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->notLike('name', '%A%');
+ });
+ # WHERE name NOT LIKE "%A%"
+ ```
+
+- `in()` Create a condition using `IN`:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->in('country_id', ['AFG', 'USA', 'EST']);
+ });
+ # WHERE country_id IN ('AFG', 'USA', 'EST')
+ ```
+
+- `notIn()` Create a negated condition using `IN`:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->notIn('country_id', ['AFG', 'USA', 'EST']);
+ });
+ # WHERE country_id NOT IN ('AFG', 'USA', 'EST')
+ ```
+
+- `gt()` Create a `>` condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->gt('population', '10000');
+ });
+ # WHERE population > 10000
+ ```
+
+- `gte()` Create a `>=` condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->gte('population', '10000');
+ });
+ # WHERE population >= 10000
+ ```
+
+- `lt()` Create a `<` condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->lt('population', '10000');
+ });
+ # WHERE population < 10000
+ ```
+
+- `lte()` Create a `<=` condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->lte('population', '10000');
+ });
+ # WHERE population <= 10000
+ ```
+
+- `isNull()` Create an `IS NULL` condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->isNull('population');
+ });
+ # WHERE (population) IS NULL
+ ```
+
+- `isNotNull()` Create a negated `IS NULL` condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->isNotNull('population');
+ });
+ # WHERE (population) IS NOT NULL
+ ```
+
+- `between()` Create a `BETWEEN` condition:
+
+ ``` php
+ $query = $cities->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->between('population', 999, 5000000);
+ });
+ # WHERE population BETWEEN 999 AND 5000000,
+ ```
+
+- `exists()` Create a condition using `EXISTS`:
+
+ ``` php
+ $subquery = $cities->find()
+ ->select(['id'])
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->equalFields('countries.id', 'cities.country_id');
+ })
+ ->andWhere(['population >' => 5000000]);
+
+ $query = $countries->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) use ($subquery) {
+ return $exp->exists($subquery);
+ });
+ # WHERE EXISTS (SELECT id FROM cities WHERE countries.id = cities.country_id AND population > 5000000)
+ ```
+
+- `notExists()` Create a negated condition using `EXISTS`:
+
+ ``` php
+ $subquery = $cities->find()
+ ->select(['id'])
+ ->where(function (QueryExpression $exp, SelectQuery $q) {
+ return $exp->equalFields('countries.id', 'cities.country_id');
+ })
+ ->andWhere(['population >' => 5000000]);
+
+ $query = $countries->find()
+ ->where(function (QueryExpression $exp, SelectQuery $q) use ($subquery) {
+ return $exp->notExists($subquery);
+ });
+ # WHERE NOT EXISTS (SELECT id FROM cities WHERE countries.id = cities.country_id AND population > 5000000)
+ ```
+
+Expression objects should cover many commonly used functions and expressions. If
+you find yourself unable to create the required conditions with expressions you
+can may be able to use `bind()` to manually bind parameters into conditions:
+
+``` php
+$query = $cities->find()
+ ->where([
+ 'start_date BETWEEN :start AND :end',
+ ])
+ ->bind(':start', '2014-01-01', 'date')
+ ->bind(':end', '2014-12-31', 'date');
+```
+
+In situations when you can't get, or don't want to use the builder methods to
+create the conditions you want you can also use snippets of SQL in where
+clauses:
+
+``` php
+// Compare two fields to each other
+$query->where(['Categories.parent_id != Parents.id']);
+```
+
+> [!WARNING]
+> The field names used in expressions, and SQL snippets should **never**
+> contain untrusted content as you will create SQL Injection vectors. See the
+> [Using Sql Functions](#using-sql-functions) section for how to safely include unsafe data
+> into function calls.
+
+### Using Identifiers in Expressions
+
+When you need to reference a column or SQL identifier in your queries you can
+use the `identifier()` method:
+
+``` php
+$query = $countries->find();
+$query->select([
+ 'year' => $query->func()->year([$query->identifier('created')])
+ ])
+ ->where(function ($exp, $query) {
+ return $exp->gt('population', 100000);
+ });
+```
+
+You can use `identifier()` in comparisons to aggregations too:
+
+``` php
+$query = $this->Orders->find();
+$query->select(['Customers.customer_name', 'total_orders' => $query->func()->count('Orders.order_id')])
+ ->contain('Customers')
+ ->groupBy(['Customers.customer_name'])
+ ->having(['total_orders >=' => $query->identifier('Customers.minimum_order_count')]);
+```
+
+> [!WARNING]
+> To prevent SQL injections, Identifier expressions should never have
+> untrusted data passed into them.
+
+### Collation
+
+In situations that you need to deal with accented characters, multilingual data
+or case-sensitive comparisons, you can use the `$collation` parameter of `IdentifierExpression`
+or `StringExpression` to apply a character expression to a certain collation:
+
+``` php
+use Cake\Database\Expression\IdentifierExpression;
+
+$collation = 'Latin1_general_CI_AI'; //sql server example
+$query = $cities->find()
+ ->where(function (QueryExpression $exp, Query $q) use ($collation) {
+ return $exp->like(new IdentifierExpression('name', $collation), '%São José%');
+ });
+# WHERE name COLLATE LIKE Latin1_general_CI_AI "%São José%"
+```
+
+### Automatically Creating IN Clauses
+
+When building queries using the ORM, you will generally not have to indicate the
+data types of the columns you are interacting with, as CakePHP can infer the
+types based on the schema data. If in your queries you'd like CakePHP to
+automatically convert equality to `IN` comparisons, you'll need to indicate
+the column data type:
+
+``` php
+$query = $articles->find()
+ ->where(['id' => $ids], ['id' => 'integer[]']);
+
+// Or include IN to automatically cast to an array.
+$query = $articles->find()
+ ->where(['id IN' => $ids]);
+```
+
+The above will automatically create `id IN (...)` instead of `id = ?`. This
+can be useful when you do not know whether you will get a scalar or array of
+parameters. The `[]` suffix on any data type name indicates to the query
+builder that you want the data handled as an array. If the data is not an array,
+it will first be cast to an array. After that, each value in the array will
+be cast using the [type system](../orm/database-basics#data-types). This works with
+complex types as well. For example, you could take a list of DateTime objects
+using:
+
+``` php
+$query = $articles->find()
+ ->where(['post_date' => $dates], ['post_date' => 'date[]']);
+```
+
+### Automatic IS NULL Creation
+
+When a condition value is expected to be `null` or any other value, you can
+use the `IS` operator to automatically create the correct expression:
+
+``` php
+$query = $categories->find()
+ ->where(['parent_id IS' => $parentId]);
+```
+
+The above will generate`parent_id = :c1` or `parent_id IS NULL` depending on
+the type of `$parentId`
+
+### Automatic IS NOT NULL Creation
+
+When a condition value is expected not to be `null` or any other value, you
+can use the `IS NOT` operator to automatically create the correct expression:
+
+``` php
+$query = $categories->find()
+ ->where(['parent_id IS NOT' => $parentId]);
+```
+
+The above will generate`parent_id != :c1` or `parent_id IS NOT NULL`
+depending on the type of `$parentId`
+
+### Raw Expressions
+
+When you cannot construct the SQL you need using the query builder, you can use
+expression objects to add snippets of SQL to your queries:
+
+``` php
+$query = $articles->find();
+$expr = $query->expr()->add('1 + 1');
+$query->select(['two' => $expr]);
+```
+
+`Expression` objects can be used with any query builder methods like
+`where()`, `limit()`, `groupBy()`, `select()` and many other methods.
+
+> [!WARNING]
+> Using expression objects leaves you vulnerable to SQL injection. You should
+> never use untrusted data into expressions.
+
+### Using Connection Roles
+
+If you have configured [Read And Write Connections](../orm/database-basics#read-and-write-connections) in your application,
+you can have a query run on the `read` connection using one of the role
+methods:
+
+``` php
+// Run a query on the read connection
+$query->useReadRole();
+
+// Run a query on the write connection (default)
+$query->useWriteRole();
+```
+
+::: info Added in version 4.5.0
+Query role methods were added in 4.5.0
+:::
+
+### Expression Conjuction
+
+It is possible to change the conjunction used to join conditions in a query
+expression using the method `setConjunction`:
+
+``` php
+$query = $articles->find();
+$expr = $query->expr(['1','1'])->setConjunction('+');
+$query->select(['two' => $expr]);
+```
+
+And can be used combined with aggregations too:
+
+``` php
+$query = $products->find();
+$query->select(function ($query) {
+ $stockQuantity = $query->func()->sum('Stocks.quantity');
+ $totalStockValue = $query->func()->sum(
+ $query->expr(['Stocks.quantity', 'Products.unit_price'])
+ ->setConjunction('*')
+ );
+
+ return [
+ 'Products.name',
+ 'stock_quantity' => $stockQuantity,
+ 'Products.unit_price',
+ 'total_stock_value' => $totalStockValue
+ ];
+ })
+ ->innerJoinWith('Stocks')
+ ->groupBy(['Products.id', 'Products.name', 'Products.unit_price']);
+```
+
+### Tuple Comparison
+
+Tuple comparison involves comparing two rows of data (tuples) element by element,
+typically using comparison operators like `<, >, =`:
+
+``` php
+$products->find()
+ ->where([
+ 'OR' => [
+ ['unit_price <' => 20],
+ ['unit_price' => 20, 'tax_percentage <=' => 5],
+ ]
+ ]);
+
+# WHERE (unit_price < 20 OR (unit_price = 20 AND tax_percentage <= 5))
+```
+
+The same result can be achieved using `TupleComparison`:
+
+``` php
+use Cake\Database\Expression\TupleComparison;
+
+$products->find()
+ ->where(
+ new TupleComparison(
+ ['unit_price', 'tax_percentage'],
+ [20, 5],
+ ['integer', 'integer'], # type of each value
+ '<='
+ )
+ );
+
+# WHERE (unit_price, tax_percentage) <= (20, 5))
+```
+
+Tuple Comparison can also be used with `IN` and the result can be transformed
+even on DBMS that does not natively support it:
+
+``` php
+$articles->find()
+ ->where(
+ new TupleComparison(
+ ['articles.id', 'articles.author_id'],
+ [[10, 10], [30, 10]],
+ ['integer', 'integer'],
+ 'IN'
+ ),
+ );
+
+# WHERE (1) = ( SELECT (1) WHERE ( ( articles.id = : 10 AND articles.author_id = : 10 ) OR ( articles.id = : 30 AND articles.author_id = : 30 ) ) )
+```
+
+> [!NOTE]
+> Tuple comparison transform only supports the `IN` and `=` operators
+
+### Optimizer Hints
+
+Optimizer hints allow you to control execution plans at the individual query
+level:
+
+``` php
+$query->optimizerHint(['NO_BKA(articles)']);
+```
+
+Optimizer hints are currently only supported by MySQL and Postgres (via an
+extension).
+
+::: info Added in version 5.3.0
+`Query::optimizerHint()` was added.
+:::
+
+## Getting Results
+
+Once you've made your query, you'll want to retrieve rows from it. There are
+a few ways of doing this:
+
+``` php
+// Iterate the query
+foreach ($query as $row) {
+ // Do stuff.
+}
+
+// Get the results
+$results = $query->all();
+```
+
+You can use [any of the collection](../core-libraries/collections) methods
+on your query objects to pre-process or transform the results:
+
+``` php
+// Use one of the collection methods.
+$ids = $query->map(function ($row) {
+ return $row->id;
+});
+
+$maxAge = $query->max(function ($max) {
+ return $max->age;
+});
+```
+
+You can use `first` or `firstOrFail` to retrieve a single record. These
+methods will alter the query adding a `LIMIT 1` clause:
+
+``` php
+// Get just the first row
+$row = $query->first();
+
+// Get the first row or an exception.
+$row = $query->firstOrFail();
+```
+
+
+
+### Returning the Total Count of Records
+
+Using a single query object, it is possible to obtain the total number of rows
+found for a set of conditions:
+
+``` php
+$total = $articles->find()->where(['is_active' => true])->count();
+```
+
+The `count()` method will ignore the `limit`, `offset` and `page`
+clauses, thus the following will return the same result:
+
+``` php
+$total = $articles->find()->where(['is_active' => true])->limit(10)->count();
+```
+
+This is useful when you need to know the total result set size in advance,
+without having to construct another `SelectQuery` object. Likewise, all result
+formatting and map-reduce routines are ignored when using the `count()`
+method.
+
+Moreover, it is possible to return the total count for a query containing group
+by clauses without having to rewrite the query in any way. For example, consider
+this query for retrieving article ids and their comments count:
+
+``` php
+$query = $articles->find();
+$query->select(['Articles.id', $query->func()->count('Comments.id')])
+ ->matching('Comments')
+ ->groupBy(['Articles.id']);
+$total = $query->count();
+```
+
+After counting, the query can still be used for fetching the associated
+records:
+
+``` php
+$list = $query->all();
+```
+
+Sometimes, you may want to provide an alternate method for counting the total
+records of a query. One common use case for this is providing
+a cached value or an estimate of the total rows, or to alter the query to remove
+expensive unneeded parts such as left joins. This becomes particularly handy
+when using the CakePHP built-in pagination system which calls the `count()`
+method:
+
+``` php
+$query = $query->where(['is_active' => true])->counter(function ($query) {
+ return 100000;
+});
+$query->count(); // Returns 100000
+```
+
+In the example above, when the pagination component calls the count method, it
+will receive the estimated hard-coded number of rows.
+
+
+
+### Caching Loaded Results
+
+When fetching entities that don't change often you may want to cache the
+results. The `SelectQuery` class makes this simple:
+
+``` php
+$query->cache('recent_articles');
+```
+
+Will enable caching on the query's result set. If only one argument is provided
+to `cache()` then the 'default' cache configuration will be used. You can
+control which caching configuration is used with the second parameter:
+
+``` php
+// String config name.
+$query->cache('recent_articles', 'dbResults');
+
+// Instance of CacheEngine
+$query->cache('recent_articles', $memcache);
+```
+
+In addition to supporting static keys, the `cache()` method accepts a function
+to generate the key. The function you give it will receive the query as an
+argument. You can then read aspects of the query to dynamically generate the
+cache key:
+
+``` php
+// Generate a key based on a simple checksum
+// of the query's where clause
+$query->cache(function ($q) {
+ return 'articles-' . md5(serialize($q->clause('where')));
+});
+```
+
+The cache method makes it simple to add cached results to your custom finders or
+through event listeners.
+
+When the results for a cached query are fetched the following happens:
+
+1. If the query has results set, those will be returned.
+2. The cache key will be resolved and cache data will be read. If the cache data
+ is not empty, those results will be returned.
+3. If the cache misses, the query will be executed, the `Model.beforeFind` event
+ will be triggered, and a new `ResultSet` will be created. This
+ `ResultSet` will be written to the cache and returned.
+
+> [!NOTE]
+> You cannot cache a streaming query result.
+
+## Loading Associations
+
+The builder can help you retrieve data from multiple tables at the same time
+with the minimum amount of queries possible. To be able to fetch associated
+data, you first need to setup associations between the tables as described in
+the [Associations - Linking Tables Together](../orm/associations) section. This technique of combining queries
+to fetch associated data from other tables is called **eager loading**.
+
+
+
+### Filtering by Associated Data
+
+
+
+
+
+### Adding Joins
+
+In addition to loading related data with `contain()`, you can also add
+additional joins with the query builder:
+
+``` php
+$query = $articles->find()
+ ->join([
+ 'table' => 'comments',
+ 'alias' => 'c',
+ 'type' => 'LEFT',
+ 'conditions' => 'c.article_id = articles.id',
+ ]);
+```
+
+You can append multiple joins at the same time by passing an associative array
+with multiple joins:
+
+``` php
+$query = $articles->find()
+ ->join([
+ 'c' => [
+ 'table' => 'comments',
+ 'type' => 'LEFT',
+ 'conditions' => 'c.article_id = articles.id',
+ ],
+ 'u' => [
+ 'table' => 'users',
+ 'type' => 'INNER',
+ 'conditions' => 'u.id = articles.user_id',
+ ]
+ ]);
+```
+
+As seen above, when adding joins the alias can be the outer array key. Join
+conditions can also be expressed as an array of conditions:
+
+``` php
+$query = $articles->find()
+ ->join([
+ 'c' => [
+ 'table' => 'comments',
+ 'type' => 'LEFT',
+ 'conditions' => [
+ 'c.created >' => new DateTime('-5 days'),
+ 'c.moderated' => true,
+ 'c.article_id = articles.id'
+ ]
+ ],
+ ], ['c.created' => 'datetime', 'c.moderated' => 'boolean']);
+```
+
+When creating joins by hand and using array based conditions, you need to
+provide the datatypes for each column in the join conditions. By providing
+datatypes for the join conditions, the ORM can correctly convert data types into
+SQL. In addition to `join()` you can use `rightJoin()`, `leftJoin()` and
+`innerJoin()` to create joins:
+
+``` php
+// Join with an alias and string conditions
+$query = $articles->find();
+$query->leftJoin(
+ ['Authors' => 'authors'],
+ ['Authors.id = Articles.author_id']);
+
+// Join with an alias, array conditions, and types
+$query = $articles->find();
+$query->innerJoin(
+ ['Authors' => 'authors'],
+ [
+ 'Authors.promoted' => true,
+ 'Authors.created' => new DateTime('-5 days'),
+ 'Authors.id = Articles.author_id',
+ ],
+ [
+ 'Authors.promoted' => 'boolean',
+ 'Authors.created' => 'datetime',
+ ]
+);
+```
+
+It should be noted that if you set the `quoteIdentifiers` option to `true` when
+defining your `Connection`, join conditions between table fields should be set as follow:
+
+``` php
+$query = $articles->find()
+ ->join([
+ 'c' => [
+ 'table' => 'comments',
+ 'type' => 'LEFT',
+ 'conditions' => [
+ 'c.article_id' => new \Cake\Database\Expression\IdentifierExpression('articles.id'),
+ ],
+ ],
+ ]);
+```
+
+This ensures that all of your identifiers will be quoted across the Query, avoiding errors with
+some database Drivers (PostgreSQL notably)
+
+## Inserting Data
+
+Unlike earlier examples, you should can't use `find()` to create insert queries.
+Instead, create a new `InsertQuery` object using `insertQuery()`:
+
+``` php
+$query = $articles->insertQuery();
+$query->insert(['title', 'body'])
+ ->values([
+ 'title' => 'First post',
+ 'body' => 'Some body text',
+ ])
+ ->execute();
+```
+
+To insert multiple rows with only one query, you can chain the `values()`
+method as many times as you need:
+
+``` php
+$query = $articles->insertQuery();
+$query->insert(['title', 'body'])
+ ->values([
+ 'title' => 'First post',
+ 'body' => 'Some body text',
+ ])
+ ->values([
+ 'title' => 'Second post',
+ 'body' => 'Another body text',
+ ])
+ ->execute();
+```
+
+Generally, it is easier to insert data using entities and
+`Cake\ORM\Table::save()`. By composing a `SELECT` and
+`INSERT` query together, you can create `INSERT INTO ... SELECT` style
+queries:
+
+``` php
+$select = $articles->find()
+ ->select(['title', 'body', 'published'])
+ ->where(['id' => 3]);
+
+$query = $articles->insertQuery()
+ ->insert(['title', 'body', 'published'])
+ ->values($select)
+ ->execute();
+```
+
+> [!NOTE]
+> Inserting records with the query builder will not trigger events such as
+> `Model.afterSave`. Instead you should use the [ORM to save
+> data](../orm/saving-data).
+
+
+
+## Updating Data
+
+As with insert queries, you should not use `find()` to create update queries.
+Instead, create new a `Query` object using `updateQuery()`:
+
+``` php
+$query = $articles->updateQuery();
+$query->set(['published' => true])
+ ->where(['id' => $id])
+ ->execute();
+```
+
+Generally, it is easier to update data using entities and
+`Cake\ORM\Table::patchEntity()`.
+
+> [!NOTE]
+> Updating records with the query builder will not trigger events such as
+> `Model.afterSave`. Instead you should use the [ORM to save
+> data](../orm/saving-data).
+
+## Deleting Data
+
+As with insert queries, you can't use `find()` to create delete queries.
+Instead, create new a query object using `deleteQuery()`:
+
+``` php
+$query = $articles->deleteQuery();
+$query->where(['id' => $id])
+ ->execute();
+```
+
+Generally, it is easier to delete data using entities and
+`Cake\ORM\Table::delete()`.
+
+## SQL Injection Prevention
+
+While the ORM and database abstraction layers prevent most SQL injections
+issues, it is still possible to leave yourself vulnerable through improper use.
+
+When using condition arrays, the key/left-hand side as well as single value
+entries must not contain user data:
+
+``` php
+$query->where([
+ // Data on the key/left-hand side is unsafe, as it will be
+ // inserted into the generated query as-is
+ $userData => $value,
+
+ // The same applies to single value entries, they are not
+ // safe to use with user data in any form
+ $userData,
+ "MATCH (comment) AGAINST ($userData)",
+ 'created < NOW() - ' . $userData
+]);
+```
+
+When using the expression builder, column names must not contain user data:
+
+``` php
+$query->where(function (QueryExpression $exp) use ($userData, $values) {
+ // Column names in all expressions are not safe.
+ return $exp->in($userData, $values);
+});
+```
+
+When building function expressions, function names should never contain user
+data:
+
+``` php
+// Not safe.
+$query->func()->{$userData}($arg1);
+
+// Also not safe to use an array of
+// user data in a function expression
+$query->func()->coalesce($userData);
+```
+
+Raw expressions are never safe:
+
+``` php
+$expr = $query->expr()->add($userData);
+$query->select(['two' => $expr]);
+```
+
+### Binding values
+
+It is possible to protect against many unsafe situations by using bindings.
+Values can be bound to queries using the `Cake\Database\Query::bind()`
+method.
+
+The following example would be a safe variant of the unsafe, SQL injection prone
+example given above:
+
+``` php
+$query
+ ->where([
+ 'MATCH (comment) AGAINST (:userData)',
+ 'created < NOW() - :moreUserData',
+ ])
+ ->bind(':userData', $userData, 'string')
+ ->bind(':moreUserData', $moreUserData, 'datetime');
+```
+
+> [!NOTE]
+> Unlike `Cake\Database\StatementInterface::bindValue()`,
+> `Query::bind()` requires to pass the named placeholders including the
+> colon!
+
+## More Complex Queries
+
+If your application requires using more complex queries, you can express many
+complex queries using the ORM query builder.
+
+### Unions
+
+Unions are created by composing one or more select queries together:
+
+``` php
+$inReview = $articles->find()
+ ->where(['need_review' => true]);
+
+$unpublished = $articles->find()
+ ->where(['published' => false]);
+
+$unpublished->union($inReview);
+```
+
+You can create `UNION ALL` queries using the `unionAll()` method:
+
+``` php
+$inReview = $articles->find()
+ ->where(['need_review' => true]);
+
+$unpublished = $articles->find()
+ ->where(['published' => false]);
+
+$unpublished->unionAll($inReview);
+```
+
+### Intersections
+
+Intersections allow you to combine the result sets of two queries together and
+finding results with overlapping results. Intersections are created by composing
+one or more select queries together:
+
+``` php
+$inReview = $articles->find()
+ ->where(['need_review' => true]);
+
+$unpublished = $articles->find()
+ ->where(['published' => false]);
+
+$unpublished->intersect($inReview);
+```
+
+You can create `INTERSECT ALL` queries using the `intersectAll()` method:
+
+``` php
+$inReview = $articles->find()
+ ->where(['need_review' => true]);
+
+$unpublished = $articles->find()
+ ->where(['published' => false]);
+
+$unpublished->intersectAll($inReview);
+```
+
+::: info Added in version 5.1.0
+`intersect()` and `intersectAll()` were added.
+:::
+
+### Subqueries
+
+Subqueries enable you to compose queries together and build conditions and
+results based on the results of other queries:
+
+``` php
+$matchingComment = $articles->getAssociation('Comments')->find()
+ ->select(['article_id'])
+ ->distinct()
+ ->where(['comment LIKE' => '%CakePHP%']);
+
+// Use a subquery to create conditions
+$query = $articles->find()
+ ->where(['id IN' => $matchingComment]);
+
+// Join the results of a subquery into another query.
+// Giving the subquery an alias provides a way to reference
+// results in subquery.
+$query = $articles->find();
+$query->from(['matches' => $matchingComment])
+ ->innerJoin(
+ ['Articles' => 'articles'],
+ ['Articles.id' => $query->identifier('matches.id') ]
+ );
+```
+
+Subqueries are accepted anywhere a query expression can be used. For example, in
+the `select()`, `from()` and `join()` methods. The above example uses a standard
+`ORM\Query\SelectQuery` object that will generate aliases, these aliases can make
+referencing results in the outer query more complex. As of 4.2.0 you can use
+`Table::subquery()` to create a specialized query instance that will not
+generate aliases:
+
+``` php
+$comments = $articles->getAssociation('Comments')->getTarget();
+
+$matchingComment = $comments->subquery()
+ ->select(['article_id'])
+ ->distinct()
+ ->where(['comment LIKE' => '%CakePHP%']);
+
+$query = $articles->find()
+ ->where(['id IN' => $matchingComment]);
+```
+
+### Adding Locking Statements
+
+Most relational database vendors support taking out locks when doing select
+operations. You can use the `epilog()` method for this:
+
+``` php
+// In MySQL
+$query->epilog('FOR UPDATE');
+```
+
+The `epilog()` method allows you to append raw SQL to the end of queries. You
+should never put raw user data into `epilog()`.
+
+### Window Functions
+
+Window functions allow you to perform calculations using rows related to the
+current row. They are commonly used to calculate totals or offsets on partial sets of rows
+in the query. For example if we wanted to find the date of the earliest and latest comment on
+each article we could use window functions:
+
+``` php
+$query = $articles->find();
+$query->select([
+ 'Articles.id',
+ 'Articles.title',
+ 'Articles.user_id'
+ 'oldest_comment' => $query->func()
+ ->min('Comments.created')
+ ->partition('Comments.article_id'),
+ 'latest_comment' => $query->func()
+ ->max('Comments.created')
+ ->partition('Comments.article_id'),
+])
+->innerJoinWith('Comments');
+```
+
+The above would generate SQL similar to:
+
+``` sql
+SELECT
+ Articles.id,
+ Articles.title,
+ Articles.user_id
+ MIN(Comments.created) OVER (PARTITION BY Comments.article_id) AS oldest_comment,
+ MAX(Comments.created) OVER (PARTITION BY Comments.article_id) AS latest_comment,
+FROM articles AS Articles
+INNER JOIN comments AS Comments
+```
+
+Window expressions can be applied to most aggregate functions. Any aggregate function
+that cake abstracts with a wrapper in `FunctionsBuilder` will return an `AggregateExpression`
+which lets you attach window expressions. You can create custom aggregate functions
+through `FunctionsBuilder::aggregate()`.
+
+These are the most commonly supported window features. Most features are provided
+by `AggregateExpresion`, but make sure you follow your database documentation on use and restrictions.
+
+- `orderBy($fields)` Order the aggregate group the same as a query ORDER BY.
+- `partition($expressions)` Add one or more partitions to the window based on column
+ names.
+- `rows($start, $end)` Define a offset of rows that precede and/or follow the
+ current row that should be included in the aggregate function.
+- `range($start, $end)` Define a range of row values that precede and/or follow
+ the current row that should be included in the aggregate function. This
+ evaluates values based on the `orderBy()` field.
+
+If you need to re-use the same window expression multiple times you can create
+named windows using the `window()` method:
+
+``` php
+$query = $articles->find();
+
+// Define a named window
+$query->window('related_article', function ($window, $query) {
+ $window->partition('Comments.article_id');
+
+ return $window;
+});
+
+$query->select([
+ 'Articles.id',
+ 'Articles.title',
+ 'Articles.user_id'
+ 'oldest_comment' => $query->func()
+ ->min('Comments.created')
+ ->over('related_article'),
+ 'latest_comment' => $query->func()
+ ->max('Comments.created')
+ ->over('related_article'),
+]);
+```
+
+### Common Table Expressions
+
+[Common Table Expressions or CTE](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression)
+are useful when building reporting queries where you need to compose the results
+of several smaller query results together. They can serve a similar purpose
+to database views or subquery results. Common Table Expressions differ from
+derived tables and views in a couple ways:
+
+1. Unlike views, you don't have to maintain schema for common table expressions.
+ The schema is implicitly based on the result set of the table expression.
+2. You can reference the results of a common table expression multiple times
+ without incurring performance penalties unlike subquery joins.
+
+As an example lets fetch a list of customers and the number of orders each of
+them has made. In SQL we would use:
+
+``` sql
+WITH orders_per_customer AS (
+ SELECT COUNT(*) AS order_count, customer_id FROM orders GROUP BY customer_id
+)
+SELECT name, orders_per_customer.order_count
+FROM customers
+INNER JOIN orders_per_customer ON orders_per_customer.customer_id = customers.id
+```
+
+To build that query with the ORM query builder we would use:
+
+``` php
+// Start the final query
+$query = $this->Customers->find();
+
+// Attach a common table expression
+$query->with(function ($cte) {
+ // Create a subquery to use in our table expression
+ $q = $this->Orders->subquery();
+ $q->select([
+ 'order_count' => $q->func()->count('*'),
+ 'customer_id',
+ ])
+ ->groupBy('customer_id');
+
+ // Attach the new query to the table expression
+ return $cte
+ ->name('orders_per_customer')
+ ->query($q);
+});
+
+// Finish building the final query
+$query->select([
+ 'name',
+ 'order_count' => 'orders_per_customer.order_count',
+])
+->join([
+ // Define the join with our table expression
+ 'orders_per_customer' => [
+ 'table' => 'orders_per_customer',
+ 'conditions' => 'orders_per_customer.customer_id = Customers.id',
+ ],
+]);
+```
+
+If you need to build a recursive query (`WITH RECURSIVE …`), chain `->recursive()` onto `return $cte`.
+
+### Executing Complex Queries
+
+While the query builder makes most queries possible through builder methods,
+very complex queries can be tedious and complicated to build. You may want to
+[execute the desired SQL directly](../orm/database-basics#running-select-statements).
+
+Executing SQL directly allows you to fine tune the query that will be run.
+However, doing so doesn't let you use `contain` or other higher level ORM
+features.
diff --git a/docs/en/orm/retrieving-data-and-resultsets.md b/docs/en/orm/retrieving-data-and-resultsets.md
new file mode 100644
index 0000000000..7cb770c61e
--- /dev/null
+++ b/docs/en/orm/retrieving-data-and-resultsets.md
@@ -0,0 +1,1410 @@
+# Retrieving Data & Results Sets
+
+`class` Cake\\ORM\\**Table**
+
+While table objects provide an abstraction around a 'repository' or collection
+of objects, when you query for individual records you get 'entity' objects.
+While this section discusses the different ways you can find and load entities,
+you should read the [Entities](../orm/entities) section for more information on
+entities.
+
+## Debugging Queries and ResultSets
+
+Since the ORM now returns Collections and Entities, debugging these objects can
+be more complicated than in previous CakePHP versions. There are now various
+ways to inspect the data returned by the ORM.
+
+- `debug($query)` Shows the SQL and bound parameters, does not show results.
+- `sql($query)` Shows the final rendered SQL when DebugKit is installed.
+- `debug($query->all())` Shows the ResultSet properties (not the results).
+- `debug($query->toList())` Show results in an array.
+- `debug(iterator_to_array($query))` Shows query results in an array format.
+- `debug(json_encode($query, JSON_PRETTY_PRINT))` More human readable results.
+- `debug($query->first())` Show the properties of a single entity.
+- `debug((string)$query->first())` Show the properties of a single entity as JSON.
+
+## Getting a Single Entity by Primary Key
+
+`method` Cake\\ORM\\Table::**get**(mixed $primaryKey, array|string $finder = 'all', CacheInterface|string|null $cache = null, Closure|string|null $cacheKey = null, mixed ...$args): EntityInterface
+
+It is often convenient to load a single entity from the database when editing or
+viewing entities and their related data. You can do this by using `get()`:
+
+``` php
+// In a controller or table method.
+
+// Get a single article
+$article = $articles->get($id);
+
+// Get a single article, and related comments
+$article = $articles->get($id, contain: ['Comments']);
+```
+
+If the get operation does not find any results a
+`Cake\Datasource\Exception\RecordNotFoundException` will be raised. You can
+either catch this exception yourself, or allow CakePHP to convert it into a 404
+error.
+
+Like `find()`, `get()` also has caching integrated. You can use the
+`cache` option when calling `get()` to perform read-through caching:
+
+``` php
+// In a controller or table method.
+
+// Use any cache config or CacheEngine instance & a generated key
+$article = $articles->get($id, cache: 'custom');
+
+// Use any cache config or CacheEngine instance & specific key
+$article = $articles->get($id, cache: 'custom', key: 'mykey');
+
+// Explicitly disable caching
+$article = $articles->get($id, cache: false);
+```
+
+Optionally you can `get()` an entity using [Custom Find Methods](#custom-find-methods). For
+example you may want to get all translations for an entity. You can achieve that
+by using the `finder` option:
+
+``` php
+$article = $articles->get($id, 'translations');
+```
+
+The parameters supported by `get()` are:
+
+- `$primaryKey` The primary key value to look up.
+- `$finder` The finder to use. Can be a string finder name or an array of finder name and options.
+- `$cache` A cache config name or `CacheInterface` instance to use for read-through caching.
+- `$cacheKey` A custom cache key or `Closure` to generate one.
+- `...$args` Additional arguments passed to the finder.
+
+## Using Finders to Load Data
+
+`method` Cake\\ORM\\Table::**find**(string $type = 'all', mixed ...$args): SelectQuery
+
+Before you can work with entities, you'll need to load them. The easiest way to
+do this is using the `find()` method. The find method provides a short and
+extensible way to find the data you are interested in:
+
+``` php
+// In a controller or table method.
+
+// Find all the articles
+$query = $articles->find('all');
+```
+
+The return value of any `find()` method is always
+a `Cake\ORM\Query\SelectQuery` object. The SelectQuery class allows you to further
+refine a query after creating it. SelectQuery objects are evaluated lazily, and do not
+execute until you start fetching rows, convert it to an array, or when the
+`all()` method is called:
+
+``` php
+// In a controller or table method.
+
+// Find all the articles.
+// At this point the query has not run.
+$query = $articles->find('all');
+
+// Calling all() will execute the query
+// and return the result set.
+$results = $query->all();
+
+// Once we have a result set we can get all the rows
+$data = $results->toList();
+
+// Converting the query to a key-value array will also execute it.
+$data = $query->toArray();
+```
+
+> [!NOTE]
+> Once you've started a query you can use the [Query Builder](../orm/query-builder)
+> interface to build more complex queries, adding additional conditions,
+> limits, or include associations using the fluent interface.
+
+``` php
+// In a controller or table method.
+$query = $articles->find('all')
+ ->where(['Articles.created >' => new DateTime('-10 days')])
+ ->contain(['Comments', 'Authors'])
+ ->limit(10);
+```
+
+You can also provide many commonly used options to `find()`:
+
+``` php
+// In a controller or table method.
+$query = $articles->find('all',
+ conditions: ['Articles.created >' => new DateTime('-10 days')],
+ contain: ['Authors', 'Comments'],
+ limit: 10
+);
+```
+
+If your finder options are in an array, you can use the [splat operator](https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list) (`...`)
+to pass them into `find()`:
+
+``` php
+$options = [
+ 'conditions' => ['Articles.created >' => new DateTime('-10 days')],
+ 'contain' => ['Authors', 'Comments'],
+ 'limit' => 10,
+]
+$query = $articles->find('all', ...$options);
+```
+
+The list of named arguments supported by find() by default are:
+
+- `conditions` provide conditions for the WHERE clause of your query.
+- `limit` Set the number of rows you want.
+- `offset` Set the page offset you want. You can also use `page` to make
+ the calculation simpler.
+- `contain` define the associations to eager load.
+- `fields` limit the fields loaded into the entity. Only loading some fields
+ can cause entities to behave incorrectly.
+- `group` add a GROUP BY clause to your query. This is useful when using
+ aggregating functions.
+- `having` add a HAVING clause to your query.
+- `join` define additional custom joins.
+- `order` order the result set.
+
+Any options that are not in this list will be passed to `beforeFind` listeners
+where they can be used to modify the query object. You can use the
+`getOptions()` method on a query object to retrieve the options used. While
+you can pass query objects to your controllers, we recommend that you package
+your queries up as [Custom Find Methods](#custom-find-methods) instead. Using custom finder
+methods will let you re-use your queries and make testing easier.
+
+By default queries and result sets will return [Entities](../orm/entities) objects. You
+can retrieve basic arrays by disabling hydration:
+
+``` php
+$query->disableHydration();
+
+// $data is ResultSet that contains array data.
+$data = $query->all();
+```
+
+
+
+## Getting the First Result
+
+The `first()` method allows you to fetch only the first row from a query. If
+the query has not been executed, a `LIMIT 1` clause will be applied:
+
+``` php
+// In a controller or table method.
+$query = $articles->find('all', order: ['Articles.created' => 'DESC']);
+$row = $query->first();
+```
+
+This approach replaces `find('first')` in previous versions of CakePHP. You
+may also want to use the `get()` method if you are loading entities by primary
+key.
+
+> [!NOTE]
+> The `first()` method will return `null` if no results are found.
+
+## Getting a Count of Results
+
+Once you have created a query object, you can use the `count()` method to get
+a result count of that query:
+
+``` php
+// In a controller or table method.
+$query = $articles->find('all', conditions: ['Articles.title LIKE' => '%Ovens%']);
+$number = $query->count();
+```
+
+See [Query Count](../orm/query-builder#query-count) for additional usage of the `count()` method.
+
+
+
+## Finding Key/Value Pairs
+
+It is often useful to generate an associative array of data from your
+application's data. For example, this is very useful when creating `