From d7380bf824fb66c6e091f4479529cf93fa033228 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 20 May 2026 15:02:44 +0200 Subject: [PATCH 1/2] Document 5.4 features: PluralRules, EnumLabelTrait, FormHelper hidden default, subcommand validation, enumOptions Adds docs and 5.4 migration guide entries for several 5.next merges: - PluralRules::setRule() / resetRules() for custom Gettext plural rules. - EnumLabelTrait + Label attribute for derived translated enum labels. - FormHelper now wraps hidden CSRF / FormProtection blocks with the HTML5 boolean hidden attribute instead of inline style display:none, so the default markup is strict-CSP compatible. - CLI rejects unknown positional tokens after a parent command that has sibling subcommands, surfacing typos instead of silently dropping them. - FormHelper::enumOptions() is now public for building select options from a backed enum without entity context. --- docs/en/appendices/5-4-migration-guide.md | 30 ++++++++++ docs/en/console-commands/commands.md | 24 ++++++++ .../internationalization-and-localization.md | 59 +++++++++++++++++++ docs/en/orm/database-basics.md | 49 ++++++++++++++- docs/en/views/helpers/form.md | 32 +++++++++- 5 files changed, 190 insertions(+), 4 deletions(-) diff --git a/docs/en/appendices/5-4-migration-guide.md b/docs/en/appendices/5-4-migration-guide.md index aa7986d036..0f27b2d7c6 100644 --- a/docs/en/appendices/5-4-migration-guide.md +++ b/docs/en/appendices/5-4-migration-guide.md @@ -26,6 +26,14 @@ Running `bin/cake` without providing a command name no longer displays the "No command provided" error message. Instead, the `help` command is shown directly. +Unknown positional tokens following a parent command that has sibling +subcommands are now rejected with a clear error listing the available +subcommands. For example, `bin/cake i18n nonsense` previously silently +invoked the parent `I18nCommand` and discarded the trailing token; it now +errors out. Commands that intentionally accept arbitrary positional arguments +(e.g. `routes generate`) are unaffected. +See [Subcommand Validation](../console-commands/commands#subcommand-validation). + The `help` command is now hidden from command listings (via `CommandHiddenInterface`). It remains accessible by running `bin/cake help` or `bin/cake help `. @@ -68,6 +76,16 @@ See [Application and Plugin Events](../core-libraries/events#registering-event-l - Loading a component with the same alias as the controller's default table now triggers a warning. See [Component Alias Conflicts](../controllers/components#component-alias-conflicts). +### View + +- `FormHelper` now wraps hidden form blocks (CSRF, FormProtection, + `postLink()` / `postButton()`) with the HTML5 boolean `hidden` attribute + instead of an inline `style="display:none;"`. This makes the default markup + compatible with a strict Content-Security-Policy (no need for + `style-src 'unsafe-inline'`). If you previously selected those wrappers via + CSS (e.g. `div[style="display:none;"]`), switch to `[hidden]` or set the + `hiddenClass` template option to opt out and emit a class instead. + ## Deprecations ### Command Helpers @@ -145,6 +163,11 @@ See [Application and Plugin Events](../core-libraries/events#registering-event-l provides constants (`Index::GIN`, `Index::GIST`, `Index::SPGIST`, `Index::BRIN`, `Index::HASH`) for these access methods. See [Reading Indexes and Constraints](../orm/schema-system#reading-indexes-and-constraints). +- Added `Cake\Database\Type\EnumLabelTrait` and the + `Cake\Database\Type\Attribute\Label` attribute. The trait provides a default + `label()` implementation backed by the translator and the attribute lets + individual cases override the derived label. See + [EnumLabelTrait and the Label Attribute](../orm/database-basics#enumlabeltrait-and-the-label-attribute). ### Http @@ -166,6 +189,10 @@ See [Application and Plugin Events](../core-libraries/events#registering-event-l - Added `I18n::setCacheConfig()` to route translator persistence to a Cache config other than the default `_cake_translations_`. - The `cake i18n extract` command now also extracts enum labels using the #[Label] attribute. +- Added `PluralRules::setRule()` to register a custom Gettext plural rule for + a locale whose built-in form is missing or differs from the layout used by + your .po/.mo files. See + [Customizing Plural Rules](../core-libraries/internationalization-and-localization#customizing-plural-rules). ### ORM @@ -191,3 +218,6 @@ See [Application and Plugin Events](../core-libraries/events#registering-event-l - Added `{{inputId}}` template variable to `inputContainer` and `error` templates in FormHelper. See [Built-in Template Variables](../views/helpers/form#built-in-template-variables). +- `FormHelper::enumOptions()` is now public. This lets you build `select` + options from a backed enum class even when the form was created without + an entity context. See [Creating Select Pickers](../views/helpers/form#creating-select-pickers). diff --git a/docs/en/console-commands/commands.md b/docs/en/console-commands/commands.md index b404286af2..84aef7e512 100644 --- a/docs/en/console-commands/commands.md +++ b/docs/en/console-commands/commands.md @@ -318,6 +318,30 @@ Usage: cake user [-h] [-q] [-v] ``` +## Subcommand Validation + +::: info Added in version 5.4.0 +Strict validation for unknown subcommands was added in 5.4.0. +::: + +When a parent command has registered subcommands (e.g. `i18n extract`, +`i18n init`), CakePHP rejects unknown positional tokens that follow the +parent name. Previously, typos such as `bin/cake i18n nonsense` silently +invoked the parent command and discarded the trailing token; now you get a +clear error listing the available subcommands: + +```text +$ bin/cake i18n nonsense +Error: Unknown command `cake i18n nonsense`. +Available subcommands: `i18n extract`, `i18n init`. +Run `cake i18n --help` to see usage. +``` + +This only kicks in when the parent command has sibling subcommands. Commands +that accept arbitrary positional arguments (e.g. `routes generate`) are +unaffected, and option-like tokens (`--help`, `-v`) following the command +name continue to be forwarded to the parser. + ## Grouping Commands By default, in the help output CakePHP will group commands into core, app, and diff --git a/docs/en/core-libraries/internationalization-and-localization.md b/docs/en/core-libraries/internationalization-and-localization.md index 50af3dfcef..c3aa0bed2c 100644 --- a/docs/en/core-libraries/internationalization-and-localization.md +++ b/docs/en/core-libraries/internationalization-and-localization.md @@ -425,6 +425,65 @@ msgstr[2] "{0} datoteka je uklonjeno" Please visit the [Launchpad languages page](https://translations.launchpad.net/+languages) for a detailed explanation of the plural form numbers for each language. +#### Customizing Plural Rules + +::: info Added in version 5.4.0 +`PluralRules::setRule()` and `PluralRules::resetRules()` were added in 5.4.0. +::: + +When `__n()` / `__dn()` and the other Gettext-style plural functions resolve a +message, CakePHP picks the plural form via `Cake\I18n\PluralRules::calculate()`. +The built-in rules cover most CLDR locales, but they can lag behind upstream +CLDR releases and they do not cover every minority language. If you hit a +locale whose plural form is missing or wrong, you can register a custom rule +without patching CakePHP: + +```php +use Cake\I18n\PluralRules; + +// Breton: 5 plural forms (CLDR) +PluralRules::setRule('br', function (int $n): int { + if ($n % 10 === 1 && $n % 100 !== 11 && $n % 100 !== 71 && $n % 100 !== 91) { + return 0; + } + if ($n % 10 === 2 && $n % 100 !== 12 && $n % 100 !== 72 && $n % 100 !== 92) { + return 1; + } + if (in_array($n % 10, [3, 4, 9], true) + && !in_array($n % 100, [13, 14, 19, 73, 74, 79, 93, 94, 99], true) + ) { + return 2; + } + if ($n !== 0 && $n % 1_000_000 === 0) { + return 3; + } + + return 4; +}); +``` + +The closure receives the integer count and must return the zero-based plural +form index that matches the `msgstr[N]` entries in your **.po** / **.mo** +files. Custom rules take precedence over the built-in map, so they can also +be used to override a built-in rule that does not match the form layout used +by your translation files. + +Register rules in **config/bootstrap.php** so they are available before any +translation is requested. The locale string is normalized via +`Locale::canonicalize()` and an invalid locale throws an +`InvalidArgumentException`. To drop all registered custom rules (typically +between tests), call: + +```php +PluralRules::resetRules(); +``` + +> [!NOTE] +> `PluralRules` is only consulted for Gettext-style messages +> (`__n()`, `__dn()`, `msgstr[0]` / `msgstr[1]` / …). The ICU plural selector +> shown above resolves its own forms via `MessageFormatter` and is unaffected +> by `setRule()`. + ## Creating Your Own Translators If you need to diverge from CakePHP conventions regarding where and how diff --git a/docs/en/orm/database-basics.md b/docs/en/orm/database-basics.md index 94a5e87e59..d95b6daf1d 100644 --- a/docs/en/orm/database-basics.md +++ b/docs/en/orm/database-basics.md @@ -596,7 +596,54 @@ enum ArticleStatus: string implements EnumLabelInterface ``` This can be useful if you want to use your enums in `FormHelper` select -inputs. You can use [bake](../bake) to generate an enum class: +inputs. + +#### EnumLabelTrait and the Label Attribute + +::: info Added in version 5.4.0 +`Cake\Database\Type\EnumLabelTrait` and the +`Cake\Database\Type\Attribute\Label` attribute were added in 5.4.0. +::: + +Writing the `label()` `match` block by hand becomes repetitive once an enum +grows past a few cases. `EnumLabelTrait` provides a default `label()` +implementation that derives the label from the case name and resolves it +through the translator. Cases can override the derived label with the +`#[Label]` attribute: + +```php +namespace App\Model\Enum; + +use Cake\Database\Type\Attribute\Label; +use Cake\Database\Type\EnumLabelInterface; +use Cake\Database\Type\EnumLabelTrait; + +enum ArticleStatus: string implements EnumLabelInterface +{ + use EnumLabelTrait; + + case Published = 'Y'; + + #[Label('Not yet published')] + case Unpublished = 'N'; + + #[Label('Archived', domain: 'articles', context: 'status')] + case Archived = 'A'; +} +``` + +For a case **without** a `#[Label]` attribute, the trait humanizes the case +name (`Unpublished` → `Unpublished`, `InReview` → `In review`) and runs it +through the translator. For cases **with** a `#[Label]`, the explicit label +string is used and is translated using the optional `domain` and `context` +constructor arguments. Labels are extracted by `cake i18n extract`, which +detects the `#[Label]` attribute and emits one msgid per case. + +> [!TIP] +> Pair `EnumLabelTrait` with `EnumLabelInterface` so type-aware consumers +> (e.g. `FormHelper`'s automatic enum support) keep working. + +You can use [bake](../bake) to generate an enum class: ```bash # generate an enum class with two cases and stored as an integer diff --git a/docs/en/views/helpers/form.md b/docs/en/views/helpers/form.md index 3ece9b408c..526f3400df 100644 --- a/docs/en/views/helpers/form.md +++ b/docs/en/views/helpers/form.md @@ -1462,6 +1462,24 @@ Output: ``` +To build `$options` from a backed enum, you can use `enumOptions()`: + +```php +use App\Model\Enum\ArticleStatus; + +echo $this->Form->select('status', $this->Form->enumOptions(ArticleStatus::class)); +``` + +When `ArticleStatus` implements `EnumLabelInterface` (or uses +`EnumLabelTrait`), the option text is taken from `label()`; otherwise the +case name is used. This is useful when the form was created without an +entity context, where the automatic enum detection on `control()` does not +apply. + +::: info Added in version 5.4.0 +`FormHelper::enumOptions()` was made public in 5.4.0. +::: + **Controlling Select Pickers via Attributes** By using specific options in the `$attributes` parameter you can control @@ -2140,7 +2158,7 @@ echo $this->Form->end(['data-type' => 'hidden']); Will output: ```html -
+