I have a factory class that creates different objects and I have a lot of loggers defined for each type of object.
And I want to get all loggers collection in my factory. Before I just used a ContainerInterface in my factory constructor, but since Symfony 5.1 container autowiring is deprecated.
Now I can not find a way to get a collection of loggers. I tried to use
!tagged_iterator { tag: 'monolog.logger' }
and also tried to set a tag for LoggerInterface and get a tagged_iterator for it, but it didn't work. I suggest that it is because loggers are not real classes.
This might be overkill but as you say the logger services are a bit unusual. Seems like they should be tagged or be taggable by LoggerInterface but I ran some test and could not get it to work. Here is a brute force approach which relies on logger services having ids of monolog.logger.name:
# Start with a service locator class
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
class LoggerLocator extends ServiceLocator
{
// Just to specify the return type to keep IDEs happy
public function get($id) : LoggerInterface
{
return parent::get($id);
}
}
...
# now make the kernel into a compiler pass
# src/Kernel.php
class Kernel extends BaseKernel implements CompilerPassInterface
...
public function process(ContainerBuilder $container)
{
$loggerServices = [];
foreach($container->getServiceIds() as $id) {
if (!strncmp($id,'monolog.logger.',15)) {
//echo 'Logger ' . $id . "\n";
$loggerServices[$id] = new Reference($id);
}
}
$loggerLocator = $container->getDefinition(LoggerLocator::class);
$loggerLocator->setArguments([$loggerServices]);
}
# and now we can inject the locator where it is needed
class SomeController {
public function index(Request $request, LoggerLocator $loggerLocator)
{
dump($loggerLocator);
$logger = $loggerLocator->get('monolog.logger.' . $name);
Seems like there should be a way to do this just through configuring but a pass is easy enough I guess.
Related
The Symfony docs shows a solution, but it doesn't appear to work (i.e. Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory needs to be replaced with Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory, and other changes). I modified the code as shown below, but am pretty certain I am not doing it correctly.
<?php
declare(strict_types=1);
namespace App\DataFixtures\Purger;
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
class CustomPurgerFactory implements PurgerFactory
{
public function __construct(private ORMPurgerFactory $purgeFactory)
{
}
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface
{
// Change $excluded, $purgeWithTruncate as desired.
return new CustomPurger($emName, $em, $excluded, $purgeWithTruncate, $this->purgeFactory);
}
}
<?php
declare(strict_types=1);
namespace App\DataFixtures\Purger;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
class CustomPurger implements ORMPurgerInterface
{
public function __construct(private ?string $emName, private EntityManagerInterface $entityManager, private array $excluded, private bool $purgeWithTruncate, private ORMPurgerFactory $purgeFactory)
{
}
public function setEntityManager(EntityManagerInterface $entityManager):void
{
// Seems rather redundent doing this even though I earlier inject $entityManager.
$this->entityManager = $entityManager;
}
public function purge() : void
{
// Delete any tables which must be deleted first to prevent FK constraint errors.
// This doesn't seem write.
$purger = $this->purgeFactory->createForEntityManager($this->emName, $this->entityManager, $this->excluded, $this->purgeWithTruncate);
$purger->purge();
}
}
services:
App\DataFixtures\Purger\DoctrinePurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'my_purger' }
arguments:
- '#doctrine.fixtures.purger.orm_purger_factory'
Or should it be done by decorating the default purger as suggested by this post?
Okay. So you do have a few things wrong and the docs are somewhat out of date. From a big picture point of view you want something like:
bin/console doctrine:fixtures:load --purger=my_purger
to use your custom purger factory (aliased as my_purger) to instantiate and execute your custom purger's purge method. The job of the factory is to just create the purger not to execute it.
I followed the docs and implemented PurgerInterface but the purge command complained about it not implementing ORMPurgerInterface which, as you noted, adds a seemingly superfluous method. I think it is still a work in progress. The default ORMPurger has a couple of additional public methods not defined in any interface which is also strange. The fact that Doctrine is inconsistent with it's usage of the Interface suffix does not help. But it is what it is.
This works under 6.1:
# CustomPurger.php
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
class CustomPurger implements ORMPurgerInterface
{
private EntityManagerInterface $em;
public function setEntityManager(EntityManagerInterface $em) : void
{
$this->em = $em;
}
public function purge() : void
{
dd(' my purger');
}
}
# CustomPurgerFactory.php
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
class CustomPurgerFactory implements PurgerFactory
{
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface
{
return new CustomPurger($em);
}
}
# services.yaml
App\Purger\CustomPurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'my_purger' }
bin/console doctrine:fixtures:load --purger=my_purger
> purging database
^ " my purger"
As far as decorating goes, you decorate a service when you want to modify some methods without extending the original class. There is only one method here and it's quite a doozy so I don't think decorating will help.
If you wanted to always use your purger without the --purger option then you could probably point the default purger factory service id to your factory. I'll leave that as an exercise for the reader.
One final note: I took a look at your decorating link. Don't know what they were trying to do but I do know it has nothing to do with decorating.
I am struggling to get a specific service via class name from group of injected tagged services.
Here is an example:
I tag all the services that implement DriverInterface as app.driver and bind it to the $drivers variable.
In some other service I need to get all those drivers that are tagged app.driver and instantiate and use only few of them. But what drivers will be needed is dynamic.
services.yml
_defaults:
autowire: true
autoconfigure: true
public: false
bind:
$drivers: [!tagged app.driver]
_instanceof:
DriverInterface:
tags: ['app.driver']
Some other service:
/**
* #var iterable
*/
private $drivers;
/**
* #param iterable $drivers
*/
public function __construct(iterable $drivers)
{
$this->drivers = $drivers;
}
public function getDriverByClassName(string $className): DriverInterface
{
????????
}
So services that implements DriverInterface are injected to $this->drivers param as iterable result. I can only foreach through them, but then all services will be instantiated.
Is there some other way to inject those services to get a specific service via class name from them without instantiating others?
I know there is a possibility to make those drivers public and use container instead, but I would like to avoid injecting container into services if it's possible to do it some other way.
You no longer (since Symfony 4) need to create a compiler pass to configure a service locator.
It's possible to do everything through configuration and let Symfony perform the "magic".
You can make do with the following additions to your configuration:
services:
_instanceof:
DriverInterface:
tags: ['app.driver']
lazy: true
DriverConsumer:
arguments:
- !tagged_locator
tag: 'app.driver'
The service that needs to access these instead of receiving an iterable, receives the ServiceLocatorInterface:
class DriverConsumer
{
private $drivers;
public function __construct(ServiceLocatorInterface $locator)
{
$this->locator = $locator;
}
public function foo() {
$driver = $this->locator->get(Driver::class);
// where Driver is a concrete implementation of DriverInterface
}
}
And that's it. You do not need anything else, it just workstm.
Complete example
A full example with all the classes involved.
We have:
FooInterface:
interface FooInterface
{
public function whoAmI(): string;
}
AbstractFoo
To ease implementation, an abstract class which we'll extend in our concrete services:
abstract class AbstractFoo implements FooInterface
{
public function whoAmI(): string {
return get_class($this);
}
}
Services implementations
A couple of services that implement FooInterface
class FooOneService extends AbstractFoo { }
class FooTwoService extends AbstractFoo { }
Services' consumer
And another service that requires a service locator to use these two we just defined:
class Bar
{
/**
* #var \Symfony\Component\DependencyInjection\ServiceLocator
*/
private $service_locator;
public function __construct(ServiceLocator $service_locator) {
$this->service_locator = $service_locator;
}
public function handle(): string {
/** #var \App\Test\FooInterface $service */
$service = $this->service_locator->get(FooOneService::class);
return $service->whoAmI();
}
}
Configuration
The only configuration needed would be this:
services:
_instanceof:
App\Test\FooInterface:
tags: ['test_foo_tag']
lazy: true
App\Test\Bar:
arguments:
- !tagged_locator
tag: 'test_foo_tag'
Alternative to FQCN for service names
If instead of using the class name you want to define your own service names, you can use a static method to define the service name. The configuration would change to:
App\Test\Bar:
arguments:
- !tagged_locator
tag: 'test_foo_tag'
default_index_method: 'fooIndex'
where fooIndex is a public static method defined on each of the services that returns a string. Caution: if you use this method, you won't be able to get the services by their class names.
A ServiceLocator will allow accessing a service by name without instantiating the rest of them. It does take a compiler pass but it's not too hard to setup.
use Symfony\Component\DependencyInjection\ServiceLocator;
class DriverLocator extends ServiceLocator
{
// Leave empty
}
# Some Service
public function __construct(DriverLocator $driverLocator)
{
$this->driverLocator = $driverLocator;
}
public function getDriverByClassName(string $className): DriverInterface
{
return $this->driverLocator->get($fullyQualifiedClassName);
}
Now comes the magic:
# src/Kernel.php
# Make your kernel a compiler pass
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
class Kernel extends BaseKernel implements CompilerPassInterface {
...
# Dynamically add all drivers to the locator using a compiler pass
public function process(ContainerBuilder $container)
{
$driverIds = [];
foreach ($container->findTaggedServiceIds('app.driver') as $id => $tags) {
$driverIds[$id] = new Reference($id);
}
$driverLocator = $container->getDefinition(DriverLocator::class);
$driverLocator->setArguments([$driverIds]);
}
And presto. It should work assuming you fix any syntax errors or typos I may have introduced.
And for extra credit, you can auto register your driver classes and get rid of that instanceof entry in your services file.
# Kernel.php
protected function build(ContainerBuilder $container)
{
$container->registerForAutoconfiguration(DriverInterface::class)
->addTag('app.driver');
}
I'm fairly new to Symfony, but experienced with PHP. Suppose I have a service which needs an unknown quantity of another service. It doesn't make sense to inject it (how many would I inject). I can use the ContainerAwareInterface and ContainerAwareTrait but I've read that that's not a good way.
Slightly contrived example:
class ProcessBuilder {
private $allCommands = [];
public function build(array $config){
foreach ($config => $command){
$this->allCommands[] = $this->getContainer()->get('app.worker.command')->init($command);
}
}
}
At the point in which I get my ProcessBuilder service, I don't know how many items will be in the $config array passed into build(). Because of how the Command class (app.worker.command service) works, they cannot share a single instance.
How is the best way to do this? Or do I need to go down the ContainerAware* route?
I hope that makes sense and thanks for your help. Sorry if this has been asked before, but I've had a good Google and not come up with anything.
You are going the right direction. Now only right place is missing.
To collect services of certain type, we need to get 1 step ahead. To dependency injection container compilation (that's how services of EventSubscriber type or Voter type are collected by Symfony.)
You can register service with Extension, and manipulate them in any way with CompilerPass.
Example
Here is example how to collect all services of type A, and add them to service of type B with setter.
Your Case in Compiler Pass
If we convert your code to compiler pass, it would look like this:
ProcessBuilder.php
class ProcessBuilder
{
/**
* #var CommandInterface[]
*/
private $allCommands = [];
public function addCommand(CommandInterface $command)
{
$this->allCommands[] = $command
}
}
AddCommandsToProcessBuilderCompilerPass.php
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
final class AddCommandsToProcessBuilderCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder): void
{
# using Symfony 3.3+ class name, you can use string name as well
$processBuilderDefinition = $this->containerBuilder->getDefinition(ProcessBuilder::class);
foreach ($this->containerBuilder->getDefinitions() as $serviceName => $definition) {
if (is_subclass_of($definition->getClass(), CommandInterface::class)) {
$processBuilderDefinition->addMethodCall('addCommand', [new Reference($serviceName)]);
}
}
}
}
AppBundle.php
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class AppBundle extends Bundle
{
public function build(ContainerBuilder $containerBuilder): void
{
$containerBuilder->addCompilerPass(new AddCommandsToProcessBuilderCompilerPass);
}
}
And add your bundle to AppKernel.php:
final class AppKernel extends Kernel
{
public function registerBundles()
{
bundles = [];
$bundles[] = new AppBundle;
}
}
That's complete process to do what you need in clean way in Symfony.
I like and use the Yii framework, particularly its "components", which are lazily-instantiated and you can swap them in or out in your configuration file. Kind of like a dependency injection-lite.
I try to keep the business logic of my code completely independent of the Framework, in case I ever want to repurpose that code, or even change frameworks.
Let's say I have a class in my service layer called AccountService, which implements IAccountService and has a one-argument constructor.
interface IAccountService
{
function getUserById($id);
}
class AccountService implements IAccountService
{
private $_userRepository;
public function __construct(IUserRepository $userRepository) {
$this->_userRepository = $userRepository;
}
public function getUserById($id) {
return $this->_userRepository->getById($id);
}
}
Great. So far, it's totally framework-free. Now I'd like to expose this as a Yii component, so it can be lazily-instantiated and easily used by Yii controllers and other Yii components.
But Yii components (which implement IApplicationComponent) must have exactly zero constructor arguments, while my class requires one!
Any ideas?
Here's what I've got. I'm not really happy with any of them; they both look over-engineered and I'm detecting a distinct smell from them.
Option 1 - compose: I create a class called "AccountServiceComponent" which implements Yii's IApplicationComponent. It cannot extend my AccountService class, because of the constructor, but it could instantiate one as a private member and wrap all of its methods, like so:
class AccountServiceComponent implements IApplicationComponent, IAccountservice
{
private $_accountService;
public __construct() {
$this->_accountService = new AccountService(new UserRepository());
}
public getUserById($id) {
return $this->_accountService->getUserById($id);
}
}
Cons: I'll have to wrap every method like that, which is tedious and could lead to "baklava code." Especially considering that there'll be multiple service classes, each with multiple methods.
Option 2 - mixin: (Or behavior or trait or whatever it's called these days.)
Yii (having been written prior to PHP 5.4) offers "behaviors" in the form of a class which implements IBehavior. I could create a behavior class which extends my service, and attach it to a component:
class AccountServicesBehavior extends AccountService implements IBehavior
{
// Implement the few required methods here
}
class AccountServiceComponent implements IApplicationComponent
{
public function __construct() {
$accountService = new AccountService(new UserRepository());
$this->attachBehavior($accountService);
}
Cons: My component no longer officially implements IAccountService. Also seems to be getting excessive with the layering.
Option 3 - optional constructor parameters:
I could just make the constructor parameter to my service class optional, and then extend it into a component:
class AccountService implements IAccountService
{
public $userRepository;
public function __construct(IUserRepository $userRepository = null) {
$this->userRepository = $userRepository;
}
public function getUserById($id) {
return $this->_userRepository->getById($id);
}
}
class AccountServiceComponent extends AccountService implements IApplicationComponent
{
}
Cons: The optional constructor parameter means this class coudld now be instantiated without supplying it with everything it needs.
...so, any other options I'm missing? Or am I just going to have to choose the one that disturbs me the least?
Option 3 but with an object as the optional argument sounds best imo:
public function __construct(IUserRepository $userRepository = new UserRepository()) {
$this->userRepository = $userRepository;
}
My Dispatcher is "choosing" correct Controller; then creating Controller's instance (DependencyInjectionContainer is passed to Controller constructor); then calling some Controller's method...
class UserController extends Controller
{
public function __construct(DependencyInjectionContainer $injection) {
$this->container = $injection;
}
public function detailsAction() {
...
}
}
DependencyInjectionContainer contains DB adapter object, Config object etc.
Now let's see what detailsAction() method contains...
public function detailsAction() {
$model = new UserModel();
$model->getDetails(12345);
}
As you see I'm creating new instance of UserModel and calling getDetails methods.
Model's getDetails() method should connect to db to get information about user. To connect to DB UserModel should be able to access DB adapter.
What is the right way to pass DependencyInjectionContainer to the UserModel?
I think that this way is wrong...
public function detailsAction() {
$model = new UserModel($this->container);
$model->getDetails(12345);
}
Instead of injecting the entire DI Container into your classes, you should inject only the dependencies you need.
Your UserController requires a DB Adapter (let's call this interface IDBAdapter). In C# this might look like this:
public class UserController
{
private readonly IDBAdapter db;
public UserController(IDBAdapter db)
{
if (db == null)
{
throw new ArgumentNullException("db");
}
this.db = db;
}
public void DetailsAction()
{
var model = new UserModel(this.db);
model.GetDetails(12345);
}
}
In this case we are injectiing the dependency into the UserModel. In most cases, however, I would tend to consider it a DI smell if the UserController only takes a dependency to pass it on, so a better approach might be for the UserController to take a dependency on an Abstract Factory like this one:
public interface IUserModelFactory
{
UserModel Create();
}
In this variation, the UserController might look like this:
public class UserController
{
private readonly IUserModelFactory factory;
public UserController(IUserModelFactory factory)
{
if (factory == null)
{
throw new ArgumentNullException("factory");
}
this.factory = factory;
}
public void DetailsAction()
{
var model = this.factory.Create();
model.GetDetails(12345);
}
}
and you could define a concrete UserModelFactory that takes a dependency on IDBAdapter:
public class UserModelFactory : IUserModelFactory
{
private readonly IDBAdapter db;
public UserModelFactory(IDBAdapter db)
{
if (db == null)
{
throw new ArgumentNullException("db");
}
this.db = db;
}
public UserModel Create()
{
return new UserModel(this.db);
}
}
This gives you better separation of concerns.
If you need more than one dependency, you just inject them through the constructor. When you start to get too many, it's a sign that you are violating the Single Responsibility Principle, and it's time to refactor to Aggregate Services.
I'd use a singleton object for all config parameters :
You set it up in your bootstrap, then choose to use it directly or pass it as parameter in your objects.
The idea being to have one method all around to retrieve your config data.
You may then provide an abstract class for db manipulation which uses your config. singleton.
DependancyInjection can still be used to override your default data.
The above link in the comment (possible 'duplicate') concludes on using constructor injection : this is close to your current method.
However if I try to figure how your model works, I guess you will have many other model classes other than "userModel". Thus an abstract class using a config singleton might be a good solution : all your next model classes will just extend this abstract class, and you don't have to worry about your config.
On the other hand, your solution is good to me as long as your dependanceInjectionContainer changes often.