Lazy load services dynamically - symfony

After watching the Laravell Nova presentation I wanted to create similar functionality to Lenses in my own app.
I have the following concepts:
Entity: Standard Doctrine Entity
Resource: A class that describes a resource including the target entity and available lenses.
Lens: Has an method apply(Request $request, QueryBuilder $qb) that allow you to modify the QueryBuilder based on the Request.
The goal is to define all Lenses as a service and then somehow assign them to a Resource. This is the problem I'm trying to solve.
Attempt 1: Directly inject the Lenses into the resource
ProjectResource.php
<?php
class ProjectResource
{
protected $lenses = [];
public function __construct(
ProjectRepository $repository,
LensInterface $activeProjectLens,
LensInterface $starredProjectLens
) {
$this->lenses = [
$activeProjectLens,
$starredProjectLens
];
}
public function getLenses() {
return $this->lenses;
}
}
The downside of this is that each Lens service is instantiated and needs to be defined manually
Attempt 2: Inject tagged Lenses into the resource
In my services.yaml tag the services and assign them as an argument to the resource:
App\Lens\ActiveProjectLens:
tags: ['resource.project.lens']
App\Lens\StarredProjectLens:
tags: ['resource.project.lens']
App\Resource\ProjectResource:
arguments:
$lenses: !tagged resource.project.lens
ProjectResource.php
<?php
class ProjectResource
{
protected $lenses = [];
public function __construct(
ProjectRepository $repository,
iterable $lenses
) {
$this->lenses = $lenses;
}
public function getLenses() {
return $this->lenses;
}
}
The downside of this approach is every Lens service and Resource must be tagged and cannot be an auto-configured service.
**Attempt 3: Add a compiler pass **
I attempted to add the process() method to the Kernel but I didn't get too far with that.
My goal is to define a list of services somehow in the Resource and have them injected. Is there any established pattern for this?

Your approach with the tags seems good. Symfony provides a way to automatically add tags to classes that implement a certain interface: Interface-based service configuration.
To use that you have to do the following:
If you don't already have one, create an interface (e.g. App\Lens\LensInterface) and let your lens classes implement the interface.
In your services.yaml file add this config:
services:
// ...
_instanceof:
App\Lens\LensInterface:
tags: ['resource.project.lens']
App\Resource\ProjectResource:
arguments:
$lenses: [!tagged resource.project.lens]
// ...
Then every class implementing your LensInterface would be injected into the ProjectResource without having to explicitly configure every single lens.

Related

Symfony override autowired services

I'm writing a Symfony 4 bundle and inside, in a compiler pass, I create multiple service definitions based on an abstract one (also enabling autowiring based on the argument name):
$managerDefinition = new ChildDefinition(Manager::class);
$managerDefinition->replaceArgument(0, $managerName);
...
$container->registerAliasForArgument($managerId, Manager::class, $managerName . 'Manager');
And this is the abstract service definition:
services:
MyBundle\Manager:
abstract: true
arguments:
- # manager name
So, in my App controller I can have this and it works correctly:
public function __construct(MyBundle\Manager $barManager)
{
// $barManager is MyBundle\Manager
}
Now, let's say at some point I decide to extend the Manager class in my App with additional methods:
class MyManager extends \MyBundle\Manager
{
public function newMethod() {
...
}
}
I override the bundle's abstract service like this:
services:
MyBundle\Manager:
class: App\Manager
abstract: true
arguments:
- # manager name
Everything still works as expected:
public function __construct(MyBundle\Manager $barManager)
{
// $barManager is App\Manager
$barManager->newMethod(); // Works
}
However, the IDE complains that newMethod() does not exist, as it doesn't exist in the typehinted MyBundle\Manager.
So, it seems more correct to change my constructor definition to let it know the actual class it's going to receive:
public function __construct(App\Manager $barManager)
However, I can't write this, as auto-wiring no longer works.
I suppose I could write a compiler pass in my App that registers autowiring for my custom App\Manager, but that seems like an overkill.
I can't shake the feeling that I'm doing something fundamentally wrong.
I guess my question is, what would be the best way to allow easy overriding of the abstract Manager definition in the bundle?

Call a method on a service created dynamically by a bundle

