Definition : addMethodCall works but methods are never invoked - symfony

I want to use custom tags on my services, so I followed the instructions in the documentation: http://symfony.com/doc/2.8/components/dependency_injection/tags.html
I have a RulesHydrator class:
<?php
namespace TestBundle\Thruway;
class RulesHydrator
{
private $container;
private $manualChecks = [];
public function __construct($container)
{
$this->container = $container;
}
public function addManualCheck($service, $rule, $method)
{
echo 'addManualCheck invoked!'.PHP_EOL;
exit;
$this->manualChecks[$rule] = $service;
}
}
Here is the compiler pass:
<?php
namespace TestBundle\Thruway;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class ThruwayCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has('thruway.rules_hydrator')) {
return;
}
$definition = $container->findDefinition('thruway.rules_hydrator');
foreach ($container->findTaggedServiceIds('thruway.manual_check') as $id => $tags) {
foreach ($tags as $attributes) {
$definition->addMethodCall('addManualCheck', [new Reference($id), $attributes['rule'], $attributes['method']]);
}
}
}
}
Here is my bundle's class:
<?php
namespace TestBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use TestBundle\Thruway\ThruwayCompilerPass;
class TestBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ThruwayCompilerPass());
}
}
My services.yml file look like this:
services:
thruway.rules_hydrator:
class: TestBundle\Thruway\RulesHydrator
thruway.customer_checker:
class: TestBundle\Thruway\MyChecker
tags:
- { name: thruway.manual_check, rule: some.rule1, method: someMethod1 }
- { name: thruway.manual_check, rule: some.rule2, method: someMethod2 }
The process method is called and the different calls to addMethodCall on my Definition object work fine (the property "calls" of the definition is correctly filled). The problem is that the calls to my method addManualCheck never occur. Any idea why?

The case could be that you did not instantiated the service. By default from what I remember the services are lazy loaded, and until you actually fetch one from the container, or it is injected into an other service, it will not be initialized.
Can you look into your appProdProjectContainer.php inside app/cache/prod for the "TestBundle\Thruway\MyChecker" and post back how it is used?
Also try a fast check by getting the thruway.customer_checker from the container.
A fast command like this could help
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class TestCommand extends ContainerAwareCommand
{
protected function configure()
{
$this->setName('service:test')->setDescription('Test service functionalities');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$service = $this->getContainer()->get('thruway.customer_checker');
}
}

Related

Create dynamic function with arguments using Symfony4

I am new in symfony 4 and I've done the CRUD. I want to enhance my code by creating a function that will lessen it.
Example:
If you have 2 modules like manage event and announcement(ofcourse you will have here add,get all,delete, and update). Instead of having a long code like this.
$fetch_item = $this->getDoctrine()
->getRepository(Event::class)
->findAll();
I want to short it like $fetch = $this->fetch(Event::class); I created a new file in my Service directory.
Service\Crud.php
<?php
namespace App\Service;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
/**
*
*/
class Crud extends AbstractController
{
public function __construct(){
parent::__construct();
}
public function fetch($table)
{
$fetch_item = $this->getDoctrine()
->getRepository($table)
->findAll();
return $fetch_item;
}
}
?>
Controller
//
...
use App\Service\Crud;
...
class EventController extends AbstractController
public function index()
{
// $fetch_item = $this->getDoctrine()
// ->getRepository(Item::class)
// ->findAll();
$fetch = $this->fetch(Item::class);
return $this->render('base.html.twig',array(
'items' => $fetch_item
));
}
Above is my code but it gives me an error "Attempted to call an undefined method named "fetch" of class "App\Controller\ItemController""
Question: How can I create a function that will lessen my code?
There is no reason for the fetch function to be part of a controller (on the contrary there are lots of reasons not to be). What you need is a simple service:
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
class CrudService {
protected $em;
public function __construct(EntityManagerInterface $em){
$this->em = $em;
}
public function fetch($entityClass) {
return $this->em->getRepository($entityClass)->findAll();
}
}
Then in your controller you just have to inject it through autowiring and use it:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Service\CrudService;
use App\Entity\Item;
...
class EventController extends AbstractController {
public function index(CrudService $crudService) {
$items = $crudService->fetch(Item::class);
return $this->render('base.html.twig',array(
'items' => $items
));
}
}

Symfony 4.1 twig extension

