I get some trouble when I would load symfony app:
php.CRITICAL: Type error: Argument 1 passed to Doctrine\Common\EventManager::addEventSubscriber() must implement interface Doctrine\Common\EventSubscriber, instance of optro\Help\ORM\Listener\MessageElasticaListener
See my service configuration:
helpdesk.listner.optro:
class: Optro\Help\ORM\Listener\MessageElasticaListener
arguments:
- '#fos_elastica.object_persister.optro.technical_assistance'
- '#fos_elastica.indexable'
- { index: technical_assistance, type: post, identifier: id }
tags:
- { name: doctrine.event_listener, event: elastica.index.index_post_populate }
This is my class MessageElasticaListener:
use Doctrine\Common\EventArgs;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use FOS\ElasticaBundle\Doctrine\Listener as ElasticaListener;
use optro\Help\Entity\HelpdeskMessage;
class MessageElasticaListener extends ElasticaListener
{
/**
* {#inheritdoc}
*/
private function isObjectIndexable($object)
{
return true;
}
/**
* {#inheritdoc}
*/
public function postPersist(LifecycleEventArgs $eventArgs)
{
if (!$eventArgs instanceof LifecycleEventArgs) {
return;
}
$entity = $eventArgs->getObject();
if ($entity instanceof HelpdeskMessage && $this->isObjectIndexable($entity->getTechnicalAssistance())) {
$this->objectPersister->replaceOne($entity->getTechnicalAssistance());
}
}
}
What's wrong ? Bad services configuration ?
I use symfony 3.4 and FOS Elasticasearch 5.0.3
Related
The application that I am building is not going to work in a traditional way. All the routes ar going to be stored in the database. And based on the route provided I need to get the correct controller and action to be executed.
As I understand this can be achieved using the "kernel.controller" event listener: https://symfony.com/doc/current/reference/events.html#kernel-controller
I am trying to use the docs provided, but the example here does not exacly show how to set up a new callable controller to be passed. And I have a problem here, because I dont know how to inject the service container to my newly called controller.
At first the setup:
services.yaml
parameters:
db_i18n.entity: App\Entity\Translation
developer: '%env(DEVELOPER)%'
category_directory: '%kernel.project_dir%/public/uploads/category'
temp_directory: '%kernel.project_dir%/public/uploads/temp'
product_directory: '%kernel.project_dir%/public/uploads/product'
app.supported_locales: 'lt|en|ru'
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
App\Translation\DbLoader:
tags:
- { name: translation.loader, alias: db }
App\Extension\TwigExtension:
arguments:
- '#service_container'
tags:
- { name: twig.extension }
App\EventListener\RequestListener:
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onControllerRequest }
The listener:
RequestListener.php
<?php
namespace App\EventListener;
use App\Controller\Shop\HomepageController;
use App\Entity\SeoUrl;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security;
class RequestListener
{
public ManagerRegistry $doctrine;
public RequestStack $requestStack;
public function __construct(ManagerRegistry $doctrine, RequestStack $requestStack)
{
$this->doctrine = $doctrine;
$this->requestStack = $requestStack;
}
/**
* #throws Exception
*/
public function onControllerRequest(ControllerEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
if(str_contains($this->requestStack->getMainRequest()->getPathInfo(), '/admin')) {
return;
}
$em = $this->doctrine->getManager();
$pathInfo = $this->requestStack->getMainRequest()->getPathInfo();
;
$route = $em->getRepository(SeoUrl::class)->findOneBy(['keyword' => $pathInfo]);
if($route instanceof SeoUrl) {
switch ($route->getController()) {
case 'homepage':
$controller = new HomepageController();
$event->setController([$controller, $route->getAction()]);
break;
default:
break;
}
} else {
throw new Exception('Route not found');
}
}
}
So this is the most basic example. I get the route from the database, if it a "homepage" route, I create the new HomepageController and set the action. However I am missing the container interface that I dont know how to inject. I get this error:
Call to a member function has() on null
on line: vendor\symfony\framework-bundle\Controller\AbstractController.php:216
which is:
/**
* Returns a rendered view.
*/
protected function renderView(string $view, array $parameters = []): string
{
if (!$this->container->has('twig')) { // here
throw new \LogicException('You cannot use the "renderView" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".');
}
return $this->container->get('twig')->render($view, $parameters);
}
The controller is as basic as it gets:
HomepageController.php
<?php
namespace App\Controller\Shop;
use App\Repository\CategoryRepository;
use App\Repository\Shop\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class HomepageController extends AbstractController
{
#[Route('/', name: 'index', methods: ['GET'])]
public function index(): Response
{
return $this->render('shop/index.html.twig', [
]);
}
}
So basically the container is not set. If I dump the $event->getController() I get this:
RequestListener.php on line 58:
array:2 [▼
0 => App\Controller\Shop\HomepageController {#417 ▼
#container: null
}
1 => "index"
]
I need to set the container by doing $controller->setContainer(), but what do I pass?
Do not inject the container, controllers are services too and manually instanciating them is preventing you from using constructor dependency injection. Use a service locator which contains only the controllers:
Declared in config/services.yaml:
# config/services.yaml
services:
App\EventListener\RequestListener:
arguments:
$serviceLocator: !tagged_locator { tag: 'controller.service_arguments' }
Then in the event listener, add the service locator argument and fetch the fully configured controllers from it:
# ...
use App\Controller\Shop\HomepageController;
use Symfony\Component\DependencyInjection\ServiceLocator;
class RequestListener
{
# ...
private ServiceLocator $serviceLocator;
public function __construct(
# ...
ServiceLocator $serviceLocator
) {
# ...
$this->serviceLocator = $serviceLocator;
}
public function onControllerRequest(ControllerEvent $event)
{
# ...
if($route instanceof SeoUrl) {
switch ($route->getController()) {
case 'homepage':
$controller = $this->serviceLocator->get(HomepageController::class);
# ...
break;
default:
break;
}
}
# ...
}
}
If you dump any controller you will see that the container is set. Same will go for additionnal service that you autowire from the constructor.
I am writing a Twig function in Symfony 4 but I cannot get it to work...
The extension class
<?php
namespace App\Twig;
use App\Utils\XXX;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class XXXExtension extends AbstractExtension
{
/**
* #return array|TwigFunction|TwigFunction[]
*/
public function getFunctions()
{
return new TwigFunction('showControllerName', [$this, 'showControllerName']);
}
public function showControllerName($sControllerPath)
{
return XXX::getControllerName($sControllerPath);
}
}
I have autowire set to true in services.yaml but just in case i tried with this also:
App\Twig\XXXExtension:
public: true
tags:
- { name: twig.extension }
usage in html.twig
{% set controllerName = showControllerName(app.request.get('_controller')) %}
and the response i get after this is:
HTTP 500 Internal Server Error
Unknown "showControllerName" function.
You need to return an array of functions, you are only returning one.
...
public function getFunctions()
{
return [
new TwigFunction('showControllerName', [$this, 'showControllerName']),
];
}
...
As an EntityListener is registered as a service, is it possible to register the same class multiple times with different argument and associate each of them with a particular entity ?
Considering the following entities :
/**
* Class EntityA
* #ORM\Entity
* #ORM\EntityListeners({"myBundle\EventListener\SharedListener"})
*/
class EntityA implements sharedBehaviourInterface
{
// stuff here
}
/**
* Class EntityB
* #ORM\Entity
* #ORM\EntityListeners({"myBundle\EventListener\SharedListener"})
*/
class EntityB implements sharedBehaviourInterface
{
// stuff here
}
I would like to register the following listener for both previous entities as this :
class SharedListener
{
private $usefulParameter;
public function __construct($usefulParameter)
{
$this->usefulParameter = $usefulParameter;
}
/**
* #PrePersist
*
*/
public function prePersist(sharedBehaviourInterface $dbFile, LifecycleEventArgs $event)
{
// code here
}
// more methods
}
Using :
mybundle.entitya.listener:
class: myBundle\EventListener\SharedListener
arguments:
- '%entitya.parameter%' # The important change goes here ...
tags:
- { name: doctrine.orm.entity_listener }
mybundle.entityb.listener:
class: myBundle\EventListener\SharedListener
arguments:
- '%entityb.parameter%' # ... and here
tags:
- { name: doctrine.orm.entity_listener }
It does not work, and I'm actually surprised that the EntityListener declaration in the Entity targets the Listener class and not the service. Is it possible to target a specific service instead ? Like :
#ORM\EntityListeners({"mybundle.entityb.listener"})
Or what I'm trying to do isn't even possible ?
You can inject other services into services with the #configured_service_id notation.This works for constructor arguments and setter injection.
Generally spoken: Do not try to find an abstraction where it isn't needed.
Most of the time a little code duplication is far easier in long term.
I would simply built two independent listeners for each purpose.
Do a simple check that jumps out of the handler if the Entity is NOT one of the two Entities that should be handled with the same listener:
<?php
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
class MyEventListener
{
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$entityManager = $args->getObjectManager();
if (!$entity instanceof EntityA && !$entity instanceof EntityB) {
return;
}
/* Your listener code */
}
}
Are you sure, this wouldn't do the trick:
public function prePersist(sharedBehaviourInterface $dbFile, LifecycleEventArgs $event)
{
$entity = $event->getObject();
if ($entity instanceof ClassA) {
// Do something
} elseif ($entity instanceof ClassB) {
// Something else
} else {
// Nah, none of the above...
return;
}
}
Is it possible, to make a time-based link / controller action in symfony2 in the annotation? With a start and a stopdate!?
For example:
/**
*#Route("/mylink", start="14.10.2015" stop="20.12.2015", name="mylink", schemes= { "http" })
public function myLinkAction()
{
.....
}
You cannot extend #Route that way but with defaults and I think the best solution without boilerplate code is a controller filter:
services.yml
services:
time_range_route_filter:
class: AppBundle\Services\TimeRangeRouteFilter
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onFilterController }
DefaultController.php
class DefaultController
{
/**
* #Route("/", name="homepage", defaults={"start"="2015-01-01", "end"="2016-01-01"})
*/
public function indexAction()
{
}
}
TimeRangeRouteFilter.php
class TimeRangeRouteFilter
{
public function onFilterController(FilterControllerEvent $event) {
$request = $event->getRequest();
$attributes = $request->attributes;
$routeParams = $attributes->get('_route_params');
$end = $routeParams['end'];
$start = $routeParams['start'];
if(!/* in range */) {
throw new NotFoundHttpException();
}
}
}
I need to validate a form field against bad words dictionary (Array for example). So to do this I have to create a new Constraint + ConstraintValidator. It works great, the only problem I have is that I want to have different dictionaries for different locales.
Example:
namespace MyNameSpace\Category\MyFormBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ContainsNoBadWordsValidator extends ConstraintValidator
{
protected $badWordsEN = array('blabla');
protected $badWordsFR = array('otherblabla');
public function validate($value, Constraint $constraint)
{
if (in_array(strtolower($value), array_map('strtolower', $this->getBadWords()))) {
$this->context->addViolation($constraint->message, array('{{ value }}' => $value));
}
}
protected function getBadWords($locale = 'EN')
{
switch ($locale) {
case 'FR':
return $this->badWordsFR;
break;
default:
return $this->badWordsEN;
break;
}
}
}
So how do I pass the locale to Constraint? Or should I implement it differently?
The locale parameter is a member of the Request object.
However, the request object is not created all the time (eg. in a CLI application)
This solution allows you to decouple your validation from the request object, and let your validation to be easily unit-tested.
The LocaleHolder is a request-listener which will hold upon creation the %locale% parameter and then, switch to the Request locale when the event is triggered.
Note: The %locale% parameter is the default parameter defined in config.yml
Your validator must then get this LocaleHolder as a constructor parameter, in order to be aware of the current locale.
services.yml
Here, declare the two services you will need, the LocaleHolder and your validator.
services:
acme.locale_holder:
class: Acme\FooBundle\LocaleHolder
arguments:
- "%locale%"
tags:
-
name: kernel.event_listener
event: kernel.request
method: onKernelRequest
acme.validator.no_badwords:
class: Acme\FooBundle\Constraints\NoBadwordsValidator
arguments:
- #acme.locale_holder
tags:
-
name: validator.constraint_validator
alias: no_badwords
Acme\FooBundle\LocaleHolder
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class LocaleHolder
{
protected $locale;
public function __construct($default = 'EN')
{
$this->setLocale($default);
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$this->setLocale($request->getLocale());
}
public function getLocale()
{
return $this->locale;
}
public function setLocale($locale)
{
$this->locale = $locale;
}
}
Acme\FooBundle\Constraints
use Acme\FooBundle\LocaleHolder;
class ContainsNoBadwordsValidator extends ConstraintValidator
{
protected $holder;
public function __construct(LocaleHolder $holder)
{
$this->holder = $holder;
}
protected function getBadwords($locale = null)
{
$locale = $locale ?: $this->holder->getLocale();
// ...
}
}