Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/en/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ EventManager::instance()->on(

By scoping event listeners to specific bake templates, you can simplify your bake-related event logic and provide callbacks that are easier to test.

## Routing Attributes in Custom Themes

Controller templates receive the resolved `attributeRouting` boolean and routing
metadata: `routingActions`, `routingImports`, `routingScopePath`,
`routingPrefixName`, and `routingPrefixPath`. The default controller template uses
this metadata for its imports and class attributes. Built-in action elements
render their own `Route` attributes without HTTP method restrictions. Themes can
override those elements to customize paths or add method restrictions.

Comment thread
josbeir marked this conversation as resolved.
Custom action elements must define their own routes when attribute routing is
enabled. Use fully qualified attribute names or add their imports in the theme's
controller template. Overriding the entire controller template also makes the
theme responsible for rendering class attributes and routing imports. Honor
`attributeRouting` when supporting both routing modes.

Plugin templates receive `attributeRouting` too. Attribute-enabled plugin
skeletons omit fallback routes.

## Bake Template Syntax

Bake template files use the [Twig](https://twig.symfony.com/) template syntax.
Expand Down
59 changes: 59 additions & 0 deletions docs/en/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,65 @@ To get help on a specific command, type `cake command_name --help`
To see full descriptions and plugin grouping, use `cake --help -v`
```

## Controller Routing

For CakePHP 6, Bake generates routing attributes on controllers by default.
This applies to `bake controller`, `bake controller all`, and `bake all`.
Generated plugin skeletons also omit fallback routes by default.

### Generated URLs

For `ArticlesController`, Bake generates these routes:

| Action | Path |
| --- | --- |
| index | `/articles` and `/articles/index` |
| view | `/articles/view/{id}` |
| add | `/articles/add` |
| edit | `/articles/edit/{id}` |
| delete | `/articles/delete/{id}` |
| login, when generated | `/users/login` for `UsersController` |

Routes use `#[Route]` attributes without HTTP method restrictions; index has an
additional attribute for its `/index` alias.

Existing array-based links, form actions, and redirects work without fallbacks or
additional `_method` routing hints, with `/articles` as the index URL:

```php
$this->Form->postLink('Delete', ['action' => 'delete', $article->id]);
```

Identifiers are not restricted to integers, so UUIDs and other string keys work too.

When generating prefixed controllers with `--prefix Admin/Management`, routing
attributes will include both the plugin and prefix path components. You can customize
the `Scope` or `Prefix` attributes as required.

### Project Default and Command Overrides

Projects managing their routes in configuration can disable attribute generation
in their CLI bootstrap:

```php
use Cake\Core\Configure;

Configure::write('Bake.attributeRouting', false);
```

Override either project default for an individual invocation:

```bash
bin/cake bake controller Articles --attribute-routing
bin/cake bake all Articles --no-attribute-routing
bin/cake bake controller all --no-attribute-routing
bin/cake bake plugin Blog --attribute-routing
```

`--attribute-routing` and `--no-attribute-routing` cannot be combined. Disabling
attributes preserves the previous generated output, including the plugin
skeleton's fallback route. It does not change existing files' routing setup.

## Bake Models

Models are generically baked from existing database tables.
Expand Down
12 changes: 11 additions & 1 deletion src/Command/AllCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOption
'default' => false,
]);

return $parser;
return $this->addAttributeRoutingOptions($parser);
}

