Symfony: Decorating a service when user has x role - php

I have been looking for an answer to this question, but I can not seem to find it anywhere.
I have currently defined a decorator service that decorates the translator service. I however want to decorate the translator service only when the user has a certain role.
services.yml
services:
app.my_translator_decorator:
class: MyBundle\MyTranslatorDecorator
decorates: translator
arguments: ['#app.my_translator_decorator.inner']
public: false
MyTranslatorDecorator.php
class MyTranslatorDecorator {
/**
* #var TranslatorInterface
*/
private $translator;
/**
* #param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
// more code...
}

The container is "compiled" before the runtime. You can't decorate a service depending of the context, it will always be decorated.
However, in your decorator, you can add a guard clause to not execute your custom code if not necessary.
Service definition:
services:
app.my_translator_decorator:
class: AppBundle\MyTranslatorDecorator
decorates: translator
arguments: ['#app.my_translator_decorator.inner', '#security.authorization_checker']
public: false
Decorator:
<?php
namespace AppBundle;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Translation\TranslatorInterface;
class MyTranslatorDecorator implements TranslatorInterface
{
private $translator;
private $authorizationChecker;
public function __construct(TranslatorInterface $translator, AuthorizationCheckerInterface $authorizationChecker)
{
$this->translator = $translator;
$this->authorizationChecker = $authorizationChecker;
}
public function trans($id, array $parameters = [], $domain = null, $locale = null)
{
if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
return $this->translator->trans($id, $parameters, $domain, $locale);
}
// return custom translation here
}
// implement other methods
}

Related

Test Service with phpunit inside symfony


How can I test Services with PHPUnit using symfony? So far, I installed and included test-pack, DAMA Doctrine Bundle, and created Test Database.
Inside .env.test I added Database connection
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
# .env.test.local
DATABASE_URL="mysql://root:root#db:3306/testdb?serverVersion=mariadb-10.4.11&charset=utf8mb4"
I included inside phpunit.xml.dist the DAMA Doctrine bundle
<extensions>
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>
Now, what I want to test is my Services (for instance CartService, ProductService etc.)
use App\Entity\Cart;
use App\Entity\CartItem;
use App\Entity\Product;
use App\Entity\User;
use App\Repository\CartItemRepository;
use App\Repository\CartRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Security;
class CartService
{
private CartRepository $cartRepository;
private ManagerRegistry $managerRegistry;
private CartItemRepository $cartItemRepository;
private Security $security;
public function __construct(Security $security, CartItemRepository $cartItemRepository, CartRepository $cartRepository, ManagerRegistry $managerRegistry)
{
$this->cartItemRepository = $cartItemRepository;
$this->cartRepository = $cartRepository;
$this->managerRegistry = $managerRegistry;
$this->security = $security;
}
/**
* Get Cart by ID
*
* #return Cart|null
*/
public function getCartByUserId(): ?Cart
{
$user = $this->security->getUser();
return $this->cartRepository->findOneBy(['customer' => $user]);
}
/**
* Show Cart and Total Price
*
* #return Cart|null
*/
public function showCart(): ?Cart
{
$cart = $this->getCartByUserId();
$this->calculateTotalPrice();
return $cart;
}
When I run phpunit test on CartServiceTest, I get this error:
1) App\Tests\CartServiceTest::testShowCart
Error: Typed property App\Tests\CartServiceTest::$cartService must not be accessed before initialization
/var/www/html/Tests/CartServiceTest.php:29
CartServiceTest look like this
<?php
namespace App\Tests;
use App\Entity\Product;
use App\Service\CartService;
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class CartServiceTest extends KernelTestCase
{
/**
* #var EntityManager
*/
private EntityManager $entityManager;
private CartService $cartService;
public function setUp(): void
{
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
}
public function testShowCart()
{
$user = 11;
$cart = $this->cartService->getCartByUserId();
dump($cart);
}
protected function tearDown(): void
{
$this->entityManager->close();
}
}
Error: Typed property App\Tests\CartServiceTest::$cartService must not be accessed before initialization
Means that you have to already Use the cartService in your application. For exemple if you already inject this service has a dependency injection in one of your controller it's okay.
But you can do better. Just create a service config for your tests "services_test.yaml" and make your service public
Something like:
#servies_test.yaml
services:
App\Service\CartService:
public: true

How to change the user locale with Symfony 4?

