How to override translator in symfony 5.2 - symfony

I'm trying to override translator class in Symfony 5.2. I tried this:
# config/services.yaml
services:
# ....
App\Translator:
decorates: translator
and this (App\Translator implements TranslatorInterface):
# config/services.yaml
services:
# ....
App\Translator:
arguments:
$translator: '#translator'
Symfony\Contracts\Translation\TranslatorInterface: '#App\Translator'
both methods work well in PHP code, but in development mode in the twig, translator service is still DataCollectorTranslator. So in twig templates the translator service remains not overridden. How can I fix it?

It's possible I am not understanding the question. If something works in one mode but not another then sometimes just deleting the var/cache directory and building a new cache with bin/console cache:clear might work.
Decorating services can be a bit interesting sometimes. I created a fresh 5.2 project and then added:
# src/Translation/Translation.php
namespace App\Translation;
use JetBrains\PhpStorm\Pure;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Bundle\FrameworkBundle\Translation\Translator as BaseTranslator;
class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
{
// Uses PHP8 constructor promotion
public function __construct(private BaseTranslator $translator)
{
}
#[Pure]
public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string
{
//return $this->translator->trans($id,$parameters,$domain,$locale);
return strtoupper($id); // Verify calling this class
}
public function getCatalogue(string $locale = null): MessageCatalogueInterface
{
return $this->translator->getCatalogue($locale);
}
#[Pure]
public function getLocale(): string
{
return $this->translator->getLocale();
}
public function setLocale(string $locale)
{
$this->translator->setLocale($locale);
}
}
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\Translation\Translator:
decorates: translator
# index.html.twig
<li>{{ 'Hello' | trans }}</li>
You can disregard the Pure stuff as well as some of the PHP8 stuff. I was using this as a PHP8 test as well.
But it all seems to work as advertised.

Related

Dynamically get an instance of a service in controller by some value

I want to get a service instance in controller (symfony 4) just by value that might look like this:
$provider = 'youtube'
That's my setup:
Class videoProvider {
//Here I have all common methods for all services
}
Class YoutubeService extends videoProvider {}
Class OtherVideoService extends videoProvider {}
Now the question is how to do something like this. If $provider = 'youtube'
Then use YouTube service new YoutubeService () and go on. But what if service does not exist? What then?
Is that even possible?
You can do something like this
Create a folder with the name Provider
Create an interface, for example, VideoProviderInterface, and put into the folder
To your interface add the method getProviderName(): string
Create your providers and put them into the folder and implement the interface
To your services.yaml add the _instanceof: option, and add to your interface some tag
Exclude your provider folders from the App\: option in the services.yaml
Create class, ProviderManager and inject your service locator
More information you can find here
services.yaml
_instanceof:
App\Provider\VideoProviderInterface:
tags: ['app.provider']
App\Provider\:
resource: '../../src/Provider/*'
App\Provider\ProviderManager:
arguments:
- !tagged_locator { tag: 'app.provider', default_index_method: 'getProviderName' }
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Provider}'
VideoProviderInterface
<?php
namespace App\Provider;
interface VideoProviderInterface
{
public function getProviderName(): string
}
ProviderManager
<?php
namespace App\Provider;
use Symfony\Component\DependencyInjection\ServiceLocator;
class ProviderManager
{
/**
* #var ServiceLocator
*/
private $serviceLocator;
public function __construct(ServiceLocator $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function findByName(string $name): ?VideoProviderInterface
{
return $this->serviceLocator->has($name) ? $this->serviceLocator->get($name) : null;
}
}
$this->container->has('my-service-name') and $this->container->get('my-service-name') in a controller is probably what you are looking for. The my-service-name is the name you give the service in your service config and make sure your service is public.
Exemple config (see doc here)
# this is the service's name
site_video_provider.youtube:
public: true
class: App\Provider\YoutubeService
[...]
Then in a controller, or any container aware classes: (see doc here)
$service_name = 'site_video_provider.'.$provider;
if($this->container->has($service_name)){
$service = $this->container->get($service_name);
}

Drupal 8 Custom Twig Extension service causes drush abnormal termination

I'm adding a custom twig extension service to a Drupal 8 module. My services file looks like this:
services:
analytics.my_twig_extension:
class: Drupal\analytics\TwigExtension\MyTwigExtension
tags:
- { name: twig.extension }
I get this error when running drush cr:
[warning] Drush command terminated abnormally. Check for an exit()
in your Drupal site.
When I remove the tags property in services file, like this:
services:
analytics.my_twig_extension:
class: Drupal\analytics\TwigExtension\MyTwigExtension
then drush cr works correctly, but my Twig extension functions are not not running at all.
The MyTwigExtension class:
<?php
namespace Drupal\analytics\TwigExtension;
use Twig_Extension;
use Twig_SimpleFilter;
class MyTwigExtension extends \TwigExtension {
public function __construct() {
}
public function getFunctions() {
return [
new \Twig_SimpleFunction('get_type', array($this, 'getType'))
];
}
public function getType($var) {
return gettype($var);
}
}
?>
Does anyone have any idea why this is happening?
Jacob, you're a dummy.
I fixed it by simply using extends \Twig_Extension instead of extends \TwigExtension. A coworker found the answer. Unfortunately, there was no indication in the logs that this was the problem.

How to globally define configuration of Serializer used in API Platform

In order to user the #MaxDepth annotation in entities, the enable_max_depth property has to be set explicitly in serializer context (e.g. in config of the #ApiPlatform annotation), so on entity level, so for each entity
Is there a way to define this property enable_max_depth=true for all entities of the project ? Something we could find in api-platform.yaml and which will look like that :
api-platform:
serializer:
enable_max_depth: true
There is no such global option for now (it can be worth adding it, PR welcome).
However, you can register a SerializerContextBuilder to add this context entry automatically for all resources:
<?php
namespace App\Serializer;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
final class MaxDepthContextBuilder implements SerializerContextBuilderInterface
{
private $decorated;
public function __construct(SerializerContextBuilderInterface $decorated)
{
$this->decorated = $decorated;
}
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
$context['enable_max_depth'] = true;
return $context;
}
}
Then register this new class as a service decorator:
# api/config/services.yaml
services:
# ...
'App\Serializer\MaxDepthContextBuilder':
decorates: 'api_platform.serializer.context_builder'
autoconfigure: false
autowire: true

