+ = $this->Html->link( + h($article->title), + ['action' => 'view', $article->slug] + ) ?> +
++ Published: = $article->created->format('F d, Y') ?> +
+= h($article->body) ?>
+diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..f590184de1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,26 @@
+; This file is for unifying the coding style for different editors and IDEs.
+; More information at https://editorconfig.org
+
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.bat]
+end_of_line = crlf
+
+[*.yml]
+indent_size = 2
+
+[*.js]
+indent_size = 2
+
+[Makefile]
+indent_style = tab
+
+[*.neon]
+indent_style = tab
diff --git a/.github/cspell.json b/.github/cspell.json
new file mode 100644
index 0000000000..0c872cb3c3
--- /dev/null
+++ b/.github/cspell.json
@@ -0,0 +1,109 @@
+{
+ "version": "0.2",
+ "language": "en",
+ "caseSensitive": false,
+ "words": [
+ "CakePHP",
+ "VitePress",
+ "php",
+ "datetime",
+ "varchar",
+ "postgresql",
+ "mysql",
+ "sqlite",
+ "webroot",
+ "csrf",
+ "xss",
+ "cakephp",
+ "bake",
+ "phinx",
+ "hasher",
+ "middlewares",
+ "namespaced",
+ "phpunit",
+ "composable",
+ "autowiring",
+ "stringable",
+ "arrayable",
+ "timestampable",
+ "paginator",
+ "Enqueue",
+ "dequeue",
+ "nullable",
+ "accessor",
+ "mutator",
+ "minphpversion",
+ "phpversion",
+ "XAMPP",
+ "WAMP",
+ "MAMP",
+ "controllername",
+ "actionname",
+ "Datamapper",
+ "webservices",
+ "nginx",
+ "apache",
+ "httpd",
+ "lighttpd",
+ "mbstring",
+ "simplexml",
+ "Laragon",
+ "composer",
+ "phar",
+ "paginate",
+ "startup",
+ "endfor",
+ "endwhile",
+ "sidebar",
+ "blocks",
+ "rewrite",
+ "before",
+ "called",
+ "sites",
+ "example",
+ "Classes",
+ "Redirect",
+ "Layout"
+ ],
+ "ignoreRegExpList": [
+ "\\$[a-zA-Z_][a-zA-Z0-9_]*",
+ "[a-zA-Z]+::[a-zA-Z]+",
+ "<[^>]+>",
+ "`[^`]+`",
+ "```[\\s\\S]*?```",
+ "\\b[A-Z][a-z]+(?:[A-Z][a-z]+)+\\b",
+ "\\b[a-z]+_[a-z_]+\\b",
+ "\\bhttps?:\\/\\/[^\\s]+",
+ "\\[[a-z]\\][a-z]+",
+ "\\b[A-Z][a-z]+\\b",
+ "\\b(true|false|null|while|for|if|else)\\b"
+ ],
+ "languageSettings": [
+ {
+ "languageId": "*",
+ "locale": "en",
+ "dictionaries": ["en", "softwareTerms", "php"]
+ },
+ {
+ "languageId": "*",
+ "locale": "ja",
+ "allowCompoundWords": true
+ }
+ ],
+ "overrides": [
+ {
+ "filename": "**/docs/ja/**/*.md",
+ "language": "ja",
+ "ignoreRegExpList": [
+ "[\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Han}]+"
+ ]
+ }
+ ],
+ "ignorePaths": [
+ "node_modules/**",
+ ".temp/**",
+ "dist/**",
+ "**/*.min.js",
+ "**/*.lock"
+ ]
+}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..b8be3562ac
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,17 @@
+version: 2
+updates:
+- package-ecosystem: pip
+ directory: "/"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: sphinx
+ versions:
+ - ">= 3.a"
+ - "< 4"
+- package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
diff --git a/.github/linkchecker-baseline.json b/.github/linkchecker-baseline.json
new file mode 100644
index 0000000000..e85fd04bbc
--- /dev/null
+++ b/.github/linkchecker-baseline.json
@@ -0,0 +1,612 @@
+[
+ {
+ "file": "docs/ja/appendices/5-0-migration-guide.md",
+ "link": "../plugins#loading-a-plugin",
+ "message": "Anchor '#loading-a-plugin' not found in docs/ja/plugins.md"
+ },
+ {
+ "file": "docs/ja/appendices/5-1-migration-guide.md",
+ "link": "../core-libraries/events#registering-event-listeners",
+ "message": "Anchor '#registering-event-listeners' not found in docs/ja/core-libraries/events.md"
+ },
+ {
+ "file": "docs/ja/appendices/5-2-migration-guide.md",
+ "link": "../views/helpers/form#customizing-templates",
+ "message": "Anchor '#customizing-templates' not found in docs/ja/views/helpers/form.md"
+ },
+ {
+ "file": "docs/ja/console-commands/shells.md",
+ "link": "../console-commands/input-output#command-helpers",
+ "message": "Anchor '#command-helpers' not found in docs/ja/console-commands/input-output.md"
+ },
+ {
+ "file": "docs/ja/contributing/code.md",
+ "link": "../orm/database-basics#database-configuration",
+ "message": "Anchor '#database-configuration' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/contributing/code.md",
+ "link": "../development/testing#running-tests",
+ "message": "Anchor '#running-tests' not found in docs/ja/development/testing.md"
+ },
+ {
+ "file": "docs/ja/controllers/components/request-handling.md",
+ "link": "../../controllers/middleware#body-parser-middleware",
+ "message": "Anchor '#body-parser-middleware' not found in docs/ja/controllers/middleware.md"
+ },
+ {
+ "file": "docs/ja/controllers/middleware.md",
+ "link": "../development/routing#route-scoped-middleware",
+ "message": "Anchor '#route-scoped-middleware' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/controllers/middleware.md",
+ "link": "../core-libraries/caching#cache-configuration",
+ "message": "Anchor '#cache-configuration' not found in docs/ja/core-libraries/caching.md"
+ },
+ {
+ "file": "docs/ja/controllers/pagination.md",
+ "link": "../orm/retrieving-data-and-resultsets#custom-find-methods",
+ "message": "Anchor '#custom-find-methods' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/controllers/pagination.md",
+ "link": "../views/helpers/paginator#paginator-helper-multiple",
+ "message": "Anchor '#paginator-helper-multiple' not found in docs/ja/views/helpers/paginator.md"
+ },
+ {
+ "file": "docs/ja/controllers/pagination.md",
+ "link": "../orm/table-objects#table-registry-usage",
+ "message": "Anchor '#table-registry-usage' not found in docs/ja/orm/table-objects.md"
+ },
+ {
+ "file": "docs/ja/controllers/request-response.md",
+ "link": "../development/routing#route-elements",
+ "message": "Anchor '#route-elements' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/controllers/request-response.md",
+ "link": "../development/routing#route-elements",
+ "message": "Anchor '#route-elements' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/controllers/request-response.md",
+ "link": "../development/routing#passed-arguments",
+ "message": "Anchor '#passed-arguments' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/controllers/request-response.md",
+ "link": "../development/routing#prefix-routing",
+ "message": "Anchor '#prefix-routing' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/controllers/request-response.md",
+ "link": "../controllers/middleware#body-parser-middleware",
+ "message": "Anchor '#body-parser-middleware' not found in docs/ja/controllers/middleware.md"
+ },
+ {
+ "file": "docs/ja/controllers/request-response.md",
+ "link": "../controllers/middleware#encrypted-cookie-middleware",
+ "message": "Anchor '#encrypted-cookie-middleware' not found in docs/ja/controllers/middleware.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/caching.md",
+ "link": "../orm/query-builder#caching-query-results",
+ "message": "Anchor '#caching-query-results' not found in docs/ja/orm/query-builder.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/caching.md",
+ "link": "../orm/query-builder#caching-query-results",
+ "message": "Anchor '#caching-query-results' not found in docs/ja/orm/query-builder.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/email.md",
+ "link": "../core-libraries/logging#logging-levels",
+ "message": "Anchor '#logging-levels' not found in docs/ja/core-libraries/logging.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/email.md",
+ "link": "../core-libraries/logging#logging-scopes",
+ "message": "Anchor '#logging-scopes' not found in docs/ja/core-libraries/logging.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/email.md",
+ "link": "../core-libraries/events#registering-event-listeners",
+ "message": "Anchor '#registering-event-listeners' not found in docs/ja/core-libraries/events.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/events.md",
+ "link": "../orm/table-objects#table-callbacks",
+ "message": "Anchor '#table-callbacks' not found in docs/ja/orm/table-objects.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/events.md",
+ "link": "../controllers#controller-life-cycle",
+ "message": "Anchor '#controller-life-cycle' not found in docs/ja/controllers.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/events.md",
+ "link": "../views#view-events",
+ "message": "Anchor '#view-events' not found in docs/ja/views.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/events.md",
+ "link": "../development/testing#testing-events",
+ "message": "Anchor '#testing-events' not found in docs/ja/development/testing.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/time.md",
+ "link": "../core-libraries/internationalization-and-localization#parsing-localized-dates",
+ "message": "Anchor '#parsing-localized-dates' not found in docs/ja/core-libraries/internationalization-and-localization.md"
+ },
+ {
+ "file": "docs/ja/core-libraries/validation.md",
+ "link": "../orm/validation#application-rules",
+ "message": "Anchor '#application-rules' not found in docs/ja/orm/validation.md"
+ },
+ {
+ "file": "docs/ja/deployment.md",
+ "link": "installation#without-url-rewriting",
+ "message": "Anchor '#without-url-rewriting' not found in docs/ja/installation.md"
+ },
+ {
+ "file": "docs/ja/development/application.md",
+ "link": "../development/testing#integration-testing",
+ "message": "Anchor '#integration-testing' not found in docs/ja/development/testing.md"
+ },
+ {
+ "file": "docs/ja/development/configuration.md",
+ "link": "../orm/database-basics#database-configuration",
+ "message": "Anchor '#database-configuration' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/development/configuration.md",
+ "link": "../core-libraries/caching#cache-configuration",
+ "message": "Anchor '#cache-configuration' not found in docs/ja/core-libraries/caching.md"
+ },
+ {
+ "file": "docs/ja/development/configuration.md",
+ "link": "../development/errors#error-configuration",
+ "message": "Anchor '#error-configuration' not found in docs/ja/development/errors.md"
+ },
+ {
+ "file": "docs/ja/development/configuration.md",
+ "link": "../core-libraries/logging#log-configuration",
+ "message": "Anchor '#log-configuration' not found in docs/ja/core-libraries/logging.md"
+ },
+ {
+ "file": "docs/ja/development/configuration.md",
+ "link": "../core-libraries/email#email-configuration",
+ "message": "Anchor '#email-configuration' not found in docs/ja/core-libraries/email.md"
+ },
+ {
+ "file": "docs/ja/development/configuration.md",
+ "link": "../development/sessions#session-configuration",
+ "message": "Anchor '#session-configuration' not found in docs/ja/development/sessions.md"
+ },
+ {
+ "file": "docs/ja/development/configuration.md",
+ "link": "../core-libraries/inflector#inflection-configuration",
+ "message": "Anchor '#inflection-configuration' not found in docs/ja/core-libraries/inflector.md"
+ },
+ {
+ "file": "docs/ja/development/errors.md",
+ "link": "../development/routing#prefix-routing",
+ "message": "Anchor '#prefix-routing' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/development/rest.md",
+ "link": "../development/routing#resource-routes",
+ "message": "Anchor '#resource-routes' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/development/rest.md",
+ "link": "../development/routing#resource-routes",
+ "message": "Anchor '#resource-routes' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/development/routing.md",
+ "link": "../plugins#plugin-routes",
+ "message": "Anchor '#plugin-routes' not found in docs/ja/plugins.md"
+ },
+ {
+ "file": "docs/ja/development/routing.md",
+ "link": "../controllers/middleware#routing-middleware",
+ "message": "Anchor '#routing-middleware' not found in docs/ja/controllers/middleware.md"
+ },
+ {
+ "file": "docs/ja/development/routing.md",
+ "link": "../controllers/middleware#routing-middleware",
+ "message": "Anchor '#routing-middleware' not found in docs/ja/controllers/middleware.md"
+ },
+ {
+ "file": "docs/ja/development/testing.md",
+ "link": "../controllers/middleware#encrypted-cookie-middleware",
+ "message": "Anchor '#encrypted-cookie-middleware' not found in docs/ja/controllers/middleware.md"
+ },
+ {
+ "file": "docs/ja/development/testing.md",
+ "link": "../controllers/request-response#request-file-uploads",
+ "message": "Anchor '#request-file-uploads' not found in docs/ja/controllers/request-response.md"
+ },
+ {
+ "file": "docs/ja/development/testing.md",
+ "link": "../console-commands/commands#console-integration-testing",
+ "message": "Anchor '#console-integration-testing' not found in docs/ja/console-commands/commands.md"
+ },
+ {
+ "file": "docs/ja/development/testing.md",
+ "link": "../development/dependency-injection#mocking-services-in-tests",
+ "message": "Anchor '#mocking-services-in-tests' not found in docs/ja/development/dependency-injection.md"
+ },
+ {
+ "file": "docs/ja/development/testing.md",
+ "link": "../core-libraries/events#tracking-events",
+ "message": "Anchor '#tracking-events' not found in docs/ja/core-libraries/events.md"
+ },
+ {
+ "file": "docs/ja/development/testing.md",
+ "link": "../core-libraries/email#email-testing",
+ "message": "Anchor '#email-testing' not found in docs/ja/core-libraries/email.md"
+ },
+ {
+ "file": "docs/ja/index.md",
+ "link": "../_downloads/en/CakePHPBook.pdf",
+ "message": "File not found: docs/_downloads/en/CakePHPBook.pdf"
+ },
+ {
+ "file": "docs/ja/index.md",
+ "link": "../_downloads/ja/CakePHP.epub",
+ "message": "File not found: docs/_downloads/ja/CakePHP.epub"
+ },
+ {
+ "file": "docs/ja/index.md",
+ "link": "intro#request-cycle",
+ "message": "Anchor '#request-cycle' not found in docs/ja/intro.md"
+ },
+ {
+ "file": "docs/ja/orm.md",
+ "link": "orm/database-basics#database-configuration",
+ "message": "Anchor '#database-configuration' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/orm.md",
+ "link": "intro/conventions#model-and-database-conventions",
+ "message": "Anchor '#model-and-database-conventions' not found in docs/ja/intro/conventions.md"
+ },
+ {
+ "file": "docs/ja/orm/associations.md",
+ "link": "../orm/retrieving-data-and-resultsets#custom-find-methods",
+ "message": "Anchor '#custom-find-methods' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/orm/associations.md",
+ "link": "../orm/retrieving-data-and-resultsets#eager-loading-associations",
+ "message": "Anchor '#eager-loading-associations' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/orm/behaviors.md",
+ "link": "../orm/table-objects#table-callbacks",
+ "message": "Anchor '#table-callbacks' not found in docs/ja/orm/table-objects.md"
+ },
+ {
+ "file": "docs/ja/orm/behaviors.md",
+ "link": "../orm/retrieving-data-and-resultsets#custom-find-methods",
+ "message": "Anchor '#custom-find-methods' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/orm/behaviors/counter-cache.md",
+ "link": "../../orm/associations#using-the-through-option",
+ "message": "Anchor '#using-the-through-option' not found in docs/ja/orm/associations.md"
+ },
+ {
+ "file": "docs/ja/orm/database-basics.md",
+ "link": "../orm/table-objects#configuring-table-connections",
+ "message": "Anchor '#configuring-table-connections' not found in docs/ja/orm/table-objects.md"
+ },
+ {
+ "file": "docs/ja/orm/database-basics.md",
+ "link": "../orm/saving-data#saving-complex-types",
+ "message": "Anchor '#saving-complex-types' not found in docs/ja/orm/saving-data.md"
+ },
+ {
+ "file": "docs/ja/orm/database-basics.md",
+ "link": "../orm/saving-data#saving-complex-types",
+ "message": "Anchor '#saving-complex-types' not found in docs/ja/orm/saving-data.md"
+ },
+ {
+ "file": "docs/ja/orm/deleting-data.md",
+ "link": "../orm/validation#application-rules",
+ "message": "Anchor '#application-rules' not found in docs/ja/orm/validation.md"
+ },
+ {
+ "file": "docs/ja/orm/entities.md",
+ "link": "../orm/saving-data#saving-entities",
+ "message": "Anchor '#saving-entities' not found in docs/ja/orm/saving-data.md"
+ },
+ {
+ "file": "docs/ja/orm/entities.md",
+ "link": "../orm/saving-data#changing-accessible-fields",
+ "message": "Anchor '#changing-accessible-fields' not found in docs/ja/orm/saving-data.md"
+ },
+ {
+ "file": "docs/ja/orm/entities.md",
+ "link": "../orm/saving-data#saving-complex-types",
+ "message": "Anchor '#saving-complex-types' not found in docs/ja/orm/saving-data.md"
+ },
+ {
+ "file": "docs/ja/orm/query-builder.md",
+ "link": "../orm/database-basics#database-queries",
+ "message": "Anchor '#database-queries' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/orm/query-builder.md",
+ "link": "../orm/retrieving-data-and-resultsets#table-find-list",
+ "message": "Anchor '#table-find-list' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/orm/query-builder.md",
+ "link": "../orm/database-basics#database-query-logging",
+ "message": "Anchor '#database-query-logging' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/orm/query-builder.md",
+ "link": "../orm/retrieving-data-and-resultsets#map-reduce",
+ "message": "Anchor '#map-reduce' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/orm/query-builder.md",
+ "link": "../orm/database-basics#data-types",
+ "message": "Anchor '#data-types' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/orm/query-builder.md",
+ "link": "../orm/database-basics#database-basics-binding-values",
+ "message": "Anchor '#database-basics-binding-values' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/orm/query-builder.md",
+ "link": "../orm/database-basics#running-select-statements",
+ "message": "Anchor '#running-select-statements' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/orm/retrieving-data-and-resultsets.md",
+ "link": "../orm/query-builder#query-count",
+ "message": "Anchor '#query-count' not found in docs/ja/orm/query-builder.md"
+ },
+ {
+ "file": "docs/ja/orm/retrieving-data-and-resultsets.md",
+ "link": "../orm/query-builder#adding-joins",
+ "message": "Anchor '#adding-joins' not found in docs/ja/orm/query-builder.md"
+ },
+ {
+ "file": "docs/ja/orm/retrieving-data-and-resultsets.md",
+ "link": "../orm/entities#lazy-load-associations",
+ "message": "Anchor '#lazy-load-associations' not found in docs/ja/orm/entities.md"
+ },
+ {
+ "file": "docs/ja/orm/retrieving-data-and-resultsets.md",
+ "link": "../orm/query-builder#format-results",
+ "message": "Anchor '#format-results' not found in docs/ja/orm/query-builder.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/entities#entities-mass-assignment",
+ "message": "Anchor '#entities-mass-assignment' not found in docs/ja/orm/entities.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/validation#validating-request-data",
+ "message": "Anchor '#validating-request-data' not found in docs/ja/orm/validation.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/validation#using-different-validators-per-association",
+ "message": "Anchor '#using-different-validators-per-association' not found in docs/ja/orm/validation.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/entities#entities-mass-assignment",
+ "message": "Anchor '#entities-mass-assignment' not found in docs/ja/orm/entities.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/validation#validating-request-data",
+ "message": "Anchor '#validating-request-data' not found in docs/ja/orm/validation.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/entities#entities-mass-assignment",
+ "message": "Anchor '#entities-mass-assignment' not found in docs/ja/orm/entities.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/validation#application-rules",
+ "message": "Anchor '#application-rules' not found in docs/ja/orm/validation.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/validation#application-rules",
+ "message": "Anchor '#application-rules' not found in docs/ja/orm/validation.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../views/helpers/form#associated-form-inputs",
+ "message": "Anchor '#associated-form-inputs' not found in docs/ja/views/helpers/form.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../core-libraries/inflector#inflector-methods-summary",
+ "message": "Anchor '#inflector-methods-summary' not found in docs/ja/core-libraries/inflector.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../core-libraries/inflector#inflector-methods-summary",
+ "message": "Anchor '#inflector-methods-summary' not found in docs/ja/core-libraries/inflector.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../core-libraries/inflector#inflector-methods-summary",
+ "message": "Anchor '#inflector-methods-summary' not found in docs/ja/core-libraries/inflector.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/associations#has-many-associations",
+ "message": "Anchor '#has-many-associations' not found in docs/ja/orm/associations.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../core-libraries/inflector#inflector-methods-summary",
+ "message": "Anchor '#inflector-methods-summary' not found in docs/ja/core-libraries/inflector.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/associations#belongs-to-many-associations",
+ "message": "Anchor '#belongs-to-many-associations' not found in docs/ja/orm/associations.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../views/helpers/form#associated-form-inputs",
+ "message": "Anchor '#associated-form-inputs' not found in docs/ja/views/helpers/form.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/database-basics#adding-custom-database-types",
+ "message": "Anchor '#adding-custom-database-types' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/orm/saving-data.md",
+ "link": "../orm/query-builder#query-builder-updating-data",
+ "message": "Anchor '#query-builder-updating-data' not found in docs/ja/orm/query-builder.md"
+ },
+ {
+ "file": "docs/ja/orm/schema-system.md",
+ "link": "../development/testing#test-fixtures",
+ "message": "Anchor '#test-fixtures' not found in docs/ja/development/testing.md"
+ },
+ {
+ "file": "docs/ja/orm/table-objects.md",
+ "link": "../orm/database-basics#database-configuration",
+ "message": "Anchor '#database-configuration' not found in docs/ja/orm/database-basics.md"
+ },
+ {
+ "file": "docs/ja/orm/table-objects.md",
+ "link": "../orm/saving-data#before-marshal",
+ "message": "Anchor '#before-marshal' not found in docs/ja/orm/saving-data.md"
+ },
+ {
+ "file": "docs/ja/orm/table-objects.md",
+ "link": "../orm/retrieving-data-and-resultsets#map-reduce",
+ "message": "Anchor '#map-reduce' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/orm/validation.md",
+ "link": "../core-libraries/validation#creating-validators",
+ "message": "Anchor '#creating-validators' not found in docs/ja/core-libraries/validation.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/blog/blog.md",
+ "link": "../../installation#without-url-rewriting",
+ "message": "Anchor '#without-url-rewriting' not found in docs/ja/installation.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/blog/part-two.md",
+ "link": "../../development/routing#named-routes",
+ "message": "Anchor '#named-routes' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/blog/part-two.md",
+ "link": "../../views#view-layouts",
+ "message": "Anchor '#view-layouts' not found in docs/ja/views.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/bookmarks/intro.md",
+ "link": "../../orm/retrieving-data-and-resultsets#custom-find-methods",
+ "message": "Anchor '#custom-find-methods' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/cms/articles-controller.md",
+ "link": "../../development/routing#named-routes",
+ "message": "Anchor '#named-routes' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/cms/articles-controller.md",
+ "link": "../../orm/retrieving-data-and-resultsets#dynamic-finders",
+ "message": "Anchor '#dynamic-finders' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/cms/articles-controller.md",
+ "link": "../../orm/table-objects#table-callbacks",
+ "message": "Anchor '#table-callbacks' not found in docs/ja/orm/table-objects.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/cms/articles-controller.md",
+ "link": "../../orm/validation#validating-request-data",
+ "message": "Anchor '#validating-request-data' not found in docs/ja/orm/validation.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/cms/database.md",
+ "link": "../../orm/entities#entities-mass-assignment",
+ "message": "Anchor '#entities-mass-assignment' not found in docs/ja/orm/entities.md"
+ },
+ {
+ "file": "docs/ja/tutorials-and-examples/cms/tags-and-users.md",
+ "link": "../../orm/retrieving-data-and-resultsets#custom-find-methods",
+ "message": "Anchor '#custom-find-methods' not found in docs/ja/orm/retrieving-data-and-resultsets.md"
+ },
+ {
+ "file": "docs/ja/views/cells.md",
+ "link": "../controllers/pagination#paginating-multiple-queries",
+ "message": "Anchor '#paginating-multiple-queries' not found in docs/ja/controllers/pagination.md"
+ },
+ {
+ "file": "docs/ja/views/helpers/form.md",
+ "link": "../../core-libraries/validation#creating-validators",
+ "message": "Anchor '#creating-validators' not found in docs/ja/core-libraries/validation.md"
+ },
+ {
+ "file": "docs/ja/views/helpers/form.md",
+ "link": "../../views#view-blocks",
+ "message": "Anchor '#view-blocks' not found in docs/ja/views.md"
+ },
+ {
+ "file": "docs/ja/views/helpers/form.md",
+ "link": "../../views#view-blocks",
+ "message": "Anchor '#view-blocks' not found in docs/ja/views.md"
+ },
+ {
+ "file": "docs/ja/views/helpers/html.md",
+ "link": "../../views#view-blocks",
+ "message": "Anchor '#view-blocks' not found in docs/ja/views.md"
+ },
+ {
+ "file": "docs/ja/views/helpers/paginator.md",
+ "link": "../../controllers/pagination#control-which-fields-used-for-ordering",
+ "message": "Anchor '#control-which-fields-used-for-ordering' not found in docs/ja/controllers/pagination.md"
+ },
+ {
+ "file": "docs/ja/views/helpers/paginator.md",
+ "link": "../../controllers/pagination#paginating-multiple-queries",
+ "message": "Anchor '#paginating-multiple-queries' not found in docs/ja/controllers/pagination.md"
+ },
+ {
+ "file": "docs/ja/views/helpers/url.md",
+ "link": "../../views/helpers#aliasing-helpers",
+ "message": "Anchor '#aliasing-helpers' not found in docs/ja/views/helpers.md"
+ },
+ {
+ "file": "docs/ja/views/json-and-xml-views.md",
+ "link": "../development/routing#file-extensions",
+ "message": "Anchor '#file-extensions' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/views/json-and-xml-views.md",
+ "link": "../development/routing#file-extensions",
+ "message": "Anchor '#file-extensions' not found in docs/ja/development/routing.md"
+ },
+ {
+ "file": "docs/ja/views/themes.md",
+ "link": "../plugins#plugin-create-your-own",
+ "message": "Anchor '#plugin-create-your-own' not found in docs/ja/plugins.md"
+ }
+]
diff --git a/.github/markdownlint.json b/.github/markdownlint.json
new file mode 100644
index 0000000000..4327587dbf
--- /dev/null
+++ b/.github/markdownlint.json
@@ -0,0 +1,50 @@
+{
+ "default": true,
+ "heading-increment": false,
+ "no-hard-tabs": false,
+ "no-multiple-blanks": false,
+ "line-length": false,
+ "commands-show-output": false,
+ "blanks-around-headings": false,
+ "no-duplicate-heading": false,
+ "single-h1": false,
+ "no-trailing-punctuation": false,
+ "no-blanks-blockquote": false,
+ "list-marker-space": false,
+ "blanks-around-fences": false,
+ "blanks-around-lists": false,
+ "no-inline-html": {
+ "allowed_elements": [
+ "Badge",
+ "div",
+ "span",
+ "br",
+ "style",
+ "details",
+ "summary",
+ "table",
+ "thead",
+ "tbody",
+ "tr",
+ "th",
+ "td",
+ "img",
+ "a",
+ "svg",
+ "path",
+ "figure",
+ "p"
+ ]
+ },
+ "no-bare-urls": false,
+ "fenced-code-language": false,
+ "first-line-heading": false,
+ "code-block-style": false,
+ "single-trailing-newline": false,
+ "link-fragments": false,
+ "table-pipe-style": false,
+ "table-column-count": false,
+ "emphasis-style": false,
+ "table-column-style": false,
+ "descriptive-link-text": false
+}
diff --git a/.github/workflows/deploy_5.yml b/.github/workflows/deploy_5.yml
new file mode 100644
index 0000000000..05caa87741
--- /dev/null
+++ b/.github/workflows/deploy_5.yml
@@ -0,0 +1,28 @@
+---
+name: 'deploy_5'
+
+on:
+ push:
+ branches:
+ - 5.x
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Cloning repo
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Push to dokku
+ uses: dokku/github-action@master
+ with:
+ branch: '5.x'
+ git_remote_url: 'ssh://dokku@apps.cakephp.org:22/newbook-5'
+ git_push_flags: '-f'
+ ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}
diff --git a/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml
new file mode 100644
index 0000000000..8bf08474f7
--- /dev/null
+++ b/.github/workflows/docs-validation.yml
@@ -0,0 +1,114 @@
+name: Documentation Validation
+
+on:
+ push:
+ branches:
+ - 5.x
+ - 6.x
+ - 5.next
+ paths:
+ - 'docs/**'
+ - '.github/**'
+ - 'toc_*.json'
+ - 'config.js'
+ pull_request:
+ paths:
+ - 'docs/**'
+ - '.github/**'
+ - 'toc_*.json'
+ - 'config.js'
+
+jobs:
+ js-lint:
+ name: Validate JavaScript Files
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Validate config.js syntax
+ run: node --check config.js
+
+ json-lint:
+ name: Validate JSON Files
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Validate JSON syntax
+ run: |
+ for file in toc_*.json; do
+ if ! jq empty "$file" 2>/dev/null; then
+ echo "Invalid JSON: $file"
+ exit 1
+ fi
+ echo "✅ Valid: $file"
+ done
+
+ markdown-lint:
+ name: Lint Markdown
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Lint markdown files
+ uses: articulate/actions-markdownlint@v1
+ with:
+ config: .github/markdownlint.json
+ files: 'docs/**/*.md'
+ ignore: 'node_modules'
+
+ spell-check:
+ name: Spell Check
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Check spelling
+ uses: streetsidesoftware/cspell-action@v5
+ with:
+ files: 'docs/**/*.md'
+ config: '.github/cspell.json'
+ incremental_files_only: true
+
+ link-check:
+ name: Check Internal Links
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Get changed markdown files
+ id: changed-files
+ run: |
+ if [ "${{ github.event_name }}" == "pull_request" ]; then
+ # Get files changed in PR
+ FILES=$(git diff --name-only --diff-filter=d origin/${{ github.base_ref }}...HEAD | grep '\.md$' || true)
+ if [ -z "$FILES" ]; then
+ echo "No markdown files changed"
+ echo "files=" >> $GITHUB_OUTPUT
+ else
+ # Convert to space-separated list for script
+ echo "files=$(echo $FILES | tr '\n' ' ')" >> $GITHUB_OUTPUT
+ echo "Checking files:"
+ echo "$FILES"
+ fi
+ else
+ # For push events, check all files using glob pattern
+ echo 'files="docs/**/*.md"' >> $GITHUB_OUTPUT
+ echo 'Checking all files: docs/**/*.md'
+ fi
+
+ - name: Check internal links
+ if: steps.changed-files.outputs.files != ''
+ run: node bin/check-links.js --baseline .github/linkchecker-baseline.json ${{ steps.changed-files.outputs.files }}
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
new file mode 100644
index 0000000000..47bbb9dbfb
--- /dev/null
+++ b/.github/workflows/stale.yml
@@ -0,0 +1,29 @@
+name: Mark stale issues and pull requests
+
+on:
+ schedule:
+ - cron: "0 0 * * *"
+
+permissions:
+ contents: read
+
+jobs:
+ stale:
+
+ permissions:
+ issues: write # for actions/stale to close stale issues
+ pull-requests: write # for actions/stale to close stale PRs
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/stale@v10
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ stale-issue-message: 'This issue is stale because it has been open for 120 days with no activity. Remove the `stale` label or comment or this will be closed in 15 days'
+ stale-pr-message: 'This pull request is stale because it has been open 120 days with no activity. Remove the `stale` label or comment on this issue, or it will be closed in 15 days'
+ stale-issue-label: 'stale'
+ stale-pr-label: 'stale'
+ days-before-stale: 120
+ days-before-close: 15
+ exempt-issue-labels: 'pinned'
+ exempt-pr-labels: 'pinned'
diff --git a/.gitignore b/.gitignore
index d0ce3d50e7..1de888e91c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,26 @@
-*.pyc
-build/*
-*/_build/*
-.DS_Store
\ No newline at end of file
+# Project specific files #
+##########################
+.vitepress/dist
+.vitepress/cache
+.vitepress/.temp
+/node_modules/
+.temp/
+
+# IDE and editor specific files #
+#################################
+/nbproject
+.idea
+.project
+.vscode
+.zed
+
+# OS generated files #
+######################
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+Icon?
+ehthumbs.db
+Thumbs.db
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000..80ffb5a73e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,51 @@
+# ----------------------
+# 1. Build stage
+# ----------------------
+FROM node:22-alpine AS builder
+
+# Install git and rsync
+RUN apk add --no-cache git rsync
+
+WORKDIR /app
+
+# Bust cache by fetching latest commit info
+ADD https://api.github.com/repos/cakephp/docs-skeleton/git/refs/heads/main /tmp/cache-bust.json
+
+# Clone cakephp-docs-skeleton into vitepress directory
+RUN git clone --depth 1 https://github.com/cakephp/docs-skeleton.git vitepress
+
+# Copy documentation and config files into the skeleton
+# Use rsync to merge docs instead of replacing to preserve shared public assets
+RUN --mount=type=bind,source=docs,target=/tmp/docs \
+ rsync -av /tmp/docs/ vitepress/docs/
+
+COPY config.js vitepress/config.js
+COPY toc_en.json vitepress/toc_en.json
+COPY toc_ja.json vitepress/toc_ja.json
+
+# Install vitepress deps
+WORKDIR /app/vitepress
+RUN npm install
+
+# Increase max-old-space-size to avoid memory issues during build
+ENV NODE_OPTIONS="--max-old-space-size=8192"
+
+# Build VitePress site
+RUN npm run docs:build
+
+# ----------------------
+# 2. Runtime stage (nginx)
+# ----------------------
+FROM nginx:1.27-alpine AS runner
+
+# Copy built files
+COPY --from=builder /app/vitepress/.vitepress/dist /usr/share/nginx/html
+
+# Expose port
+EXPOSE 80
+
+# Health check (optional)
+HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1
+
+# Start nginx
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 9675c4d452..0000000000
--- a/Makefile
+++ /dev/null
@@ -1,70 +0,0 @@
-# MakeFile for building all the docs at once.
-# Inspired by the Makefile used by bazaar.
-# http://bazaar.launchpad.net/~bzr-pqm/bzr/2.3/
-
-PYTHON = python
-
-.PHONY: all clean html latexpdf epub htmlhelp website website-dirs
-
-# Languages that can be built.
-LANGS = en es fr ja pt ru
-DEST = website
-
-# Dependencies to perform before running other builds.
-# Clone the en/Makefile everywhere.
-SPHINX_DEPENDENCIES = $(foreach lang, $(LANGS), $(lang)/Makefile)
-
-# Copy-paste the english Makefile everwhere its needed.
-%/Makefile: en/Makefile
- cp $< $@
-
-#
-# The various formats the documentation can be created in.
-#
-# Loop over the possible languages and call other build targets.
-#
-html: $(foreach lang, $(LANGS), html-$(lang))
-htmlhelp: $(foreach lang, $(LANGS), htmlhelp-$(lang))
-epub: $(foreach lang, $(LANGS), epub-$(lang))
-htmlhelp: $(foreach lang, $(LANGS), htmlhelp-$(lang))
-populate-index: $(foreach lang, $(LANGS), populate-index-$(lang))
-
-
-# Make the HTML version of the documentation with correctly nested language folders.
-html-%: $(SPHINX_DEPENDENCIES)
- cd $* && make html LANG=$*
-
-htmlhelp-%: $(SPHINX_DEPENDENCIES)
- cd $* && make htmlhelp LANG=$*
-
-epub-%: $(SPHINX_DEPENDENCIES)
- cd $* && make epub LANG=$*
-
-latexpdf-%: $(SPHINX_DEPENDENCIES)
- cd $* && make latexpdf LANG=$*
-
-populate-index-%: $(SPHINX_DEPENDENCIES)
- php scripts/populate_search_index.php $*
-
-website-dirs:
- # Make the directory if its not there already.
- [ ! -d $(DEST) ] && mkdir $(DEST) || true
-
- # Make the downloads directory
- [ ! -d $(DEST)/_downloads ] && mkdir $(DEST)/_downloads || true
-
- # Make downloads for each language
- $(foreach lang, $(LANGS), [ ! -d $(DEST)/_downloads/$(lang) ] && mkdir $(DEST)/_downloads/$(lang) || true;)
-
-website: website-dirs html populate-index epub
- # Move HTML
- $(foreach lang, $(LANGS), cp -r build/html/$(lang) $(DEST)/$(lang);)
-
- # Move EPUB files
- $(foreach lang, $(LANGS), cp -r build/epub/$(lang)/*.epub $(DEST)/_downloads/$(lang);)
-
-clean:
- rm -rf build/*
-
-clean-website:
- rm -rf $(DEST)/*
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..affdffdede
--- /dev/null
+++ b/README.md
@@ -0,0 +1,98 @@
+CakePHP Documentation
+=====================
+
+[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
+[](https://github.com/cakephp/docs/actions/workflows/docs-validation.yml)
+[](https://github.com/cakephp/docs/actions/workflows/deploy_5.yml)
+
+This is the official documentation for the CakePHP project. It is available
+online at https://book.cakephp.org.
+
+Contributing to the documentation is pretty simple. Please read the
+documentation on contributing to the documentation over on [the
+cookbook](https://book.cakephp.org/5/en/contributing/documentation.html) for
+help. You can read all the documentation within as it is just in plain text
+files, marked up with Markdown formatting.
+
+## Local Development
+
+For working with the documentation markdown files locally, use the provided development server script:
+
+```bash
+./bin/dev-server.sh
+```
+
+This script will:
+- Set up a clean `.temp` working directory
+- Clone the VitePress skeleton repository
+- Sync your documentation files
+- Install dependencies
+- Start a local development server with hot-reload
+
+The documentation will be available at `http://localhost:5173`
+
+### Development Server Options
+
+```bash
+# Start on a custom port
+./bin/dev-server.sh --port 3000
+
+# Adjust docs sync interval (default: 1 second)
+./bin/dev-server.sh --sync-interval 2
+```
+
+### Prerequisites
+
+The development server requires:
+- `git` - Version control
+- `node` - JavaScript runtime
+- `npm` - Package manager
+- `rsync` - File synchronization
+
+Press `Ctrl+C` to stop the development server.
+
+## Build the Documentation with Docker
+
+Docker will let you create a container with all packages needed to build the
+docs. You need to have docker installed, see the [official docs of
+docker](https://docs.docker.com/desktop/) for more information.
+
+### Build the image locally
+
+Starting in the top-level directory, you can build the provided `Dockerfile`
+and tag it with the name `cakephp/docs` by running:
+
+```bash
+docker build -f Dockerfile -t cakephp/docs .
+```
+
+This can take a little while, because all packages needs to be downloaded, but
+you'll only need to do this once.
+
+Now that the image is built, you can run the commands to build the docs:
+
+##### To build the static site:
+```bash
+docker build --progress=plain --no-cache -f Dockerfile -t cake-docs .
+```
+
+##### To run the development server:
+```bash
+docker run -d -p 8080:80 --name cakedocs cake-vitepress
+```
+
+The built documentation will output to the `.vitepress/dist` directory.
+
+## Contributing
+
+You are welcome to make suggestions for new content as commits in a
+GitHub fork. Please make any totally new sections in a separate branch. This
+makes changes far easier to integrate later on.
+
+The documentation is written in Markdown and uses VitePress for static site generation.
+All documentation files are located in the `docs/` directory, organized by version.
+
+## Search Functionality
+
+The documentation includes built-in search functionality powered by VitePress's local search feature.
+Search works automatically in both development and production builds without requiring any additional setup.
diff --git a/README.mdown b/README.mdown
deleted file mode 100644
index db344309bc..0000000000
--- a/README.mdown
+++ /dev/null
@@ -1,89 +0,0 @@
-CakePHP Documentation
-=====================
-
-This documentation is planned to replace the existing cookbook. Hopefully enabling community contributions and enabling multiple format documentation generation.
-
-Requirements
-------------
-
-You can read all of the documentation within as its just in plain text files, marked up with ReST text formatting. To build the documentation you'll need the following:
-
-* Make
-* Python
-* Sphinx
-* PhpDomain for sphinx
-
-You can install sphinx using:
-
- easy_install sphinx
-
-You can install the phpdomain using:
-
- easy_install sphinxcontrib-phpdomain
-
-Building the documentation
---------------------------
-
-After installing the require packages you can build the documentation using `Make`
-
- # Create all the HTML docs. Including all the languages.
- make html
-
- # Create just the english HTML docs.
- make html-en
-
- # Create all the EPUB (e-book) docs.
- make epub
-
- # Create just the engish EPUB docs.
- make epub-en
-
- # Populate the search index
- make populate-index
-
-This will generate all the documentation in an html form. Other output such as 'pdf' and 'htmlhelp' are not fully complete at this time.
-
-After making changes to the documentation, you can build the html version of the docs by using `make html` again. This will build only the html files that have had changes made to them.
-
-
-Contributing
-------------
-
-Contributing to the documentation is pretty simple. There are currently a number of outstanding issues that need to be addressed. We've tried to flag these with `.. todo::` where possible. To see all the outstanding todo's add the following to your `config/all.py`
-
- todo_include_todos = True
-
-After rebuilding the html content, you should see a list of existing todo items at the bottom of the table of contents.
-
-You are also welcome to make and suggestions for new content as commits in a github fork. Please make any totally new sections in a separate branch. This makes changes far easier to integrate later on.
-
-Translations
-------------
-
-Contributing translations requires that you make a new directory using the two letter name for your language. As content is translated, directories mirroring the english content should be created with localized content.
-
-
-Generating Meta tags
---------------------
-
-If you are providing translations and want to automatically generate meta tags for the resulting html files, a MetatagShell is provided in
-the `scripts` folder of this repo. In order to use it, copy it into any CakePHP 2.0 empty application inside `app/Console/Command`, execute
-`Console/cake metatag` and follow the instructions.
-
-The script will process all the files under one of the translation folders and generate the possible description terms using an external API,
-it is a good idea to go over the generated terms and clean-up whatever noise it might have generated.
-
-Making search work locally
---------------------------
-
-* Install elasticesearch. This varies based on your platform, but most
- package managers have a package for it.
-* Clone the [docs_search](https://github.com/cakephp/docs_search) into a
- web accessible directory directory.
-* Modify `searchUrl` in `themes/cakephp/static/app.js` to point at the
- baseurl for your docs_search clone.
-* Start elasticsearch with the default configuration.
-* Populate the search index using `make populate-index`.
-* You should now be able to search the docs using elasticsearch.
-
-
diff --git a/bin/check-links.js b/bin/check-links.js
new file mode 100755
index 0000000000..f30abad3d4
--- /dev/null
+++ b/bin/check-links.js
@@ -0,0 +1,459 @@
+#!/usr/bin/env node
+
+/**
+ * Internal Markdown Link Checker
+ *
+ * Validates internal markdown links in documentation files.
+ * Only checks relative links - ignores external URLs.
+ *
+ * Usage:
+ * node bin/check-links.js 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.↩︎ 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.↩︎
+
+
+
+
+
+If you...
+Backwards compatibility?
+
+
+Typehint against the class
+Yes
+
+
+Create a new instance
+Yes
+
+
+Extend the class
+Yes
+
+
+Access a public property
+Yes
+
+
+Call a public method
+Yes
+
+
+Extend a class and...
+
+
+Override a public property
+Yes
+
+
+Access a protected property
+No1
+
+
+Override a protected property
+No2
+
+
+Override a protected method
+No3
+
+
+Call a protected method
+No4
+
+
+Add a public property
+No
+
+
+Add a public method
+No
+
+
+Add an argument to an overridden method
+No5
+
+
+
+Add a default argument value to an existing method argument
+Yes
+
+
+
+
+
+
+
+
+
+In a minor release can you...
+
+
+Classes
+
+
+Remove a class
+No
+
+
+Remove an interface
+No
+
+
+Remove a trait
+No
+
+
+Make final
+No
+
+
+Make abstract
+No
+
+
+Change name
+Yes1
+
+
+Properties
+
+
+Add a public property
+Yes
+
+
+Remove a public property
+No
+
+
+Add a protected property
+Yes
+
+
+Remove a protected property
+Yes2
+
+
+Methods
+
+
+Add a public method
+Yes
+
+
+Remove a public method
+No
+
+
+Add a protected method
+Yes
+
+
+Move to parent class
+Yes
+
+
+Remove a protected method
+Yes3
+
+
+Reduce visibility
+No
+
+
+Change method name
+Yes4
+
+
+Add a new argument with default value
+Yes
+
+
+Add a new required argument to an existing method.
+No
+
+
+Remove a default value from an existing argument
+No
+
+
+
+Change method type void
+Yes
+
+
+
+
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 `@versionHere 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**(array $attachments): static + +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 `` 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..e94c7d994f
--- /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**(ArrayAccess|array $data, array|string|int|null $path, mixed $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**(ArrayAccess|array $data, string $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` Cake\\Utility\\Hash::**insert**(ArrayAccess|array $data, string $path, mixed $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**(ArrayAccess|array $data, string $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, array|string|null $keyPath, array|string|null $valuePath = null, ?string $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, string $format): ?array
+
+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): 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, ?callable $callback = null): 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, string $path, callable $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, string $path, callable $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, string $path, callable $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, string $path, string|int $dir = 'asc', array|string $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, bool $assoc = true, mixed $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..5f5cedfbaf
--- /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 | +
* @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:
+
+``` php
+// 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..0e1dd8f01b
--- /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()`:
+
+``` php
+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:
+
+ ``` php
+ '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,
+));
+// ...
+```
+
++ Published: = $article->created->format('F d, Y') ?> +
+= h($article->body) ?>
+
+Ready to Build Something Amazing?
+ +**Start your CakePHP journey today and join thousands of developers building modern web applications.** + ++ Get Started → + • + Join Community + • + View Examples +
+ +
+| 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 | +
| Class | +ArticlesController | +MenuLinksController | ++ |
| Controller | +ArticlesController | +MenuLinksController | +Plural, CamelCased, end in Controller | +
| Templates | +Articles/index.php Articles/add.php Articles/get_list.php | +MenuLinks/index.php MenuLinks/add.php MenuLinks/get_list.php | +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. | +|||
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. |
+