Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[DependencyInjection] Add #[AutowireMethodOf] attribute to autowire a method of a service as a callable #54016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Feb 20, 2024

Q A
Branch? 7.1
Bug fix? no
New feature? yes
Deprecations? no
Issues -
License MIT

Let's take a controller for example, with one action that has this argument:

        #[AutowireMethodOf(CommentRepository::class)]
        \Closure $getCommentPaginator,

The proposed attribute tells Symfony to create a closure that will call CommentRepository::getCommentPaginator(). The name of the method to be called is inferred from the name of the parameter.

This is already doable with this syntax, so that the proposed attribute is just a shortcut for this:

        #[AutowireCallable(service: CommentRepository::class, method: 'getCommentPaginator')]
        \Closure $getCommentPaginator,

Using this style allows turning e.g. entity repositories into query functions, which are way more flexible. But because the existing syntax is quite verbose, i looked for a more concise alternative, so here we are with this proposal.

Benefits:

  • Increased Flexibility: Allows developers to inject specific methods as Closures, providing greater control over what functionality is injected into
  • Improved Readability: By using the attribute, the intention behind the dependency injection is made explicit, improving code clarity.
  • Enhanced Modularity: Facilitates a more modular architecture by decoupling services from direct dependencies on specific class methods, making the codebase more maintainable and testable.

Because we leverage the existing code infrastructure for AutowireCallable, if I declare this interface:

interface GetCommentPaginatorInterface
{
    public function __invoke(Conference $conference, int $page): Paginator;
}

then I can also do native types (vs a closure) without doing anything else:

        #[AutowireMethodOf(CommentRepository::class)]
        GetCommentPaginatorInterface $getCommentPaginator,

@GromNaN
Copy link
Member

GromNaN commented Feb 20, 2024

What is the reason to create a new attribute instead of making method optional?

@nicolas-grekas nicolas-grekas changed the title [DependnecyInjection] Add #[AutowireMethodOf] attribute to autowire a method of a service as a callable [DependencyInjection] Add #[AutowireMethodOf] attribute to autowire a method of a service as a callable Feb 20, 2024
@nicolas-grekas nicolas-grekas force-pushed the di-autowire-method-as-callable branch from 963b4d6 to 8b85b8f Compare February 20, 2024 20:09
@nicolas-grekas
Copy link
Member Author

What is the reason to create a new attribute instead of making method optional?

Because method is already optional and it currently means __invoke, so that would be confusing.
A new attribute makes it explicit that some convention is used also IMHO (inferring the name of the method from the name of the parameter)

@alexandre-daubois
Copy link
Member

alexandre-daubois commented Feb 20, 2024

Really subjective point of view here, I definitely see an improvement of "instant understanding of what's being done". I think having this "alias attribute" helps to get what's happening at first sight, where the existing notation needs a bit of reflection before getting it.

It feels more human friendly to me

@Kocal
Copy link
Member

Kocal commented Feb 20, 2024

What are the benefits about AutowireMethodOf and AutowireCallable, versus directly injecting CommentRepository?

@OskarStark
Copy link
Contributor

Only one con IMHO, if you refactor the method name via IDE this code breaks, I know it’s the same problem for the long attribute version. Not sure I would use it, because if you have no tests and even with static analysis this bug can go to prod. Sounds like a footgun but indeed nice DX

@OskarStark
Copy link
Contributor

@Kocal my repositories are final so I cannot mock them, so I would need an interface. This way you don’t need an interface, can have a final class and you can easily test it.

@nicolas-grekas
Copy link
Member Author

versus directly injecting CommentRepository

think about testing a service that has CommentRepository as a dependency: you cannot test it without having a database or without heavy mocking. Query functions are just trivial to replace instead.

@Kocal
Copy link
Member

Kocal commented Feb 20, 2024

Hum alright, I can understand the issue, but then doesn't it make sense to create by yourself a invokable class that you can easily mock?

IINW you lose the autocompletion/static analysis by using AutowireMethodOf/AutowireCallable no?

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Feb 21, 2024

doesn't it make sense to create by yourself a invokable class that you can easily mock

It's like invokable controllers vs action methods: everyone their preferences 🤷 Mine is this attribute because of the low verbosity: just write a repository, and still get decoupling without creating a myriad of class boilerplate.