I am using the m6web_guzzle bundle to register several http clients:
m6web_guzzlehttp:
clients:
myclient:
timeout: 3
headers:
"Accept": "application/json"
delay: 0
verify: false
I want to call a method on a service that it dynamically generates. In this case the generated service name is:
#m6web_guzzlehttp.guzzle.handlerstack.myclient
Here is what I do in my service constructor: (the 3rd parameter injected is '#m6web_guzzlehttp.guzzle.handlerstack.myclient')
/**
* #param array $parameters
* #param Client $client
* #param HandlerStack $handlerStack
*/
public function __construct(array $parameters, Client $client, HandlerStack $handlerStack)
{
$this->parameters = $parameters;
$this->client = $client;
$this->handlerStack->push(Middleware::retry([$this, 'retryDecider']));
}
So far, it works well, but how can I transfer the last line (the push call) in my services.yml file? Or another cleaner method to register this retry handler?
So compiler passes were mentioned before. That is one option.
Use factories to create instances
But you can nearly express this also directly in your services definition. I say nearly, because you will need some kind of code as Symfony service definitions cannot (AFAIK) evaluate to a Closure - which is what we need for the Guzzle Middleware.
I wrote up this services.yml as an example:
m6web_guzzlehttp.guzzle.handlerstack.myclient:
class: GuzzleHttp\HandlerStack
factory: ['GuzzleHttp\HandlerStack', create]
retry_decider:
class: MyBundle\RetryDecider
factory: ['MyBundle\RetryDecider', createInstance]
retry_handler:
class: GuzzleHttp\Middleware
factory: ['GuzzleHttp\Middleware', retry]
arguments:
- '#retry_decider'
handlerstack_pushed:
parent: m6web_guzzlehttp.guzzle.handlerstack.myclient
calls:
- [push, ['#retry_handler']]
What is what?
m6web_guzzlehttp.guzzle.handlerstack.myclient - Your dynamic service - remove from example as you have this already created.
retry_decider - Your decider. We return a Closure in the createInstance method. You can add more parameters if you need, just add arguments to your YML.
retry_handler - Here we compose the middleware using our decider
handlerstack_pushed - Here we push() our handler into the stack, using the dynamic service as a parent service.
Et voilà - we have the stack that the dynamic service defined, but pushed our retry middleware.
Here is the source for our decider:
<?php
namespace MyBundle;
class RetryDecider {
public static function createInstance() {
return function() {
// do your deciding here
};
}
}
--> You now have the service handlerstack_pushed which is the complete Stack.
Configuring more
Please note that you could add m6web_guzzlehttp.guzzle.handlerstack.myclient to parameters.yml:
parameters:
baseHandlerStackService: m6web_guzzlehttp.guzzle.handlerstack.myclient
Then use that on handlerstack_pushed:
handlerstack_pushed:
parent: "%baseHandlerStackService%"
calls:
- [push, ['#retry_handler']]
Just nicer like that ;-)
In your bundle's Extension.php file, you can override the load method and add:
$definition = $container->getDefinition('m6web_guzzlehttp.guzzle.handlerstack.myclient');
$definition->addMethodCall('push', [Middleware::retry([$this, 'retryDecider'])]);
You can write a compiler pass that grabs the definition in question and adds the method call to it.

Service in symfony2 - how service file should look like?

