Skip to content

Commit e022523

Browse files
committed
created article - attributes vs factories
1 parent d7a9cf1 commit e022523

4 files changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
---
2+
title: "Attributes vs. factories"
3+
description: "Dotkernel's dot-dependency-injection package lets you wire a service with an #[Inject] attribute or a hand-written factory. This article compares both approaches with real dotkernel/api code and explains when each one fits."
4+
author: "Florin Bidirean"
5+
date_published: "2026-10-01"
6+
canonical_url: "https://www.dotkernel.com/best-practice/attributes-vs-factories/"
7+
category: "Best Practice"
8+
language: "en"
9+
---
10+
11+
# Attributes vs. factories
12+
13+
## TL;DR
14+
15+
Attributes remove one factory file per service and keep dependencies next to the constructor they feed, while hand-written factories keep static type checking and full control over construction. Use #[Inject] for plain constructor wiring, keep factories for computed, conditional, decorated or third-party classes, and mix both freely in the same PSR-11 application.
16+
17+
## Introduction
18+
19+
Dependency injection (DI) is simple to define: a class receives the objects it needs instead of building them itself.
20+
As Fabien Potencier puts it in [What is Dependency Injection?](https://fabien.potencier.org/what-is-dependency-injection.html), components are given their dependencies through their constructors, methods or fields.
21+
The payoff is flexibility, because you can swap an implementation without touching the class that uses it, testability, because you can pass a mock, and separation of concerns, because a class does its own job and leaves object creation to someone else.
22+
For required dependencies, the constructor is the natural place to receive them.
23+
24+
Someone still has to build those objects, and in a [PSR-11](https://www.php-fig.org/psr/psr-11/) application that someone is the container.
25+
PSR-11 defines only two methods on `ContainerInterface`: `get()` and `has()`.
26+
It also gives a clear warning: "Users SHOULD NOT pass a container into an object so that the object can retrieve its own dependencies."
27+
That is the service locator anti-pattern, and it is why the container belongs in a factory, not inside your service.
28+
29+
The classic answer is one factory class per service.
30+
It works, but every new constructor argument means editing two files, and the factory can quietly drift out of sync with the class it builds.
31+
Dotkernel's [dot-dependency-injection](https://github.com/dotkernel/dot-dependency-injection) package offers a second answer: declare the dependencies with an `#[Inject]` attribute on the constructor and register one shared factory, `AttributedServiceFactory`, for every service.
32+
We introduced the package in [Dependency Injection made easy in Laminas/Mezzio applications](https://www.dotkernel.com/dotkernel/dependency-injection-made-easy-in-laminas-mezzio-applications/), and the [package documentation](https://docs.dotkernel.org/dot-dependency-injection/v1/attributes-vs-factories/) covers the trade-offs.
33+
34+
This article puts the two side by side and helps you choose.
35+
The real-world code comes from the [Dotkernel API](https://github.com/dotkernel/api) repository.
36+
37+
## Comparison table
38+
39+
| Aspect | Hand-written factory | `#[Inject]` attribute |
40+
| --- | --- | --- |
41+
| Files per service | 2: the class and its factory | 1: the class |
42+
| Type checking | Statically verified | None, dependencies are resolved from strings at runtime |
43+
| Argument order | Written by you in the `new` call | Must match the constructor manually |
44+
| Reflection | None when the service is built | One `ReflectionClass` per service creation |
45+
| Conditional wiring | Fully supported | Not supported |
46+
| Third-party classes | Works on any class | Only classes you can annotate |
47+
| Config access | `$container->get('config')['user']` | `'config.user'` |
48+
| Error handling | Whatever you write | Uniform `Dot\DependencyInjection\Exception\ExceptionInterface` |
49+
| Doctrine repositories | A factory for each repository | `AttributedRepositoryFactory` |
50+
51+
Rule of thumb: if the constructor only needs things the container already holds, use the attribute. If construction needs logic, use a factory.
52+
53+
## Code examples from dotkernel/api
54+
55+
### A service wired with an attribute
56+
57+
`UserAvatarService` needs a Doctrine repository and the application config.
58+
In dotkernel/api, the class declares both on the constructor:
59+
60+
```php
61+
use Core\User\Repository\UserAvatarRepository;
62+
use Dot\DependencyInjection\Attribute\Inject;
63+
64+
class UserAvatarService implements UserAvatarServiceInterface
65+
{
66+
/**
67+
* @param array<non-empty-string, mixed> $config
68+
*/
69+
#[Inject(
70+
UserAvatarRepository::class,
71+
'config',
72+
)]
73+
public function __construct(
74+
protected UserAvatarRepository $userAvatarRepository,
75+
protected array $config,
76+
) {
77+
}
78+
}
79+
```
80+
81+
The only wiring left is one line in the module's `ConfigProvider`:
82+
83+
```php
84+
use Dot\DependencyInjection\Factory\AttributedServiceFactory;
85+
86+
'factories' => [
87+
UserAvatarService::class => AttributedServiceFactory::class,
88+
],
89+
```
90+
91+
The same `AttributedServiceFactory` entry is repeated for every handler, service and middleware in the module.
92+
The attribute lists the container identifiers in constructor order, which is the order you must keep in sync by hand.
93+
94+
### The same service with a hand-written factory
95+
96+
The factory below is illustrative: dotkernel/api does not ship it, but it is what the same wiring looks like without the attribute.
97+
98+
```php
99+
use Core\User\Repository\UserAvatarRepository;
100+
use Psr\Container\ContainerInterface;
101+
102+
class UserAvatarServiceFactory
103+
{
104+
public function __invoke(ContainerInterface $container): UserAvatarService
105+
{
106+
return new UserAvatarService(
107+
$container->get(UserAvatarRepository::class),
108+
$container->get('config'),
109+
);
110+
}
111+
}
112+
```
113+
114+
Its `ConfigProvider` entry points to the new class, not to the shared one:
115+
116+
```php
117+
'factories' => [
118+
UserAvatarService::class => UserAvatarServiceFactory::class,
119+
],
120+
```
121+
122+
That is two files instead of one, and a second place to edit whenever the constructor changes.
123+
In return, the `new UserAvatarService(...)` call is checked statically: a wrong argument type or order is caught by your IDE and static analysis before the code runs.
124+
With the attribute, the same mistake shows up when the service is built.
125+
126+
### Doctrine repositories
127+
128+
Repositories are where attributes save the most repetition.
129+
In dotkernel/api, the repository declares which entity it serves:
130+
131+
```php
132+
use Core\App\Repository\AbstractRepository;
133+
use Core\User\Entity\UserAvatar;
134+
use Dot\DependencyInjection\Attribute\Entity;
135+
136+
#[Entity(name: UserAvatar::class)]
137+
class UserAvatarRepository extends AbstractRepository
138+
{
139+
}
140+
```
141+
142+
The module `ConfigProvider` registers it with the dedicated factory:
143+
144+
```php
145+
use Dot\DependencyInjection\Factory\AttributedRepositoryFactory;
146+
147+
'factories' => [
148+
UserAvatarRepository::class => AttributedRepositoryFactory::class,
149+
UserDetailRepository::class => AttributedRepositoryFactory::class,
150+
UserRepository::class => AttributedRepositoryFactory::class,
151+
],
152+
```
153+
154+
Without it, each repository needs its own factory that fetches the entity manager and asks it for the right repository, the same boilerplate repeated per entity.
155+
156+
### Where a factory still wins
157+
158+
Some objects cannot be described by a list of container identifiers.
159+
`ErrorResponseGeneratorFactory` in dotkernel/api reads a config value, with a fallback when the container has no config at all:
160+
161+
```php
162+
class ErrorResponseGeneratorFactory
163+
{
164+
public function __invoke(ContainerInterface $container): ErrorResponseGenerator
165+
{
166+
$config = $container->has('config') ? $container->get('config') : [];
167+
assert(is_array($config));
168+
169+
return new ErrorResponseGenerator($config['debug'] ?? false);
170+
}
171+
}
172+
```
173+
174+
Three things make this a factory job.
175+
`ErrorResponseGenerator` belongs to Mezzio, so you cannot annotate it.
176+
The `has()` check is conditional logic.
177+
The constructor receives a computed scalar, `$config['debug'] ?? false`, not something the container already holds.
178+
179+
`EntityListenerResolverFactory` is another case: it builds an `EntityListenerResolver` that receives the container itself.
180+
181+
```php
182+
class EntityListenerResolverFactory
183+
{
184+
public function __invoke(ContainerInterface $container): EntityListenerResolver
185+
{
186+
return new EntityListenerResolver($container);
187+
}
188+
}
189+
```
190+
191+
An attribute cannot express "give me the container", and this resolver exists to look up Doctrine entity listeners on demand.
192+
That is a deliberate exception.
193+
It does not make the container a general-purpose dependency for your services.
194+
195+
### Using both in one project
196+
197+
The two approaches share the same `ConfigProvider`.
198+
dotkernel/api's `App` module, for example, wires its services with the attribute and still decorates selected services with delegators:
199+
200+
```php
201+
'delegators' => [
202+
GetIndexResourceHandler::class => [HandlerDelegatorFactory::class],
203+
ProblemDetailsMiddleware::class => [ProblemDetailsDelegatorFactory::class],
204+
],
205+
'factories' => [
206+
GetIndexResourceHandler::class => AttributedServiceFactory::class,
207+
ErrorReportService::class => AttributedServiceFactory::class,
208+
],
209+
```
210+
211+
A service can be built by `AttributedServiceFactory` and still be wrapped by a delegator.
212+
Which factory applies to which service is decided entirely by the configuration.
213+
214+
### The anti-pattern to avoid
215+
216+
Both approaches keep the container out of your classes.
217+
This is what PSR-11 warns against:
218+
219+
```php
220+
class UserAvatarService
221+
{
222+
public function __construct(
223+
protected ContainerInterface $container,
224+
) {
225+
}
226+
227+
public function deleteAvatar(User $user): void
228+
{
229+
$repository = $this->container->get(UserAvatarRepository::class);
230+
// ...
231+
}
232+
}
233+
```
234+
235+
The dependencies are now hidden: the constructor signature no longer says what the class needs, and a missing service fails in the middle of a request instead of at construction.
236+
Tests also have to build a container just to run one method.
237+
Declare `UserAvatarRepository` in the constructor instead, using either approach from this article.
238+
239+
## Conclusion
240+
241+
Attributes and factories are two ways of delivering the same constructor injection.
242+
Default to `#[Inject]` when a class only needs services and config the container already holds: it means one file per service, dependencies visible next to the constructor, and uniform error messages.
243+
Reach for a factory when construction involves logic, when a class is not yours to annotate, or when you need a delegator or a decorated service.
244+
You do not have to choose once for the whole project, since the `ConfigProvider` decides per service.
245+
246+
To go further, read the [dot-dependency-injection documentation](https://docs.dotkernel.org/dot-dependency-injection/v1/attributes-vs-factories/) and browse the [Dotkernel API](https://github.com/dotkernel/api) source for both styles in a working application.
247+
248+
## FAQ
249+
250+
**Q: Can I mix attributes and factories in the same project?**
251+
A: Yes. The `ConfigProvider` decides which factory builds each service, so some services can use `AttributedServiceFactory` while others use a hand-written factory. dotkernel/api does exactly that.
252+
253+
**Q: Do attributes slow the application down?**
254+
A: There is a small cost: the attribute approach uses one `ReflectionClass` per service creation, while a hand-written factory has no reflection when the service is built. The documentation does not publish benchmark figures, so measure your own application if it matters.
255+
256+
**Q: Why does `#[Inject]` lose type checking?**
257+
A: The attribute lists container identifiers as strings, such as `UserAvatarRepository::class` or `'config'`, and they are resolved at runtime. Static analysis cannot check that they match the constructor's parameter types or order, whereas a factory's `new` call is checked statically.
258+
259+
**Q: When must I write a factory?**
260+
A: When construction is computed or conditional, when the class is a third-party class you cannot annotate, when you need a delegator or decorated service, or when the dependency is not a constructor argument.
261+
262+
**Q: How does this relate to PSR-11 and the service locator anti-pattern?**
263+
A: PSR-11 says users should not pass a container into an object so that it can retrieve its own dependencies. Both approaches follow that: the container is used inside the factory, never inside your service, and your service receives its dependencies through the constructor.
264+
265+
**Q: Does `#[Inject]` need Doctrine?**
266+
A: The package requires Doctrine ORM, but you can use `#[Inject]` for services that have nothing to do with Doctrine. `AttributedRepositoryFactory` and the `#[Entity]` attribute are the Doctrine-specific parts.
246 KB
Loading
246 KB
Loading

‎src/App/src/Fixture/articles_cleaned.json‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3127,6 +3127,20 @@
31273127
"excerpt": "Choosing a lightweight PHP framework for a microservice is less about which option is smallest and more about which complexity you want to own. This article compares Dotkernel API, Slim 4, and Lumen, looking at various features and tradeoffs.",
31283128
"tl_dr": "Dotkernel API, Slim 4, and Lumen take very different approaches to building lightweight PHP microservices. Explore how they compare in terms of simplicity, authentication, authorization, database support, operational tooling, dependency management, and long-term maintainability—and understand why the right choice depends on whether you need a genuinely minimal service or an authenticated API designed to evolve over time.",
31293129
"tags": []
3130+
},
3131+
{
3132+
"post_title": "Attributes vs. factories",
3133+
"post_date": "2026-10-01 12:00:00",
3134+
"post_status": "draft",
3135+
"author": {
3136+
"display_name": "Florin Bidirean",
3137+
"github": "bidi47"
3138+
},
3139+
"isObsolete": false,
3140+
"opengraph_img": "/opengraph/article/twitter-card-attributes-vs-factories.png",
3141+
"excerpt": "Dotkernel's dot-dependency-injection package lets you wire a service with an #[Inject] attribute or a hand-written factory. This article compares both approaches with real dotkernel/api code and explains when each one fits.",
3142+
"tl_dr": "Attributes remove one factory file per service and keep dependencies next to the constructor they feed, while hand-written factories keep static type checking and full control over construction. Use #[Inject] for plain constructor wiring, keep factories for computed, conditional, decorated or third-party classes, and mix both freely in the same PSR-11 application.",
3143+
"tags": []
31303144
}
31313145
]
31323146
}

0 commit comments

Comments
 (0)