How to serve same functionality with different layouts in a Symfony2 app?

I have in a Symfony2 application the following bundle architecture:
CommonBundle
FirstBundle
SecondBundle
Several features are implemented in the CommonBundle.
These features have to be available in the 2 other bundles.
The FirstBundle and SecondBundle have therefore their own features + the ones of the CommonBundle. These bundles each have their own host defined in the main application routing.yml file.
What I'm trying to do:
Features of the CommonBundle should be displayed with the layout of the current bundle.
For instance, if I hit http://firstbundle.myapp.com/common/feature1, I should see the layout of the FirstBundle bundle.
And if I hit http://secondbundle.myapp.com/common/feature1, the layout of the SecondBundle bundle should be used.
How can I do that?
I can't use bundle inheritance as the same bundle can't be extended twice.
In my current implementation, each bundle imports the routes of the CommonBundle in its own host.
You should create a controller response listener and change the template name depending on the request hostname in there.
A good read is the How to setup before/after filters chapter of the documentation.
You could aswell use a twig extension registering a global variable and decide which template to extend inside your base template:
config.yml
services:
twig.extension.your_extension:
class: Vendor\YourBundle\Twig\Extension\YourExtension
arguments: [ #request ]
tags:
- { name: twig.extension, alias: your_extension }
YourExtension.php
use Symfony\Component\HttpFoundation\Request;
class YourExtension extends \Twig_Extension
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function getGlobals()
{
// some logic involving $this->request
$baseTemplate = ($this->request->getHost() === 'first.host.tld') ? 'FirstBundle::base.html.twig' : 'SecondBundle::base.html.twig';
return array(
'base_template' => $baseTemplate,
);
}
public function getName()
{
return 'your_extension';
}
base.html.twig
{% extends base_template %}

Symfony2, autoload service in service

Question is simple but...
So we have main service:
class ManagerOne {}
and have several another services we want to use in main service:
class ServiceOne{}
class ServiceTwo{}
class ServiceThree{}
class ServiceFour{}
...
Each named as (in services.yml)
service.one
service.two
service.three
service.four
...
Locations of services is different, not in one folder (but I don't think it's a huge trouble for custom autoloader).
Regarding manual we can inject them via __construct() in main service (ManagerOne) but what if we got 20 such services need to be injected? Or use only that we need. Describe them in services as simple inject? O.o I think it's not good idea so.... Also we can inject container and that's it. BUT! Everywhere people saying that inject container worst solution.
What I want. I need method for ManagerOne service which will load service i need by 'service.name' or 'path' with checker 'service exist'.
You could use service tagging and tag each service you want to use in your ManagerOne class. And either use constructor dependency injection or method injection.
Example:
First of all you need a compiler pass to collect your tagged services:
namespace ...\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ExamplePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition("manager.one")) {
return;
}
$services = array();
foreach ($container->findTaggedServiceIds('managed_service') as $serviceId => $tag) {
$alias = isset($tag[0]['alias'])
? $tag[0]['alias']
: $serviceId;
// Flip, because we want tag aliases (= type identifiers) as keys
$services[$alias] = new Reference($serviceId);
}
$container->getDefinition('manager.one')->replaceArgument(0, $services);
}
}
Then you need to add the compiler pass to your bundle class:
namespace Example\ExampleBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use ...\DependencyInjection\Compiler\ExamplePass;
class ExampleBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ExamplePass());
}
}
Now you can use your services:
# services.yml
manager.one:
class: ManagerClass
arguments:
- [] # will be replaced by our compiler pass
services.one:
class: ServiceOne
tags:
- { name: managed_service, alias: service_one }
services.two:
class: ServiceTwo
tags:
- { name: managed_service, alias: service_two }
But caution if you get your manager, all service classes will be automatically created. If this is a performance drawback for you could pass only the service ids (not the Reference) to your management class. Add the #service_container as second argument and create the service as needed.
Since 2017, Symfony 3.3 and Symplify\PackageBuilder this gets even easier.
Thanks to this package, you can:
drop tags
have simple 5 line CompilerPass using strict types over strings
Let's get to your example
Suppose you have
1 manager - UpdateManager class
many updaters - a class that implements UpdaterInterface
1. Service Config using PSR-4 autodiscovery
# app/config/services.yml
services:
_defaults:
autowire: true
App\:
resource: ../../src/App
2. Collecting Compiler Pass
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symplify\PackageBuilder\Adapter\Symfony\DependencyInjection\DefinitionCollector;
final class CollectorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder)
{
DefinitionCollector::loadCollectorWithType(
$containerBuilder,
UpdateManager::class,
UpdaterInterface::class,
'addUpdater'
);
}
}
It collect all services of UpdaterInterface type
and adds them via addUpdater() method to UpdateManager.
3. Register Compiler Pass in Bundle
namespace App;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class UpdaterBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new CollectorCompilerPass);
}
}
And that's all!
How to add new updater?
Just create class, that implements UpdaterInterface and it will be loaded to UpdateManager.
no tagging
no manual service registration
no boring work
Enjoy!

Resources