Problem/Motivation

#3311365: Use PHP attributes for route discovery introduced #[Route] attributes on methods.

Routes that return pages generally need to specify a page title. This is currently done in the defaults._title property:

#[Route(
  path: '/admin/config/system/site-information',
  name: 'system.site_information_settings',
  requirements: [
    '_permission' => 'administer site configuration',
  ],
  defaults: ['_title' => new TranslatableMarkup('Basic site settings')],
)]

Steps to reproduce

Proposed resolution

Promote defaults._title to a top level title attribute by extending the Symfony Route attribute class:

#[Route(
  path: '/admin/config/system/site-information',
  name: 'system.site_information_settings',
  title: new TranslatableMarkup('Basic site settings'),
  requirements: [
    '_permission' => 'administer site configuration',
  ],
)]

In turn this also lets us use closures in place of separate _title_callback methods:

#[Route(
  path: '/block/add/{block_content_type}',
  name: 'block_content.add_form',
  title: static function (BlockContentTypeInterface $block_content_type) {
    return new TranslatableMarkup('Add %type content block', ['%type' => $block_content_type->label()]);
  },
  requirements: ['_entity_create_access' => 'block_content:{block_content_type}'],
  options: ['_admin_route' => TRUE],
)]

Remaining tasks

User interface changes

Introduced terminology

API changes

A new Drupal-specific #[Route] attribute can be used, extending the Symfony one.

Data model changes

Release notes snippet

Issue fork drupal-3607968

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

longwave created an issue. See original summary.

longwave’s picture

Issue summary: View changes

longwave’s picture

Status: Active » Needs review

First pass at this, added the new attribute subclass and promoted the title property in system.module routes.

This MR was assisted by Claude Code.

longwave’s picture

Issue tags: +Needs change record
godotislate’s picture

Are there any other properties we might want to promote to top level? It'd be better to get them in now.
Like _title_callback?

longwave’s picture

Not sure. _title_callback has accompanying properties that aren't as widely used - _title_arguments and _title_context - so we would likely need to do those as well, and then that feels like a slippery slope. This was intended just as DX convenience for the 90% use case where you are routing a controller or form that has a static title.

There might be some more common ones that we should promote? Maybe worth doing an analysis of *.routing.yml?

godotislate’s picture

Agreed on _title_callback.

One thought, if _csrf_token is going to be required per #3508087: Make it harder to have routes vulnerable to CSRF, that might be a good candidate. OTOH, my own preference would be not making it required, though I don't have any alternative suggestions.

longwave’s picture

In core there are 414 defaults._title definitions. The only thing there is more of is requirements._access, which has 509. There are also 179 requirements._permission. After that there's a long tail, none more than 100. I think if we convert any more it should only be these three at most.

Alternatively, what if we used a separate attribute here, instead of extending Symfony? We could even add other convenience attributes:

#[Route(
  path: '/admin/config/system/site-information',
  name: 'system.site_information_settings',
)]
#[PageTitle(new TranslatableMarkup('Basic site settings'))]
#[Permission('administer site configuration')]

Not sure if this is really better DX or not though.

longwave’s picture

FWIW Symfony does handle access control with a separate attribute, because it's in a separate package: https://symfony.com/doc/current/security.html

class PostController extends AbstractController
{
    #[Route('/posts/{id}/edit', name: 'post_edit')]
    // 'post' refers to the $post parameter of the controller method
    #[IsGranted('edit', 'post')]
    public function edit(Post $post): Response
    {
        // ...
    }
}
longwave’s picture

#[PageTitle] could be extended to handle static and dynamic titles from a single attribute:

#[PageTitle(new TranslatableMarkup('Basic site settings'))]
#[PageTitle(callback: 'self::title')]
longwave’s picture

Also I was wrong in #7 - title_arguments and title_context are actually for adding args/context to title when it is automatically translated, and we sidestepped that by using new TranslatableMarkup() directly (although you can't pass dynamic args now...)

longwave’s picture

And because this is no longer stored with the routing data we might even be able to use closures to inline dynamic title code:

#[PageTitle(static fn(NodeInterface $node) => new TranslatableMarkup('Edit @label', ['@label' => $node->label()]))]

Having said that for dynamic titles you can also just return #title from the controller...

godotislate’s picture

Alternatively, what if we used a separate attribute here, instead of extending Symfony? We could even add other convenience attributes:

There are advantages to this approach, and I don't think I mind it. But a similar issue for entity type definitions has had a mixed response: #3488054: Make it possible to define entity types with multiple smaller attributes.

longwave’s picture

Issue summary: View changes

Updated IS with the two options - a single extended #[Route] attribute or a separate #[PageTitle] attribute.

mstrelan’s picture

I think a seperate attribute for PageTitle is awkward. Another option, that might also be awkward, is a title param that takes a PageTitleInterface, which could either be a simple PageTitle, which is essentially just a TranslatableMarkup, or a PageTitleCallback. I haven't really thought this through, but it might be a way to cover several different options without needing to worry about all the permutations at once.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new98 bytes

