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

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"]]

Related

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

Symfony 3: How can I inject a service dynamically depending on some runtime variable

let's say I have the following interface/concrete classes:
interface EmailFormatter
class CvEmailFormatter implements EmailFormatter
class RegistrationEmailFormatter implements EmailFormatter
class LostPasswordEmailFormatter implements EmailFormatter
I then have a custom 'mailer' service that's called from my controller actions in order to send an email.
What options do I have for injecting the correct implementation of EmailFormatter to my mailer service depending on the type of email being sent?
I would create a service that picks the right formatter during runtime, either some kind of factory or if your formatters have dependencies maybe a service were you inject the formatters from the container. Something like this:
class MailController extends AbstractController
{
private $mailer;
private $mailFormatterSelector;
public function __construct(...) { ... }
public function someAction()
{
// Do stuff ...
if (...some condition) {
$formatter = $this->mailFormatterSelector->getRegisterMailFormatter();
} else {
$formatter = $this->mailFormatterSelector->getLostPasswordEmailFormatter();
}
$mailer->sendEmail($formatter);
// Do more stuff ...
}
}
class MailFormatterSelector()
{
private $registrationFormatter;
public function __construct(EmailFormatter $registrationFormatter, ...)
{
$this->registrationFormatter = $registrationFormatter;
...
}
public function getRegisterMailFormatter(): EmailFormatter
{
return $this->registrationFormatter;
}
// ...
}
Alternatively if you have to pass the formatters into your mailer during construction, you can also create multiple, differently set up instances with different aliases and then inject them as needed into the services and controllers like this:
# config/services.yaml
mailer1:
class: MyMailler
arguments:
$formatter: '#formatter1'
mailer2:
class: MyMailler
arguments:
$formatter: '#formatter2'
MyMailController:
arguments:
$mailer: '#mailer2'
In your controller or action you can then pass in mailer1, mailer2, ... (maybe with nicer names) via.

Laravel service provider explanation

