Symfony2 FOSUserBundle Overriding Forms - symfony

I am trying to change the template for the registration form in my application so that I can add some other HTML to it. Here is the /My/UserBundle/Resources/views/Registration/register.html.twig file:
{% extends "MyUserBundle::layout.html.twig" %}
{% block fos_user_content %}
<section class="register site-content">
<header>
<h1>{{ 'layout.register'|trans({}, 'FOSUserBundle') }}</h1>
</header>
<div class="block">
{% include "FOSUserBundle:Registration:register_content.html.twig" %}
</div>
</section>
{% endblock fos_user_content %}
And I have successfully overridden the layout.html.twig:
{% extends 'MyMainBundle::layout.html.twig' %}
{% block title %}{{ site_name }}{% endblock %}
{% block content %}
{% for key, message in app.session.getFlashes() %}
<div class="{{ key }}">
{{ message|trans({}, 'FOSUserBundle') }}
</div>
{% endfor %}
{% block fos_user_content %}{% endblock %}
{% endblock %}
as well as form.html.twig:
{% extends 'FOSUserBundle::form.html.twig' %}
{% block field_row %}
<li class="form_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</li>
{% endblock field_row %}
{% block form_widget %}
<ul {{ block('container_attributes') }}>
{{ block('field_rows') }}
{{ form_rest(form) }}
</ul>
{% endblock form_widget %}
config parts:
# FOS User Configuration
fos_user:
db_driver: orm
firewall_name: main
user_class: My\UserBundle\Entity\User
from_email:
address: %admin_email%
sender_name: %site_name%
template:
engine: twig
theme: MyUserBundle::form.html.twig
I have cleared my cache.
Whenever I go to
http://localhost/register/
apache just hangs until it times out.
The best I can figure out, is the PHP maximum execution message says it crashes on a twig template in the cache on line 16. That line is function doGetParent(...) The file is:
<?php
/* FOSUserBundle::form.html.twig */
class __TwigTemplate_9cf68a2af1db50466c556a735bcdeba0 extends Twig_Template
{
public function __construct(Twig_Environment $env)
{
parent::__construct($env);
$this->blocks = array(
'field_row' => array($this, 'block_field_row'),
'form_widget' => array($this, 'block_form_widget'),
);
}
protected function doGetParent(array $context)
{
return "FOSUserBundle::form.html.twig";
}
protected function doDisplay(array $context, array $blocks = array())
{
$this->getParent($context)->display($context, array_merge($this->blocks, $blocks));
}
// line 3
public function block_field_row($context, array $blocks = array())
{
// line 4
echo " <li class=\"form_row\">
";
// line 5
echo $this->env->getExtension('form')->renderLabel($this->getContext($context, "form"));
echo "
";
// line 6
echo $this->env->getExtension('form')->renderErrors($this->getContext($context, "form"));
echo "
";
// line 7
echo $this->env->getExtension('form')->renderWidget($this->getContext($context, "form"));
echo "
</li>
";
}
// line 11
public function block_form_widget($context, array $blocks = array())
{
// line 12
echo " <ul ";
$this->displayBlock("container_attributes", $context, $blocks);
echo ">
";
// line 13
$this->displayBlock("field_rows", $context, $blocks);
echo "
";
// line 14
echo $this->env->getExtension('form')->renderRest($this->getContext($context, "form"));
echo "
</ul>
";
}
public function getTemplateName()
{
return "FOSUserBundle::form.html.twig";
}
public function isTraitable()
{
return false;
}
}
It has also timed out on \vendor\twig\lib\Twig\Template.php on line 65 Which is public function getParent(array $context)
So clearly there is some problem with getParent but I don't know what that means or how to fix it.

According to the FOSUserBundle documentation:
The easiest way to override a bundle's template is to simply place a
new one in your app/Resources folder. To override the layout template
located at Resources/views/layout.html.twig in the FOSUserBundle
directory, you would place you new layout template at
app/Resources/FOSUserBundle/views/layout.html.twig.
As you can see the pattern for overriding templates in this way is to
create a folder with the name of the bundle class in the app/Resources
directory. Then add your new template to this folder, preserving the
directory structure from the original bundle.
In my project I override FOSUserBundle's layout as they said and it's work like a charm.
So doing it at the same way you will need to create app/Resources/FOSUserBundle/views/Registration/register.html.twig. (or the form you want to override)
EDIT
Ok, I just realize that you've chosen to extend the FOSUserBundle. In that case instead of app/Resources/ you need to do it inside your bundle. But you don't need to put
{% extends 'FOSUserBundle::form.html.twig' %}
The FOSUserBundle will detect that you are overriding the bundle and will be extended automatically.
And you also need to tell your bundle that FOSUserBundle is its parent.
class YourBundle extends Bundle
{
public function getParent()
{
return 'FOSUserBundle';
}
}

