How to automatically call a service in symfony 4 - php

I have a service in my project
services:
exemple.annotation_reader:
class: App\Annotation\ModelAnnotationReader
public: true
arguments: ["#annotations.reader"]
calls:
- [getAnnotation]
And I want call the methode "getAnnotation" without adding this line in my controller
$this->get('exemple.annotation_reader')->getAnnotation();
And there is my function getAnnotation
public function getAnnotation(){
$reflClass = new \ReflectionMethod('App\Manager\BigDataTestManager', 'getAllData');
$classAnnotations = $this->reader->getMethodAnnotations($reflClass);
foreach ($classAnnotations AS $annot) {
if ($annot instanceof ModelAnnotation) {
print_r($annot);
}
}
}
the goal of my project is to create a custom annotation in symfony 4, I want show the annotation's parameters
/**
* #Model(
* namespace="App\Model\Exemple",
* version=50,
* types={"json","xml"}
* )
*/
so I want create a service who call a function automatically when there is an annotation on a function.

Related

Symfony 4 dependecy injection - Define constructor arguments depending on use case [duplicate]

This question already has answers here:
Symfony AutoWire multiple services same class
(3 answers)
Closed 1 year ago.
I have a generic service who uses a configuration to do its processing.
<?php
namespace App\Service;
class MyCustomService
{
/**
* #var array
*/
private $config;
/**
* MyCustomService constructor.
*
* #param array $config
*/
public function __construct(array $config)
{
$this->config = $config;
}
public function getConfig()
{
return $this->config;
}
}
I want to inject that service in my controller's actions. But with a specific configuration for each action.
<?php
namespace App\Controller;
use App\Service\MyCustomService;
class MyCustomController
{
public function action_1(MyCustomService $myCustomService)
{
/**
* $config must contain
* ['foo' => 'bar']
*/
$config = $myCustomService->getConfig();
}
public function action_2(MyCustomService $myCustomService)
{
/**
* $config must contain
* ['foo' => 'baz']
*/
$config = $myCustomService->getConfig();
}
}
How can I do that using config/services. Yaml?
Is there a way to configure a controller action?
Like this, for example:
services:
#...
#...
#...
App\Controller\MyCustomController:action_1:
arguments:
$myCustomService:
App\Service\MyCustomService:
arguments:
$config: {foo: 'bar'}
App\Controller\MyCustomController:action_2:
arguments:
$myCustomService:
App\Service\MyCustomService:
arguments:
$config: {foo: 'baz'}
I could use a config method inside MyCustomService, and call it inside every controller's action. But it's not that elegant.
You can define different instances (eg.: with different configurations) by doing something like
services:
App\Service\MyCustomService $s1:
config:
- foo: 'bar'
And inject in controller action like
public function action_1(MyCustomService $s1)
Arguments are matched by name and everytime you define an argument with that signature (class name + argument name) Symfony will inject the right instance.
You should also set autowire and register controllers as services

Symfony vich uploader and doctrine loggable extension problem?

