How to decorate PageAdminController in SonataPageBundle v4.x - symfony

I'm trying to use the SonataPageBundle in my project in its version 4.x.
I'm seeing that since version 3 a lot of classes have been made final, like PageAdminController.
I used to extend this PageAdminController to customize its behaviour.
Now, this cannot be done anymore. How can I customize that controller (or other final classes) without inheritance please ?
I tried to use Symfony decoration but it doesn't seems to do the trick.
Here is my code :
services.yaml
App\Controller\Page\PageAdminController:
decorates: 'sonata.page.controller.admin.page'
public: true
tags:
- 'container.service_subscriber'
calls:
- [ setContainer, [ "#service_container" ] ]
my PageAdminController
use Sonata\PageBundle\Controller\PageAdminController as SonataPageAdminController;
class PageAdminController extends CRUDController
{
public function __construct(
private SonataPageAdminController $basePageAdminController,
)
{
}
// I redefined all the needed methods the same way as this one
public function batchActionSnapshot(ProxyQueryInterface $query): RedirectResponse
{
return $this->basePageAdminController->batchActionSnapshot($query);
}

Related

How to override translator in symfony 5.2

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.

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.

Inject Container in my Repository Class

i try to inject Container in my RepositoryClass, but it does not work.
BaseRepository:
<?php
namespace MyApp\ApplicationBundle\Repository;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class BaseRepository implements ContainerAwareInterface
{
protected $container;
public function setContainer(ContainerInterface $container=null)
{
echo "container";
var_dump($container);
}
public function __construct()
{
echo __CLASS__;
}
}
services.yml
services:
myapp.base_repository:
class: MyApp\ApplicationBundle\Repository\BaseRepository
calls:
- [ setContainer, [ '#service_container' ] ]
DefaultController:
$baseRep = new BaseRepository();
The only output that i get, is the echo FILE from the BaseRepository Construct.
The second way that i tried, is to inject the GuzzleClient self (this is the reason why i tried to inject the container, because i need my guzzle-configuraton-settings.
services.yml
myapp.base_repository:
class: MyApp\ApplicationBundle\Repository\BaseRepository
arguments: ['#csa_guzzle.client.mce']
BaseRepository:
use GuzzleHttp\Client;
class BaseRepository
{
public function __construct(Client $client)
{
var_dump($client);
echo __CLASS__;
}
}
But then i got the following error:
Type error: Argument 1 passed to
MyApp\ApplicationBundle\Repository\BaseRepository::__construct() must
be an instance of GuzzleHttp\Client, none given, called in
MyApp/src/Chameleon/DefaultBundle/Controller/DefaultController.php on
line 20
Anyone know what i can do?
Thank you!
To get the class that is managed by the Service Container you have to use said container to get the service with that id myapp.base_repository as Twifty says:
$this->get('myapp.base_repository');
// or more generally in classes implementing ContainerAwareInterface:
$this->container->get('myapp.base_repository');
If you create a new instance yourself you will have to manage all dependencies:
// In your controller extending Symfony's Controller:
$repository = new BaseRepository();
$repository->setContainer($this->container);
Similarly if you inject a Guzzle-client into the repository you have to either retrieve the service from the container or create it yourself with all the dependencies:
// $this->get() assumes you are in the controller as well
$repositoryWithClientFromServiceContainer = new BaseRepository(
$this->get('csa_guzzle.client.mce')
);
// This obviously works everywhere
$repositoryWithNewDefaultClient = new BaseRepository(
new GuzzleHttp\Client()
);
Furthermore injecting the service container into a class violates the dependency inversion you try to achieve by using the Service Container in the first place. This means, instead of making your repository ContainerAware you should only add the services you need in that repository, not the whole container. Just as you do in the 2nd example with the Guzzle-client.
Some people argue it's okay for controllers to violate that principle, but I personally prefer controller's being defined as services to be able to quickly see which dependencies they have by looking at the constructor.
As a general rule I would avoid using the ContainerAwareInterface.
Similarly if you inject a Guzzle-client into the repository you have
to either retrieve the service from the container or create it
yourself with all the dependencies:
// $this->get() assumes you are in the controller as well
$repositoryWithClientFromServiceContainer = new BaseRepository(
$this->get('csa_guzzle.client.mce')
);
// This obviously works everywhere
$repositoryWithNewDefaultClient = new BaseRepository(
new GuzzleHttp\Client()
);
Furthermore injecting the service container into a class violates the
dependency inversion you try to achieve by using the Service Container
in the first place. This means, instead of making your repository
ContainerAware you should only add the services you need in that
repository, not the whole container. Just as you do in the 2nd example
with the Guzzle-client.
Some people argue it's okay for controllers to violate that principle,
but I personally prefer [controller's being defined as services][1] to
be able to quickly see which dependencies they have by looking at the
constructor.
As a general rule I would avoid using the ContainerAwareInterface.
[1]: http://symfony.com/doc/current/cookbook/controller/service.html
Thank you.
So, it would be the better solution, if i inject only the guzzleClient, right?
As you can see, i have a few classes that extends from my BaseRepository and they need the guzzleClient.
But how is it possible to inject the guzzleClient for this scenario? If the programmer only want to create his basic "MyRep" Repositoryclass in the controller without any params.
services.yml
myapp.base_repository:
class: MyApp\ApplicationBundle\Repository\BaseRepository
arguments: ['#csa_guzzle.client.mce']
BaseRepository:
use GuzzleHttp\Client;
class BaseRepository
{
private $client = null;
public function __construct(Client $client)
{
var_dump($client);
$this->client = $client;
}
public getClient() {
return $this->client;
}
}
MyRepository:
MyRep extends BaseRepository:
use GuzzleHttp\Client;
class BaseRepository
{
public function __construct()
{
var_dump($this->getClient());
}
}
Thank you!

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