you lose the autocompletion/static analysis by using AutowireMethodOf/AutowireCallable no?

@param Closure(Conference, int): Paginator $getCommentPaginator to the rescue in my example (or inline @var Paginator $result)). The attribute doesn't introduce this concern also since the current alternative works the same.

Only one con IMHO, if you refactor the method name via IDE this code breaks

true with the current tooling, but this is fixable since the tools can very well be taught about the attribute. On this topic also the current situation is similar and the proposed attribute doesn't bring anything new.

@Kocal
Copy link
Member

Kocal commented Feb 21, 2024

Alright thanks for the explanations :)

@smnandre
Copy link
Member

You mentionned "query" but -naive question- would this work with setters / persisters too ?

Could this work ?

public function __invoke(
    #[AutowireMethodOf(CommentRepository::class)] \Closure $saveComment,
    #[MapRequestPayload] Comment $comment,
) {
    $saveComment($comment);
}

@nicolas-grekas
Copy link
Member Author

@smnandre yes of course

@nicolas-grekas nicolas-grekas force-pushed the di-autowire-method-as-callable branch 2 times, most recently from 840e2f4 to 139cc14 Compare February 22, 2024 12:57
@nicolas-grekas
Copy link
Member Author

PR updated with better error handling and a test case.

@nicolas-grekas nicolas-grekas force-pushed the di-autowire-method-as-callable branch from 0dface6 to dbd45e3 Compare February 23, 2024 14:55
@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Mar 15, 2024

FTR, if I define this interface:

interface GetCommentPaginatorInterface
{
    public function __invoke(Conference $conference, int $page): Paginator;
}

Then I can also do native types (vs @param + closure) without doing anything else:

        #[AutowireMethodOf(CommentRepository::class)]
        GetCommentPaginatorInterface $getCommentPaginator,

@nicolas-grekas nicolas-grekas force-pushed the di-autowire-method-as-callable branch from dbd45e3 to df11660 Compare March 15, 2024 13:05
@nicolas-grekas nicolas-grekas merged commit 10bc796 into symfony:7.1 Mar 15, 2024
@nicolas-grekas nicolas-grekas deleted the di-autowire-method-as-callable branch March 15, 2024 13:15
@fabpot fabpot mentioned this pull request May 2, 2024
nicolas-grekas added a commit that referenced this pull request Jun 27, 2025
…from `AbstractController` as a standalone service (nicolas-grekas)

This PR was merged into the 7.4 branch.

Discussion
----------

[FrameworkBundle] Add `ControllerHelper`; the helpers from `AbstractController` as a standalone service

| Q             | A
| ------------- | ---
| Branch?       | 7.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | -
| License       | MIT

This PR is a follow up of #16863 by `@derrabus` almost 10 years ago 😱, which was seeking for a solution to reuse helpers provided by `AbstractController` that'd be free from coupling by inheritance and that'd allow for more granular cherry-picking of the desired helpers.

At the time, granular traits were proposed as the reusability unit.

In this PR, I'm proposing to add a `ControllerHelper` class that'd allow achieving both goals using functions as the reusability unit instead.

