I have a Symfony2 project and I am using Translation component for translating text. I have all translations in yml file like so
translation-identifier: Translated text here
Translating text looks like this from Twig
'translation-identifier'|trans({}, 'domain')
The thing is, in some cases I would like to have two different texts for same translation (not for pluralization). Here's how I would like it to work:
Define two texts in yml file for translations that need to have different texts. Each would have it's own unique suffix
translation-identifier-suffix1
translation-identifier-suffix2
Define a global rule that would define which suffix should be choosen. Psuedocode below:
public function getSuffix() {
return rand(0, 10) < 5 ? '-suffix1' : '-suffix2';
}
Twig (and PHP) would look the same - I would still specify just the identifier without suffix. Translator would then append suffix to the identifier and try to find a match. If there would be no match it would try to find a match again without suffix.
AFAIK, Translator component doesn't support it.
But if you want same kind of behavior, you could do by overriding the translator service.
1) Override the service
# app/config/config.yml
parameters:
translator.class: Acme\HelloBundle\Translation\Translator
First, you can set the parameter holding the service's class name to your own class by setting it in app/config/config.yml.
FYI: https://github.com/symfony/FrameworkBundle/blob/master/Resources/config/translation.xml
2) Extend the translator class provided symfony framework bundle.
FYI: https://github.com/symfony/FrameworkBundle/blob/master/Translation/Translator.php
3) Overwrite the trans function which is provider by translator component.
https://github.com/symfony/Translation/blob/master/Translator.php
Hope this helps!
Here is the extended translator class in case anyone ever needs it
<?php
namespace Acme\HelloBundle\Translation;
use Symfony\Bundle\FrameworkBundle\Translation\Translator as BaseTranslator;
use Symfony\Component\Translation\MessageSelector;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Translator extends BaseTranslator {
const SUFFIX_1 = '_suffix1';
const SUFFIX_2 = '_suffix2';
private $suffix;
public function __construct(ContainerInterface $container, MessageSelector $selector, $loaderIds = array(), array $options = array()) {
parent::__construct($container, $selector, $loaderIds, $options);
$this->suffix = $this->getSuffix($container);
}
public function trans($id, array $parameters = array(), $domain = 'messages', $locale = null) {
if ($locale === null)
$locale = $this->getLocale();
if (!isset($this->catalogues[$locale]))
$this->loadCatalogue($locale);
if($this->suffix !== null && $this->catalogues[$locale]->has((string) ($id . $this->suffix), $domain))
$id .= $this->suffix;
return strtr($this->catalogues[$locale]->get((string) $id, $domain), $parameters);
}
private function getSuffix($container) {
return rand(0, 10) < 5 ? self::SUFFIX_1 : self::SUFFIX_2;
}
}
?>
As of Symfony 3, Venu's answer no longer works completely, as the translator.class parameter is no longer used.
To load your custom translator class, you now need to create a compiler pass.
<?php
namespace Acme\HelloBundle\DependencyInjection\Compiler;
use Acme\HelloBundle\Translation\Translator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class TranslatorOverridePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$container->getDefinition('translator.default')->setClass(Translator::class);
}
}
And this compiler pass needs to be added to the container.
<?php
namespace Acme\HelloBundle;
use Acme\HelloBundle\DependencyInjection\Compiler\TranslatorOverridePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeHelloBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new TranslatorOverridePass());
}
}
Related
The Symfony docs shows a solution, but it doesn't appear to work (i.e. Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory needs to be replaced with Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory, and other changes). I modified the code as shown below, but am pretty certain I am not doing it correctly.
<?php
declare(strict_types=1);
namespace App\DataFixtures\Purger;
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
class CustomPurgerFactory implements PurgerFactory
{
public function __construct(private ORMPurgerFactory $purgeFactory)
{
}
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface
{
// Change $excluded, $purgeWithTruncate as desired.
return new CustomPurger($emName, $em, $excluded, $purgeWithTruncate, $this->purgeFactory);
}
}
<?php
declare(strict_types=1);
namespace App\DataFixtures\Purger;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
class CustomPurger implements ORMPurgerInterface
{
public function __construct(private ?string $emName, private EntityManagerInterface $entityManager, private array $excluded, private bool $purgeWithTruncate, private ORMPurgerFactory $purgeFactory)
{
}
public function setEntityManager(EntityManagerInterface $entityManager):void
{
// Seems rather redundent doing this even though I earlier inject $entityManager.
$this->entityManager = $entityManager;
}
public function purge() : void
{
// Delete any tables which must be deleted first to prevent FK constraint errors.
// This doesn't seem write.
$purger = $this->purgeFactory->createForEntityManager($this->emName, $this->entityManager, $this->excluded, $this->purgeWithTruncate);
$purger->purge();
}
}
services:
App\DataFixtures\Purger\DoctrinePurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'my_purger' }
arguments:
- '#doctrine.fixtures.purger.orm_purger_factory'
Or should it be done by decorating the default purger as suggested by this post?
Okay. So you do have a few things wrong and the docs are somewhat out of date. From a big picture point of view you want something like:
bin/console doctrine:fixtures:load --purger=my_purger
to use your custom purger factory (aliased as my_purger) to instantiate and execute your custom purger's purge method. The job of the factory is to just create the purger not to execute it.
I followed the docs and implemented PurgerInterface but the purge command complained about it not implementing ORMPurgerInterface which, as you noted, adds a seemingly superfluous method. I think it is still a work in progress. The default ORMPurger has a couple of additional public methods not defined in any interface which is also strange. The fact that Doctrine is inconsistent with it's usage of the Interface suffix does not help. But it is what it is.
This works under 6.1:
# CustomPurger.php
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
class CustomPurger implements ORMPurgerInterface
{
private EntityManagerInterface $em;
public function setEntityManager(EntityManagerInterface $em) : void
{
$this->em = $em;
}
public function purge() : void
{
dd(' my purger');
}
}
# CustomPurgerFactory.php
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
class CustomPurgerFactory implements PurgerFactory
{
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface
{
return new CustomPurger($em);
}
}
# services.yaml
App\Purger\CustomPurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'my_purger' }
bin/console doctrine:fixtures:load --purger=my_purger
> purging database
^ " my purger"
As far as decorating goes, you decorate a service when you want to modify some methods without extending the original class. There is only one method here and it's quite a doozy so I don't think decorating will help.
If you wanted to always use your purger without the --purger option then you could probably point the default purger factory service id to your factory. I'll leave that as an exercise for the reader.
One final note: I took a look at your decorating link. Don't know what they were trying to do but I do know it has nothing to do with decorating.
I'm trying to make a custom maker with the Symfony make bundle.
The maker command looks like this:
<?php
namespace App\Maker;
use Doctrine\Common\Annotations\Annotation;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
final class MakeCustomEntity extends AbstractMaker
{
public static function getCommandName(): string
{
return 'make:custom-entity';
}
public static function getCommandDescription(): string
{
return 'Creates a new entity';
}
public function configureCommand(Command $command, InputConfiguration $inputConf)
{
$command
->addArgument('entity-class', InputArgument::OPTIONAL, sprintf('Choose a name for your entity class (e.g. <fg=yellow>%s</>)', Str::asClassName(Str::getRandomTerm())));
}
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
{
}
public function configureDependencies(DependencyBuilder $dependencies)
{
$dependencies->addClassDependency(
Annotation::class,
'doctrine/annotations'
);
}
}
So far so good, the custom maker shows up when listing all commands.
However I would like to write a test for this maker (inspired from the tests I have found on the bundles github):
<?php
namespace Tests\Maker;
use App\Maker\MakeCustomEntity;
use Symfony\Bundle\MakerBundle\Test\MakerTestCase;
use Symfony\Bundle\MakerBundle\Test\MakerTestDetails;
class MakeCustomEntityTest extends MakerTestCase
{
public function getTestDetails()
{
yield 'entity_full_custom_namespace' => [
MakerTestDetails::createTest(
$this->getMakerInstance(MakeCustomEntity::class),
[
// entity class name
'\App\Domain\Entity\Test\Test',
]
)
->assert(function (string $output, string $directory) {
$this->assertStringContainsString('created: src/Domain/Entity/Test/Test.php', $output);
}),
];
}
}
When I try to run this test I get the following warning and test doesn't fail even though it should:
The data provider specified for Tests\Maker\MakeCustomEntityTest::testExecute is invalid.
You have requested a non-existent service "maker.maker.make_custom_entity". Did you mean one of these: "maker.maker.make_authenticator",...
Is this the correct way to testing custom makers? What should I do to avoid this?
I'm trying to change the output of {{ url('app_route_name') }} or {{ path('app_route_name') }}. Normally the output might be /app/route/name I'm wanting to modify the twig output only.
My first attempt was decorating UrlGenerator->generate, but changing $name also changed the controller called. I just want to change the final output.
Here is the code I'm currently trying. The error exception I'm getting is:
Unknown "path" function. Did you mean "logout_path", "relative_path", "impersonation_exit_path"?
App\Service\TwigUrlDecorator:
decorates: 'twig.extension.routing'
arguments: ['#.inner']
public: false
// src/Service/TwigUrlDecorator.php
namespace App\Service;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Extension\AbstractExtension;
use Symfony\Bridge\Twig\Extension\RoutingExtension;
class TwigUrlDecorator extends AbstractExtension
{
private $generator;
private $router;
public function __construct(RoutingExtension $router, UrlGeneratorInterface $generator)
{
$this->router = $router;
$this->generator = $generator;
}
public function getPath(string $name, array $parameters = [], bool $relative = false): string
{
return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH);
}
public function getUrl(string $name, array $parameters = [], bool $schemeRelative = false): string
{
return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
}
}
My original question might shed some background on what I'm looking for.
Where did you define the Twig function path? You've decorated twig.extension.routing without defining any Twig functions. Currently, the method getFunctions of the AbstractExtension is called, and this returns an empty list of functions.
As usual in decoration: if you want to execute any methods on the original service, you need to route such calls through. Something like this might help:
public function getFunctions(): array {
return $this->router->getFunction();
}
Additionally, you should check the naming of the properties. In the current state, $router does not contain any router, but the Twig extension for routing. To me, this looks pretty confusing, as a proper router would provide completely different methods than a Twig extension does
When setting up a custom Twig filter (see https://symfony.com/doc/current/templating/twig_extension.html ), how can I call an existing Twig filter in my custom function?
https://stackoverflow.com/a/41551944/1668200 suggests parent::dateFilter($timestamp, $format); but that isn't working:
Attempted to call an undefined method named "dateFilter" of class "Twig_Extension".
The example you've linked is actually incorrect. The proper way would be like this,
class DateEmptyIfNull extends Twig_Extension // or: extends AbstractExtension
{
public function getFilters()
{
return array(
new TwigFilter('date', [ $this, 'dateFilter'], ['needs_environment' => true, ]),
);
}
public function dateFilter(Twig_Environment $env, $timestamp, $format = 'F j, Y H:i')
{
return $timestamp === null ? '' : twig_date_format_filter($env, $timestamp, $format);
}
}
not all twig extensions have their dedicated gloal php function (mainly true for 3rd party twig extensions i think) then the best way if you use the symfony framework is to use autowiring as twig functions are public and can be called via php.
in my case i wanted to create a asset function which downloads remote files to the local filesystem and then returns the local path which can be used with the imagine_filter filter (which only easily works with local images)
add the twig class to services yaml to allow autowiring if necessary.
services.yaml
services:
Symfony\Bridge\Twig\Extension\AssetExtension: '#twig.extension.assets'
create your own filter filter/function with a constructor where the other extension can be incjected.
<?php
declare(strict_types=1);
namespace App\Twig;
use Symfony\Bridge\Twig\Extension\AssetExtension;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class ImageAssetExtension extends AbstractExtension
{
private AssetExtension $assetExtension;
public function __construct(
AssetExtension $assetExtension,
) {
$this->assetExtension = $assetExtension;
}
public function getFunctions(): array
{
return [
new TwigFunction('asset_image', [$this, 'assetImage']),
];
}
public function assetImage(string $path, string $packageName = null): string
{
// do something more here (like downloading a remote asset and returning the local path)
return $this->assetExtension->getAssetUrl($path, $packageName);
}
}
The method dateFilter() belongs to class DateEmptyIfNull. In this case your class must extend this class
Let's say I have a User Entity :
$user = new User(007);
echo $user->getName(); // display Bond
echo $user->getGender(); // display "Male";
echo $user->getDesignation() // display "Monsieur Bond" or "Mister Bond"
With this function :
public function getDesignation() {
if ($this->getGender() == 'Male') return "Monsieur ".$this->getName();
else return "Madame ".$this->getName();
}
How can I use the translator service inside this Entity to translate "Monsieur" and "Madame" ?
It seems the translator service should be used only inside a Controller, but I think it's appropriate in that case to use it inside this Entity.
The translator service is, like you say, a "service" you can use a service inside any class (i.e. defining it as a service too and using the dependency injector container). So, you can use the translator almost wherever you want.
But the entities like aldo said shouldn't have that responsability. In the worst scenario if you really want to translate things inside the entity, you could pass the translator to the entity with a set method, i.e.
$entity->setTranslator($translator);
but I recommend you too to create a class that handles the problem outside the entity, i.e. using the twig template
{{ entity.property|trans }}).
You shouldn't and in general it is not possible. According to the Single Responsibility Principle the entity have already their purpose, which is representing data on a database. Moreover the translation is a matter of representation, so it is unlikely that you want to address such a problem in the entity layer (unless you want to provide entities translated in different languages, which totally a different problem and shouldn't even be solved using the translator).
Rethink to your logic and try something different for this. Are you sure that you don't want to do this translation on the view layer? That would be the best thing probably. Otherwise (if your logic really need to have translation at a model level) you could create a wrapper class for entities and a factory to generate this "wrapped entities"; in that factory you could inject the translator service.
I ran into the similar problem and finally found this solution. This is not a direct answer to your problem because I'm also aware that an entity should have nothing to do with a service, like translator. So you should leave the getDesignation function untouched. Instead, in the presentation layer, twig for example, you translate that French designation.
<div>{% trans %}{{ entity.designation }}{% endtrans %} {{ entity.name }}</div>
And in your messages.en.yml
Monsieur: Mr.
Madame: Mrs.
I ran into this problem several times over the last years and always found a good enough workaround. This time my getIdentifyingName() methods that are heavily used across the whole project (like an explicit __toString()) had to translate some keywords used in the data layer, so there was no elegant workaround.
My solution this time is a TranslateObject and a corresponding helper service. The TranslateObject is a plain object holding a translation key and an array of placeholders which also can be TranslateObjects to allow multi level translation (like a getIdentifyingNameTranslateObject() calling another related object's getIdentifyingNameTranslateObject() within one of the placeholders):
namespace App\Utils;
class TranslateObject
{
/** #var string */
protected $transKey;
/** #var array */
protected $placeholders;
public function __construct(string $transKey, array $placeholders = [])
{
$this->transKey = $transKey;
$this->placeholders = self::normalizePlaceholders($placeholders);
}
public static function normalizePlaceholders(array $placeholders): array
{
foreach ($placeholders as $key => &$placeholder) {
if (substr($key, 0, 1) !== '%' || substr($key, -1, 1) !== '%') {
throw new \InvalidArgumentException('The $placeholder attribute must only contain keys in format "%placeholder%".');
}
if ($placeholder instanceof TranslateObject) {
continue;
}
if (is_scalar($placeholder)) {
$placeholder = ['value' => $placeholder];
}
if (!isset($placeholder['value']) || !is_scalar($placeholder['value'])) {
throw new \InvalidArgumentException('$placeholders[\'%placeholder%\'][\'value\'] must be present and a scalar value.');
}
if (!isset($placeholder['translate'])) {
$placeholder['translate'] = false;
}
if (!is_bool($placeholder['translate'])) {
throw new \InvalidArgumentException('$placeholders[\'%placeholder%\'][\'translate\'] must be a boolean.');
}
}
return $placeholders;
}
public function getTransKey(): string
{
return $this->transKey;
}
public function getPlaceholders(): array
{
return $this->placeholders;
}
}
The helper looks like this and does the work:
namespace App\Services;
use App\Utils\TranslateObject;
use Symfony\Contracts\Translation\TranslatorInterface;
class TranslateObjectHelper
{
/** #var TranslatorInterface */
protected $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function trans(TranslateObject $translateObject): string
{
$placeholders = $translateObject->getPlaceholders();
foreach ($placeholders as $key => &$placeholder) {
if ($placeholder instanceof TranslateObject) {
$placeholder = $this->trans($placeholder);
}
elseif (true === $placeholder['translate']) {
$placeholder = $this->translator->trans($placeholder['value']);
}
else {
$placeholder = $placeholder['value'];
}
}
return $this->translator->trans($translateObject->getTransKey(), $placeholders);
}
}
And then within the Entity there is a getIdentifyingNameTranslateObject() method returning a TranslateObject.
/**
* Get an identifying name as a TranslateObject (for use with TranslateObjectHelper)
*/
public function getIdentifyingNameTranslateObject(): TranslateObject
{
return new TranslateObject('my.whatever.translation.key', [
'%placeholder1%' => $this->myEntityProperty1,
'%placeholderWithANeedOfTranslation%' => [
'value' => 'my.whatever.translation.values.' . $this->myPropertyWithANeedOfTranslation,
'translate' => true,
],
'%placeholderWithCascadingTranslationNeeds%' => $this->getRelatedEntity()->getIdentifyingNameTranslateObject(),
]);
}
When I need to return such a translated property, I need access to my injected TranslateObjectHelper service and use its trans() method like in a controller or any other service:
$this->translateObjectHelper->trans($myObject->getIdentifyingNameTranslateObject());
Then I created a twig filter as a simple helper like this:
namespace App\Twig;
use App\Services\TranslateObjectHelper;
use App\Utils\TranslateObject;
class TranslateObjectExtension extends \Twig_Extension
{
/** #var TranslateObjectHelper */
protected $translateObjectHelper;
public function __construct(TranslateObjectHelper $translateObjectHelper)
{
$this->translateObjectHelper = $translateObjectHelper;
}
public function getFilters()
{
return array(
new \Twig_SimpleFilter('translateObject', [$this, 'translateObject']),
);
}
/**
* sends a TranslateObject through a the translateObjectHelper->trans() method
*/
public function translateObject(TranslateObject $translateObject): string
{
return $this->translateObjectHelper->trans($translateObject);
}
public function getName(): string
{
return 'translate_object_twig_extension';
}
}
So in Twig I can translate like this:
{{ myObject.getIdentifyingNameTranslateObject()|translateObject }}
In the end, I "just" needed to find all getIdentifyingName() calls (or .identifyingName in Twig) on that entities and replace them with getIdentifyingNameTranslateObject() with a call to the trans() method of the TranslateObjectHelper (or the translateObject filter in Twig).