Change Twig url() output in Symfony - php

What is the file/class that ultimately executes the Twig url() function, like here: Home in twig.
I know I can use a decorator to change functions with something like:
App\Services\MyRouter:
decorates: 'router'
arguments: ['#App\Services\MyRouter.inner']
and
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
$name = 'test'.$name;
return $this->router->generate($name, $parameters, $referenceType);
}
The above changes the route which affects other areas like loading controllers.
All I'm after is the final output in the twig document. I haven't been able to find the the correct service to decorate.
edit
Based off the answers I have been working with twig.extension.routing but now I just get an "Unknown "path" function." exception. My expectation would be for nothing to happen and my function returns the original method.
App\Service\TwigUrlDecorator:
decorates: 'twig.extension.routing'
arguments: ['#App\Service\TwigUrlDecorator.inner']
public: false
<?php
// src/Service/TwigUrlDecorator.php
namespace App\Service;
use Twig\Extension\AbstractExtension;
class TwigUrlDecorator extends AbstractExtension
{
public function getPath($name, $parameters = array(), $relative = false)
{
return parent::getPath($name, $parameters, $relative);
}
}

The url twig function is executed by Symfony\Bridge\Twig\Extension\RoutingExtension::getUrl().
You can find the class definition here, and the specific method here. The service is defined here, where you can see the service name is twig.extension.routing.
I guess you could decorate the extension, but considering how simple it is, it might be simpler just to define your own URL generating twig function by defining a new Twig Extension.

Related

How to use Symfony Decorator pattern to change a Twig function?

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

Call built-in filter in custom Twig filter

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

Symfony 4, get .env parameter from a controller, is it possible and how?

From symfony 4, I want create a global parameter and get the value of this parameter from a controller.
This parameter is the path of an another app in the server. So, I thinked add the paramerter in the .env file. But how can I get the value of this parameter from a controller?
For Symfony 5
in your .env file
APP_PARAM=paramvaluehere
in your config/services.yaml
parameters:
app.paramname: '%env(APP_PARAM)%'
in your controller
$this->getParameter('app.paramname');
If someone is stil looking for a quick fix but not much symfonish,
define your parameter in .env
MY_PARAM='my param value'
and then in controller call it
echo $_ENV['MY_PARAM'];
if you call the varaible in the controller you better define it in the config/services.yaml under parameters section and access it through parameterBag is much symfonish way.
Did you try with:
$this->getParameter('your parameter');
Edit:
This may help you -> https://symfony.com/doc/current/components/dotenv.html
Before to user getParameter() you have to add this in services.yaml
parameters:
your_parameter: '%env(your_parameter)%' # from .env file
Add the variable in the config parameters :
parameters:
your_variable: '%env(YOUR_ENV_VARIABLE)%'
Fetch it from the controller
$var = $this->getParameter('your_variable');
Yes it is. You have 03 steps configuration : Filstly - declare yours variables in env. Secondly - configure service file and finally - call your parameter in your controller :
_ In controllers extending from the AbstractController, use the getParameter() helper :
YAML file config
# config/services.yaml
parameters:
kernel.project_dir: "%env(variable_name)%"
app.admin_email: "%env(variable_name)%"
In your controller,
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class UserController extends AbstractController
{
// ...
public function index(): Response
{
$projectDir = $this->getParameter('kernel.project_dir');
$adminEmail = $this->getParameter('app.admin_email');
// ...
}
}
_ If controllers not extending from AbstractController, inject the parameters as arguments of their constructors.
YAML file config
# config/services.yaml
parameters:
app.contents_dir: "%env(variable_name)%"
services:
App\Controllers\UserController :
arguments:
$contentsDir: '%app.contents_dir%'
In your controller,
class UserController
{
private $params;
public function __construct(string $contentsDir)
{
$this->params = $contentsDir;
}
public function someMethod()
{
$parameterValue = $this->params;
// ...
}
}
_ Finally, if some controllers needs access to lots of parameters, instead of injecting each of them individually, you can inject all the application parameters at once by type-hinting any of its constructor arguments with the ContainerBagInterface:
YAML file config
# config/services.yaml
parameters:
app.parameter_name: "%env(variable_name)%"
In your service,
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
class UserController
{
private $params;
public function __construct(ContainerBagInterface $params)
{
$this->params = $params;
}
public function someMethod()
{
$parameterValue = $this->params->get('app.parameter_name');
// ...
}
}
source Accessing Configuration Parameters
In Symfony 4.4 this works fine:
<?php
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
class SomeController extends AbstractController
{
public function someMethod(Request $request)
{
$parameterValue = $request->server->get('env_varname');
// ...
}
}
also in TWIG:
{{ app.request.server.get('env_varname') }}

Twig can't find function created inside TwigExtension class