The Needs Review Queue Bot tested this issue. The merge request has merge conflicts and cannot be merged. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

longwave’s picture

Status: Needs work » Needs review

Added support for title closures to the #[Route] attribute and tested it by converting block_content.routing.yml with a _title_callback to use an inline closure instead.

This is awkward when there is more than one Route attribute on a method, because I'm not sure we know which attribute to look up again, but maybe that's an edge case we don't need to handle.

godotislate’s picture

This is awkward when there is more than one Route attribute on a method

This brings up a problem if we introduce a #[PageTitle] separate attribute, because if there are more than one Route attributes on a method, which does it target?

I think it makes sense to go with the Drupal Route attribute subclass, because the use of _title_callback is more of an edge case.

longwave’s picture

I guess this is the question here: is the page title tied to the page (so there should only be one), or the route?

mstrelan’s picture

When you consider that a route can return a json response or a redirect response it's clear the title doesn't belong to the route. That said, the route should be able to suggest a default title, which is exactly what we already have. Ideally we should return something like a HtmlResponse object that has title as a property so we don't have to rely on this being manually added to a render array.

longwave’s picture

We can support multiple attributes - instead of [class, method] we can store [class, method, index] so we know which attribute has the closure. Added this, and some test coverage.

This now has the same capabilities as the existing YAML route declarations, just with additional syntactic sugar for both static and dynamic titles.

longwave’s picture

If people are happy with the current direction I'll update the IS and write a CR.

godotislate’s picture

I guess this is the question here: is the page title tied to the page (so there should only be one), or the route?

Don't know about "should", but for example in SystemController, systemAdminMenuBlockPage has several different titles.

longwave’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Yeah, so that rules out #[PageTitle] being a separate attribute.

longwave’s picture

Issue summary: View changes
godotislate’s picture

Status: Needs review » Needs work

This is really nice work! I like the closure handling.

A few small comments on the MR, but we're just about there.

I did double check that the skip file logic works for the new attribute, and it does:

                // Skip files that do not contain a Route attribute.
                $contents = file_get_contents($fileinfo->getPathname());
                if (!str_contains($contents, '#[Route') && !str_contains($contents, 'Routing\\Attribute\\Route')) {
                  continue;
                }
longwave’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record

Thanks for the review. I accepted the suggestions and wrote a CR: https://www.drupal.org/node/3614321

godotislate’s picture

Status: Needs review » Reviewed & tested by the community

I think the MR and CR look good now.

  • catch committed 9db8dc8e on 11.x
    task: #3607968 Promote defaults._title to top level in route attributes...

  • catch committed c51fe139 on main
    task: #3607968 Promote defaults._title to top level in route attributes...
catch’s picture

Version: main » 11.x-dev
Status: Reviewed & tested by the community » Fixed

Yes this looks great. The closure handling is very nice, and I couldn't find anything to complain about.

This is soft-blocking a lot of route conversions from YAML, so let's get it into main and 11.5.x now, doubt we'll find problems to iron out but gives time for that too.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

mcdruid’s picture

Looks like the cherry-pick to 11.x has caused some test problems:

https://www.drupal.org/project/drupal/issues/3614402

mcdruid’s picture

longwave’s picture

Status: Fixed » Needs work

As per the above issue, closures in attribute arguments are only allowed in PHP 8.5 and above: https://wiki.php.net/rfc/closures_in_const_expr

We need to revert this from 11.x, not sure whether to just keep it in 12 or disallow closures in 11 or something else.

  • catch committed 5883f3cf on 11.x
    Revert "task: #3607968 Promote defaults._title to top level in route...
catch’s picture

Reverted from 11.x

godotislate’s picture

Why don't we backport the closure functionality as it is, but remove the test cases. Then document in code and the CR that setting the attribute property value to a closure is only supported for PHP 8.5+?

longwave’s picture

We do have to revert the block_content conversion from 11.x. This also blocks converting the remaining routing.yml in a similar way, but I guess we can just continue that in main only.

We could even keep the test cases and skip on PHP 8.4 and earlier I guess.

quietone’s picture

The change record branch/version is 11.5.x/11.5.0 so that may need to be changed.

longwave-bot made their first commit to this issue’s fork.

longwave’s picture

Status: Needs work » Needs review

11.x backport in MR!16652, keeps the closure functionality but limits the tests to run on PHP 8.5+ only. block_content.routing.yml is still converted but uses _title_callback as before.

longwave’s picture

Updated the CR to mention closures are only available in PHP 8.5.

godotislate’s picture

11.x MR looks good, just one question about a title callback method we're planning to deprecate.

godotislate’s picture

Status: Needs review » Reviewed & tested by the community

lgtm!

  • catch committed 66f44a0b on 11.x
    task: #3607968 Promote defaults._title to top level in route attributes...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 11.x, thanks!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.