DEV Community

Improve your factories in Symfony

Marc Guinea on March 03, 2022

Sometimes we need to retrieve some kind of handler class that depending on an attribute is different. Thanks to Factory pattern we can solve this ...
Collapse
 
vasilvestre profile image
Valentin Silvestre • Edited

I love this pattern ! It's really clean.
You could even simplify it using tagged_iterator.

_instanceof:
        App\ChannelHandlerInterface:
            tags: ['app.channel_handler']

App\ChannelFactory:
    arguments:
      - !tagged_iterator app.channel_handler
Enter fullscreen mode Exit fullscreen mode

Thanks to DI feature, you do not have to declare anything manually !

The idea come from Thibault Richard (github.com/t-richard) :)

Collapse
 
mguinea profile image
Marc Guinea

Wow! looks awesome! even better, thanks for sharing

Collapse
 
vasilvestre profile image
Valentin Silvestre

I think you could update your post with it ;)

Thread Thread
 
mguinea profile image
Marc Guinea

Done! many thanks :)

Collapse
 
aheiland profile image
Andreas Rau • Edited

How about?

final class ChannelFactory
{
    /** @var array<string, ChannelInterface> */
    private array $lookUp = [];

    public function __construct(...ChannelInterface channels)
    {
        foreach($channels as $channel) {
            $this->lookUp[$channel->getType()] = $channel;
        }
    }

    public function create(string $type): ChannelInteface
    {
        if (!array_key_exists($type, $this->lookUp)) {
            throw new InvalidArgumentException('Unknown channel given');
        }
        return $this->lookUp[$type];
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
aless673 profile image
Alessandro GIULIANI • Edited

And this one ? ^^

final class ChannelFactory
{
    /** @var array<string, ChannelInterface> */
    private array $lookUp;

    public function __construct(ChannelInterface ...$channels)
    {
        $this->lookUp = array_combine(array_column($channels, 'type'), $channels);
    }

    public function create(string $type): ChannelInteface
    {
        return $this->lookUp[$type] ?? throw new InvalidArgumentException('Unknown channel given');
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tomasvotruba profile image
Tomáš Votruba

You can also take it one step further, avoid tags and any YAML coding by using annotation autowire:

final class ChannelFactory
{
    /**
     * @param ChannelInterface[] $channels
     */
    public function __construct(private array $channels)
    {
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mguinea profile image
Marc Guinea

Yep, but then you are placing something from the Framework (infrastructure layer) to the factory (domain layer).

It's also a valid approach, but I prefer keeping things separated (depends on the architecture of your app, of course).

Anyway, thanks for the idea!

Collapse
 
fd6130 profile image
fd6130 • Edited

Hi, how do you use those class property in a static method? I try to call $this->channels and my IDE show error Cannot use '$this' in non-object context.

And do I still need to inject the factory class or I can just directly use it like ChannelFactory::create()?

Collapse
 
mguinea profile image
Marc Guinea

This was an error! just fixed... this shouldn't be a static class, we want to use service container to manage that data. Thanks & fixed!

Collapse
 
dziugasdraksas profile image
DziugasDraksas

I would suggest minor optimization.
Do not create new $channel, since all channels are already initialized.
Just return it:

foreach($this->channels as $channel) {
    if ($type === $channel::getType()) {
        return $channel;
     }
}
Enter fullscreen mode Exit fullscreen mode