Copy the file:
{% include "FOSUserBundle:Registration:register_content.html.twig" %}
to the: app/Resources/FOSUserBundle/views/ catalogue. Then it will overwrite the base FOS form. You can edit the copied file.
Otherwise you must override the FOS RegisterController.

I feel stupid, just removed the {% extends ... %} line in form.html.twig and it worked. I guess I don't understand enough about what is inherited in bundles. (I did want to inherit from the FOSUserBundle form.html.twig, but I don't know how to access that from a bundle which inherits from FOSUserBundle)

Related

How to import multiple macros?

I want to elegantly import multiple macros from a single place.
I created a file called "macros.twig" and have included it into my template:
{% include "_includes/macros" %}
Within that file, I hoped to import all my available macros like so:
{% import "_includes/macros/snippets" as snippets %}
{% import "_includes/macros/timestamp" as timestamp %}
{% import "_includes/macros/telephone" as telephone %}
{% import "_includes/macros/subscribe" as subscribe %}
{% import "_includes/macros/image" as image %}
{% import "_includes/macros/admin" as admin %}
This sort-of modular approach was suppose to make it easier to manage the macros I want to use globally; without cluttering the head of my main layout.
Currently, when I call a macro this way, I get a "Variable "subscribe" does not exist" error.
What's the preferred method of importing multiple macros at one time?
Thanks
macro tag in Twig is a kind of function you can use to avoid code repetition and is meant for a single template with {% import _self as macro %} or to be shared between some different templates for a group of controllers using the same view variables.
If you need to use a function globally in twig you better create a \Twig_SimpleFunction.
see http://twig.sensiolabs.org/doc/advanced.html#functions and http://symfony.com/doc/current/cookbook/templating/twig_extension.html
Edited based on comment
Anyway you could have something like this to autoload macro :
<?php
// src/AppBundle/Twig/MacroAutoloadExtension.php
namespace AppBundle\Twig;
class MacroAutoloadExtension extends \Twig_Extension
{
public function getFunctions()
{
return array(
// "*"" is used to get "template_macro" as $macro as third argument
new \Twig_SimpleFunction('macro_*', array($this, 'getMacro'), array(
'needs_environment' => true, // $env first argument will render the macro
'needs_context' => true, // $context second argument an array of view vars
'is_safe' => array('html'), // function returns escaped html
'is_variadic' => true, // and takes any number of arguments
))
);
}
public function getMacro(\Twig_Environment $env, array $context, $macro, array $vars = array())
{
list($name, $func) = explode('_', $macro);
$notInContext = 0; // helps generate unique context key
$varToContextKey = function ($var) use (&$context, $name, $func, &$notInContext) {
if (false !== $idx = array_search($var, $context, true)) {
return $idx;
}
// else the var does not belong to context
$key = '_'.$name.'_'.$func.'_'.++$notInContext;
$context[$key] = $var;
return $key;
};
$args = implode(', ', array_map($varToContextKey, $vars));
$twig = <<<EOT
{% import '_includes/macros/$name.twig' as $name %}
{{ $name.$func($args) }}
EOT;
try {
$html = $env->createTemplate($twig)->render($context);
} catch (\Twig_Error $e) {
$e->setTemplateFile(sprintf('_includes/macro/%s.twig', $name));
throw $e;
}
return $html;
}
public function getName()
{
return 'macro_autoload_extension';
}
}
Register the extension :
# app/config/sevices.yml
services:
...
app.macro_autoload_extension:
class: AppBundle\Twig\MacroAutoloadExtension
public: false
tags:
- { name: twig.extension }
Write some macros :
{# app/Resources/views/_includes/macros/list.twig #}
{% macro ol(array) %}
{% if array is iterable %}
<ol>
{% for item in array %}
<li>
{% if item is iterable %}
{% for sub_item in item %}{{ macro_list_ul(sub_item) }}{% endfor %}
{% else %}
{{ item }}
{% endif %}
</li>
{% endfor %}
</ol>
{% else %}
<ol><li>{{ array }}</li></ol>
{% endif %}
{% endmacro %}
{% macro ul(array) %}
{% if array is iterable %}
<ul>
{% for key, item in array %}
{{ key }}:
{% if item is iterable %}
{% for sub_item in item %}{{ macro_list_ul(sub_item) }}{% endfor %}
{% else %}{{ item }}{% endif %}
{% endfor %}
</ul>
{% else %}
<ul><li>{{ array }}</li></ul>
{% endif %}
{% endmacro %}
Then you can use everywhere in you views :
{{ macro_list_ol(['un', 'deux', 'trois']) }}
or:
{% set hash = { 'one': 1, 'two': 'deux', 'posts': posts } %}
{{ macro_list_ul(hash) }}
Bonus
Usually when you import macro in a template (one file) with _self or from another template, if you need a macro in a set tag, the macro is not available since set tag has a different scope than _self (even if it shares context) :
{# /app/Resources/views/includes/macro/outer.html.twig #}
{% macro function(args) %}
...
{% endmacro %}
plus
{# /app/Resources/views/Bundle/Controller/action.html.twig #}
{% macro inner_macro(arg1, arg2) %}
{# render something #}
{# cannot access context of this view, only args #}
{% endmacro %}
{% import _self as inner %}
{% import '/includes/macro/outer_macro.html.twig' as outer %} {# cannot access context either %}
...
{% set some_var %}
{# can access context but neither outer or inner #}
{{ inner.inner_macro('yes', 64) }} {# will not work #}
{# you need to do import _self as inner again %}
{# this is fix by both my answer and the one by #KalZekdor #}
{{ macro_outer_function(var_from_context) }} {# will work #}
{% endset %}
{{ some_var }}
You can even call macro from macro without using import.
Update
I created a gist
This was a bit difficult for me, as well, but I put together a nice bundle with an Event Listener that autoloads the macros.
namespace App\Common\UIExtensionBundle\Listeners;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Bundle\TwigBundle\TwigEngine;
class UIExtenderListener
{
private $macroNamespace = 'ui';
public function __construct(\Twig_Environment $oTwig, $aMacros)
{
$this->twig = $oTwig;
//Macros
$this->macros = $aMacros;
}
public function onKernelRequest(GetResponseEvent $oEvent)
{
$templates = [];
foreach ($this->macros as $macro => $template)
{
$templates[$macro] = $this->twig->loadTemplate($template);
}
$this->twig->addGlobal($this->macroNamespace, $templates);
}
}
The services.xml for the bundle:
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="uiext.macros" type="collection">
<parameter key="test">UIXBundle:ui:test.html.twig</parameter>
<parameter key="list">UIXBundle:ui:list.html.twig</parameter>
<parameter key="entity">UIXBundle:ui:entity.html.twig</parameter>
</parameter>
</parameters>
<services>
<service id="uiext.extender" class="App\Common\UIExtensionBundle\Listeners\UIExtenderListener" scope="container">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="1000" />
<argument type="service" id="twig"/>
<argument>%uiext.macros%</argument>
</service>
</services>
</container>
Here's the views\ui\list.html.twig file:
{% macro ol(arr) %}
<ol>
{% for item in arr %}
<li>{{ item }}</li>
{% endfor %}
</ol>
{% endmacro %}
{% macro ul(arr) %}
<ul>
{% for item in arr %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% endmacro %}
Then, from any twig template, just add {{ ui.list.ul(listArr) }}

Sonata Admin - Disable list view

I'm facing a problem with Sonata Admin. Is there a way to disable the "list view" ? I would like to fetch the first entity in the database and to go on it when clicking on the link in the sidebar. But not for all entry.
Is there a clean way to do it ? (I have the idea to check the entity in a custom controller, and to redirect to list view or edit view depending on the entity, but that's not really clean)
If you want to do a custom query for your list view, you could override the createQuery method in your Admin class like this :
class EntityAdmin
{
public function createQuery($context = 'list')
{
$query = parent::createQuery($context);
$query->andWhere(
$query->expr()->eq($query->getRootAlias() . '.id', ':id')
);
$query->setParameter('id', 1);
return $query;
}
}
You will have only your first entity in your list view.
UPDATE
You could override the standard_layout.html.twig to change the link in your sidebar :
First you need to set where your template is located:
app/config/config.yml
sonata_admin:
templates:
layout: ApplicationSonataAdminBundle::standard_layout.html.twig
Change the behaviour of the sidebar, for Sonata Admin 2.3 this is how you do it :
src/Application/Sonata/AdminBundle/Resources/Views/standard_layout.html.twig l.224
<ul class="treeview-menu{% if active %} active{% endif %}">
{% for admin in group.items %}
{% if admin.code == 'sonata.admin.entity' and
admin.hasroute('edit') and
admin.isGranted('EDIT') %}
<li{% if app.request.get('_sonata_admin') == admin.code %} class="active"{% endif %}><i class="fa fa-angle-double-right"></i> {{ admin.label|trans({}, admin.translationdomain) }}</li>
{% else %}
{% if admin.hasroute('list') and admin.isGranted('LIST') %}
<li{% if app.request.get('_sonata_admin') == admin.code %} class="active"{% endif %}><i class="fa fa-angle-double-right"></i> {{ admin.label|trans({}, admin.translationdomain) }}</li>
{% endif %}
{% endif %}
{% endfor %}
</ul>
You must change 'sonata.admin.entity' by the identifier of your admin service.
Also if you want to remove access to the list you should add in your Admin class
use Sonata\AdminBundle\Route\RouteCollection;
class EntityAdmin
{
protected function configureRoutes(RouteCollection $collection)
{
$collection->remove('list');
}
}

Twig: Cannot override a block that's defined in a macro

I am importing a macro from a layout file, and inside that macro I define a few twig blocks.
When I extend my 'base' layout file in a new 'view' file, I cannot override the content of the blocks that I defined in the macro.
Here is a very simplified, minimal version of my use-case.
Structure:
base.twig
{% import 'macros' as macros %}
<section>
{{ macros.render_sections() }}
{% block working %}
content1-default
{% endblock %}
</section>
macro.twig
{% macro render_sections() %}
<section>
{% block notworking %}
content2-default
{% endblock %}
</section>
{% endmacro %}
view.twig
{% extends "base" %}
{% block working %}
content1-override
{% endblock %}
{% block notworking %}
content2-override
{% endblock %}
Expected behavior:
I expect to see "content1-override content2-override" in my html.
What actually happens:
I see 'content1-override content2-default'
Is there a way to pass the blocks scope to a macro?
I have tried defining the macro inside the base.twig file, as to rule out the import function, but that didn't help.
and obviously everything else works because I can see the block that does get overriden.
You should learn about Twig's internals:
all Twig files are converted to PHP classes
{% extends %} is the equivalent for the litteral PHP extends (used for inheritance).
{% import %} assign a property of your context to an object of type "the twig file you're importing"
blocks are no more than php methods, and the native php inheritance let you overwrite Twig blocks sweetly.
So, taking this into account, your converted twig code will look like this:
class base {
public function display() {
$context['macros'] = new macros();
$context['macros']->render_sections();
echo '<section>';
echo $this->blockWorking();
echo '</section>';
}
public function blockWorking() {
echo "content1-default";
}
}
class macros {
public function render_sections() {
echo '<section>';
echo $this->blockNotWorking();
echo '</section>';
}
public function blockNotWorking() {
echo "content2-defualt";
}
}
class view extends base {
public function blockWorking() {
echo "content1-override";
}
public function blockNotWorking() {
echo "content2-override";
}
}
$view = new view();
$view->display();
You can clearly see here that the blockNotWorking() method of the view class can never overwrite the macro.

Unable to override KnpMenuBundle template

With ...MyBundle\Resources\views\Menu\knp_menu.html.twig, deleting the </li> has no effect on the rendered menu. (Removing the tag is done to remove the space between inline list elements.) I have followed the advice provided in this answer, including the {% import 'knp_menu.html.twig' as knp_menu %} mentioned toward the bottom of that post. Is this because knp_menu.html.twig already extends knp_menu_base.html.twig? Or what?
layout.html.twig:
...
{{ render(controller('VolVolBundle:Default:userMenu')) }}
...
userMenuAction:
$user = $this->getUser();
$tool = $this->container->get('vol.toolbox');
$type = $tool->getUserType($user);
return $this->render(
'VolVolBundle:Default:userMenu.html.twig', array('type' => $type)
);
userMenu.html.twig
...
{% if type is not null %}
{% set menu = "VolVolBundle:Builder:"~type~"Menu" %}
{{ knp_menu_render(menu) }}
{% endif %}
The answer was found deep in here. All that's required to do a global override of the template is to modify config.yml.
config.yml:
...
knp_menu:
twig: # use "twig: false" to disable the Twig extension and the TwigRenderer
template: VolVolBundle:Menu:knp_menu.html.twig
...

Symfony 2 - Access mapped Object property form twig

I hava a entity with the following flied:
/**
* #ORM\ManyToOne(targetEntity="Document", inversedBy="posts")
* #ORM\JoinColumn(name="document_id", referencedColumnName="id")
*/
protected $image;
which I get with the following method:
public function indexAction() {
$posts = $this->getDoctrine()
->getRepository('airpaprcms2Bundle:Post')
->findByPageType(1);
if (!$posts) {
throw $this->createNotFoundException('No posts in Database!');
}
return $this->render('airpaprcms2Bundle:Default:index.html.twig', array('posts' => $posts));
}
How can I access a property of the mapped object form within twig? I tried post.image.file (the mapped entity Document has a property file)
{% for post in posts %}
<div>
<h1>
{{ post.title }}
</h1>
<p>{{ post.text }}</p>
<img src="{{ post.image.name }}" alt="{{ post.title }}" />
</div>
{% endfor %}
and get the following error message:
Item "file" for "" does not exist in airpaprcms2Bundle:Default:index.html.twig at line 11
What is the right syntax to access the mapped document property?
You can access the property of your linked entity with the following twig syntax:
{% for post in posts %}
{{ post.entityName.propertyName }}
{% endfor %}
In your case that would be:
{% for post in posts %}
{{ post.image.propertyName }}
{% endfor %}
Be sure to check that all post entities are linked to an image object. If one post entity has no link to an image, you will get a property not found error when trying to render your page.

Resources