How to declare parameters of a service of my bundle - php

I'm creating a bundle for a few Symfony projects. This bundle contains one service and this service is created with a constructor containing an array as first and unique parameter. Developers who will use my bundle have to declare this array in the config/properties.yaml file.
Here is how the array in the config/properties.yaml is loaded. I use the new Symfony6 AbstractBundle class.
class LongitudeOnePropertyBundle extends AbstractBundle
{
public function getPath(): string
{
return \dirname(__DIR__);
}
public function configure(DefinitionConfigurator $definition): void
{
$definition->rootNode()
->children()
->arrayNode('extendable_entities')
->defaultValue([])
->scalarPrototype()->end()
->end()
->end()
->end();
}
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
$loader = new PhpFileLoader($builder, new FileLocator(__DIR__ . '/Resources/config/'));
$loader->load('services.php');
// dump($container->services()->get('longitude-one.property_bundle.property_service'));
$container->services()
->get('longitude-one.property_bundle.property_service')
->arg(0, $config['extendable_entities']);
// dump($container->services()->get('longitude-one.property_bundle.property_service'));
}
}
My service is very simple: a constructor with an array as parameter:
//src/Service.php
namespace LongitudeOne\PropertyBundle\Service;
class PropertyService
{
/**
* #var string[] list of all extendable classes
*/
private array $classes;
/**
* #param string[] $classes list of all extendable classes
*/
public function __construct(array $classes)
{
die('constructor is called');
}
Here is how I configure my services inside the bundle:
//src/Resources/config/services.php
return static function (ContainerConfigurator $container) {
$container->services()
->set('longitude-one.property_bundle.property_service', PropertyService::class)
->class(PropertyService::class)
->autoconfigure()
->autowire()
->public()
->arg(0, param('lopb.extendable_entities'));
dump($container->services()->get(PropertyService::class));
};
In my test application, as soon as my controller call my service with Dependency Injection Pattern, I got this error.
Cannot resolve argument $propertyService of "App\Controller\Admin\DashboardController::list()": Cannot autowire service "LongitudeOne\PropertyBundle\Service\PropertyService": argument "$classes" of method "__construct()" is type-hinted "array", you should configure its value explicitly.
In the service.php file, I added a dump to check that the bundle is well implemented. The container is dumped:
Symfony\Component\DependencyInjection\Loader\Configurator\ServiceConfigurator {#4798 ▼
#definition: Symfony\Component\DependencyInjection\Definition {#4800 ▼
-class: 'LongitudeOne\PropertyBundle\Service\PropertyService'
...
-autowired: true
...
#arguments: array:1 [▼
0 => []
]
}
...
#id: "LongitudeOne\PropertyBundle\Service\PropertyService"
-defaultTags: []
-container: Symfony\Component\DependencyInjection\Compiler
I tried to replace the array parameter set by developer in the config/property.yaml file by an empty array ( [] ) ;
I checked that bundle is well declared ;
I checked that my services.php file (dump is well written) ;
I tried to replace the constructor by a method and I added call('setClasses'), but method isn't called.
I checked my service with symfony console debug:container longitude-one
After reading this question, I set my service as a public service.
/var/www # symfony console debug:container longitude-one
Information for Service "longitude-one.property_bundle.property_service"
========================================================================
---------------- -----------------------------------------------------
Option Value
---------------- -----------------------------------------------------
Service ID longitude-one.property_bundle.property_service
Class LongitudeOne\PropertyBundle\Service\PropertyService
Tags -
Public yes
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired yes
Autoconfigured yes
Usages none
What am I missing?

First of all, a small miss-configuration was present in my services.yaml of my application overriding the vendor service declarations. Because of it, I got a wrong error message. As soon as I remove it, I got a more explicit message:
Cannot autowire argument $propertyService of "LongitudeOne\PropertyBundle\Tests\App\Controller\Admin\DashboardController::list()": it references class "LongitudeOne\PropertyBundle\Service\PropertyService" but no such service exists. You should maybe alias this class to the existing "longitude-one.property_bundle.property_service" service.
I added it:
->alias(PropertyService::class, 'longitude-one.property_bundle.property_service')
It works with this services.php file:
<?php
/**
* This file is part of the LongitudeOne/PropertyBundle
*
* PHP 8.1 | Symfony 6.1+
*
* Copyright LongitudeOne - Alexandre Tranchant
* Copyright 2021 - 2022
*/
namespace LongitudeOne\PropertyBundle\DependencyInjection\Loader\Configurator;
use LongitudeOne\PropertyBundle\Service\PropertyService;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
//use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
return static function (ContainerConfigurator $container) {
$container->services()
->set('longitude-one.property_bundle.property_service', PropertyService::class)
->class(PropertyService::class)
->arg(0, param('extendable_entities'))
->alias(PropertyService::class, 'longitude-one.property_bundle.property_service');
};