I'm not much familiar with Laravel Service provider and I have a question about it.
Example: I have three classes SystemProfiler, SurveyProfiler and OfferProfiler which implements ProfilerInterface. And also I have ProfilerService class which inject ProfilerInterface in the constructor. I need to create different ProfilerService services with injection of each of that profilers.
ProfilerService:
class ProfilerService {
$this->profiler;
function __construct(ProfilerInterface $profiler) {
$this->profiler = profiler;
}
}
I know how to do that in symfony2 framework:
system_profiler:
class: App\MyBundle\Profiles\SystemProfiler
survey_profiler:
class: App\MyBundle\Profiles\SurveyProfiler
offer_profiler:
class: App\MyBundle\Profiles\OfferProfiler
system_profile_service:
class: App\MyBundle\Services\ProfilerService
arguments:
- system_profiler
survey_profile_service:
class: App\MyBundle\Services\ProfilerService
arguments:
- survey_profiler
offer_profile_service:
class: App\MyBundle\Services\ProfilerService
arguments:
- offer_profiler
and then just call $this->container->get() with alias of ProfilerService realization
But Laravel documentation said that "there is no need to bind classes into the container if they do not depend on any interfaces.". And ProfilerService not depend on interface. So I can bind each profiler to interface like so:
$this->app->bind('App\MyBundle\Contracts\ProfilerInterface','App\MyBundle\Profiles\SystemProfiler');
or
$this->app->bind('App\MyBundle\Contracts\ProfilerInterface','App\MyBundle\Profiles\SurveyProfiler');
or
$this->app->bind('App\MyBundle\Contracts\ProfilerInterface','App\MyBundle\Profiles\OfferProfiler');
but how I should bind which of the Profilers should be injected to the ProfilerService and when???
I would appreciate any help and explanations
Here it goes (read the docs):
// ServiceProvider
public function register()
{
// Simple binding
$this->app->bind('some_service.one', \App\ImplOne::class);
$this->app->bind('some_service.two', \App\ImplTwo::class);
// Aliasing interface - container will inject some_service.one
// whenever interface is required...
$this->app->alias('some_service.one', \App\SomeInterface::class);
// ...except for the Contextual Binding:
$this->app->when(\App\DependantTwo::class)
->needs(\App\SomeInterface::class)
->give('some_service.two');
}
USAGE:
$ php artisan tinker
// Aliases
>>> app('some_service.one')
=> App\ImplOne {#669}
>>> app('some_service.two')
=> App\ImplTwo {#671}
// Aliased interface
>>> app('App\SomeInterface')
=> App\ImplOne {#677}
>>> app('App\DependantOne')->dependency
=> App\ImplOne {#677}
// Contextual
>>> app('App\DependantTwo')->dependency
=> App\ImplOne {#676}
Given this setup:
namespace App;
class ImplOne implements SomeInterface {}
class ImplTwo implements SomeInterface {}
class DependantOne
{
public function __construct(SomeInterface $dependency)
{
$this->dependency = $dependency;
}
}
class DependantTwo
{
public function __construct(SomeInterface $dependency)
{
$this->dependency = $dependency;
}
}
The constructor of your ProfilerService typehints an interface, which means that your ProfilerService does depend on an interface.
Without any additional setup, if you attempted to App::make('App\MyBundle\Services\ProfilerService');, you would get an error because Laravel wouldn't know how to resolve the interface dependency.
When you then do $this->app->bind('App\MyBundle\Contracts\ProfilerInterface','App\MyBundle\Profiles\SystemProfiler'); in your service provider, you're telling Laravel "whenever you need to resolve a ProfilerInterface, create a new SystemProfiler".
With that binding setup, if you then attempted to App::make('App\MyBundle\Services\ProfilerService');, Laravel would create a new ProfilerService instance, and inject a new SystemProfiler instance in the constructor.
However, this isn't exactly what you want, since you have three different implementations of the ProfilerInterface. You don't want Laravel always injecting just one. In this case, you would create custom bindings, similar to what you've done in Symfony.
In your service provide, your bindings would look something like this:
$this->app->bind('system_profile_service', function($app) {
return $app->make('App\MyBundle\Services\ProfilerService', [$app->make('App\MyBundle\Profiles\SystemProfiler')]);
});
$this->app->bind('survey_profile_service', function($app) {
return $app->make('App\MyBundle\Services\ProfilerService', [$app->make('App\MyBundle\Profiles\SurveyProfiler')]);
});
$this->app->bind('offer_profile_service', function($app) {
return $app->make('App\MyBundle\Services\ProfilerService', [$app->make('App\MyBundle\Profiles\OfferProfiler')]);
});
Now, with those bindings setup, you resolve your custom bindings from the IOC whenever you need one.
$systemProfiler = App::make('system_profiler_service');
$surveyProfiler = App::make('survey_profile_service');
$offerProfiler = App::make('offer_profile_service');

Symfony 2.1 - Custom Json Annotation Listener

For a Symfony 2.1 project, I'm trying to create a new annotation #Json() that will register a listener that will create the JsonResponse object automatically when I return an array. I've got it working, but for some reason the listener is always called, even on methods that don't have the #Json annotation. I'm assuming my approach works, since the Sensio extra bundle does this with the #Template annotation.
Here is my annotation code.
<?php
namespace Company\Bundle\Annotations;
/**
* #Annotation
*/
class Json extends \Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationAnnotation
{
public function getAliasName()
{
return 'json';
}
}
Here is my listener code.
<?php
namespace Company\Bundle\Listener\Response\Json;
class JsonListener
{
//..
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
$data = $event->getControllerResult();
if(is_array($data) || is_object($data)) {
if ($request->attributes->get('_json')) {
$event->setResponse(new JsonResponse($data));
}
}
}
}
This is my yaml definition for the listener.
json.listener:
class: Company\Bundle\Listener\Response\Json
arguments: [#service_container]
tags:
- { name: kernel.event_listener, event: kernel.view, method: onKernelView }
I'm obviously missing something here because its being registered as a kernel.view listener. How do I change this so that it is only called when a #Json() annotation is present on the controller action?
Not pretend to be the definitive answer.
I'm not sure why your are extending ConfigurationAnnotation: its constructor accepts an array, but you don't need any configuration for your annotation. Instead, implement ConfigurationInterface:
namespace Company\Bundle\Annotations;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface;
/**
* #Annotation
*/
class Json implements ConfigurationInterface
{
public function getAliasName()
{
return 'json';
}
public function allowArray()
{
return false;
}
}
Sensio ControllerListener from SensionFrameworkExtraBundle will read your annotation (merging class with methods annotations) and perform this check:
if ($configuration instanceof ConfigurationInterface) {
if ($configuration->allowArray()) {
$configurations['_'.$configuration->getAliasName()][] = $configuration;
} else {
$configurations['_'.$configuration->getAliasName()] = $configuration;
}
}
Setting a request attribute prefixed with _. You are correctly checking for _json, so it should work. Try dumping $request->attributes in your view event listener. Be sure that your json.listener service is correctly loaded too (dump them with php app/console container:debug >> container.txt).
If it doesn't work, try adding some debug and print statements here (find ControllerListener.php in your vendor folder):
var_dump(array_keys($configurations)); // Should contain _json
Remember to make a copy of it before edits, otherwise Composer will throw and error when updating dependencies.

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