I am trying to create service in symfony2 which will verify if session contains certain information and if not redirect the user to another controller. I want this piece of code to work as a service as I will be using it in many controllers.
I have problem as manual on Symfony2 book does not provide information how service file should look like. Should it be a normal php class?
Please find below dump of my files with information on error that I receive.
In \AppBundle\Services I create file my_isbookchosencheck.php containing:
<?php
namespace AppBundle\my_isbookchosencheck;
class my_isbookchosencheck
{
public function __construct();
{
$session = new Session();
$session->getFlashBag()->add('msg', 'No book choosen. Redirected to proper form');
if(!$session->get("App_Books_Chosen_Lp")) return new RedirectResponse($this->generateUrl('app_listbooks'));
}
}
My service.yml:
my_isbookchosencheck:
class: AppBundle\Services\my_isbookchosencheck
My conntroller file:
/**
* This code is aimed at checking if the book is choseen and therefore whether any further works may be carried out
*/
$checker = $this->get('my_isbookchosencheck');
Error:
FileLoaderLoadException in FileLoader.php line 125: There is no extension able to load the configuration for "my_isbookchosencheck" (in C:/wamp/www/symfony_learn/app/config\services.yml). Looked for namespace "my_isbookchosencheck", found "framework", "security", "twig", "monolog", "swiftmailer", "assetic", "doctrine", "sensio_framework_extra", "fos_user", "knp_paginator", "genemu_form", "debug", "acme_demo", "web_profiler", "sensio_distribution" in C:/wamp/www/symfony_learn/app/config\services.yml (which is being imported from "C:/wamp/www/symfony_learn/app/config\config.yml").
There are few mistakes that you made, which I am going to explain in short, and I will give you an example of the service you want to create.
You created your service in AppBundle\Services, yet your namespace is registered differently - namespace AppBundle\Services\my_isbookchosencheck;. It should be namespace AppBundle\Services;. I would also advise you to use singular names when creating directories - in this case Service would be better, instead of Services.
You're using your __constructor directly to apply some logic and return the result of it. Better way would be to create a custom method, which could be accessed when necessary.
You're creating new instance of Session which means that you wont be able to access anything that was previously added and stored in session. The right way here, would be to inject RequestStack which holds the current Request and get the session from there.
I believe you also registered your service wrong. In your services.yml file, it should be under services: option. This is why you got the error you pasted.
So, let's see how your service should like.
services.yml
services:
book_service:
class: AppBundle\Service\BookService
arguments:
- #request_stack
- #router
BookService.php
namespace AppBundle\Service;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
class BookService {
/* #var $request Request */
private $request;
/* #var $router RouterInterface */
private $router;
public function __construct(RequestStack $requestStack, RouterInterface $router) {
$this->request = $requestStack->getCurrentRequest();
$this->router = $router;
}
public function isBookChoosen() {
$session = $this->request->getSession();
// Now you can access session the proper way.
// If anything was added in session from your controller
// you can access it here as well.
// Apply your logic here and use $this->router->generate()
}
}
Now in your controller you can simply use it like this:
$this->get('book_service')->isBookChoosen()
Well this is a short example, but I hope you got the idea.
try
services:
my_isbookchosencheck:
class: AppBundle\Services\my_isbookchosencheck
in your services.yml, and check that you use the correct namespaces.
Your Class is fine and it should work, however may i suggest that you use
symfony2 session service instead of creating the session object yourself, you can pass it as a constructor argument:
<?php
// namespace edited
namespace AppBundle\Services;
use Symfony\Component\HttpFoundation\Session\Session;
class my_isbookchosencheck
{
public function __construct(Session $session);
{
$session->getFlashBag()->add('msg', 'No book choosen. Redirected to proper form');
if(!$session->get("App_Books_Chosen_Lp")) return new RedirectResponse($this->generateUrl('app_listbooks'));
}
}
and then edit your services.yml accordingly, so the service container will inject the session object:
services:
my_isbookchosencheck:
class: AppBundle\Services\my_isbookchosencheck
arguments: [#session]
Also check out his question on so:
How do you access a users session from a service in Symfony2?
Services are just regular PHP classes, nothing special. But you must register it in order to be recognized by the system. Here are the steps how you do it,
Create a regular PHP class (you can inject other services if it requires)
namespace Acme\DemoBundle\Service;
class MyService
{
private $session;
public function _construct(SessionInterface $session /* here we're injecting the session service which implements the SessionInterface */)
{
$this->session = $session;
}
// other methods go here, which holds the business logic of this class
}
ok, we created a class, we need to register it to be able to use it by service container, here how you do it:
the simplest way is to put it into config.yml file, like this:
services:
my_service:
class: Acme\DemoBundle\Service\MyService
arguments:
- #session
or, another way, is to create a file (e.g. services.yml, may be in config folder), and import it inside the config.yml file (the content of the file is the same as the first way):
imports:
- { resource: services.yml }
or, you can create a services.yml(the content of the file is the same as the first way) file inside you bundle's Resources folder, specify it under the load method of your Extension class (under the DependencyInjection folder), (this way requires some special directory and file structure, read about it in the doc):
class AcmeDemoExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources'));
$loader->load('services.yml');
}
}
In you case, you're not registering your service, the service container just couldn't find it. Register it by one of the above ways.

Symfony2; domain/host based info in controller and base template

