Generated Factories
Nette DI can automatically generate factory code based on interfaces, saving you from writing code.
A factory is a class that is responsible for creating objects and passing their dependencies. Please do not confuse this with the factory method design pattern, which describes a specific way of using factories and is unrelated to this topic.
We have shown what such a factory looks like in the introductory chapter:
class ArticleFactory
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
public function create(): Article
{
return new Article($this->db);
}
}
Nette DI can automatically generate factory code. All you need to do is create an interface, and Nette DI will generate the
implementation. The interface must have exactly one method named create
and declare a return type:
interface ArticleFactory
{
function create(): Article;
}
So, the factory ArticleFactory
has a method create
that creates Article
objects. The
Article
class might look like this, for example:
class Article
{
public function __construct(
private Nette\Database\Connection $db,
) {
}
}
Add the factory to the configuration file:
services:
- ArticleFactory
Nette DI will generate the corresponding factory implementation.
In the code that uses the factory, request the object by its interface, and Nette DI will provide the generated implementation:
class UserController
{
public function __construct(
private ArticleFactory $articleFactory,
) {
}
public function foo()
{
// let the factory create an object
$article = $this->articleFactory->create();
}
}
Parameterized Factory
The factory method create
can accept parameters, which it then passes to the constructor. For example, let's add
the article author's ID to the Article
class:
class Article
{
public function __construct(
private Nette\Database\Connection $db,
private int $authorId,
) {
}
}
We will also add the parameter to the factory:
interface ArticleFactory
{
function create(int $authorId): Article;
}
Since the parameter name in the constructor ($authorId
) matches the parameter name in the factory method, Nette DI
automatically passes it.
Advanced Definition
The definition can also be written in multi-line form using the implement
key:
services:
articleFactory:
implement: ArticleFactory
Using this longer format allows specifying additional arguments for the constructor via the arguments
key and
further configuration using setup
, similar to regular service definitions.
Example: If the create()
method didn't accept the $authorId
parameter, we could provide a fixed value
in the configuration to be passed to the Article
constructor:
services:
articleFactory:
implement: ArticleFactory
arguments:
authorId: 123
Conversely, if create()
accepted $authorId
, but it wasn't part of the constructor and was instead
passed via a method like Article::setAuthorId()
, we would reference the parameter in the setup
section:
services:
articleFactory:
implement: ArticleFactory
setup:
- setAuthorId($authorId)
Accessor
Besides factories, Nette can also generate so-called accessors. These are objects with a get()
method that returns
a specific service from the DI container. Repeated calls to get()
always return the same instance.
Accessors provide lazy-loading for dependencies. Consider a class that logs errors to a dedicated database. If this class
received the database connection via constructor dependency injection, the connection would always be established, even if errors
occur rarely and the connection remains unused most of the time. Instead, the class can receive an accessor. The database object
(connection) is only created when the accessor's get()
method is called for the first time:
How to create an accessor? Just write an interface, and Nette DI will generate the implementation. The interface must have
exactly one method named get
and declare the return type:
interface PDOAccessor
{
function get(): PDO;
}
Add the accessor to the configuration file, along with the definition of the service it should return:
services:
- PDOAccessor
- PDO(%dsn%, %user%, %password%)
Because the accessor returns a PDO
service, and there's only one such service defined in the configuration, the
accessor will return that specific service. If multiple services of that type exist, specify which one the accessor should return
by name, e.g., - PDOAccessor(@db1)
.
Multifactory/Accessor
So far, our factories and accessors could only create or return a single type of object. However, you can easily create
multifactories, which combine features of factories and accessors. The interface for such a component can contain multiple methods
named create<Name>()
and get<Name>()
, for example:
interface MultiFactory
{
function createArticle(): Article;
function getDb(): PDO;
}
So, instead of injecting multiple individual factories and accessors, you can inject a single, more comprehensive component.
Alternatively, instead of multiple methods, get()
with a parameter can be used:
interface MultiFactoryAlt
{
function get($name): PDO;
}
Then, MultiFactory::getDb()
does the same thing as MultiFactoryAlt::get('db')
. However, this
alternative notation has the disadvantage that the supported values for $name
are not explicitly clear from the
interface signature. Additionally, you cannot define different return types for different $name
values within the
interface.
Definition with a List
You can define a multifactory in the configuration using a list:
services:
- MultiFactory(
article: Article # defines createArticle()
db: PDO(%dsn%, %user%, %password%) # defines getDb()
)
Alternatively, you can refer to existing services in the multifactory definition using references:
services:
article: Article
- PDO(%dsn%, %user%, %password%)
- MultiFactory(
article: @article # defines createArticle()
db: @\PDO # defines getDb()
)
Definition with Tags
Another option how to define a multifactory is to use tags:
services:
- App\Core\RouterFactory::createRouter
- App\Model\DatabaseAccessor(
db1: @database.db1.explorer
)