I am trying to call Twig function created in TwigExtension (Symfony 3.3). Problem is that I can't find what I did wrong and I am not sure why it is not working
Does someone knows where is the problem?
This is error I am getting:
Unknown "getCurrentLocale" function.
Here is my code:
Twig Extension:
<?php
namespace AppBundle\Extension;
use Symfony\Component\HttpFoundation\Request;
class AppTwigExtensions extends \Twig_Extension
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function getFunctions()
{
return [
new \Twig_SimpleFunction('getCurrentLocale', [$this, 'getCurrentLocale']),
];
}
public function getCurrentLocale()
{
dump ($this->request);
/*
* Some code
*/
return "EN";
}
public function getName()
{
return 'App Twig Repository';
}
}
Services:
services:
twig.extension:
class: AppBundle\Extension\AppTwigExtensions
arguments: ["#request"]
tags:
- { name: twig.extension }
Twig:
{{ attribute(country.country, 'name' ~ getCurrentLocale() ) }}
So what is your overall plan with the extension. Do you still need it when app.request.locale in twig returns the current locale? (which it does)
Also by default the #request service does not exist anymore.
In Symfony 3.0, we will fix the problem once and for all by removing the request service — from New in Symfony 2.4: The Request Stack
This is why you should get something like:
The service "twig.extension" has a dependency on a non-existent service "request".
So you made this service? Is it loaded? What is it? You can see all available services names matching request using bin/console debug:container request.
If you do need the request object in the extension, if you are planning to do more with you would want to inject the request_stack service together with $request = $requestStack->getCurrentRequest();.
Somehow the code, symfony version and the error message you posted don't correlate. Also in my test, once removing the service arguments it worked fine. Try it yourself reduce the footprint and keep it as simple as possible, which in my case was:
services.yml:
twig.extension:
class: AppBundle\Extension\AppTwigExtensions
tags:
- { name: twig.extension }
AppTwigExtensions.php:
namespace AppBundle\Extension;
class AppTwigExtensions extends \Twig_Extension {
public function getFunctions() {
return [
new \Twig_SimpleFunction('getCurrentLocale', function () {
return 'en';
}),
];
}
}
And take it from there, figure out when it goes wrong.

How does Symfony2 passes the parameter of a URI to the controller Action method?

I have started learning Symfony2. I came across a doubt: if I have this route:
# app/config/routing.yml
hello:
path: /hello/{name}
defaults: { _controller: AcmeHelloBundle:Hello:index }
And this controller:
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction($name)
{
return new Response('<html><body>Ciao '.$name.'!</body></html>');
}
}
Internally Symfony2 (inside app/bootstrap.php.cache) calls the call user_func_array() PHP built-in function:
$arguments = $this->resolver->getArguments($request, $controller);
$response = call_user_func_array($controller, $arguments);
And the call to the getArguments() method returns an array of arguments to pass to the action method. But if the controller were:
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction($n)
{
return new Response('<html><body>Ciao '.$n.'!</body></html>');
}
}
Symfony would have complained with a RuntimeException because no param $n is set.
My question is: how Symfony controls this behaviour, I mean, if a route has a {name} param, why the controller must have an action method with a $name parameter and the parameter must be called $name?
Cause in plain PHP, this would work:
$name = 'Alex';
function indexAction($n) {
echo $n;
}
$f = 'indexAction';
$arguments = array($name);
call_user_func_array($f, $arguments);
Even if the functions signature accepts a param named as $n and not as $name.
I hope that this question is understandable, if not, please tell me and I will make an edit.
Thanks for the attention!
It's all done in the Controller Resolver HttpKernel/Controller/ControllerResolver.php via getArguments() & doArguments().
For a better understanding, you will find what you need in Getting The Controller Arguments
Edit: Answer comment.
Does Symfony use the ReflectionParameter class internally in order to keep track of the params of the method's signature and match them with the route params?
Yes, the ControllerResolver uses:
ReflectionMethod to keep track of the params of the method's signature if $controller is a method
ReflectionObject if $controller is an object
ReflectionFunction if $controller is an function
Here is how:
public function getArguments(Request $request, $controller)
{
if (is_array($controller)) {
$r = new \ReflectionMethod($controller[0], $controller[1]);
} elseif (is_object($controller) && !$controller instanceof \Closure) {
$r = new \ReflectionObject($controller);
$r = $r->getMethod('__invoke');
} else {
$r = new \ReflectionFunction($controller);
}
return $this->doGetArguments($request, $controller, $r->getParameters());
}
The request is handled by the Symfony front controller (e.g. app.php);
The Symfony core (i.e. Kernel) asks the router to inspect the request;
The router matches the incoming URL to a specific route and returns information about the route, including the controller that should be executed;
The Symfony Kernel executes the controller, which ultimately returns a Response object.
Resource
In your example you only have one parameter on your action, so it's obvious to us that it needs to be populated from the route.
To extend your example, if you added another parameter to the route such as:
# app/config/routing.yml
hello:
path: /hello/{name}/{surname}
defaults: { _controller: AcmeHelloBundle:Hello:index }
And changed your controller to:
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction($s, $n)
{
return new Response('<html><body>Ciao '.$n.' '.$s.'!</body></html>');
}
}
Symfony wouldn't know which variable $s or $n to populate with which route parameter.
If you change your action:
public function indexAction($surname, $name)
{
return new Response('<html><body>Ciao '.$name.' '.$surname.'!</body></html>');
}
Now, Symfony can look at your argument names and map them to the route parameters. It also means you can have your indexAction arguments in any order as long as their names match the route parameters. I believe Symfony internally uses the Reflection API to figure all this out.

Categories