Skip to content

Commit d7380bf

Browse files
committed
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.
1 parent 3a1c8df commit d7380bf

5 files changed

Lines changed: 190 additions & 4 deletions

File tree

docs/en/appendices/5-4-migration-guide.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ Running `bin/cake` without providing a command name no longer displays the
2626
"No command provided" error message. Instead, the `help` command is shown
2727
directly.
2828

29+
Unknown positional tokens following a parent command that has sibling
30+
subcommands are now rejected with a clear error listing the available
31+
subcommands. For example, `bin/cake i18n nonsense` previously silently
32+
invoked the parent `I18nCommand` and discarded the trailing token; it now
33+
errors out. Commands that intentionally accept arbitrary positional arguments
34+
(e.g. `routes generate`) are unaffected.
35+
See [Subcommand Validation](../console-commands/commands#subcommand-validation).
36+
2937
The `help` command is now hidden from command listings (via
3038
`CommandHiddenInterface`). It remains accessible by running `bin/cake help` or
3139
`bin/cake help <command>`.
@@ -68,6 +76,16 @@ See [Application and Plugin Events](../core-libraries/events#registering-event-l
6876
- Loading a component with the same alias as the controller's default table now
6977
triggers a warning. See [Component Alias Conflicts](../controllers/components#component-alias-conflicts).
7078

79+
### View
80+
81+
- `FormHelper` now wraps hidden form blocks (CSRF, FormProtection,
82+
`postLink()` / `postButton()`) with the HTML5 boolean `hidden` attribute
83+
instead of an inline `style="display:none;"`. This makes the default markup
84+
compatible with a strict Content-Security-Policy (no need for
85+
`style-src 'unsafe-inline'`). If you previously selected those wrappers via
86+
CSS (e.g. `div[style="display:none;"]`), switch to `[hidden]` or set the
87+
`hiddenClass` template option to opt out and emit a class instead.
88+
7189
## Deprecations
7290

7391
### Command Helpers
@@ -145,6 +163,11 @@ See [Application and Plugin Events](../core-libraries/events#registering-event-l
145163
provides constants (`Index::GIN`, `Index::GIST`, `Index::SPGIST`,
146164
`Index::BRIN`, `Index::HASH`) for these access methods.
147165
See [Reading Indexes and Constraints](../orm/schema-system#reading-indexes-and-constraints).
166+
- Added `Cake\Database\Type\EnumLabelTrait` and the
167+
`Cake\Database\Type\Attribute\Label` attribute. The trait provides a default
168+
`label()` implementation backed by the translator and the attribute lets
169+
individual cases override the derived label. See
170+
[EnumLabelTrait and the Label Attribute](../orm/database-basics#enumlabeltrait-and-the-label-attribute).
148171

149172
### Http
150173

@@ -166,6 +189,10 @@ See [Application and Plugin Events](../core-libraries/events#registering-event-l
166189
- Added `I18n::setCacheConfig()` to route translator persistence to a Cache
167190
config other than the default `_cake_translations_`.
168191
- The `cake i18n extract` command now also extracts enum labels using the #[Label] attribute.
192+
- Added `PluralRules::setRule()` to register a custom Gettext plural rule for
193+
a locale whose built-in form is missing or differs from the layout used by
194+
your .po/.mo files. See
195+
[Customizing Plural Rules](../core-libraries/internationalization-and-localization#customizing-plural-rules).
169196

170197
### ORM
171198

@@ -191,3 +218,6 @@ See [Application and Plugin Events](../core-libraries/events#registering-event-l
191218

192219
- Added `{{inputId}}` template variable to `inputContainer` and `error` templates
193220
in FormHelper. See [Built-in Template Variables](../views/helpers/form#built-in-template-variables).
221+
- `FormHelper::enumOptions()` is now public. This lets you build `select`
222+
options from a backed enum class even when the form was created without
223+
an entity context. See [Creating Select Pickers](../views/helpers/form#creating-select-pickers).

docs/en/console-commands/commands.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,30 @@ Usage:
318318
cake user [-h] [-q] [-v]
319319
```
320320

321+
## Subcommand Validation
322+
323+
::: info Added in version 5.4.0
324+
Strict validation for unknown subcommands was added in 5.4.0.
325+
:::
326+
327+
When a parent command has registered subcommands (e.g. `i18n extract`,
328+
`i18n init`), CakePHP rejects unknown positional tokens that follow the
329+
parent name. Previously, typos such as `bin/cake i18n nonsense` silently
330+
invoked the parent command and discarded the trailing token; now you get a
331+
clear error listing the available subcommands:
332+
333+
```text
334+
$ bin/cake i18n nonsense
335+
Error: Unknown command `cake i18n nonsense`.
336+
Available subcommands: `i18n extract`, `i18n init`.
337+
Run `cake i18n --help` to see usage.
338+
```
339+
340+
This only kicks in when the parent command has sibling subcommands. Commands
341+
that accept arbitrary positional arguments (e.g. `routes generate`) are
342+
unaffected, and option-like tokens (`--help`, `-v`) following the command
343+
name continue to be forwarded to the parser.
344+
321345
## Grouping Commands
322346

323347
By default, in the help output CakePHP will group commands into core, app, and

docs/en/core-libraries/internationalization-and-localization.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,65 @@ msgstr[2] "{0} datoteka je uklonjeno"
425425
Please visit the [Launchpad languages page](https://translations.launchpad.net/+languages)
426426
for a detailed explanation of the plural form numbers for each language.
427427

428+
#### Customizing Plural Rules
429+
430+
::: info Added in version 5.4.0
431+
`PluralRules::setRule()` and `PluralRules::resetRules()` were added in 5.4.0.
432+
:::
433+
434+
When `__n()` / `__dn()` and the other Gettext-style plural functions resolve a
435+
message, CakePHP picks the plural form via `Cake\I18n\PluralRules::calculate()`.
436+
The built-in rules cover most CLDR locales, but they can lag behind upstream
437+
CLDR releases and they do not cover every minority language. If you hit a
438+
locale whose plural form is missing or wrong, you can register a custom rule
439+
without patching CakePHP:
440+
441+
```php
442+
use Cake\I18n\PluralRules;
443+
444+
// Breton: 5 plural forms (CLDR)
445+
PluralRules::setRule('br', function (int $n): int {
446+
if ($n % 10 === 1 && $n % 100 !== 11 && $n % 100 !== 71 && $n % 100 !== 91) {
447+
return 0;
448+
}
449+
if ($n % 10 === 2 && $n % 100 !== 12 && $n % 100 !== 72 && $n % 100 !== 92) {
450+
return 1;
451+
}
452+
if (in_array($n % 10, [3, 4, 9], true)
453+
&& !in_array($n % 100, [13, 14, 19, 73, 74, 79, 93, 94, 99], true)
454+
) {
455+
return 2;
456+
}
457+
if ($n !== 0 && $n % 1_000_000 === 0) {
458+
return 3;
459+
}
460+
461+
return 4;
462+
});
463+
```
464+
465+
The closure receives the integer count and must return the zero-based plural
466+
form index that matches the `msgstr[N]` entries in your **.po** / **.mo**
467+
files. Custom rules take precedence over the built-in map, so they can also
468+
be used to override a built-in rule that does not match the form layout used
469+
by your translation files.
470+
471+
Register rules in **config/bootstrap.php** so they are available before any
472+
translation is requested. The locale string is normalized via
473+
`Locale::canonicalize()` and an invalid locale throws an
474+
`InvalidArgumentException`. To drop all registered custom rules (typically
475+
between tests), call:
476+
477+
```php
478+
PluralRules::resetRules();
479+
```
480+
481+
> [!NOTE]
482+
> `PluralRules` is only consulted for Gettext-style messages
483+
> (`__n()`, `__dn()`, `msgstr[0]` / `msgstr[1]` / …). The ICU plural selector
484+
> shown above resolves its own forms via `MessageFormatter` and is unaffected
485+
> by `setRule()`.
486+
428487
## Creating Your Own Translators
429488

430489
If you need to diverge from CakePHP conventions regarding where and how

docs/en/orm/database-basics.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,54 @@ enum ArticleStatus: string implements EnumLabelInterface
596596
```
597597

598598
This can be useful if you want to use your enums in `FormHelper` select
599-
inputs. You can use [bake](../bake) to generate an enum class:
599+
inputs.
600+
601+
#### EnumLabelTrait and the Label Attribute
602+
603+
::: info Added in version 5.4.0
604+
`Cake\Database\Type\EnumLabelTrait` and the
605+
`Cake\Database\Type\Attribute\Label` attribute were added in 5.4.0.
606+
:::
607+
608+
Writing the `label()` `match` block by hand becomes repetitive once an enum
609+
grows past a few cases. `EnumLabelTrait` provides a default `label()`
610+
implementation that derives the label from the case name and resolves it
611+
through the translator. Cases can override the derived label with the
612+
`#[Label]` attribute:
613+
614+
```php
615+
namespace App\Model\Enum;
616+
617+
use Cake\Database\Type\Attribute\Label;
618+
use Cake\Database\Type\EnumLabelInterface;
619+
use Cake\Database\Type\EnumLabelTrait;
620+
621+
enum ArticleStatus: string implements EnumLabelInterface
622+
{
623+
use EnumLabelTrait;
624+
625+
case Published = 'Y';
626+
627+
#[Label('Not yet published')]
628+
case Unpublished = 'N';
629+
630+
#[Label('Archived', domain: 'articles', context: 'status')]
631+
case Archived = 'A';
632+
}
633+
```
634+
635+
For a case **without** a `#[Label]` attribute, the trait humanizes the case
636+
name (`Unpublished``Unpublished`, `InReview``In review`) and runs it
637+
through the translator. For cases **with** a `#[Label]`, the explicit label
638+
string is used and is translated using the optional `domain` and `context`
639+
constructor arguments. Labels are extracted by `cake i18n extract`, which
640+
detects the `#[Label]` attribute and emits one msgid per case.
641+
642+
> [!TIP]
643+
> Pair `EnumLabelTrait` with `EnumLabelInterface` so type-aware consumers
644+
> (e.g. `FormHelper`'s automatic enum support) keep working.
645+
646+
You can use [bake](../bake) to generate an enum class:
600647

601648
```bash
602649
# generate an enum class with two cases and stored as an integer

docs/en/views/helpers/form.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,24 @@ Output:
14621462
</select>
14631463
```
14641464

1465+
To build `$options` from a backed enum, you can use `enumOptions()`:
1466+
1467+
```php
1468+
use App\Model\Enum\ArticleStatus;
1469+
1470+
echo $this->Form->select('status', $this->Form->enumOptions(ArticleStatus::class));
1471+
```
1472+
1473+
When `ArticleStatus` implements `EnumLabelInterface` (or uses
1474+
`EnumLabelTrait`), the option text is taken from `label()`; otherwise the
1475+
case name is used. This is useful when the form was created without an
1476+
entity context, where the automatic enum detection on `control()` does not
1477+
apply.
1478+
1479+
::: info Added in version 5.4.0
1480+
`FormHelper::enumOptions()` was made public in 5.4.0.
1481+
:::
1482+
14651483
**Controlling Select Pickers via Attributes**
14661484

14671485
By using specific options in the `$attributes` parameter you can control
@@ -2140,14 +2158,22 @@ echo $this->Form->end(['data-type' => 'hidden']);
21402158
Will output:
21412159

21422160
```html
2143-
<div style="display:none;">
2161+
<div hidden="hidden">
21442162
<input type="hidden" name="_Token[fields]" data-type="hidden"
21452163
value="2981c38990f3f6ba935e6561dc77277966fabd6d%3AAddresses.id">
21462164
<input type="hidden" name="_Token[unlocked]" data-type="hidden"
21472165
value="address%7Cfirst_name">
21482166
</div>
21492167
```
21502168

2169+
::: info Added in version 5.4.0
2170+
The wrapper around hidden security tokens now defaults to the HTML5
2171+
`hidden` boolean attribute instead of `style="display:none;"`. This avoids
2172+
needing `style-src 'unsafe-inline'` under a strict Content-Security-Policy.
2173+
You can still opt for a CSS class instead by setting the `hiddenClass`
2174+
template option on `FormHelper`.
2175+
:::
2176+
21512177
> [!NOTE]
21522178
> If you are using
21532179
> `Cake\Controller\Component\FormProtectionComponent` in your
@@ -2190,11 +2216,11 @@ Will output HTML similar to:
21902216

21912217
```html
21922218
<form method="post" accept-charset="utf-8" action="/Rtools/tickets/delete/5">
2193-
<div style="display:none;">
2219+
<div hidden="hidden">
21942220
<input name="_method" value="POST" type="hidden">
21952221
</div>
21962222
<button type="submit">Delete Record</button>
2197-
<div style="display:none;">
2223+
<div hidden="hidden">
21982224
<input name="_Token[fields]" value="186cfbfc6f519622e19d1e688633c4028229081f%3A" type="hidden">
21992225
<input name="_Token[unlocked]" value="" type="hidden">
22002226
<input name="_Token[debug]" value="%5B%22%5C%2FRtools%5C%2Ftickets%5C%2Fdelete%5C%2F1%22%2C%5B%5D%2C%5B%5D%5D" type="hidden">

0 commit comments

Comments
 (0)