/**
Expand All @@ -71,6 +71,10 @@ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOption
*/
public function execute(): ?int
{
if (!$this->validateAttributeRoutingOptions()) {
return static::CODE_ERROR;
}

$this->extractCommonProperties($this->args);
$name = $this->args->getArgument('name') ?? '';
$name = $this->getNameWithoutPrefix($name);
Expand All @@ -97,6 +101,12 @@ public function execute(): ?int
$command = new $commandName($this->factory);

$options = $this->args->getOptions();
if ($command instanceof ControllerCommand) {
$options['attribute-routing'] = $this->useAttributeRouting();
$options['no-attribute-routing'] = !$this->useAttributeRouting();
} else {
unset($options['attribute-routing'], $options['no-attribute-routing']);
}
if (
$this->args->hasOption('prefix') &&
!($command instanceof ControllerCommand) &&
Expand Down
51 changes: 51 additions & 0 deletions src/Command/BakeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIoInterface;
use Cake\Console\ConsoleOptionParser;
use Cake\Core\Configure;
use Cake\Core\ConventionsTrait;
use Cake\Event\Event;
Expand Down Expand Up @@ -93,6 +94,56 @@ public function setArgs(Arguments $args): void
$this->args = $args;
}

/**
* Add the per-command overrides for the project routing default.
*
* @param \Cake\Console\ConsoleOptionParser $parser Option parser.
* @return \Cake\Console\ConsoleOptionParser
*/
protected function addAttributeRoutingOptions(ConsoleOptionParser $parser): ConsoleOptionParser
{
return $parser->addOption('attribute-routing', [
'boolean' => true,
'help' => 'Generate routing attributes, overriding Bake.attributeRouting.',
])->addOption('no-attribute-routing', [
'boolean' => true,
'help' => 'Generate without routing attributes, overriding Bake.attributeRouting.',
]);
}

/**
* Reject conflicting overrides before generating any files.
*
* @return bool
*/
protected function validateAttributeRoutingOptions(): bool
{
if ($this->args->getOption('attribute-routing') && $this->args->getOption('no-attribute-routing')) {
$this->io->error('The --attribute-routing and --no-attribute-routing options cannot be used together.');

return false;
}

return true;
}

/**
* Resolve explicit command options before the project default.
*
* @return bool
*/
protected function useAttributeRouting(): bool
{
if ($this->args->getOption('no-attribute-routing')) {
return false;
}
if ($this->args->getOption('attribute-routing')) {
return true;
}

return (bool)Configure::read('Bake.attributeRouting', true);
}

/**
* Handles splitting up the plugin prefix and classname.
*
Expand Down
4 changes: 4 additions & 0 deletions src/Command/ControllerAllCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ public static function defaultName(): string
*/
public function execute(): ?int
{
if (!$this->validateAttributeRoutingOptions()) {
return static::CODE_ERROR;
}

$this->extractCommonProperties($this->args);
/** @var \Cake\Database\Connection $connection */
$connection = ConnectionManager::get($this->connection);
Expand Down
53 changes: 52 additions & 1 deletion src/Command/ControllerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Cake\Core\Configure;
use Cake\Core\Plugin;
use Cake\Datasource\ConnectionManager;
use Cake\Utility\Inflector;

/**
* Task class for creating and updating controller files.
Expand All @@ -48,6 +49,10 @@ public static function getDescription(): string
*/
public function execute(): ?int
{
if (!$this->validateAttributeRoutingOptions()) {
return static::CODE_ERROR;
}

$this->extractCommonProperties($this->args);
$name = $this->args->getArgument('name') ?? '';
$name = $this->getNameWithoutPrefix($name);
Expand Down Expand Up @@ -184,6 +189,8 @@ public function bakeController(string $controllerName, array $data): void
'pluginPath' => null,
];

$data += $this->getAttributeRoutingData($controllerName, $data);

$contents = $this->createTemplateRenderer()
->set($data)
->generate('Bake.Controller/controller');
Expand All @@ -193,6 +200,50 @@ public function bakeController(string $controllerName, array $data): void
$this->io->createFile($filename, $contents, $this->force);
}

/**
* Prepare routing metadata while leaving the PHP source customizable in templates.
*
* @param string $controllerName Controller name.
* @param array<string, mixed> $data Controller template variables.
* @return array<string, mixed>
*/
protected function getAttributeRoutingData(string $controllerName, array $data): array
{
$enabled = $data['attributeRouting'] ?? $this->useAttributeRouting();
$actions = $enabled ? array_values(array_intersect(
$data['actions'] ?? [],
['index', 'view', 'add', 'edit', 'delete', 'login'],
)) : [];
$imports = [];
$scopePath = '/' . Inflector::dasherize($controllerName);
$prefixName = str_replace('\\', '/', ltrim($data['prefix'] ?? '', '\\'));
$prefixPath = null;
if ($actions !== []) {
$imports = ['Cake\\Routing\\Attribute\\Route', 'Cake\\Routing\\Attribute\\Scope'];
if ($data['plugin']) {
$pluginPath = '/' . Inflector::dasherize($data['plugin']);
if ($prefixName) {
$imports[] = 'Cake\\Routing\\Attribute\\Prefix';
$prefixPath = $pluginPath . '/' . implode('/', array_map(
Inflector::dasherize(...),
explode('/', $prefixName),
));
} else {
$scopePath = $pluginPath . $scopePath;
}
}
}

return [
'attributeRouting' => $enabled,
'routingActions' => $actions,
'routingImports' => $imports,
'routingScopePath' => $scopePath,
'routingPrefixName' => $prefixName,
'routingPrefixPath' => $prefixPath,
];
}

/**
* Assembles and writes a unit test file
*
Expand Down Expand Up @@ -280,6 +331,6 @@ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOption
'help' => 'Do not generate basic CRUD action methods.',
]);

return $parser;
return $this->addAttributeRoutingOptions($parser);
}
}
7 changes: 6 additions & 1 deletion src/Command/PluginCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ public static function getDescription(): string
*/
public function execute(): ?int
{
if (!$this->validateAttributeRoutingOptions()) {
return static::CODE_ERROR;
}

$name = $this->args->getArgument('name');
if (empty($name)) {
$this->io->error('You must provide a plugin name in CamelCase format.');
Expand Down Expand Up @@ -204,6 +208,7 @@ protected function generateFiles(

$renderer = $this->createTemplateRenderer()
->set([
'attributeRouting' => $this->useAttributeRouting(),
'name' => $name,
'package' => $package,
'namespace' => $namespace,
Expand Down Expand Up @@ -379,7 +384,7 @@ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOption
'help' => 'Generate only the plugin class.',
]);

return $parser;
return $this->addAttributeRoutingOptions($parser);
}

/**
Expand Down
8 changes: 7 additions & 1 deletion templates/bake/Controller/controller.twig
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
#}
{{ element('Bake.file_header', {
namespace: "#{namespace}\\Controller#{prefix}",
classImports: (plugin or prefix) ? ["#{baseNamespace}\\Controller\\AppController"] : [],
classImports: ((plugin or prefix) ? ["#{baseNamespace}\\Controller\\AppController"] : [])|merge(routingImports),
}) }}
{% set has_login = 'login' in actions and Bake.hasPlugin('Authentication') %}
{% set include_initialize = components or helpers or has_login %}
Expand All @@ -36,6 +36,12 @@
* @property {{ classInfo.fqn }} ${{ classInfo.name }}
{% endfor %}
*/
{% if routingActions %}
#[Scope('{{ routingScopePath }}')]
{% if routingPrefixPath %}
#[Prefix('{{ routingPrefixName }}', path: '{{ routingPrefixPath }}')]
{% endif %}
{% endif %}
class {{ name }}Controller extends AppController
{
{% if include_initialize %}
Expand Down
2 changes: 2 additions & 0 deletions templates/bake/Plugin/src/Plugin.php.twig
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ class {{ name }}Plugin extends BasePlugin
['path' => '/{{ routePath }}'],
function (RouteBuilder $builder) {
// Add custom routes here
{% if not attributeRouting %}

$builder->fallbacks();
{% endif %}
}
);
parent::routes($routes);
Expand Down
3 changes: 3 additions & 0 deletions templates/bake/element/Controller/add.twig
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
*
* @return \Cake\Http\Response|null|void Redirects on successful add, renders view otherwise.
*/
{% if attributeRouting %}
#[Route('/add')]
{% endif %}
public function add()
{
${{ singularName }} = $this->{{ currentModelName }}->newEmptyEntity();
Expand Down
3 changes: 3 additions & 0 deletions templates/bake/element/Controller/delete.twig
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
* @return \Cake\Http\Response|null Redirects to index.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
{% if attributeRouting %}
#[Route('/delete/{id}')]
{% endif %}
public function delete($id = null)
{
$this->request->allowMethod(['post', 'delete']);
Expand Down
3 changes: 3 additions & 0 deletions templates/bake/element/Controller/edit.twig
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
* @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
{% if attributeRouting %}
#[Route('/edit/{id}')]
{% endif %}
public function edit($id = null)
{
${{ singularName }} = $this->{{ currentModelName }}->get($id, contain: {{ Bake.exportArray(belongsToMany)|raw }});
Expand Down
4 changes: 4 additions & 0 deletions templates/bake/element/Controller/index.twig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
*
* @return \Cake\Http\Response|null|void Renders view
*/
{% if attributeRouting %}
#[Route('/')]
#[Route('/index')]
{% endif %}
public function index()
{
{% set belongsTo = Bake.aliasExtractor(modelObj, 'BelongsTo') %}
Expand Down
3 changes: 3 additions & 0 deletions templates/bake/element/Controller/login.twig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
*
* @return \Cake\Http\Response|null|void Redirects on successful login, renders view otherwise.
*/
{% if attributeRouting %}
#[Route('/login')]
{% endif %}
public function login()
{
{% if Bake.hasPlugin('Authorization') %}
Expand Down
Loading
Loading