I'm building an symfony2 app that is configurable up to some point based on what domain is used to access the site.
For ease of this question, lets say there is an "Domain" entity in the database containing the hostname and further configuration.
Think about minor template differences, some differences in header/footer. A difference in products being offered.
The routes available would not be different.
There are 2 places where I need this Domain object.
* in a Controller::action
* in a base template (even if the controller didn't need it)
I would not need it somewhere else, if I did, I could simply pass it from the controller.
What would be the best way to get this object without creating too much overhead and not fetching it when we don't actually need it.
Some thoughts I got so far:
* I could override the ControllerResolver and determine the Domain object based on the Request object. Although I don't seem to have access to the ServiceContainer there.
* I could add some method to a BaseController that can retrieve the domain for me when I'm in a Controller:Action.
* For usage in the template I could create a TwigExtension that adds a global variable. But it would need access to the Request object or RequestStack. Also, this would only help me in the template, I might be doing the same thing twice.
Any suggestions what might be a good approach here?
Don't know if this is the best solution, but worked well for me so far.
Since the domain information depends on the request it is NOT a service, so don't try to inject it in services or you'll get a bad headache. The most natural place to set information about the domain is in the request, and allow the controllers to read this information to interact with the services.
So, you can setup a Kernel event listeners which read the information from the database and set a domain Request attribute, like this:
<?php
namespace Acme\SiteBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Doctrine\ORM\EntityRepository;
class DomainSubscriber implements EventSubscriberInterface
{
protected $domainRepository;
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'onKernelRequest'
);
}
public function __construct(EntityRepository $domainRepository)
{
$this->domainRepository = $domainRepository;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
// Console/CLI commands don't have Domain info
if ($request === null)
return;
$domain = $this->domainRepository->find($request->getHost());
if ($domain === null)
throw new \RuntimeException(sprintf("Cannot find domain %s", $request->getHost()));
$request->attributes->set('domain', $domain);
}
}
Which must be registered in services.yml (or XML) with:
acme_site.manager:
class: Doctrine\ORM\EntityManager
factory_service: doctrine
factory_method: getManager
acme_site.domain_repository:
class: Doctrine\ORM\EntityRepository
factory_service: acme_site.manager
factory_method: getRepository
arguments:
- 'AcmeSiteBundle:Domain'
acme_site.domain_subscriber:
class: Acme\SiteBundle\EventListener\DomainSubscriber
arguments:
- "#acme_site.domain_repository"
tags:
- { name: kernel.event_subscriber }
In your Controller you can now access the data by simply doing this:
public function someAction(Request $request) {
$domain = $request->attributes->get('domain');
$domain->getWhatever();
}
And in Twig you can always access the request with this:
{% set domain = app.request.attributes.get('domain') %}
whatever: {{ domain.whatever }}
Hope this help!
DISCLAIMER: the code is copy-pasted and then edited, so it may contain some minor error.
NOTE: If you really need to inject the request in services, then I suggest you to read the docs about the RequestStack (Symfony 2.4+), or use a setRequest method and take care of container scopes.

Symfony2-How to use access a service from outside of a controller

In my Symfony2 controller, this works fine:
$uploadManager = $this->get('upload.upload_manager');
but when I move it to a custom Listener:
use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\UploadBundle\Upload\UploadManager;
class PersonChange
{
public function postRemove(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
$uploadManager = $this->get('ep_upload.upload_manager');
echo "the upload dir is " . $uploadManager->getUploadDir();
}
}
I get an error:
Fatal error: Call to undefined method Acme\MainBundle\Listener\PersonChange::get() in /home/frank/...
I know I must need a use statement but don't know what to use.
Update: Defining controllers as services is no longer officially recommended in Symfony.
The get() method in the Controller class is just a helper method to get services from the container, and it was meant to get new Symfony2 developers up to speed faster. Once people get comfortable with the framework and dependency injection, it's recommended to define controllers as services and inject each required service explicitly.
Since your PersonChange class is not a controller and doesn't extend the Controller class, you don't have that get() helper method. Instead, you need to define your class as a service and inject needed services explicitly. Read the Service Container chapter for details.
As I ran into the exact same problem maybe I can help
What Elnur said is perfectly fine and I'll just try to pop up a real life example.
In my case I wanted to access
$lucenemanager = $this->get('ivory.lucene.manager')
Even by extending the controller I couldn't get it to work while the controller does access the container (I still did not understand why)
In config.yml my listener (searchindexer.listener) is declared as follow :
services:
searchindexer.listener:
class: ripr\WfBundle\Listener\SearchIndexer
arguments:
luceneSearch: "#ivory_lucene_search"
tags:
- { name: doctrine.event_listener, event: postPersist }
A service (ivory.lucene.search) is passed as argument in my service/listener.
Then in my class
protected $lucenemanager;
public function __construct($luceneSearch)
{
$this->lucenemanager = $luceneSearch;
}
Then you can use the get method against $this
An approach that always works, despite not being the best practice in OO
global $kernel;
$assetsManager = $kernel->getContainer()->get('acme_assets.assets_manager');‏
If you need to access a Service, define it in the class constructor:
class PersonChange{
protected $uploadManager;
public function __construct(UploadManager $uploadManager){
$this->uploadManager = $uploadManager;
}
// Now you can use $this->uploadManager.
}
Now you can pass the Service as argument when calling the class (example 1) or define the clas itself as a Service (recommended, example 2)
Example 1:
use Acme\PersonChange;
class appController{
function buzzAction(){
$uploadManager = $this->get('upload.upload_manager');
$personChange = new PersonChange($uploadManager);
Example 2 (better):
Define PersonChange as a Service itself, and define the other Service as an argument in services.yml file:
# app/config/services.yml
services:
upload.upload_manager:
class: AppBundle\uploadManager
PersonChange:
class: AppBundle\PersonChange
arguments: ['#upload.upload_manager']
In this way, you don't have to bother with the upload_manager service in the Controller, since it's implicitely passed as an argument for the constructor, so your Controller can be:
class appController{
function buzzAction(){
$personChange = $this->get('PersonChange');

Resources