Related

symfony 5 - event not being dispatched

I use an event subscriber to handle some actions when my order form is submitted.
Problem my event is not being dispached but symfony is able to find him because he tells me that my OrderEvent::ORDER_CREATE is orphan.
I excpected that execution was stopped with die('Hello you from subscriber'); but it's not.
Controller
public function commanderPanierAction(Request $request, SelectionWeb $selectionWeb, TableLumineuse $tableLumineuse, EventDispatcherInterface $eventDispatcher)
{
// DO PREVIOUS STUFF
$Order = new Order();
$OrderForm = $this->createForm(OrderForm::class, $Order);
if ($request->isMethod('POST')) {
$OrderForm->handleRequest($request);
if ($OrderForm->isSubmitted() && $OrderForm->isValid()) {
// OrderForm is valid proceed
$eventDispatcher->dispatch(
new OrderEvent($Order),
OrderEvent::ORDER_CREATE
);
}
}
OrderEvent
<?php
namespace App\Event;
use App\Entity\Order;
use Symfony\Contracts\EventDispatcher\Event;
class OrderEvent extends Event
{
public const ORDER_CREATE = 'order.created';
protected $order;
public function __construct(Order $order)
{
$this->order= $order;
}
public function getOrder(): Order
{
return $this->order;
}
}
OrderSubscriber
<?php
namespace App\EventSubscriber;
use App\Event\CommandeWebEvent;
use App\Service\SelectionWeb\SelectionWeb;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class OrderSubscriber implements EventSubscriberInterface
{
private $entityManager;
private $selectionWeb;
public function __construct(EntityManagerInterface $entityManager, SelectionWeb $selectionWeb)
{
$this->entityManager = $entityManager;
$this->selectionWeb = $selectionWeb;
}
public static function getSubscribedEvents()
{
return [
OrderEvent::ORDER_CREATE => [
// The higher the number, the earlier the method is called.
['processOrder', 10],
['notifyOrder', -10]
]
];
}
public function processOrder(OrderEvent $event)
{
// TODO
die('Hello you from subscriber');
}
public function notifyOrder(OrderEvent $event)
{
// TODO
}
}
EDIT
The only workaround found (thx to #nikserg) is to inject subscriber into controller action (subscriber has dependencies) then register my subscriber as service in services.yaml finaly use $eventDispatcher->addSubscriber($subscriber); before $eventDispatcher->dispatch(new OrderEvent($Order),OrderEvent::ORDER_CREATE);
It seems all that stuff is really complex for a task as simple as that
EDIT2
I found an another way I'm able to execute my subscriber without usage of $eventDispatcher->addSubscriber($subscriber); and only with $eventDispatcher->dispatch(new OrderEvent($Order)); only if I configure my subscriber as service in services.yaml but why symfony does need this information in services.yaml ? Thought that everything in src/ is avaible to be used as service..
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
# If I add those It works
App\EventSubscriber\OrderSubscriber:
autowire: true
EDIT3
My OrderSubscriber is loaded into container so why I should set it explicitly to being execute ? I can't figure out what's going on
php bin/console debug:container
---------------------------------------- ---------------------------------------
Service ID Class name
---------------------------------------- ----------------------------------------
App\EventSubscriber\OrderSuscriber App\EventSubscriber\OrderSuscriber
EDIT 4
If I set my OrderSubscriber explicitly there is two instances of it into container.
Why symfony execute one set explicitly and not the one set with resource: '../src/*'
Symfony will autowire your subscriber as service, if you will require it as argument in action:
public function commanderPanierAction(Request $request, SelectionWeb $selectionWeb, TableLumineuse $tableLumineuse, OrderSubscriber $orderSubscriber)
Of course, if your subscriber is registered properly.
But let me advice you not to create subscribers as objects manually. The main good thing about subscribers is that you know nothing about them, when you fire event. There could be dozens of subscribers to this event, and all of them will proceed your event. That will keep your code nice and lower cohesion.
It's in docs: https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-subscriber
First I want to thank you all for your time and let me apologize my problem was due to a typo I wrote OrderSuscriber instead of OrderSubscriber that's why there was 2 services into my container and why defined service explicitly was working.

Get service via class name from iterable - injected tagged services

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');
}

Object Mocking - How to replace a factory with a service in the service manager?

I'm having some trouble getting my unit test to work. I'm testing a controller that uses a service that is created by a factory. What I want to achieve is to replace a factory with a mocked service so I can perform tests without using an active database connection.
The setup
In my service manager's configuration file I point to a factory.
The factory requires an active database connection that I don't want to use during my unit test.
Namespace MyModule;
return [
'factories' => [
MyService::class => Factory\Service\MyServiceFactory::class,
],
];
Note: I have changed class names and simplified configuration for illustration purposes.
The service uses a mapper that I won't be going into now because that is not relevant to the situation. The mappers are tested in their own testcases. The service itself has it's own testcase as well but needs to be present for the controller's actions to work.
The controller action simply receives information from the service.
Namespace MyModule\Controller;
use MyModule\Service\MyService;
use Zend\Mvc\Controller\AbstractActionController;
class MyController extends AbstractActionController
{
/**
* #var MyService
*/
private $service;
/**
* #param MyService $service
*/
public function __construct(MyService $service)
{
$this->service = $service;
}
/**
* Provides information to the user
*/
public function infoAction()
{
return [
'result' => $this->service->findAll(),
];
}
}
Note: Again, I have changed class names and simplified the example for illustration purposes.
What I've tried
In my testcase I've tried to override the desired factory like this:
/**
* #return \Prophecy\Prophecy\ObjectProphecy|MyService
*/
private function mockService()
{
$service = $this->prophesize(MyService::class);
$service->findAll()->willReturn(['foo', 'bar']);
return $service;
}
/**
* #param \Zend\ServiceManager\ServiceManager $services
*/
private function configureServiceManager(ServiceManager $services)
{
$services->setAllowOverride(true);
$services->setService(MyService::class, $this->mockService()->reveal());
$services->setAllowOverride(false);
}
At first sight this looks great, but it doesn't work. It just seems to append the service to the service manager's list of services, not overriding the factory.
Changing $services->setService to $services->setFactory requires me to build another factory. What I could do is create a factory that injects a mock-mapper into the service but that feels wrong. I'm testing the controller, not the service or mapper so I am trying to avoid complex solutions like that to keep my test cases simple and clear.
Are there any options regarding my situation? Is it possible to override a factory with a service in the service manager or am I looking at it wrong?
I think you need a separate config file for unit testing.
phpunit.xml
<?xml version="1.0"?>
<phpunit bootstrap="./Bootstrap.php">
Bootstrap.php
require 'vendor/autoload.php';
$configuration = include 'config/phpunit.config.php';
Zend\Mvc\Application::init ($configuration);
config/phpunit.config.php is a config file created for unit testing only:
config/phpunit.config.php
$configuration = include (__DIR__ . '/application.config.php');
$configuration ['module_listener_options'] ['config_glob_paths'] [] = 'config/phpunit/{,*.}local.php';
config/phpunit/yourfile.local.php
return [
'service_manager' => array (
'factories' => [
MyService::class => ...
]
)
];
In config/phpunit/yourfile.local.php you can let MyService::class be whatever you want, even a closure.
There is no need to build new factories for this. Just use a simple closure instead:
/**
* #param \Zend\ServiceManager\ServiceManager $services
*/
private function configureServiceManager(ServiceManager $services)
{
$services->setAllowOverride(true);
$mockedService = $this->mockService();
$services->setFactory(MyService::class, function() use ($mockedService) {
$mockedService->reveal();
});
$services->setAllowOverride(false);
}
Now you can still mock only the required service. Adding expectations in the test case is still as flexible as it should be:
public function testMyCase()
{
$expected = ['foo', 'bar'];
$this->mockService()->findAll()->willReturn($expected);
$result = $this->service->findAll();
$this->assertSame($expected, $result);
}

How does Symfony internally sets the service name in #Route annotation?

I'm trying to emulate the behavior of Route Symfony annotation(documentation), which extends Symfony\Component\Routing\Annotation\Route adding the service property:
class Route extends BaseRoute
{
protected $service;
public function setService($service)
{
$this->service = $service;
}
// ...
}
It adds the service property in order to set the _controller parameter to servicename:method when controller is actually a service. This is done in the AnnotatedRouteControllerLoader class:
protected function configureRoute(Route $route, \ReflectionClass $class,
\ReflectionMethod $method, $annot)
{
// ...
if ($classAnnot && $service = $classAnnot->getService()) {
$route->setDefault('_controller', $service.':'.$method->getName());
} else {
// Not a service ...
}
// ...
}
My question is how/when the setService($service) is invoked?
I've tried to define my custom MyCustomRoute annotation (with the above service property), loop each container service and call setService($serviceId) to "notify" that the controller is actually a service:
foreach ($container->getServiceIds() as $serviceId) {
if ($container->hasDefinition($serviceId)) {
$definition = $container->getDefinition($serviceId);
$reflector = new \ReflectionClass($definition->getClass());
// If the service is a controller then flag it for the
// AnnotatedRouteControllerLoader
if ($annot = $reader->getClassAnnotation($reflector,
'My\CustomAnnotations\MyCustomRoute')) {
$annot->setServiceName($serviceId);
}
}
}
Here $container is Symfony service container, $reader is doctrine annotation reader.
This is not working because annotation is read again in AnnotatedRouteControllerLoader resulting in a different instance, loosing the service property.
I'm using the routing component alone (without the entire Symfony framework).
The Route class is declared as a service, refering to the doc, you can inject dependencies by controller, but also with "setter injection". take a look here:
http://symfony.com/doc/current/book/service_container.html#optional-dependencies-setter-injection
So you can declare your service as:
my_custom.router:
class: "Acme\MyBundle\MyServices\MyRouter"
calls:
- [setService, ["#service_key"]]

How to inject a service in another service in Symfony?

I am trying to use the logging service in another service in order to trouble shoot that service.
My config.yml looks like this:
services:
userbundle_service:
class: Main\UserBundle\Controller\UserBundleService
arguments: [#security.context]
log_handler:
class: %monolog.handler.stream.class%
arguments: [ %kernel.logs_dir%/%kernel.environment%.jini.log ]
logger:
class: %monolog.logger.class%
arguments: [ jini ]
calls: [ [pushHandler, [#log_handler]] ]
This works fine in controllers etc. however I get no out put when I use it in other services.
Any tips?
You pass service id as argument to constructor or setter of a service.
Assuming your other service is the userbundle_service:
userbundle_service:
class: Main\UserBundle\Controller\UserBundleService
arguments: [#security.context, #logger]
Now Logger is passed to UserBundleService constructor provided you properly update it, e.G.
protected $securityContext;
protected $logger;
public function __construct(SecurityContextInterface $securityContext, Logger $logger)
{
$this->securityContext = $securityContext;
$this->logger = $logger;
}
For Symfony 3.3, 4.x, 5.x and above, the easiest solution is to use Dependency Injection
You can directly inject the service into another service, (say MainService)
// AppBundle/Services/MainService.php
// 'serviceName' is the service we want to inject
public function __construct(\AppBundle\Services\serviceName $injectedService) {
$this->injectedService = $injectedService;
}
Then simply, use the injected service in any method of the MainService as
// AppBundle/Services/MainService.php
public function mainServiceMethod() {
$this->injectedService->doSomething();
}
And viola! You can access any function of the Injected Service!
For older versions of Symfony where autowiring does not exist -
// services.yml
services:
\AppBundle\Services\MainService:
arguments: ['#injectedService']
More versatile option, is to once create a trait for the class you would want to be injected. For instance:
Traits/SomeServiceTrait.php
Trait SomeServiceTrait
{
protected SomeService $someService;
/**
* #param SomeService $someService
* #required
*/
public function setSomeService(SomeService $someService): void
{
$this->someService = $someService;
}
}
And where you need some service:
class AnyClassThatNeedsSomeService
{
use SomeServiceTrait;
public function getSomethingFromSomeService()
{
return $this->someService->something();
}
}
The class will autoload due to #required annotation. This generaly makes it much faster to implement when you want to inject services into numerous classes (like event handlers).

Categories