Multifactory with parameters
- rp
- Member | 20
tldr;
It seems it is possible to pass parameters to generated factories (i.e.
create($param)
) but not to generated multifactories (i.e.
createFoo($param)
).
It is possible to pass parameters to factory method definitions like this:
interface SimpleFactory
{
public function create(int $id): FooControl;
}
In presenter I can create FooControl
object like this:
$foo = $this->simpleFactory->create($this->id);
So far so good. The problem comes when I try to group more factories in one interface like this
interface MultiFactory
{
public function createFooControl(int $id): FooControl;
public function createBarControl(int $id): BarControl;
}
In presenter I would be able to get both FooControl
and
BarControl
objects by using only one injected
factory, like this:
$foo = $this->multiFactory->createFooControl($this->id);
$bar = $this->multiFactory->createBarControl($this->id);
However, Nette says: Service ‘XXX’: Method MultiFactory::createFooControl() does not meet the requirements: is create($name), get($name), create*() or get*() and is non-static.
I have this neon configuration in place for MultiFactory:
- FooControl
- MultiFactory(
fooControl: @FooControl
)
Does it mean that parameters cannot be passed in multifactory methods? Or do I have an error in my neon config and params have to be passed in another way? Thanks.
Last edited by rp (2023-07-28 09:40)
- rp
- Member | 20
To make it work pass parameters with setter rather than in constructor.
FooControl.php
class FooControl extends \Nette\Application\UI\Control
{
private int $id;
public function __construct(
// these services are automatically injected by DI
// which is cool as we do not polute our presenter
// and have it encapsulated in the component
private DummyService1 $ds1,
private DummyService2 $ds2,
) {}
public function setId(int $id): void
{
$this->id = $id
}
public function render(): void
{
// ...
}
}
config.neon
- DummyService1
- DummyService2
- FooControl
- MultiFactory(
fooControl: @FooControl
)
factory
interface MultiFactory
{
public function createFooControl(): FooControl;
}
presenter
$foo = $this->multiFactory->createFooControl();
$foo->setId($this->id); // pass param with setter
That works well. Problem solved.
Should someone know about better solution please let me know in the comments. Thanks.
Last edited by rp (2023-07-17 18:04)
- dakur
- Member | 493
@rp The problem with setting ID after creating the object is that the object is in invalid state. You can render it but having no ID it will crash. So it's a workaround rather than proper solution to the problem.
I would go with rewriting the interface into usual class. It doesn't pollute presenters either and you have freedom to express yourself. It's a little bit more writing but my experience says it's worth it.
final class MultiFactory
{
public function __construct(
private DummyService1 $ds1,
private DummyService2 $ds2,
) {}
public function createFooControl(int $id): FooControl
{
return new FooControl($id, $this->ds1, $this->ds2);
}
public function createBarControl(int $id): BarControl
{
return new BarControl($id);
}
}
Last edited by dakur (2023-07-27 10:53)
- Marek Bartoš
- Nette Blogger | 1261
Or just use the regular generated factories and inject them into hand-written multi factory.
class FooControl extends \Nette\Application\UI\Control
{
public function __construct(int $id) {}
}
class BarControl extends \Nette\Application\UI\Control
{
public function __construct(int $id) {}
}
interface FooControlFactory
{
public function create(int $id): FooControl;
}
interface BarControlFactory
{
public function create(int $id): BarControl;
}
final class MultiFactory
{
public function __construct(
private FooControlFactory $fooControlFactory,
private BarControlFactory $barControlFactory,
) {}
public function createFooControl(int $id): FooControl
{
return $this->fooControlFactory->create($id);
}
public function createBarControl(int $id): BarControl
{
return $this->barControlFactory->create($id);
}
}
services:
- MultiFactory
- FooControlFactory
- BarControlFactory
Last edited by Marek Bartoš (2023-07-28 13:50)
- rp
- Member | 20
@MarekBartoš
Let me describe an issue that I experience using your solution.
I move $id
parameter into constructor:
class FooControl extends \Nette\Application\UI\Control
{
public function __construct(
private DummyService1 $ds1,
private DummyService2 $ds2,
private int $id; // Great, I do not need a setter any more
) {}
Let's create FooControl:
public function createFooControl(int $id): FooControl
{
return $this->fooControlFactory->create($id);
}
I receive an Nette\DI\ServiceCreationException
:
Service of type FooControl: Parameter $id in FooControl::__construct() has no
class type or default value, so its value must be specified.
I could assign a value to $id in constructor to make it work:
class FooControl extends \Nette\Application\UI\Control
{
public function __construct(
private DummyService1 $ds1,
private DummyService2 $ds2,
// Could be any value as it is overwritten by the caller,
// i.e. by fooControlFactory->create($id)
private int $id = 1; // Ugly, but works
) {}
However, that is far from ideal. What do you think?
Edit:
There maybe a problem with my config.neon
:
- DummyService1
- DummyService2
- FooControl # how do I say that $id should not be injected
When ContainerBuilder
tries to build a container it bumps into
FooControl
. It looks at its constructor and finds out there are
three parameters he should inject:
- DummyService1
- DummyService2
- $id
It can find and inject the first two. Third one is unknown. That might be the source of the problem.
Last edited by rp (2023-07-28 12:57)
- rp
- Member | 20
@dakur
That solution could get problematic very fast. Imagine you have 100 controls, each with its own set of dependencies:
Foo1Control(Foo1Dependency1, Foo1Dependency2)
Foo2Control(Foo2Dependency1, Foo2Dependency2)
...
Foo100Control(Foo100Dependency1, Foo100Dependency2)
In that case, you will ask for 200 dependencies in your constructor, 198 of which are useless for each control (as each control only needs its 2 specific dependencies).
It might look far fetched, however, there is a very same problem with the example you provided:
public function __construct(
private DummyService1 $ds1,
private DummyService2 $ds2,
) {}
// somebody removed createFooControl but forgot to remove DummyServices
public function createBarControl(int $id): BarControl
{
// this control does not need any of Dummy services
// but they are still injected into constructor
return new BarControl($id);
}
You might rely on a person who removed createFooControl
that he
will also check for related services in the constructor and remove them.
Therefore I made up that far fetched example to demonstrate that it most likely
won't happen.
What do you think?
- Marek Bartoš
- Nette Blogger | 1261
You are supposed to create a factory which creates control and register it as a service. You are registering control instead of factory. Check my example, I updated it a bit.
- Marek Bartoš
- Nette Blogger | 1261
You might rely on a person who removed createFooControl that he will also check for related services in the constructor and remove them.
That's problem of the programmer and not the actual code. However, unused services are easily checked by tools – PHPStorm and PHPStan report unused private properties.
- rp
- Member | 20
@MarekBartoš
I see where the misunderstanding is coming from.
Constructor of FooControl
in my example:
public function __construct(
private DummyService1 $ds1,
private DummyService2 $ds2,
) {}
Constructor of FooControl
in your example:
public function __construct(int $id) {}
Your example omits Dummy services, therefore FooControl
does not
have to be registered in config.neon.
My FooControl
requires Dummy services to be injected so
I register it in config.neon (as well as FooControlFactory
) and
receive Nette\DI\ServiceCreationException
. I am almost sure that
I receive that exception because DI tries to inject $id parameter that cannot
be found.
One way around it is @dakur suggestion to have services injected in MultiFactory constructor and pass them to factory methods:
public function __construct(
private DummyService1 $ds1,
private DummyService2 $ds2,
) {}
public function createFooControl(int $id): FooControl
{
return new FooControl($id, $this->ds1, $this->ds2);
}
At this point I should let you know that I'm more than happy with my solution and there is no need, unless you want to discuss it in more detail, to spend time on it. Thank you @dakur and @MarekBartoš for your time and your suggestions.
- Marek Bartoš
- Nette Blogger | 1261
Services are automatically passed to control from generated factory, you don't have to do anything except defining them in control constructor.
- rp
- Member | 20
@MarekBartoš
Yes, you are correct!
The mistake was in registering FooControl
in DI. That threw that
Exception. I thought it has to be registered otherwise nothing will be
injected. But it will be automatically, exactly as you said.
So your suggestion really works and the advatage is that I can pass $id in constructor.
Presenter.php
$foo = $this->multiFactory->createFooControl($this->id);
MultiFactory.php
class MultiFactory
{
public function __construct(
private FooControlFactory $fooControlFactory
){}
public function createFooControl(int $id): FooControl
{
return $this->fooControlFactory->create($id);
}
}
FooControlFactory.php
interface FooControlFactory
{
// When Nette generates class from this interface
// it looks at FooControl constructor and passes all
// present dependencies that are also registered in config.neon.
// On top of that it passes all params from create method
// i.e. $id parameter
public function create(int $id): FooControl;
}
FooControl.php
class FooControl extends Nette\Application\UI\Control
{
public function __construct(
// this is passed by create($id)
private $id,
// these will be injected automatically
// no need to register FooControl in DI
private DummyService1 $ds1,
private DummyService2 $ds2,
) {}
}
config.neon
- DummyService1
- DummyService2
- FooControlFactory
- MultiFactory
Thanks @MarekBartoš for your time and effort.