I'm trying to change the user locale with Symfony from a field "locale" in the database. I read the Symfony manual (how to sticky a session for example), but nothing works in my application. Translator still gets the default locale...
I created listeners, subscribers... to dynamically change the locale, but as they are loaded before the firewall listener, I'm unable to change the current value.
I tried to change the priority subscriber, but I lost the user entity. I tried to set locale request in controllers, but I think it's too late.
I don't want to add locales in URLs.
Here my subscriber - listener - code:
public function onKernelRequest(RequestEvent $event)
{
$user = $this->tokenStorage->getToken()->getUser();
$request = $event->getRequest();
$request->setLocale($user->getLocale());
}
In subscribers, I added:
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => [['onKernelRequest', 0]],
];
}
Here, my full code:
framework.yml:
default_locale: fr
services.yml:
parameters:
locale: 'fr'
app_locales: fr|en|
translation.yml:
framework:
default_locale: '%locale%'
translator:
paths:
- '%kernel.project_dir%/translations'
fallbacks:
- '%locale%'
LocaleSubscriber.php:
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LocaleSubscriber implements EventSubscriberInterface
{
private $defaultLocale;
public function __construct($defaultLocale = 'en')
{
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
// try to see if the locale has been set as a _locale routing parameter
if ($locale = $request->attributes->get('_locale')) {
$request->getSession()->set('_locale', $locale);
} else {
// if no explicit locale has been set on this request, use one from the session
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 20]],
];
}
}
UserLocaleSubscriber.php
// src/EventSubscriber/UserLocaleSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
/**
* Stores the locale of the user in the session after the
* login. This can be used by the LocaleSubscriber afterwards.
*/
class UserLocaleSubscriber implements EventSubscriberInterface
{
private $session;
public function __construct(SessionInterface $session)
{
$this->session = $session;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if (null !== $user->getLocale()) {
$this->session->set('_locale', $user->getLocale());
}
}
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin',
];
}
}
Ex controller annotation:
/**
* #Route("/user/locale", name="user_locale", requirements={"_locale" = "%app.locales%"})
* #Route("/{_locale}/user/locale", name="user_locale_locale", requirements={"_locale" = "%app.locales%"})
*/
Find the priority of the firewall listener using debug:event kernel.request.
Make sure your UserLocaleSubscriber is executed right after the firewall listener.
Autowire the TranslatorInterface and manually set the translator locale.
// src/EventSubscriber/UserLocaleSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Stores the locale of the user in the session after the
* login. This can be used by the LocaleSubscriber afterwards.
*/
class UserLocaleSubscriber implements EventSubscriberInterface
{
private $session;
private $translator;
public function __construct(SessionInterface $session, TranslatorInterface $translator)
{
$this->session = $session;
$this->translator = $translator;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if (null !== $user->getLocale()) {
$this->translator->setLocale($user->getLocale());
}
}
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => ['onInteractiveLogin', 7]
];
}
}
It's hard to help without seeing the full code you are using.
Symfony has it's own LocaleListener as well. Make sure your's is executed first.
/**
* #return array
*/
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 20]],
];
}

Cannot load translation from database in symfony 4.3.3

i'm trying to load translations from database in Symfony 4. The Translator instance doesn't call the custom loader i wrote using this tutorial (https://medium.com/#andrew72ru/store-translation-messages-in-database-in-symfony-3f12e579df74).
I created dummy files in the /translation folder (messages.it.db) to trigger the loader but it doesn't get called.
services.yaml
parameters:
locales: ['it','en']
db_i18n.entity: App\Entity\Translation
services:
translation.loader.db:
class: App\Loader\DbLoader
arguments:
- '#service_container'
- '#doctrine.orm.entity_manager'
tags:
- { name: translation.loader, alias: db}
DbLoader.php
namespace App\Loader;
use Creative\DbI18nBundle\Interfaces\EntityInterface;
use Creative\DbI18nBundle\Interfaces\TranslationRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;
class DbLoader implements LoaderInterface
{
/**
* #var EntityManagerInterface
*/
private $doctrine;
/**
* #var string
*/
private $entityClass;
public function __construct(ContainerInterface $container, EntityManagerInterface $doctrine)
{
$this->doctrine = $doctrine;
$this->entityClass = $container->getParameter('db_i18n.entity');
}
public function load($resource, $locale, $domain = 'messages')
{
$messages = $this->getRepository()->findByDomainAndLocale($domain, $locale);
$values = array_map(static function (EntityInterface $entity) {
return $entity->getTranslation();
}, $messages);
$catalogue = new MessageCatalogue($locale, [
$domain => $values
]);
return $catalogue;
}
public function getRepository(): TranslationRepositoryInterface
{
return $this->doctrine->getRepository($this->entityClass);
}
}
Here's my translation table
Here is the test code i'm using to call the Translator
TestController.php
class TestController extends AbstractController
{
/**
* #Route("/test", name="test")
*/
public function index(TranslatorInterface $translator)
{
$translator->trans('prova', [], 'messages', 'it');
return new Response();
}
}
The result is supposed to be "prova it" but I get "prova" instead, which is the key of the translation. I tried to put a dd() on the DbLoader constructor and it's never been called.
I also have in my project Api Platform, but i don't think it's causing this problem.
I resolved my issue.
By using dd() on my Translator instance i discovered that Symfony wasn't loading my translation files correctly. Looking through the properties i noticed the path of my translation files were not correct.
I placed them in src/Resources/translations instead and then it worked!

Symfony 4.2 - How to decorate the UrlGenerator

I want to decorate the Symfony UrlGenerator class.
Symfony\Component\Routing\Generator\UrlGenerator: ~
my.url_generator:
class: AppBundle\Service\UrlGenerator
decorates: Symfony\Component\Routing\Generator\UrlGenerator
arguments: ['#my.url_generator.inner']
public: false
I've added this to the services.yml but my AppBundle\Service\UrlGenerator class is ignored:
I tried the following configuration again.
config/services.yaml
parameters:
locale: 'en'
router.options.generator_class: AppBundle\Service\UrlGenerator
router.options.generator_base_class: AppBundle\Service\UrlGenerator
Still it doesn't work
How to decorate the UrlGenerator in Symfony 4.2?
The right answer is : you shouldn't decorate UrlGeneratorInterface.
You have to decorate 'router' service. Check here : https://github.com/symfony/symfony/issues/28663
** services.yml :
services:
App\Services\MyRouter:
decorates: 'router'
arguments: ['#App\Services\MyRouter.inner']
** MyRouter.php :
<?php
namespace App\Services;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouterInterface;
class MyRouter implements RouterInterface
{
/**
* #var RouterInterface
*/
private $router;
/**
* MyRouter constructor.
* #param RouterInterface $router
*/
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
/**
* #inheritdoc
*/
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
// Your code here
return $this->router->generate($name, $parameters, $referenceType);
}
/**
* #inheritdoc
*/
public function setContext(RequestContext $context)
{
$this->router->setContext($context);
}
/**
* #inheritdoc
*/
public function getContext()
{
return $this->router->getContext();
}
/**
* #inheritdoc
*/
public function getRouteCollection()
{
return $this->router->getRouteCollection();
}
/**
* #inheritdoc
*/
public function match($pathinfo)
{
return $this->router->match($pathinfo);
}
}
I believe the issue is that UrlGenerator service name is Symfony\Component\Routing\Generator\UrlGeneratorInterface, and not Symfony\Component\Routing\Generator\UrlGenerator (cf. this code).
Secondly, when you decorate a service, the decorator will take the service name. So you should not need to modify router.options.generator_class.
Try with this configuration:
my.url_generator:
class: AppBundle\Service\UrlGenerator
decorates: Symfony\Component\Routing\Generator\UrlGeneratorInterface
arguments: ['#my.url_generator.inner']
Setting public to false is likely not needed, as on Symfony4/Flex it should be the default value.
Update for comments:
decorated service may look like this:
class MyUrlGenerator implements UrlGeneratorInterface
{
private $originalUrlGenerator;
public function __construct(UrlGeneratorInterface $innerUrlGenerator)
{
$this->originalUrlGenerator = $innerUrlGenerator;
}
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
// Maybe add your custom logic here...
// or completely override base method
return $this->originalUrlGenerator->generate($name, $parameters, $referenceType);
}
}
I believe you have to decorate the Symfony\Component\Routing\Generator\UrlGeneratorInterface, because the services should depend on the interface and not on a specific implementation (class).

Symfony 3 Inject service into entity repository

I tried inject memcached service into entity repository, but my variant not work.
services:
work.repository.company:
class: WorkBundle\Repository\CompanyRepository
factory: ['#doctrine.orm.entity_manager', getRepository]
arguments:
- 'WorkBundle:Company'
calls:
- [setCacheService, ['#memcache.default']]
CompanyRepository have setter setCacheService, but it's not called.
class CompanyExtension extends \Twig_Extension
{
/**
* #var EntityManager
*/
private $em;
public function setEntityManager(EntityManager $entityManager)
{
$this->em = $entityManager;
}
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('getCompaniesCount', array($this, 'getCompaniesCount'))
);
}
/**
* #return integer
*/
public function getCompaniesCount()
{
return $this->em->getRepository('WorkBundle:Company')->getActiveCompaniesCount();
}
public function getName()
{
return 'work_company_extension';
}
}
Why this code not works?
Have you registered repositoryClass in your WorkBundle:Company entity? Your entity should contain something like: #ORM\Entity(repositoryClass="Work\Company") or yaml equivalent.
You should let Symfony create the repository by injecting work.repository.company into your Twig extension.

Categories