I am using this two libraries to create an entity that has a picture using vich/uploader-bundle and I am logging entity changes history using the loggable doctrine extension provided from stof/doctrine-extensions-bundle which provides the extension from atlantic18/doctrineextensions.
So here is the problem: I have an entity that has a Vich uploadable picture field and it is using doctrine's Gedmo loggable extension with annotations.
/**
* #var VersionedFile
*
* #ORM\Embedded(class="App\Entity\Embedded\VersionedFile")
*
* #Gedmo\Versioned()
*/
private $picture;
/**
* #var File
*
* #Vich\UploadableField(
* mapping="user_picture",
* fileNameProperty="picture.name",
* size="picture.size",
* mimeType="picture.mimeType",
* originalName="picture.originalName",
* dimensions="picture.dimensions
* )
*/
private $pictureFile;
/**
* #var DateTimeInterface
*
* #ORM\Column(type="datetime", nullable=true)
*
* #Gedmo\Versioned()
*/
private $pictureUpdatedAt;
The embedded entity class App\Entity\Embedded\VersionedFile has all the needed annotations in order to version properly using the loggable doctrine extension.
// Not the whole code but just to get the idea for property versioning
/**
* #ORM\Column(name="name", nullable=true)
*
* #Gedmo\Versioned()
*/
protected $name;
And now the problem. When I upload the file and persist the entity the following thing happens. The entity manager persist the entity and the onFlush method of the Gedmo loggable listener (Gedmo\Loggable\LoggableListener) is called. This listeners checks the changes and schedules log entries to be inserted.
The problem is that the VichUploaders upload listener (Vich\UploaderBundle\EventListener\Doctrine\UploadListener) is called after the loggable listener and then the file is uploaded which changes the properties name, size, etc. The computed changes about name, size, etc. are not available in theLoggableListener` becaues it is called first and so it doesn't know that they should be inserted.
Am I missing some configuration or am I doing something wrong. The idea is to log changes made to the picture. For now in the database the log entries consist only of the $pictureUpdatedAt field.
I debugged the problem and all I can see is the order and that in LoggableListener the method getObjectChangeSetData is returning only the $pictureUpdatedAt field that has changed. I don't think this has something in common with the Embedded entity because I think the calling order of the listeners is the problem. The first idea I had was to change the listeners priority but even if I do that the order of the calling is not changed mainly because when onFlush is called it is triggering the preUpdate method which triggers the UploadListener of the uploader bundle.
You are correct, the root of the problem is the UploadListener listens to prePersist and preUpdate while the LoggableListener listens to onFlush. Since onFlush is triggered before preUpdate, file changes are never logged. This can be fixed in a few steps.
1. Create New UploadListener
First, you can write your own UploadListener to listen to onFlush instead.
// src/EventListener/VichUploadListener.php using Flex
// src/AppBundle/EventListener/VichUploadListener.php otherwise
namespace App\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Vich\UploaderBundle\EventListener\Doctrine\UploadListener;
class VichUploadListener extends UploadListener
{
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
$this->preUpdate(new LifecycleEventArgs($entity, $em));
}
// Required if using property namer on sluggable field. Otherwise, you
// can also subscribe to "prePersist" and remove this foreach.
foreach ($uow->getScheduledEntityInsertions() as $entity) {
// We use "preUpdate" here so the changeset is recomputed.
$this->preUpdate(new LifecycleEventArgs($entity, $em));
}
}
public function getSubscribedEvents(): array
{
return [Events::onFlush];
}
}
In this example, I reuse the original UploadListener to make things easier. Since we are listening to onFlush, it is important we recompute the entity changeset after the file is uploaded which is why I used the "preUpdate" method for both scheduled updates and inserts.
You do have to be careful when changing events like this. If you have another listener that expects the value of one of your file fields to be set (or unset), this may change the expected behavior. This is especially true if you use the second foreach to handle new uploads. prePersist is triggered before onFlush, so this would make new uploads get set later than before.
2. Create New CleanListener
Next, we now have to create a new CleanListener. This listener deletes old files when we update the file field if delete_on_update is set to true. Since it listens to preUpdate, we have to change it to onFlush so old files are properly deleted.
// src/EventListener/VichCleanListener.php on Flex
// src/AppBundle/EventListener/VichCleanListener.php otherwise
namespace App\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Vich\UploaderBundle\EventListener\Doctrine\CleanListener;
class VichCleanListener extends CleanListener
{
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
$this->preUpdate(new LifecycleEventArgs($entity, $em));
}
}
public function getSubscribedEvents(): array
{
return [Events::onFlush];
}
}
3. Configure New Listeners
Now, we need to override the default listeners in our config with the ones we just wrote.
# config/services.yaml on Flex
# app/config/services.yml otherwise
services:
# ...
vich_uploader.listener.upload.orm:
class: 'App\EventListener\VichUploadListener'
parent: 'vich_uploader.listener.doctrine.base'
autowire: false
autoconfigure: false
public: false
vich_uploader.listener.clean.orm:
class: 'App\EventListener\VichCleanListener'
parent: 'vich_uploader.listener.doctrine.base'
autowire: false
autoconfigure: false
public: false
4. Change Gedmo Extension Priorities
If all that wasn't enough, now comes the other problem you brought up: listener priority. At a minimum, we need to make sure LoggableListener is triggered after our upload/clean listeners. If you are using any of the other Gedmo extensions, you need to make sure they are loaded in the order you need them. The defaults set by VichUploaderExtension set the CleanListener to 50 and the UploadListener to 0. You can see the Gedmo Listener defaults in StofDoctrineExtensionsExtension.
For me, I have a property namer that depends on a sluggable field, so I want to make sure SluggableListener is called before the UploadListener. I also use softdeleteable and want soft deletes logged as "remove", so I want to make sure LoggableListener is registered before SoftDeleteableListener. You can change these priorities by overriding the services in your config.
# config/services.yaml on Flex
# app/config/services.yml otherwise
services:
# ...
stof_doctrine_extensions.listener.sluggable:
class: '%stof_doctrine_extensions.listener.sluggable.class%'
autowire: false
autoconfigure: false
public: false
calls:
- { method: 'setAnnotationReader', arguments: ['#annotation_reader'] }
tags:
- { name: 'doctrine.event_subscriber', connection: 'default', priority: 5 }
stof_doctrine_extensions.listener.loggable:
class: '%stof_doctrine_extensions.listener.loggable.class%'
autowire: false
autoconfigure: false
public: false
calls:
- { method: 'setAnnotationReader', arguments: ['#annotation_reader'] }
tags:
- { name: 'doctrine.event_subscriber', connection: 'default', priority: -1 }
stof_doctrine_extensions.listener.softdeleteable:
class: '%stof_doctrine_extensions.listener.softdeleteable.class%'
autowire: false
autoconfigure: false
public: false
calls:
- { method: 'setAnnotationReader', arguments: ['#annotation_reader'] }
tags:
- { name: 'doctrine.event_subscriber', connection: 'default', priority: -2 }
Alternatively, you could create a compiler pass to just change the priorities of the doctrine.event_subscriber tags for these services.
// src/DependencyInjection/Compiler/DoctrineExtensionsCompilerPass.php on Flex
// src/AppBundle/DependencyInjection/Compiler/DoctrineExtensionsCompilerPass.php otherwise
namespace App\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class DoctrineExtensionsCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$listenerPriorities = [
'sluggable' => 5,
'loggable' => -1,
'softdeleteable' => -2,
];
foreach ($listenerPriorities as $ext => $priority) {
$id = sprintf('stof_doctrine_extensions.listener.%s', $ext);
if (!$container->hasDefinition($id)) {
continue;
}
$definition = $container->getDefinition($id);
$tags = $definition->getTag('doctrine.event_subscriber');
$definition->clearTag('doctrine.event_subscriber');
foreach ($tags as $tag) {
$tag['priority'] = $priority;
$definition->addTag('doctrine.event_subscriber', $tag);
}
}
}
}
If you go this route, make sure to register the compiler pass with a higher priority (higher than 0) to ensure it is ran before RegisterEventListenersAndSubscribersPass.
// src/Kernel.php on Flex
// src/AppBundle/AppBundle.php otherwsie
// ...
use App\DependencyInjection\Compiler\DoctrineExtensionsCompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
// ...
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new DoctrineExtensionsCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 5);
}
Now, just ensure your cache is cleared.

Custom Sonata admin batch action error

followed by batch action documentation in sonata admin's website, created this custom batch action called AnalayseController.php:
<?php
namespace Admin\Store\Receipt\ReceiptBundle\Controller;
use Sonata\AdminBundle\Controller\CRUDController as BaseController;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class AnalyseController extends BaseController
{
/**
* #param ProxyQueryInterface $selectedModelQuery
* #param Request $request
*
* #return RedirectResponse
*/
public function batchActionAnalyse(ProxyQueryInterface $selectedModelQuery, Request $request = null)
{
$request = $this->get('request');
$modelManager = $this->admin->getModelManager();
$target = $modelManager->find($this->admin->getClass(), $request->get('targetId'));
if ($target === null){
$this->addFlash('sonata_flash_info', 'No target!');
return new RedirectResponse(
$this->admin->generateUrl('list', $this->admin->getFilterParameters())
);
}
$selectedModels = $selectedModelQuery->execute();
$this->addFlash('sonata_flash_success', 'Done');
return new RedirectResponse(
$this->admin->generateUrl('list', $this->admin->getFilterParameters())
);
}
}
but I'm getting this error: Catchable Fatal Error: Argument 1 passed to Admin\Store\Receipt\ReceiptBundle\Controller\AnalyseController::batchActionAnalyse() must implement interface Sonata\AdminBundle\Datagrid\ProxyQueryInterface, string given, called in /home/aien/Web/Mr Alef/MRA_Dev/app/cache/dev/appDevDebugProjectContainer.php on line 852 and defined.
looked everywhere, but couldn't find any solution!
Ok, I've fixed the issue just by changing serives.yml calls first argument to setTranslationDomain
admin_store_receipt_receipt.admin.analyse:
class: Admin\Store\Receipt\ReceiptBundle\Admin\ReceiptAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: admin, label: Receipt }
arguments:
- ~
- Admin\Store\Receipt\ReceiptBundle\Entity\Receipt
- AdminStoreReceiptReceiptBundle:Analyse
calls:
- [ setTranslationDomain , [AdminStoreReceiptReceiptBundle]]

Symfony 2 How To Dynamically Add Routes With Access To The Request Object

Background:
I am trying to conditionally load routes based on the request host. I have a database setup that has hosts in it that map to templates. If a user comes in from the host A and that uses template TA I want to load the routes for that template. If they come in from host B then load the routes for that template (TB).
The reason I have to do this is because each template will share many routes. There are however some unique routes for a given template.
It would be fine to restrict each template routes to a given host, except that there are literally 1000's of hosts.
What I Have Tried:
I have tried a custom route loader as described in the documentation here:
http://symfony.com/doc/current/cookbook/routing/custom_route_loader.html
However when i configure the service and try and inject the "#request" the constructor fails because $request is null
services:
acme_demo.routing_loader:
class: Acme\DemoBundle\Routing\ExtraLoader
arguments: ["#request"]
tags:
- { name: routing.loader }
Class:
<?php
namespace: Acme\DemoBundle\Routing;
use Symfony\Component\HttpFoundation\Request;
class ExtraLoader
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
// ...
}
This also doesnt work if I try and switch "#request" for "#service_container" then call
$this->container->get('request');
The closest I got to getting this working was following a guide found here:
http://marcjschmidt.de/blog/2013/11/30/symfony-custom-dynamic-router.html
The problem i have with this on is im trying to use annotation on a controller and i cant seem to get the Symfony\Component\Routing\Loader\AnnotationFileLoader working.
Ok I have finally figured out a working solution, Its a mix of several of the above mentioned guides.
Im using a kernel request listener:
services:
website_listener:
class: NameSpace\Bundle\EventListener\WebsiteListener
arguments:
- "#website_service"
- "#template_service"
- "#sensio_framework_extra.routing.loader.annot_dir"
- "#router"
- "%admin_domain%"
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 33 }
The Listener:
<?php
namespace NameSpace\WebsiteBundle\EventListener;
use NameSpace\TemplateBundle\Service\TemplateService;
use NameSpace\WebsiteBundle\Service\WebsiteService;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\PropertyAccess\PropertyAccess;
class WebsiteListener
{
/**
* #var WebsiteService
*/
protected $websiteService;
/**
* #var TemplateService
*/
protected $templateService;
/**
* #var AnnotationDirectoryLoader
*/
protected $loader;
/**
* #var Router
*/
protected $router;
/**
* #var string
*/
protected $adminDomain;
public function __construct(WebsiteService $websiteService, TemplateService $templateService, AnnotationDirectoryLoader $loader, Router $router, $adminDomain)
{
$this->websiteService = $websiteService;
$this->templateService = $templateService;
$this->loader = $loader;
$this->router = $router;
$this->adminDomain = $adminDomain;
}
public function loadRoutes()
{
$template = $this->templateService->getTemplateByAlias($this->websiteService->getWebsite()->getTemplate());
$routes = $this->loader->load($template['routes'],'annotation');
$allRoutes = $this->router->getRouteCollection();
$allRoutes->addCollection($routes);
}
public function onKernelRequest(GetResponseEvent $event)
{
try {
$this->websiteService->handleRequest($event->getRequest());
$this->loadRoutes();
} catch(NotFoundHttpException $e) {
if($event->getRequest()->getHost() !== $this->adminDomain){
throw $e;
}
}
}
}
The Key parts of this are:
The Loader - I found "#sensio_framework_extra.routing.loader.annot_dir" in the source code. That the annotation directory loader that symfony uses by default so thats the one that I want to use too. But if you want to use a different loader there are others available.
The Router - This is what i use to get all of the current routes. NOTE that the $allRoutes->addCollection($routes) call is on a seperate line. Im not sure why it makes a difference but calling it all in 1 like was not working.
$template['routes'] is just a namespaces controller reference like you would use to add routing in your routing.yml. Something like: "#NamespaceBundle/Controller"

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