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!
Related
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).
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
}
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.
I am developing a application with symfony2. Im facing a problem with localization. I want to set the in the postLoad event in doctrine lifecycle, but can find a way to do that. I am using the route method to set my local for example:
http://example.com/en/content
here is my listener:
namespace MyApiBundle\Listener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
class LocaleListener
{
private $local;
public function __construct($local) {
$this->local = $local;
}
public function postLoad(LifecycleEventArgs $args)
{
$local= 'en'; // I need to get the local from here
$entity = $args->getEntity();
if(method_exists($entity, 'setLocale')) {
$entity->setLocale($local);
}
}
}
Is there any quick way get the local from here? Cant use the new Request() as it always returning the en I also have 3 other language. Thanks for help
Yes, you can. You can inject #request_stack service into your listener, get request from it and read locale.
There is, however, a Doctrine extension that probably does what you want: Translatable
Thanks #Igor Pantovic
here I got it work, here is my local listner:
#/src/MyApiBUndle/Listner/LocalListner.php
namespace MyApiBundle\Listener;
use Symfony\Component\HttpFoundation\RequestStack;
use Doctrine\ORM\Event\LifecycleEventArgs;
class LocaleListener {
private $requestStack;
/**
* #param RequestStack $requestStack
*/
public function __construct(RequestStack $requestStack) {
$this->requestStack = $requestStack;
}
/**
* #param LifecycleEventArgs $args
*/
public function postLoad(LifecycleEventArgs $args)
{
$local= $this->requestStack->getCurrentRequest()->getLocale();
$entity = $args->getEntity();
if(method_exists($entity, 'setLocale')) {
$entity->setLocale($local);
}
}
}
and my service
services:
my_api.listener.locale_listener:
class: MyApiBundle\Listener\LocaleListener
tags:
- { name: doctrine.event_listener, event: postLoad }
# #request_stack must be quoted "":
arguments: ["#request_stack"]
hope this will help other too
I was wondering if there is a way to compare old and new values in a validator within an entity prior to a flush.
I have a Server entity which renders to a form fine. The entity has a relationship to status (N->1) which, when the status is changed from Unracked to Racked, needs to check for SSH and FTP access to the server. If access is not achieved, the validator should fail.
I have mapped a validator callback to the method isServerValid() within the Server entity as described here
http://symfony.com/doc/current/reference/constraints/Callback.html. I can obviously access the 'new' values via $this->status, but how can I get the original value?
In pseudo code, something like this:
public function isAuthorValid(ExecutionContextInterface $context)
{
$original = ... ; // get old values
if( $this->status !== $original->status && $this->status === 'Racked' && $original->status === 'Unracked' )
{
// check ftp and ssh connection
// $context->addViolationAt('status', 'Unable to connect etc etc');
}
}
Thanks in advance!
A complete example for Symfony 2.5 (http://symfony.com/doc/current/cookbook/validation/custom_constraint.html)
In this example, the new value for the field "integerField" of the entity "NoDecreasingInteger" must be higher of the stored value.
Creating the constraint:
// src/Acme/AcmeBundle/Validator/Constraints/IncrementOnly.php;
<?php
namespace Acme\AcmeBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class IncrementOnly extends Constraint
{
public $message = 'The new value %new% is least than the old %old%';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return 'increment_only';
}
}
Creating the constraint validator:
// src/Acme/AcmeBundle/Validator/Constraints/IncrementOnlyValidator.php
<?php
namespace Acme\AcmeBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Doctrine\ORM\EntityManager;
class IncrementOnlyValidator extends ConstraintValidator
{
protected $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function validate($object, Constraint $constraint)
{
$new_value = $object->getIntegerField();
$old_data = $this->em
->getUnitOfWork()
->getOriginalEntityData($object);
// $old_data is empty if we create a new NoDecreasingInteger object.
if (is_array($old_data) and !empty($old_data))
{
$old_value = $old_data['integerField'];
if ($new_value < $old_value)
{
$this->context->buildViolation($constraint->message)
->setParameter("%new%", $new_value)
->setParameter('%old%', $old_value)
->addViolation();
}
}
}
}
Binding the validator to entity:
// src/Acme/AcmeBundle/Resources/config/validator.yml
Acme\AcmeBundle\Entity\NoDecreasingInteger:
constraints:
- Acme\AcmeBundle\Validator\Constraints\IncrementOnly: ~
Injecting the EntityManager to IncrementOnlyValidator:
// src/Acme/AcmeBundle/Resources/config/services.yml
services:
validator.increment_only:
class: Acme\AcmeBundle\Validator\Constraints\IncrementOnlyValidator
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: validator.constraint_validator, alias: increment_only }
Accessing the EntityManager inside a custom validator in symfony2
you could check for the previous value inside your controller action ... but that would not really be a clean solution!
normal form-validation will only access the data bound to the form ... no "previous" data accessible by default.
The callback constraint you're trying to use does not have access to the container or any other service ... therefore you cant easily access the entity-manager (or whatever previous-data provider) to check for the previous value.
What you need is a custom validator on class level. class-level is needed because you need to access the whole object not only a single value if you want to fetch the entity.
The validator itself might look like this:
namespace Vendor\YourBundle\Validation\Constraints;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class StatusValidator extends ConstraintValidator
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function validate($status, Constraint $constraint)
{
$em = $this->container->get('doctrine')->getEntityManager('default');
$previousStatus = $em->getRepository('YourBundle:Status')->findOneBy(array('id' => $status->getId()));
// ... do something with the previous status here
if ( $previousStatus->getValue() != $status->getValue() ) {
$this->context->addViolationAt('whatever', $constraint->message, array(), null);
}
}
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return 'previous_value';
}
}
... afterwards register the validator as a service and tag it as validator
services:
validator.previous_value:
class: Vendor\YourBundle\Validation\Constraints\StatusValidator
# example! better inject only the services you need ...
# i.e. ... #doctrine.orm.entity_manager
arguments: [ #service_container ]
tags:
- { name: validator.constraint_validator, alias: previous_value }
finally use the constraint for your status entity ( i.e. using annotations )
use Vendor\YourBundle\Validation\Constraints as MyValidation;
/**
* #MyValidation\StatusValidator
*/
class Status
{
For the record, here is the way to do it with Symfony5.
First, you need to inject your EntityManagerInterface service in the constructor of your validator.
Then, use it to retrieve the original entity.
/** #var EntityManagerInterface */
private $entityManager;
/**
* MyValidator constructor.
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #param string $value
* #param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
{
$originalEntity = $this->entityManager
->getUnitOfWork()
->getOriginalEntityData($this->context->getObject());
// ...
}
Previous answers are perfectly valid, and may fit your use case.
For "simple" use case, it may fill heavy though.
In the case of an entity editable through (only) a form, you can simply add the constraint on the FormBuilder:
<?php
namespace AppBundle\Form\Type;
// ...
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
/**
* Class MyFormType
*/
class MyFormType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('fooField', IntegerType::class, [
'constraints' => [
new GreaterThanOrEqual(['value' => $builder->getData()->getFooField()])
]
])
;
}
}
This is valid for any Symfony 2+ version.