To achieve true decoupling and granular helper injection, one would have to use the `#[AutowireMethodOf]` attribute (see #54016).

Here is the chain of thoughts and concepts that underpin the proposal. It should be noted that this reasoning should be read as an example that could be extended to any helper-like class, e.g it fits perfectly for cherry-picking query functions from entity repositories.

So, here is the chain for controllers:

 1. The Problem: The Monolithic Base Class

    Symfony's `AbstractController` offers a convenient set of helper methods for common controller tasks. However, by relying on inheritance, our controllers become tightly coupled to the framework. This can make them more difficult to test in isolation and provides them with a broad set of methods, even when only a few are needed.

 2. The Initial Goal: Reusability without Inheritance

    The long-standing goal has been to decouple controllers from this base class while retaining easy access to its valuable helper methods. The ideal solution would allow for a more granular, "à la carte" selection of these helpers.

 3. The Path Not Taken: Granular Traits

    The original proposal in PR #16863 explored the use of granular traits (e.g., `RenderTrait`, `RedirectTrait`). This was a step towards greater modularity, allowing a developer to use only the necessary functionalities. However, a trait-based approach has its own set of challenges:
      - Implicit Dependencies: The services required by the traits (like the templating engine or the router) are not explicitly declared as dependencies of the controller.
      - A Different Form of Coupling: While avoiding vertical inheritance, traits introduce a form of horizontal coupling.

 4. A More Modern Approach: The Injectable Helper Service

    This PR introduces a `ControllerHelper` service. This class extends `AbstractController` to leverage its existing, battle-tested logic but exposes all of its protected methods as public ones. This aligns with modern dependency injection principles, where services are explicitly injected rather than inherited. A controller can now inject the `ControllerHelper` and access the helper methods through it.

 5. The Final Step: True Granularity with `#[AutowireMethodOf]`

    While injecting the entire `ControllerHelper` is a significant improvement, it still provides the controller with access to all helper methods. The introduction of the `#[AutowireMethodOf]` attribute (see #54016) is the final piece of the puzzle, enabling the ultimate goal of using individual functions as the unit of reuse.

    With `#[AutowireMethodOf]`, we can inject just the specific helper method we need as a callable:

    ```php
    class MyController
    {
        public function __construct(
            #[AutowireMethodOf(ControllerHelper::class)]
            private \Closure $render,
            #[AutowireMethodOf(ControllerHelper::class)]
            private \Closure $redirectToRoute,
        ) {
        }

        public function showProduct(int $id): Response
        {
            if (!$id) {
                return ($this->redirectToRoute)('product_list');
            }

            return ($this->render)('product/show.html.twig', ['product_id' => $id]);
        }
    }
    ```

    This solution provides numerous benefits:
      - Maximum Decoupling: The controller has no direct dependency on `AbstractController` or even the `ControllerHelper` class in its methods. It only depends on the injected callables.
      - Explicit and Granular Dependencies: The controller's constructor clearly and precisely declares the exact functions it needs to operate.
      - Improved Testability (less relevant for controllers but quite nice for dependents of e.g. entity repositories): Mocking the injected callables in unit tests is straightforward and clean.

 6. Bonus: Auto-generated Adapters for Functional Interfaces

    For even better type-safety and application-level contracts, `#[AutowireMethodOf]` can generate adapters for functional interfaces. One can define an interface within their application's domain to achieve better type-coverage without any new coupling:

    ```php
    interface RenderInterface
    {
        public function __invoke(string $view, array $parameters = [], ?Response $response = null): Response;
    }
    ```

    Then update the previous controller example to use this interface:
    ```diff
             #[AutowireMethodOf(ControllerHelper::class)]
    -        private \Closure $render,
    +        private RenderInterface $render,
    ````

    Symfony's DI container will automatically create an adapter that implements `RenderInterface` and whose `__invoke` method calls the `render` method of the `ControllerHelper`. This gives full static analysis and autocompletion benefits with zero extra boilerplate code.

This pull request, therefore, not only provides a solution to a long-standing desire for more reusable controller logic but does so in a way that is modern, flexible, and fully embraces the power of Symfony's dependency injection container (while still preserving really good usability when the DIC is not used, as is the case when unit testing.)

Commits
-------

c2020bb [FrameworkBundle] Add `ControllerHelper`; the helpers from AbstractController as a standalone service
symfony-splitter pushed a commit to symfony/framework-bundle that referenced this pull request Jun 27, 2025
…from `AbstractController` as a standalone service (nicolas-grekas)

This PR was merged into the 7.4 branch.

Discussion
----------

[FrameworkBundle] Add `ControllerHelper`; the helpers from `AbstractController` as a standalone service

| Q             | A
| ------------- | ---
| Branch?       | 7.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | -
| License       | MIT

This PR is a follow up of symfony/symfony#16863 by `@derrabus` almost 10 years ago 😱, which was seeking for a solution to reuse helpers provided by `AbstractController` that'd be free from coupling by inheritance and that'd allow for more granular cherry-picking of the desired helpers.

At the time, granular traits were proposed as the reusability unit.

In this PR, I'm proposing to add a `ControllerHelper` class that'd allow achieving both goals using functions as the reusability unit instead.

To achieve true decoupling and granular helper injection, one would have to use the `#[AutowireMethodOf]` attribute (see symfony/symfony#54016).

Here is the chain of thoughts and concepts that underpin the proposal. It should be noted that this reasoning should be read as an example that could be extended to any helper-like class, e.g it fits perfectly for cherry-picking query functions from entity repositories.

So, here is the chain for controllers:

 1. The Problem: The Monolithic Base Class

    Symfony's `AbstractController` offers a convenient set of helper methods for common controller tasks. However, by relying on inheritance, our controllers become tightly coupled to the framework. This can make them more difficult to test in isolation and provides them with a broad set of methods, even when only a few are needed.

 2. The Initial Goal: Reusability without Inheritance

    The long-standing goal has been to decouple controllers from this base class while retaining easy access to its valuable helper methods. The ideal solution would allow for a more granular, "à la carte" selection of these helpers.

 3. The Path Not Taken: Granular Traits

    The original proposal in PR #16863 explored the use of granular traits (e.g., `RenderTrait`, `RedirectTrait`). This was a step towards greater modularity, allowing a developer to use only the necessary functionalities. However, a trait-based approach has its own set of challenges:
      - Implicit Dependencies: The services required by the traits (like the templating engine or the router) are not explicitly declared as dependencies of the controller.
      - A Different Form of Coupling: While avoiding vertical inheritance, traits introduce a form of horizontal coupling.

 4. A More Modern Approach: The Injectable Helper Service

    This PR introduces a `ControllerHelper` service. This class extends `AbstractController` to leverage its existing, battle-tested logic but exposes all of its protected methods as public ones. This aligns with modern dependency injection principles, where services are explicitly injected rather than inherited. A controller can now inject the `ControllerHelper` and access the helper methods through it.

 5. The Final Step: True Granularity with `#[AutowireMethodOf]`

    While injecting the entire `ControllerHelper` is a significant improvement, it still provides the controller with access to all helper methods. The introduction of the `#[AutowireMethodOf]` attribute (see #54016) is the final piece of the puzzle, enabling the ultimate goal of using individual functions as the unit of reuse.

    With `#[AutowireMethodOf]`, we can inject just the specific helper method we need as a callable:

    ```php
    class MyController
    {
        public function __construct(
            #[AutowireMethodOf(ControllerHelper::class)]
            private \Closure $render,
            #[AutowireMethodOf(ControllerHelper::class)]
            private \Closure $redirectToRoute,
        ) {
        }

        public function showProduct(int $id): Response
        {
            if (!$id) {
                return ($this->redirectToRoute)('product_list');
            }

            return ($this->render)('product/show.html.twig', ['product_id' => $id]);
        }
    }
    ```

    This solution provides numerous benefits:
      - Maximum Decoupling: The controller has no direct dependency on `AbstractController` or even the `ControllerHelper` class in its methods. It only depends on the injected callables.
      - Explicit and Granular Dependencies: The controller's constructor clearly and precisely declares the exact functions it needs to operate.
      - Improved Testability (less relevant for controllers but quite nice for dependents of e.g. entity repositories): Mocking the injected callables in unit tests is straightforward and clean.

 6. Bonus: Auto-generated Adapters for Functional Interfaces

    For even better type-safety and application-level contracts, `#[AutowireMethodOf]` can generate adapters for functional interfaces. One can define an interface within their application's domain to achieve better type-coverage without any new coupling:

    ```php
    interface RenderInterface
    {
        public function __invoke(string $view, array $parameters = [], ?Response $response = null): Response;
    }
    ```

    Then update the previous controller example to use this interface:
    ```diff
             #[AutowireMethodOf(ControllerHelper::class)]
    -        private \Closure $render,
    +        private RenderInterface $render,
    ````

    Symfony's DI container will automatically create an adapter that implements `RenderInterface` and whose `__invoke` method calls the `render` method of the `ControllerHelper`. This gives full static analysis and autocompletion benefits with zero extra boilerplate code.

This pull request, therefore, not only provides a solution to a long-standing desire for more reusable controller logic but does so in a way that is modern, flexible, and fully embraces the power of Symfony's dependency injection container (while still preserving really good usability when the DIC is not used, as is the case when unit testing.)

Commits
-------

c2020bb3a6c [FrameworkBundle] Add `ControllerHelper`; the helpers from AbstractController as a standalone service
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants