Multifactory with parameters

rp
Member | 20
+
0
-

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
+
0
-

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
+
+1
-

@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
+
+2
-

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
+
0
-

@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
+
0
-

@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
+
+1
-

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
+
0
-

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
+
0
-

@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
+
+2
-

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
+
0
-

@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.