In Symfony 4.1 I created an twig extension and I tried to use it as an service
twig.extension.active.algos:
class: App\Twig\AppExtension
public: true
tags:
- { name: twig.extension, priority: 1024 }
Unfortunately I receive 'Unable to register extension "App\Twig\AppExtension" as it is already registered'
After many searches I saw that there was a bag in the version of symfony 3.4 but they say the error would have solved. So it's my mistake or just another mistake from symfony team.
My extension is:
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class AppExtension extends \Twig_Extension {
public function getFunctions() {
return array(
new \Twig_SimpleFunction('get_active_algos', array($this, getActiveAlgos')),
);
}
public function getActiveAlgos()
{
return [1,2,3];
}
public function getName()
{
return 'get_active_algos';
}
}
Got bored. Here is a working example of a custom twig function for S4.1. No service configuration required (Update: except for the added $answer argument). I even injected the default entity manager using autowire just because.
namespace App\Twig;
use Doctrine\ORM\EntityManagerInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class TwigExtension extends AbstractExtension
{
private $em;
private $answer;
public function __construct(EntityManagerInterface $em, int $answer)
{
$this->em = $em;
$this->answer = $answer;
}
public function getFunctions()
{
return array(
new TwigFunction('get_active_algos', [$this, 'getActiveAlgos']),
);
}
public function getActiveAlgos()
{
$dbName = $this->em->getConnection()->getDatabase();
return 'Some Active Algos ' . $dbName . ' ' . $answer;
}
}
Update: Based on the first comment, I updated the example to show injecting a scaler parameter which autowire cannot handle.
# services.yaml
App\Twig\TwigExtension:
$answer: 42
Note that there is still no need to tag the service as an extension. Autoconfig takes care of that by automatically tagging all classes which extend the AbstractExtension.

RenderView in Symfony Command usage

How can I use $this->renderView inside a symfony Command (not inside a controller)? I new about the function "renderView" but what do I have to setup to use it wihtin a command?
Thank you in advance an regards
Your command class must extends the ContainerAwareCommand abstract class and then you can do:
$this->getContainer()->get('templating')->render($view, $parameters);
When it comes to commands that extend ContainerAwareCommand the proper way to obtain the container is by getContainer() unlike in controller shortcut.
In Symfony 4 I could not get $this->getContainer()->get('templating')->render($view, $parameters); to work.
I set the namespace use for Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand and extended ContainerAwareCommand class EmailCommand extends ContainerAwareCommand
I get an exception thrown
[Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException]
You have requested a non-existent service "templating".
For Symfony 4, this is the solution I came up with.
First I installed Twig.
composer require twig
Then created my own twig service.
<?php
# src/Service/Twig.php
namespace App\Service;
use Symfony\Component\HttpKernel\KernelInterface;
class Twig extends \Twig_Environment {
public function __construct(KernelInterface $kernel) {
$loader = new \Twig_Loader_Filesystem($kernel->getProjectDir());
parent::__construct($loader);
}
}
Now my email command looks like this.
<?php
# src/Command/EmailCommand.php
namespace App\Command;
use Symfony\Component\Console\Command\Command,
Symfony\Component\Console\Input\InputInterface,
Symfony\Component\Console\Output\OutputInterface,
App\Service\Twig;
class EmailCommand extends Command {
protected static $defaultName = 'mybot:email';
private $mailer,
$twig;
public function __construct(\Swift_Mailer $mailer, Twig $twig) {
$this->mailer = $mailer;
$this->twig = $twig;
parent::__construct();
}
protected function configure() {
$this->setDescription('Email bot.');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$template = $this->twig->load('templates/email.html.twig');
$message = (new \Swift_Message('Hello Email'))
->setFrom('emailbot#domain.com')
->setTo('someone#somewhere.com')
->setBody(
$template->render(['name' => 'Fabien']),
'text/html'
);
$this->mailer->send($message);
}
}
Yet another one: rely on dependency injection, i.e. inject ContainerInterface
namespace AppBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Psr\Container\ContainerInterface;
class SampleCommand extends Command
{
public function __construct(ContainerInterface $container)
{
$this->templating = $container->get('templating');
parent::__construct();
}
protected function configure()
{
$this->setName('app:my-command')
->setDescription('Do my command using render');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$data = retrieveSomeData();
$csv = $this->templating->render('path/to/sample.csv.twig',
array('data' => $data));
$output->write($csv);
}
private $templating;
}
This relies on Symfony to inject the container, which in turn is used to retrieve either templating or twig or whatever you need for your custom command.

Use helper service from a Twig Extension in Symfony2

I'm trying to set a twig filter working this way: {{entities|fieldnames}} that will return an array containing the properties names of a entity object.
My problem, after reading and trying for hours, is that I'm not able to execute $this->container->get("helpers") from the Twig Extension php. It seems that I'm not linking the service container properly... Help, please ;)
Error: Call to a member function get() on a non-object in /Users/a77/Dropbox/06.Proyectos/2011 U-Vox/DEV U-Vox/Uvox Web/src/Acme/DemoBundle/Extension/FieldnamesTwigExtension.php line 38
Or if construct without =null
Error
ContextErrorException: Catchable Fatal Error: Argument 1 passed to Acme\DemoBundle\Extension\FieldnamesTwigExtension::__construct() must be an instance of Acme\DemoBundle\Extension\Container, none given, called in
services.yml
services:
helpers:
class: Acme\DemoBundle\Services\Helpers
twig.extension.acme.demo:
class: Acme\DemoBundle\Twig\Extension\DemoExtension
arguments: [twig.loader]
acme.demo.listener:
class: Acme\DemoBundle\EventListener\ControllerListener
arguments: [twig.extension.acme.demo]
fieldnames:
class: Acme\DemoBundle\Extension\FieldnamesTwigExtension
arguments: [#service_container]
Extension\FieldnamesTwigExtension.php
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
namespace Acme\DemoBundle\Extension;
class FieldnamesTwigExtension extends \Twig_Extension {
private $container;
public function __construct(Container $container=null)
{
$this->container = $container;
//var_dump ($container); exit; // prints null !!!
}
protected function get($service)
{
return $this->container->get($service);
}
public function getFilters() {
return array(
'fieldnames' => new \Twig_Filter_Method($this, 'fieldnamesFilter'),
);
}
public function getName() {
return 'fieldnames_twig_extension';
}
public function fieldnamesFilter($obj) {
if (is_array($obj)) {
$first = $obj[0];
// GET (HELPERS) NOT WORKING :
$fieldnames = $this->container->get("helpers")->getFieldnames($first);
return $fieldnames;
}
return null;
}
public function twig_array_get_function($array, $name) {
return $array[$name];
}
}
Helpers.php
namespace Acme\DemoBundle\Services;
class Helpers {
public function sum($n1, $n2) {
return $n1 + $n2;
}
public function getFieldnames($entities) {
$reflect = new \ReflectionClass($entities[0]);
$props = $reflect->getProperties();
$fieldnames = Array();
foreach ($props as $prop) {
$fieldnames[] = $prop->getName();
}
return $fieldnames;
}
}
AcmeDemoExtension.php
namespace Acme\DemoBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder; use
Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use
Symfony\Component\HttpKernel\DependencyInjection\Extension; use
Symfony\Component\Config\FileLocator; use
Symfony\Component\DependencyInjection\Definition; // Added
class AcmeDemoExtension extends Extension {
public function load(array $configs, ContainerBuilder $container) {
$definition = new Definition('Acme\DemoBundle\Extension\AccessTwigExtension');
$definition->addTag('twig.extension');
$container->setDefinition('access_twig_extension', $definition);
$definition2 = new Definition('Acme\DemoBundle\Extension\FieldnamesTwigExtension');
$definition2->addTag('twig.extension');
$container->setDefinition('fieldnames_twig_extension', $definition2);
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
}
public function getAlias() {
return 'acme_demo';
}
}
I tried your code on my application and had the same error. To resolve it :
Acme\DemoBundle\DependencyInjection\AcmeDemoExtension.php
Remove this lines :
$definition2 = new Definition('Acme\DemoBundle\Extension\FieldnamesTwigExtension');
$definition2->addTag('twig.extension');
$container->setDefinition('fieldnames_twig_extension', $definition2);
Acme\DemoBundle\Resources\config\services.yml
Replace this lines:
fieldnames:
class: Acme\DemoBundle\Extension\FieldnamesTwigExtension
arguments: [#service_container]
By :
fieldnames:
class: Acme\DemoBundle\Extension\FieldnamesTwigExtension
arguments: [#service_container]
tags: [{ name: twig.extension }]
You are making this too hard. You should be injecting helpers directly into you twig extension. You should also be doing your tagging in services.yml. Not in the dependency injection extension.
http://symfony.com/doc/current/cookbook/templating/twig_extension.html
services.yml
fieldnames:
class: Acme\DemoBundle\Extension\FieldnamesTwigExtension
arguments: [#helpers]
tags:
- { name: twig.extension }
And adjust the rest of your code accordingly.
In your Extension\FieldnamesTwigExtension.php, is this normal that your namespace is called after use instruction ?
Which error Symfony return ?

Symfony, How to generate Asset URL in a Twig Extension class?

I have one class which extends \Twig_Extension like below :
class MYTwigExtension extends \Twig_Extension
{
protected $doctrine;
protected $router;
public function __construct(RegistryInterface $doctrine , $router)
{
$this->doctrine = $doctrine;
$this->router = $router;
}
public function auth_links($user , $request)
{
// Some other codes here ...
// HOW TO GENERATE $iconlink which is like '/path/to/an/image'
$html .= "<img src=\"$iconlink\" alt=\"\" /> ";
echo $html;
}
}
My question is How to generate Asset links in a Twig Extension ? I would like a replacement for ASSET helper in my class. Bassically I have no idea what I have to inject or use here ! Thanks in advance.
<img src="{{ asset('img/icons/modules/timesheet.png') }}" alt="" />
You can use the templating.helper.assets service directly.
use Symfony\Component\DependencyInjection\ContainerInterface;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
and use it like so:
$this->container->get('templating.helper.assets')->getUrl($iconlink);
Injecting just the templating.helper.assets directly does not work in this case because the twig extension cannot be in the request scope. See the documentation here: https://symfony.com/doc/2.3/cookbook/service_container/scopes.html#using-a-service-from-a-narrower-scope
I didn't want to deal with the Dependency Injection Container. This is what I did:
use Twig_Environment as Environment;
class MyTwigExtension extends \Twig_Extension
{
protected $twig;
protected $assetFunction;
public function initRuntime(Environment $twig)
{
$this->twig = $twig;
}
protected function asset($asset)
{
if (empty($this->assetFunction)) {
$this->assetFunction = $this->twig->getFunction('asset')->getCallable();
}
return call_user_func($this->assetFunction, $asset);
}
I've looked at Twig_Extension class code, and found this initRuntime method there, to be overriden in our custom Extension class. It receives the Twig_Environment as an argument! This object has a getFunction method, which returns a Twig_Function instance. We only need to pass the function name (asset, in our case).
The Twig_Function object has a getCallable method, so we finally can have a callable asset function.
I've gone a bit further creating an asset method for my own extension class. Anywhere else on it, I can simply call $this->asset() and obtain the same result as {{ asset() }} in the templates.
EDIT: The getFunction call at initRuntime throws a scope exception when clearing the cache. So I moved it to the custom asset method. It works fine.
Here's a simple and clean way for Symfony 2.8:
services.yml:
app.twig_extension:
class: Path\To\AcmeExtension
arguments:
assets: "#templating.helper.assets"
In the TWIG extension:
use Symfony\Bundle\FrameworkBundle\Templating\Helper\AssetsHelper;
class AcmeExtension
{
protected $assets;
public function __construct(AssetsHelper $assets)
{
$this->assets = $assets;
}
}
Then you can use it in any function of the extension like this:
$this->assets->getUrl('myurl');
In Symfony 5.3 that worked for me:
(just do what the assets extension does and inject Packages)
use Symfony\Component\Asset\Packages;
use Twig\Extension\AbstractExtension;
use Twig\Extension\ExtensionInterface;
class AppExtension extends AbstractExtension implements ExtensionInterface
{
public function __construct(Packages $packages)
{
$this->packages = $packages;
}
// ... your other methods
private function asset($path, $packageName = null)
{
return $this->packages->getUrl($path, $packageName);
}
}
In Symfony 2.8 that works for me:
# services.yml
services:
app.twig_extension:
class: AppBundle\Twig\AppTwigExtension
public: false
arguments:
- #templating.helper.assets
tags:
- { name: twig.extension }
AppTwigExtension class:
namespace AppBundle\Twig;
use Symfony\Bundle\FrameworkBundle\Templating\Helper\AssetsHelper;
/**
* Class AppTwigExtension
* #package AppBundle\Twig
*/
class AppTwigExtension extends \Twig_Extension
{
const IMG_PATH = 'bundles/app/images/';
private $assetsHelper;
public function __construct(AssetsHelper $assetsHelper)
{
$this->assetsHelper = $assetsHelper;
}
public function getFilters()
{
return array(
new \Twig_SimpleFilter('img', array($this, 'imagePathFilter'))
);
}
/**
* Get image path relatively to host
* Usage in Twig template: {{ 'my_image.png'|img }} - equal to
* {{ asset('bundles/app/images/my_image.png') }} in Twig template:
*
* #param string $imageName (e.g. my_image.png)
* #return string
*/
public function imagePathFilter($imageName)
{
return $this->assetsHelper->getUrl(self::IMG_PATH . $imageName);
}
public function getName()
{
return 'app_twig_extension';
}
}

Resources