You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit be4572c
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: public/md-articles/best-practice/attributes-vs-factories.md
+27-20Lines changed: 27 additions & 20 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,11 +8,11 @@ category: "Best Practice"
8
8
language: "en"
9
9
---
10
10
11
-
# Attributes vs. factories
11
+
# Attributes vs. Hand-Written Factories
12
12
13
13
## TL;DR
14
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.
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 Mezzio application.
16
16
17
17
## Introduction
18
18
@@ -22,7 +22,8 @@ The payoff is flexibility, because you can swap an implementation without touchi
22
22
For required dependencies, the constructor is the natural place to receive them.
23
23
24
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()`.
25
+
[PSR-11](https://www.php-fig.org/psr/psr-11/) standardizes only how you retrieve entries from a container, not how they are built: `ContainerInterface` defines just two methods, `get()` and `has()`.
26
+
The factories and delegators configuration you'll see below belongs to `ServiceManager`.
26
27
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
28
That is the service locator anti-pattern, and it is why the container belongs in a factory, not inside your service.
28
29
@@ -34,14 +35,14 @@ We introduced the package in [Dependency Injection made easy in Laminas/Mezzio a
34
35
This article puts the two side by side and helps you choose.
35
36
The real-world code comes from the [Dotkernel API](https://github.com/dotkernel/api) repository.
| 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 |
43
+
|Static type checking |Verified by your IDE and static analysis | None; identifiers are strings resolved at runtime, and PHP's type declarations catch a mismatch only when the service is built|
44
+
| Argument order |Kept in sync manually, but verified statically in the `new` call (named arguments also possible) | Kept in sync manually; a mistake surfaces only at runtime|
45
+
| Reflection | None when the service is built | One `ReflectionClass` per service creation (usually once per request, since services are shared by default) |
45
46
| Conditional wiring | Fully supported | Not supported |
46
47
| Third-party classes | Works on any class | Only classes you can annotate |
@@ -50,9 +51,9 @@ The real-world code comes from the [Dotkernel API](https://github.com/dotkernel/
50
51
51
52
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
53
-
## Code examples from dotkernel/api
54
+
## Code Examples from dotkernel/api
54
55
55
-
### A service wired with an attribute
56
+
### A Service Wired with an Attribute
56
57
57
58
`UserAvatarService` needs a Doctrine repository and the application config.
58
59
In dotkernel/api, the class declares both on the constructor:
@@ -91,7 +92,7 @@ use Dot\DependencyInjection\Factory\AttributedServiceFactory;
91
92
The same `AttributedServiceFactory` entry is repeated for every handler, service and middleware in the module.
92
93
The attribute lists the container identifiers in constructor order, which is the order you must keep in sync by hand.
93
94
94
-
### The same service with a hand-written factory
95
+
### The Same Service with a Hand-Written Factory
95
96
96
97
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
@@ -123,7 +124,7 @@ That is two files instead of one, and a second place to edit whenever the constr
123
124
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
125
With the attribute, the same mistake shows up when the service is built.
125
126
126
-
### Doctrine repositories
127
+
### Doctrine Repositories
127
128
128
129
Repositories are where attributes save the most repetition.
129
130
In dotkernel/api, the repository declares which entity it serves:
@@ -153,7 +154,7 @@ use Dot\DependencyInjection\Factory\AttributedRepositoryFactory;
153
154
154
155
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
156
-
### Where a factory still wins
157
+
### Where a Factory Still Wins
157
158
158
159
Some objects cannot be described by a list of container identifiers.
159
160
`ErrorResponseGeneratorFactory` in dotkernel/api reads a config value, with a fallback when the container has no config at all:
@@ -192,7 +193,7 @@ An attribute cannot express "give me the container", and this resolver exists to
192
193
That is a deliberate exception.
193
194
It does not make the container a general-purpose dependency for your services.
194
195
195
-
### Using both in one project
196
+
### Using Both in One Project
196
197
197
198
The two approaches share the same `ConfigProvider`.
198
199
dotkernel/api's `App` module, for example, wires its services with the attribute and still decorates selected services with delegators:
@@ -211,7 +212,7 @@ dotkernel/api's `App` module, for example, wires its services with the attribute
211
212
A service can be built by `AttributedServiceFactory` and still be wrapped by a delegator.
212
213
Which factory applies to which service is decided entirely by the configuration.
213
214
214
-
### The anti-pattern to avoid
215
+
### The Anti-Pattern to Avoid
215
216
216
217
Both approaches keep the container out of your classes.
217
218
This is what PSR-11 warns against:
@@ -232,15 +233,17 @@ class UserAvatarService
232
233
}
233
234
```
234
235
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
+
The dependencies are now hidden: the constructor signature no longer says what the class needs, and a missing service no longer fails when `UserAvatarService` is built, but only when `deleteAvatar()` is called.
237
+
That may be a rarely used code path that slips past your tests and fails in production.
236
238
Tests also have to build a container just to run one method.
237
239
Declare `UserAvatarRepository` in the constructor instead, using either approach from this article.
238
240
239
241
## Conclusion
240
242
241
243
Attributes and factories are two ways of delivering the same constructor injection.
242
244
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.
245
+
Reach for a hand-written factory when construction involves logic or when a class is not yours to annotate.
246
+
Decorating a service is a separate concern: the delegator is a factory you write, but the service it wraps can still be built with `#[Inject]`.
244
247
You do not have to choose once for the whole project, since the `ConfigProvider` decides per service.
245
248
246
249
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.
@@ -251,13 +254,17 @@ To go further, read the [dot-dependency-injection documentation](https://docs.do
251
254
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
255
253
256
**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.
257
+
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.
258
+
Because ServiceManager shares services by default, a service is usually created once per request rather than on every `get()` call, so the cost is paid once per service per request.
259
+
The documentation does not publish benchmark figures, so measure your own application if it matters.
255
260
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.
261
+
**Q: Why does `#[Inject]` lose static type checking?**
262
+
A: The attribute lists container identifiers as strings, such as `UserAvatarRepository::class` or `'config'`, and they are resolved at runtime.
263
+
Static analysis cannot check that they match the constructor's parameter types or order, whereas a factory's `new` call is checked statically.
264
+
PHP still enforces the constructor's type declarations, so a mismatch throws a `TypeError` when the service is built rather than going unnoticed; it just isn't caught before the code runs.
258
265
259
266
**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.
267
+
A: When construction is computed or conditional, when the class is a third-party class you cannot annotate, or when the dependency is not a constructor argument. To decorate a service you write a delegator factory, but the service itself can still be wired with `#[Inject]`, as the `App` module example shows.
261
268
262
269
**Q: How does this relate to PSR-11 and the service locator anti-pattern?**
263
270
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.
0 commit comments