I want to create a Twig extension and use this:
{{ new_func(route-name) }}
To do the same thing as:
{{ render_esi(url(route-name)) }}
...but with some adjustments
It's nearly done but it's this line that needs to be changed, but I can't see how I can call an ESI from this code (outside of Twig):
return $environment->render($route); /// needs to receive route and render an ESI
-
namespace Acme\Bundle\MyBundle\Twig;
class NewTwigFunction extends \Twig_Extension
{
private $request;
public function __construct($container)
{
$this->request = $container->get('request');
}
public function getFunctions() {
return array(
'new_func' => new \Twig_Function_Method($this, 'newFunction', array('needs_environment' => true) )
);
}
public function newFunction(\Twig_Environment $environment, $route) {
$r = $this->request;
return $environment->render($route);
}
public function getName() {
return "new_func";
}
}
I'm not sure I follow why would you need this, but I think it's great as an example of an abstract question:
How do I track down & extend core functionality of Symfony2?
Finding the functionality
Seems that you're having trouble finding where is this render_esi executed, so let's tackle that!
This doesn't seem like a standard Twig feature, so it must be an extension, just like the one you're creating.
It should be located somewhere in Symfony2 core files, so we start looking into vendor/symfony/src folder. Since we already know that we're dealing with an extension of Twig, Component folder is out of the question (because Twig is a separate library from Symfony2 core components).
So we've narrowed it down to Bridge and Bundle. If we look inside them then we see Bundle/TwigBundle or Bridge/Twig. We also know that Symfony2 developers follow a strict code/architecture style, so we know exactly which folder to look for - Extension. Now it's just a matter of checking them both.
Long story short we find what we're looking for in vendor/symfony/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension, where we see a render_* function. Jackpot!
Extending the functionality
Before changing anything, we need to first emulate what's already there, so we create something like this:
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
class NewTwigFunction extends \Twig_Extension
{
private $handler;
public function __construct(FragmentHandler $handler)
{
$this->handler = $handler;
}
public function getFunctions()
{
return array(
'new_func' => new \Twig_Function_Method($this, 'newFunction', array('is_safe' => array('html')) )
);
}
public function newFunction($uri, $options = array())
{
return $this->handler->render($uri, 'esi', $options);
}
public function getName()
{
return "new_func";
}
}
Now when you call
{{ new_func(url(route-name)) }}
you should see same results as
{{ render_esi(url(route-name)) }}
But we still need to get rid of the url part.
Easy as pie, we just add the router service to our extension! Now our extension could look like this:
use Symfony\Component\Routing\Router;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
class NewTwigFunction extends \Twig_Extension
{
private $handler;
private $router;
public function __construct(FragmentHandler $handler, Router $router)
{
$this->handler = $handler;
$this->router = $router;
}
public function getFunctions()
{
return array(
'new_func' => new \Twig_Function_Method($this, 'newFunction', array('is_safe' => array('html')) )
);
}
public function newFunction($routeName, $options = array())
{
$uri = $this->router->generate($routeName);
return $this->handler->render($uri, 'esi', $options);
}
public function getName()
{
return "new_func";
}
}
and {{ new_func(route-name) }} should work as expected.
Hooking in-between
The way I understood it, you want almost the same functionality as render_esi, but with slight changes to output.
So that means that we need to hook somewhere in-between return and $this->handler->render($uri, $strategy, $options);.
How deep down the rabbit hole we need to go depends on the change.
For example, if you want to alter Response object before it's turned into actual html string, you need to find the spot where it's turned in the first place. A good bet would be to look into FragmentHandler:
protected function deliver(Response $response)
{
if (!$response->isSuccessful()) {
throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $this->request->getUri(), $response->getStatusCode()));
}
if (!$response instanceof StreamedResponse) {
return $response->getContent();
}
$response->sendContent();
}
Got it! Now you just need to extend FragmentHandler::deliver() and pass your implementation of it into your twig extenion.
Tracking down configuration
You have to understand that Symfony2 core code is not that different from what you write in your everyday life, it still abides by its own rules.
For example, when normally creating a Twig extension in Symfony2 you need to configure it as a service, right? Well, Symfony2 core extensions are configured in the same way. You just need to find where the configuration files are located.
Following the logic from Extending the functionality we know for sure that they're not located in Component. Bridge is actually a name for a design pattern - not a place where you'd place your service configuration :)
So we're left with Bundle - and obviously that's where we find all the information we need: vendor/symfony/src/Bundle/TwigBundle/Resources/config/twig.xml
Now we simply look up how original HttpKernelExtension is configured and follow its lead:
<service id="twig.extension.httpkernel" class="%twig.extension.httpkernel.class%" public="false">
<argument type="service" id="fragment.handler" />
</service>
Transforming it into a more commonly used .yml format, our extension config could look like this:
new_func:
class: Acme\Bundle\MyBundle\Twig\NewTwigFunction
arguments:
- "#fragment.handler"
# Uncomment when implementing code from 2nd example
# - "#router"
tags:
- { name: twig.extension }
public: false
Related
Aware that there is a lot of information around the net regarding this, I am still having a lot of trouble getting this to work.
I have created a custom service:
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\AccommodationType;
use App\Entity\Night;
class AvailabilityChecks {
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function nightAvailable(string $RoomCode, string $NightDate) {
$GetRoom = $this->em->getDoctrine()->getRepository(AccommodationType::class)->findOneBy([
'RoomCode' => $RoomCode
]);
$RoomQnt = $GetRoom->getNightlyQnt();
$GetNight = $this->em->getDoctrine()->getRepository(Night::class)->findOneBy([
'RoomCode' => $RoomCode,
'NightDate' => $NightDate
]);
$NumberOfNights = $GetNight->count();
if($NumberOfNights<$RoomQnt) {
return true;
}
else {
return false;
}
}
}
and have put this in services.yaml:
AvailabilityChecks.service:
class: App\Service\AvailabilityChecks
arguments: ['#doctrine.orm.entity_manager']
So when I try and use this in my controller, I get this error:
Too few arguments to function App\Service\AvailabilityChecks::__construct(), 0 passed in /mypath/src/Controller/BookController.php on line 40 and exactly 1 expected
I just can't figure out why it's not injecting the ORM stuff into the constructor! Any help greatly appreciated
The problem is in your BookController. Even though you didn't posted its code I can assume you create new AvailabilityChecks in it (on line 40).
In Symfony every service is intantiated by service container. You should never intantiate service objects by yourself. Instead BookController must ask service container for AvailabilityChecks service. How should it do it ?
In Symfony <3.3 we used generally :
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller
{
public function myAction()
{
$em = $this->get('doctrine.orm.entity_manager');
// ...
}
}
Nowadays services can be injected in controllers using autowiring which is way easier:
use Doctrine\ORM\EntityManagerInterface;
class MyController extends Controller
{
public function myAction(EntityManagerInterface $em)
{
// ...
}
}
You are using the wrong service for what you want to do. The alias doctrine that is used, e.g. in the AbstractController when you call getDoctrine() is bound to the service Doctrine\Common\Persistence\ManagerRegistry.
So the code you wrote fits better with that and you should either add #doctrine or #Doctrine\Common\Persistence\ManagerRegistry to the service definition.
Both with your current configuration or the changed one, you don't have to call $this->em->getDoctrine(), because $this->em is already equivalent to $this->getDoctrine() from your controller. Instead you could create a (private) method to make it look more like that code, e.g.:
private function getDoctrine()
{
return $this->em;
}
Then you can call $this->getDoctrine()->getRepository(...) or use $this->em->getRepository(...) directly.
In Symfony 4, you dont need to create it as services. This is automatically now. Just inject the dependencies what you need in the constructor. Be sure that you have autowire property with true value in services.yml (it is by default)
Remove this from services.yml:
AvailabilityChecks.service:
class: App\Service\AvailabilityChecks
arguments: ['#doctrine.orm.entity_manager']
You dont need EntityManagerInterface because you are not persisting anything, so inject repositories only.
<?php
namespace App\Service;
use App\Entity\AccommodationType;
use App\Entity\Night;
use App\Repository\AccommodationTypeRepository;
use App\Repository\NightRepository;
class AvailabilityChecks {
private $accommodationTypeRepository;
private $nightRepository
public function __construct(
AcommodationTypeRepository $acommodationTypeRepository,
NightRepository $nightRepository
)
{
$this->acommodationTypeRepository = $acommodationTypeRepository;
$this->nightRepository = $nightRepository;
}
public function nightAvailable(string $RoomCode, string $NightDate) {
$GetRoom = $this->acommodationTypeRepository->findOneBy([
'RoomCode' => $RoomCode
]);
$RoomQnt = $GetRoom->getNightlyQnt();
$GetNight = $this->nightRepository->findOneBy([
'RoomCode' => $RoomCode,
'NightDate' => $NightDate
]);
$NumberOfNights = $GetNight->count();
if($NumberOfNights<$RoomQnt) {
return true;
}
else {
return false;
}
}
}
In SF4, you no longer need to specify dependencies required by your custom service in the service.yaml file. All you have to do is to use dependency injection.
So remove config lines, and call your service directly in the controller method :
<?php
namespace App\Controller;
use App\Service\AvailabilityChecks ;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class AppController extends AbstractController
{
public function index(AvailabilityChecks $service)
{
...
}
}
Having said that, i think you don't need custom service to do simple operations on database. Use repository instead.
I am new with Symfony and I've searched a lot on the net how to use core PHP functions like array functions (in_array , array_combine) , string functions (strpos , strlen) , date functions (date,timestamp),etc.. In Symfony Twig file?
Firstly, create Twig/Extension directory into your bundle. Sample: AppBundle/Twig/Extension
Then, create a class for your function name. I create a JsonDecode and i use every twig file this function;
namespace AppBundle\Twig\Extension;
class JsonDecode extends \Twig_Extension {
public function getName() {
return 'twig.json_decode';
}
public function getFilters() {
return array(
'json_decode' => new \Twig_Filter_Method($this, 'jsonDecode')
);
}
public function jsonDecode($string) {
return json_decode($string);
}
}
Then,
add lines into services.yml;
twig.json_decode:
class: AppBundle\Twig\Extension\JsonDecode
tags:
- { twig.extension }
that's enough.
You have to create a custom twig extension that you could use in your twig template.
You can follow the symfony documentation
in my symfony2 project i need call the same action in many controllers and this action should return a very simple php array that then will be passed to a twig template by these controllers. How can i do it?
A pratical example can explain my situation better.
1.shared controller
// Acme/DemoBundle/Controller/MetasController
class MetasController extends Controller {
public function metasAction() {
$myArray= array();
return $myAarray;
}
}
page render controller
// Acme/DemoBundle/Controller/PageController
class PageController extends Controller {
protected $property = "test";
public function indexAction() {
$metas= $this->forward('AcmeDemoBundle:Metas:metas');
return $this->render('AcmeDemoBundle:Page:index.html.twig', array('property'=>property, 'metas'=>$metas));
}
}
when i do this i get an error: the controller must be a response array given.
You should create a service
// Acme/DemoBundle/Controller/MetasController
class MetasController {
public function metasAction() {
$myArray= array();
return $myAarray;
}
}
declare as service in Acme\DemoBundle\Resources\config\services.yml
services:
demo.metas:
class: "Acme\DemoBundle\Controller\MetasController"
Then you can use it in any other controller
// Acme/DemoBundle/Controller/PageController
class PageController extends Controller {
protected $property = "test";
public function indexAction() {
$metas= $this->get('demo.metas')->metas();
return $this->render('AcmeDemoBundle:Page:index.html.twig', array('property'=>property, 'metas'=>$metas));
}
}
In your action controller :
<?php
...
$arrayExample = array();
return $this->render('ExampleBundle:ExampleFolder:exampleTemplate', array('myArray' => $arrayExample));
And in your twig template now you have access to your array using myArray
Example :
{% for data in myArray %}
...
{% endfor %}
Try this :
use Symfony\Component\HttpFoundation\Response;
public function indexAction()
{
...
$content = $this->renderView(
'AcmeDemoBundle:Page:index.html.twig',
array('property'=> $property,
'metas' => $metas
));
return new Response($content);
}
Yes, you can register your controller as a service as it said above but I would recommend to isolate this logic in a different place. It might be a service but not controller.
As I understand you need the same array in several places. So, it might be some class registered as service or some simple class with static method providing this array. In this case your code will be much cleaner.
If you need this array only in view you can define custom twig method which will return array you need. If this array might be different time to time (if it might depend on some data) you can pass entity manager to the service providing this array or to the twig extension.
(The best use of controllers is to be just a proxy between view and data layer. It's not a good idea to use it for such purposes as you described (in my opinion of course).)
I need to populate a variable with some HTML code and make it available to my base.html.twig file.
To achive this I have made a twig extension. This is my first time using twig extentions so im not sure if this is the correct way of doing things.
Here is what I have so far:
Extension code:
class GlobalFooterExtension extends \Twig_Extension
{
public function getFilters()
{
return array(
new \Twig_Filter_Function('GlobalFooter', array($this, 'GlobalFooter')),
);
}
public function GlobalFooter()
{
$GlobalFooter = file_get_contents('http://mysite.co.uk/footer/footer.html.twig');
return $GlobalFooter;
}
public function getName()
{
return 'GlobalFooter_extention';
}
}
config.yml:
services:
imagine.twig.GlobalFooterExtension:
class: Imagine\GdmBundle\Twig\GlobalFooterExtension
tags:
- { name: twig.extension }
base.html.twig:
{{GlobalFooter}}
This give the following error:
Twig_Error_Runtime: Variable "GlobalFooter" does not exist in "ImagineGdmBundle:Default:product.html.twig" at line 2
Im sure im missing something really obvious. How do I make $GlobalFooter from my GlobalFooterExtension class available to my base.hmtl.twig file?
You want to set a global variable, not a function.
Just use getGlobals and return your variable:
class GlobalFooterExtension extends \Twig_Extension
{
public function getGlobals()
{
return array(
"GlobalFooter" => file_get_contents('http://mysite.co.uk/footer/footer.html.twig'),
);
}
public function getName()
{
return 'GlobalFooter_extention';
}
}
Or, if you want to lazy load the value of the variable, create a function and change your template to:
{{ GlobalFooter() }}
Besides this, if the footer file is on the same site, it's better to use the {% include '...' %} tag.
rename function getFilters to getFunctions
I have a symfony2 web project consisting of ten pages which are rendered via 5 controllers. User can land a site through any of the pages above (via shared link for example). And I need to show a welcome popup (just div with position:absolute) to users who opens the page for the first time during the current session.
I've already placed my popup in the common twig template which is used by all the pages needed. Now I have to determine whether to show popup or not. I'm going to show popup based on boolean value from controller.
I have to work with session and cookies, but I have to do that on each page and I don't want to write the same code (check and set cookies, output a boolean to show popup in template) in every method of each controller. Is there a way to this according to DRY concepts?
You could make a wrapper class which handles checking, setting, and getting the current session values and make it a service.
<?php
namespace My\Bundle\My\Namespace;
use Symfony\Component\HttpFoundation\Session\Session;
class SessionManager /* or whatever you want to call it */
{
public function __construct(Session $session)
{
$this->session = $session;
}
public function getMyValue()
{
return $this->session->get('my.value.key',null);
}
public function setMyValue($value)
{
$this->session->set('my.value.key',$value);
return $this;
}
public function hasMyValue()
{
return $this->session->has('my.value.key');
}
}
And in your bundle services.yml
<service id="mytag.session_manager" class="My\Bundle\My\Namespace\SesionManager">
<argument type="service" id="session" />
</service>
And in your controllers
public function someAction()
{
$sessionManager = $this->get('mytag.session_manager');
if($sessionManager->hasMyValue())
{
// do something
}
}
Thanks to the Sgoettschkes answer here Where to place common business logic for all pages in symfony2
I tried this method
http://symfony.com/doc/current/book/templating.html#embedding-controllers
And it looks awesome:
My Popup is included in the main template like this
{{ render(controller('MalyutkaMainBundle:WelcomePopup:index')) }}
Than inside the controller I manipulate session vars
class WelcomePopupController extends Controller {
public function indexAction(Request $request) {
$session = $this->get('session');
$showWelcomePopup = 0;
if ($session->has("have_seen_welcome_popup_on")) {
// tbd compare to the date of publishing of the new popup screen
} else {
$showWelcomePopup = 1;
$session->set("have_seen_welcome_popup_on", new \DateTime());
}
$params = array (
'show_welcome_popup' => $showWelcomePopup
);
return $this->render('MalyutkaMainBundle:WelcomePopup:welcome_popup.html.twig', $params);
}
}
And nothing is to be added in other controllers - just what I wanted to do.
But it is impossible to change cookies that way, so I store my data in the session.