Sonata admin form field on same line - css

Is there a way in configureformsfields method from admin to show field on a same line and not one below the other? A CSS class for example?

You can add CSS class with an appropriate display option to the selected field as:
->add('fieldname', null, [
'attr' => ["class" => "your-custom-class"]
])
Also you can modify .form-group class (to make all fields inline):
.form-group {
display: inline-block;
}
If you want your input to be inline with its label:
div.sonata-ba-field.sonata-ba-field-standard-natural {
display: inline-block;
}
Tutorial how to create a CSS file and load it into Sonata template could be found here.

In symfony 4 you can configure it globally in the config/packages/sonata_admin.yaml
sonata_admin:
options:
form_type: 'horizontal'
Gives:

There is some workaround:
- Create custom form type to add new option to form field
class CustomTextType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['parent_div_class'] = $options['parent_div_class'];
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'custom_text_type';
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'parent_div_class' => null,
]
);
}
Create custom form theme that extends sonata one
{% extends '#SonataDoctrineORMAdmin/Form/form_admin_fields.html.twig' %}
{% block form_row %}
{% if parent_div_class is defined and parent_div_class %}
<div class="{{ parent_div_class }}">
{{ parent() }}
</div>
{% else %}
{{ parent() }}
{% endif %}
{% endblock %}
Set this theme in your admin class
$this->setFormTheme(['#Admin/form/form_theme.html.twig']);
enjoy.

The SonataCoreBundle provides the form_type option for this.
# app/config/config.yml
sonata_core:
form_type: horizontal

Related

How to add a "fallback label" in sonata admin list views

I am displaying some entity relations in a Sonata Admin list view. The problem: When no relation exists (what is legal in my case) the table cell stays empty:
What I want is basically this:
I tried to overwrite the default template (base_list_field.html.twig):
$listMapper
->add(
'example',
null,
array(
'template' => 'AppBundle:Admin:listItemWithFallback.html.twig'
)
)
;
But even when I only extend the default template, all links for existing references stop working. Also I cannot figure out where to add my fallback. My AppBundle:Admin:listItemWithFallback.html.twig looks like this:
{% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %}
With this result:
Even when I copy the whole code from base_list_field.html.twig into my own template, the links stop to work.
So: How can I add a fallback label without overwriting the whole default templates? I want to modify as less of the base templates as possible.
Edit:
This is a workaround using a kernel.event_listener, hope there is a nicer way to achieve this:
services.yml:
services:
empty_cells.listener:
class: AppBundle\Listener\EmptyAdminTableCellListener
arguments:
- '#translator'
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
EmptyAdminTableCellListener.php:
namespace AppBundle\Listener;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
class EmptyAdminTableCellListener
{
/**
* #var TranslatorInterface
*/
protected $translator;
/**
* #param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* #param FilterResponseEvent $event
*/
public function onKernelResponse(FilterResponseEvent $event)
{
$request = $event->getRequest();
$path = $request->getPathInfo();
if (strpos($path, '/admin/') !== false) {
$emptyText = $this->translator->trans('Not set', [], 'admin');
$response = $event->getResponse();
$content = $response->getContent();
$content = preg_replace(
';(<td[^>]*?>)[\s]+?(</td>);is',
sprintf('$1%s$2', $emptyText),
$content
);
$response->setContent($content);
}
}
}
Here is how to recreate the working links in your custom template:
{{ value }}
with the fallback and all:
{% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %}
{% block field %}
{% if object.species %}
{{ value }}
{% else %}
{{ 'Your fallback text'|trans() }}
{% endif %}
{% endblock %}
Hope that helps

Symfony 2 - rearrange form fields

In our Symfony2 project we have a very complex structure for forms with embedded forms.... Now we got the requirement to bring the output of the form in an specific order.
And here is the problem: We use the form_widget(form) and now we are looking for a solution in the object (e.g. via annotations) or the formbuilder to move a specific field to the end of the form. in symfony 1.4 it was the widget-movefield() function, i guess...
Thx...
You can re-order the fields using this bundle: https://github.com/egeloen/IvoryOrderedFormBundle
This allows you to do things like this:
$builder
->add('g', 'text', array('position' => 'last'))
->add('a', 'text', array('position' => 'first'))
->add('c', 'text')
->add('f', 'text')
->add('e', 'text', array('position' => array('before' => 'f')))
->add('d', 'text', array('position' => array('after' => 'c')))
->add('b', 'text', array('position' => 'first'));
This was going to be in core, but was rejected and pulled out into a bundle.
Had same issue today with the form elements ordering.
Ended up with a trait that will override finishView method and reorder items in children property of a FormView:
trait OrderedTrait
{
abstract function getFieldsOrder();
public function finishView(FormView $view, FormInterface $form, array $options)
{
/** #var FormView[] $fields */
$fields = [];
foreach ($this->getFieldsOrder() as $field) {
if ($view->offsetExists($field)) {
$fields[$field] = $view->offsetGet($field);
$view->offsetUnset($field);
}
}
$view->children = $fields + $view->children;
parent::finishView($view, $form, $options);
}
}
Then in type implement getFieldsOrder method:
use OrderedTrait;
function getFieldsOrder()
{
return [
'first',
'second',
'next',
'etc.',
];
}
There's no need to "reorder" fields. All you need to do is to call form_label and/or form_widget for each field individually. Assuming you use Twig you could, for example, do:
<div>{{ form_label(form.firstName) }}</div>
<div>{{ form_widget(form.firstName) }}</div>
<div>{{ form_label(form.lastName) }}</div>
<div>{{ form_widget(form.lastName) }}</div>
here is the solution I came up with.
I created a class that my types extend.
namespace WF\CORE\CoreBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class WfBaseForm extends AbstractType
{
protected function useFields(FormBuilderInterface $builder, $fields)
{
foreach ($builder->all() as $field) {
if (!in_array($field->getName(), $fields))
$builder->remove($field->getName());
}
}
public function reorder(FormBuilderInterface $builder, $keys = array())
{
$fields = $builder->all();
$ordered_fields = array_merge(array_flip($keys), $fields);
$this->useFields($builder, array());
foreach($ordered_fields as $field)
$builder->add($field);
}
public function getName()
{
return 'base';
}
}
Then you can use it in your inherited classes.
class AddressType extends WfBaseForm
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// I add as many fields as I need
$builder->add( '…');
}
This class extends the one above
class ModifyAddressType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('title', 'text', array('constraints' => array(new NotBlank())));
$this->reorder($builder, array('title', 'firstname', 'lastname'));
}
}
I had the same problem, but solved it in a different way. Here is my solution, as an idea for others who are searching for this problem.
You could add all the form fields in an event listener, because here you have all the objects data to decide on the fields order. You could for example use a method from the data object to decide on the fields order:
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Don't use $builder->add(), or just for those fields which are always
// at the beginning of the form.
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$entry = $event->getData();
$form = $event->getForm();
// Now add all the fields in the order you need them to be, e.g by reading
// the needed order from the data entry.
if ($entry->isFieldAFirst()) {
$form->add(fieldA);
$form->add(fieldB);
} else {
$form->add(fieldB);
$form->add(fieldA);
}
}
}
As I understand, you want to use only form_widget(form) in final template.
Let assume we have two inherited models (ModelA, ModelB) and form types for them (ModelAType, ModelBType)
class ModelA {
private $A;
private $B;
// Getters and setters
}
class ModelB extends ModelA {
private $C;
// Getters and setters
}
/**
* #DI\Service(id = "form.type.modelA")
* #DI\Tag("form.type", attributes={ "alias":"model_a_type" })
*/
class FormAType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('A')
->add('B')
;
}
// getName and so on
}
/**
* #DI\Service(id = "form.type.modelA")
* #DI\Tag("form.type", attributes={ "alias":"model_b_type" })
*/
class FormAType extends AbstractType {
public function getParent() {
return "model_a_type";
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('C')
;
}
// getName and so on
}
If you render formB, you will get A,B,C order, but you want A,C,B.
To accomplish that, create form template and add reference to app. config file:
#app/config/config.yml
twig:
....
form:
resources:
- YourAppBundle:Form:fields.html.twig
{# src/YourAppBundle/Resources/views/Form/fields.html.twig #}
{% block model_a_type_widget %}
{{ form_widget(form.A) }}
{{ form_widget(form.B) }}
{% endblock model_a_type_widget %}
{% block model_b_type_widget %}
{{ form_widget(form.A) }}
{{ form_widget(form.C) }}
{{ block('model_a_type_widget') }}
{% endblock model_b_type_widget %}
Now, when you render formB, you will see desired order and keep code structured.
It happens because every widget rendered only once, so if you render it before calling parent block, you will change their order.
You can do it right in your controller before you render the template:
$form = ...
$view = $form->createView();
// Perform standard array operations to reorder: `$view->children`
// Here's a very simple example:
$firstField = $view->children['firstField'];
unset($view->children['firstField']);
$view->children['firstField'] = $firstField;
return array('form' => $view);
uasort works as well, however usort creates a non-associative array which will break the form rendering later on.
{{ form_start(form) }}
<div>{{ form_label(form.title) }}</div>
<div>{{ form_widget(form.title,{'id': 'blog_title'} )}}</div>
<div>{{ form_label(form.tag) }}</div>
<div>{{ form_widget(form.tag,{'id': 'blog_tag'} )}}</div>
<div>{{ form_label(form.category) }}</div>
<div>{{ form_widget(form.category,{'id': 'blog_category'} )}}</div>
<div>{{ form_label(form.image) }}</div>
<div>{{ form_widget(form.image,{'id': 'blog_image'} )}}</div>
<div>{{ form_label(form.body) }}</div>
<div>{{ form_widget(form.body,{'id': 'editor'} )}}</div>
<input type="submit" class="btn btn-success" value="Create" />
{{ form_end(form) }}

Double validation errors in custom form type

I'm implementing a custom form type that provides an autocomplete field to select a location (country,city or spot). The form type creates two fields, one text field for the autocomplete search input and one hidden field to hold the selected id of the selected location.
When typing into the text field, a server call is made and results are displayed via jquery autocomplete. If a location is selected, the id of the selected location is written to the hidden field whereas the name of the location is displayed in the text field. On the server, I use a client transformer to lookup the entity of the id passed by the hidden field. The text field is ignored.
My model class defines a location field with a property to write back the location entity annotated with a NotNull validation constraint.
Everything works perfectly fine so far but if I do not select a location, the validation message "This value should not be null." is displayed two times.
The bundle is public and can be found in my github repo. The relevant classes are the LocationFieldType and the LocationDataTransformer and the form theme.
And now for how I'm integrating the form type into my project. I added the whole code, sorry for the mass;)
In the model, I define the property as following:
class JourneyCreate
{
/**
* #Assert\NotNull()
* #Assert\Choice(choices = {"offer", "request"})
*/
public $type;
/**
* #Assert\NotNull()
* #Assert\Date()
*/
public $date;
/**
* #Assert\NotNull()
* #Assert\Time()
*/
public $time;
/**
* #Assert\NotNull()
*
*/
public $start;
/**
* #Assert\NotNull()
*
*/
public $destination;
public function buildJourney(User $owner)
{
switch($this->type)
{
case 'offer':
$journey = new JourneyOffer();
break;
case 'request':
$journey = new JourneyRequest();
break;
default:
throw new \InvalidArgumentException('Invalid journey type');
}
$journey->setDate($this->date);
$journey->setTime($this->time);
$journey->addStation(new JourneyStation($this->start));
$journey->addStation(new JourneyStation($this->destination));
$journey->setOwner($owner);
return $journey;
}
}
And in the main form I add the field as following:
class JourneyCreateType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type','choice', array(
'choices' => array(
'offer' => 'Driver',
'request' => 'Passanger',
),
'empty_value'=>'',
'multiple' => false,
'expanded' => true,
))
->add('date','date',array(
'widget' => 'single_text',
'format' => $this->getDateFormat(\IntlDateFormatter::TRADITIONAL),
))
->add('time','time',array(
'widget' => 'single_text',
))
->add('start','room13_geo_location')
->add('destination','room13_geo_location')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Form\Model\JourneyCreate',
));
}
public function getName()
{
return 'journey_create';
}
}
And the controller code:
/**
* #Route("/create/{type}", defaults={"type" = null})
* #Template()
*/
public function createAction($type=null)
{
if($type !== null && !in_array($type,array('request','offer')))
{
throw new NotFoundHttpException();
}
$journeyCreate = new JourneyCreate();
$journeyCreate->type = $type;
$form = $this->createForm(new JourneyCreateType(),$journeyCreate);
if($this->isPost())
{
$form->bind($this->getRequest());
if($form->isValid())
{
$journeyCreate = $form->getData();
$journey = $journeyCreate->buildJourney($this->getCurrentUser());
$this->persistAndFlush($journey);
return $this->redirect($this->generateUrl('acme_demo_journey_edit',array('id'=>$journey->getId())));
}
}
return array(
'form' => $form->createView(),
);
}
And finaly the template code to display the form:
{% block page_body %}
<form class="form-horizontal" action="{{ path('acme_demo_journey_create') }}" method="post" novalidate>
{{form_widget(form)}}
<div class="form-actions">
<button class="btn btn-primary" type="submit">{{'form.submit'|trans}}</button>
{{'form.cancel'|trans}}
</div>
</form>
{% endblock %}
I'm having the theory that this could be because I use two form fields but don't know how to fix this. Any suggestions about how to solve this more elegant are welcome.
As complicated as this question might look, the answer is as simple as removing the {{form_errors(form)}} from the widget template block. Because the *form_row* block looks like:
{% block form_row %}
{% spaceless %}
<div class="form_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock form_row %}
The error was simply outputted two times.

Symfony2 Entity type form with name and image

I have an entity with both a name (string) and a file (also a string representing the filename). This is the "Icon" entity.
I have another entity called "Category" which has a name (string) and a relation to an Icon (OneToMany). I want the form to allow a user to select an Icon for a Category.
So I could display it in the form as:
$builder->add('icon', 'entity', array(
'class' => 'CroltsMainBundle:Icon',
'expanded' => true,
'multiple' => false
));
But what I really want is to display something like this in twig for each radio button:
<div>
<label for="something"><img src="/icons/{{icon.file }}" />{{icon.name}}</label>
<input type="radio" name="something" value="{{ icon.id }}" />
</div>
Is there a good way to make that type of radio form with Symfony forms? Like would a custom Type be what I want? I really haven't done too much with custom types to know how much of this is possible.
Not sure it is the best way but here is how I manage this kind of situation :
create a new formtype that behalf as entityType, IconCheckType for instance:
(http://symfony.com/doc/master/cookbook/form/create_custom_field_type.html)
namespace .....\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
class IconCheckType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilder $builder, array $options) {
$builder -> setAttribute('dataType', $options['dataType']);
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form) {
$view -> set('dataType', $form -> getAttribute('dataType'));
}
/**
* {#inheritdoc}
*/
public function getDefaultOptions(array $options) {
return array('required' => false,'dataType'=>'entity');
}
/**
* Returns the allowed option values for each option (if any).
*
* #param array $options
*
* #return array The allowed option values
*/
public function getAllowedOptionValues(array $options)
{
return array('required' => array(false));
}
/**
* {#inheritdoc}
*/
public function getParent(array $options) {
return 'entity';
}
/**
* {#inheritdoc}
*/
public function getName() {
return 'iconcheck';
}
}
in your Form
...
->add('icon', 'iconcheck', array(
'class' => 'CroltsMainBundle:Icon',
'property'=>'formField',
'multiple'=>false,
'expanded'=>true
))
...
Note the property=>'formField', that means that instead of return the __toString as label it will return whatever you want from the function getFormField from your entity class
So, in your entity class:
class Icon {
....
public function getFormField() {
return $this; /* or an array with only the needed attributes */
}
....
}
then you can render your custom field
{% block iconcheck_widget %}
{% for child in form %}
{% set obj=child.vars.label %}
<div>
<label for="something"><img src="/icons/{{obj.file }}" />{{obj.name}}</label>
{{ form_widget(child) }} {# the radio/checkbox #}
</div>
{{ form_widget(child) }}#}
{% endfor %}
{% endblock %}
Could you potentially make your __toString() method:
<?php
// Icon entity
public function __toString()
{
return '<img src="/icons/'. $this->file .'" />' . $this->name';
}
If not then you will have to create a custom type. However it is really easy
<?php
namespace Your\NameSpace;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class MyCustomType extends AbstractType
{
public function getParent()
{
// By calling get parent here your custom type will
// automatically inherit all the properties/functionality
// of the type you extend
return 'radio';
}
}
Then you can make your custom widget for your type. I would read the cookbook entry if I was you because it explains the process very well. You can look at the default Twig widgets for forms to learn how to write your own.
I had to add a thumbnail in front of the select file button for image uploading today. I wound up doing this. Sorry I don't have time to create a complete example for your case.
I'm only accessing the parent to get the entity to pass to the vich_uploadable_asset() helper.
/src/AcmeBundle/Form/Type/AcmeFormType.php
<?php
namespace Acme\AcmeBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class AcmeFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('icon', 'vich_uploadable')
...
config.yml
twig:
form:
resources:
- 'AcmeBundle:Form:fields.html.twig'
services:
acme.type.vich_uploadable:
class: Acme\AcmeBundle\Form\Type\VichUploadableFieldType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type, alias: vich_uploadable }
/src/Acme/AcmeBundle/Form/fields.html.twig
{% block vich_uploadable_widget %}
{% spaceless %}
{% if attribute(form.parent.vars.value, form.name) is not empty %}
<img src="{{ vich_uploader_asset(form.parent.vars.value, form.name) | imagine_filter('thumb_square') }}" />
{% endif %}
{{ form_widget(form) }} {# If you're extending the radio button, it would show here #}
{% endspaceless %}
{% endblock %}
Here is what I ended up doing. It took a lot of trial and error and digging into the EntityType class hierarchy and learning how Form types really work. The hardest part is looking at source code and figuring out how to get from PHP classes to Twig templates (what variables are available).
Here is what I did. It is not a perfect solution (feels a bit hacky) but it works for my purposes. The idea is to expose the underlying Entity to my view so I can get its properties.
The biggest problem is that the file property which holds the file path is hardcoded in the view. Anyway, I'm posting the entire solution as it may be helpful to others. Also I'm open to critique if someone can find a better solution.
(namespaces omitted)
Extended Entity Type
<?php
class ExtendedEntityType extends EntityType
{
public function getParent()
{
return 'extended_choice';
}
public function getName()
{
return 'extended_entity';
}
}
Extended Choice Type (just had to change addSubForms but it's private)
<?php
class ExtendedChoiceType extends ChoiceType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
throw new FormException('Either the option "choices" or "choice_list" must be set.');
}
if ($options['expanded']) {
$this->addSubForms($builder, $options['choice_list']->getPreferredViews(), $options);
$this->addSubForms($builder, $options['choice_list']->getRemainingViews(), $options);
if ($options['multiple']) {
$builder
->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']))
->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10)
;
} else {
$builder
->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list']))
->addEventSubscriber(new FixRadioInputListener($options['choice_list']), 10)
;
}
} else {
if ($options['multiple']) {
$builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
} else {
$builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
}
}
if ($options['multiple'] && $options['by_reference']) {
// Make sure the collection created during the client->norm
// transformation is merged back into the original collection
$builder->addEventSubscriber(new MergeCollectionListener(true, true));
}
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return 'choice';
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'extended_choice';
}
/**
* Adds the sub fields for an expanded choice field.
*
* #param FormBuilderInterface $builder The form builder.
* #param array $choiceViews The choice view objects.
* #param array $options The build options.
*/
private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
{
foreach ($choiceViews as $i => $choiceView) {
if (is_array($choiceView)) {
// Flatten groups
$this->addSubForms($builder, $choiceView, $options);
} else {
$choiceOpts = array(
'value' => $choiceView->value,
// Expose more data
'label' => array(
'data' => $choiceView->data,
'label' => $choiceView->label,
),
'translation_domain' => $options['translation_domain'],
);
if ($options['multiple']) {
$choiceType = 'checkbox';
// The user can check 0 or more checkboxes. If required
// is true, he is required to check all of them.
$choiceOpts['required'] = false;
} else {
$choiceType = 'radio';
}
$builder->add((string) $i, $choiceType, $choiceOpts);
}
}
}
}
Services
<service id="crolts_main.type.extended_choice" class="My\MainBundle\Form\Type\ExtendedChoiceType">
<tag name="form.type" alias="extended_choice" />
</service>
<service id="crolts_main.type.extended_entity" class="My\MainBundle\Form\Type\ExtendedEntityType">
<tag name="form.type" alias="extended_entity" />
<argument type="service" id="doctrine" />
</service>
form_layout.html.twig
(this is based on MopaBootStrapBundle but the idea is the same. The difference is that MopaBootstrap wraps the <label> around the <radio>)
{% block extended_choice_widget %}
{% spaceless %}
{% if expanded %}
{{ block('extended_choice_widget_expanded') }}
{% else %}
{# not being used, just default #}
{{ block('choice_widget_collapsed') }}
{% endif %}
{% endspaceless %}
{% endblock extended_choice_widget %}
{% block extended_choice_widget_expanded %}
{% spaceless %}
<div {{ block('widget_container_attributes') }}>
{% for child in form %}
<label class="{{ (multiple ? 'checkbox' : 'radio') ~ (widget_type ? ' ' ~ widget_type : '') ~ (inline is defined and inline ? ' inline' : '') }}">
{{ form_widget(child, {'attr': {'class': attr.widget_class|default('')}}) }}
{% if child.vars.label.data.file is defined %}
<img src="{{ vich_uploader_asset(child.vars.label.data, 'file')}}" alt="">
{% endif %}
{{ child.vars.label.label|trans({}, translation_domain) }}
</label>
{% endfor %}
</div>
{% endspaceless %}
{% endblock extended_choice_widget_expanded %}
Usage
<?php
$builder->add('icon', 'extended_entity', array(
'class' => 'MyMainBundle:MenuIcon',
'property' => 'name', // this is still used in label.label
'expanded' => true,
'multiple' => false
));

Instanceof operator in Twig/Symfony 2?

I've a mixed array like this one (mobile numbers and entities):
$targets = array();
$targets[] = '+32647651212';
$targets[] = new Customer();
In my Twig template i have to call getMobile() if target is a Customer or just print the number if it's actually a number (string).
Is there something like instanceof operator in Twig?
<ul>
{% for target in targets %}
<li>{{ target instance of MyEntity ? target.getMobile : target }}</li>
{% else %}
<li>Nothing found.</li>
</ul>
In \Twig_Extension you can add tests
public function getTests()
{
return [
'instanceof' => new \Twig_Function_Method($this, 'isInstanceof')
];
}
/**
* #param $var
* #param $instance
* #return bool
*/
public function isInstanceof($var, $instance) {
return $var instanceof $instance;
}
And then use like
{% if value is instanceof('DateTime') %}
UPDATE 10-2021
Please be aware this answer has been written for symfony 3, and twig 2.
If you use a more recent version, please refer to the answer of #Garri Figueroa on this post.
As you can see in the twig documentation the class \Twig_Extension, \Twig_SimpleTest are now deprecated.
If you use a more recent version of symfony (I recommend it), please use the new class AbstractExtension, TwigFunction, etc
https://symfony.com/doc/5.3/templating/twig_extension.html
OLD VERSION : symfony 3.4
Here a nice way to do instanceof operator in twig with Extension :
1) Create your extention file where you want
(ex: src/OC/YourBundle/Twig/InstanceOfExtension.php )
With \Twig_Extension you can do many things, filter, fonction, but now we will create a Test.
So we implement function getTests(), and inside it we create a new \Twig_SimpleTest
The 1st arugment is the name of test you create, and the seconde a callable.
(can be a function() {}).
<?php
namespace OC\YourBundle\Twig;
class InstanceOfExtension extends \Twig_Extension {
public function getTests() {
return array(
new \Twig_SimpleTest('instanceof', array($this, 'isInstanceOf')),
);
}
public function isInstanceOf($var, $instance) {
$reflexionClass = new \ReflectionClass($instance);
return $reflexionClass->isInstance($var);
}
}
2) Register it in services.yml
(ex: src/OC/YourBundle/Resources/config/services.yml)
services:
[...may you have other services ...]
app.twig_extension:
class: OC\YourBundle\Twig\InstanceOfExtension
public: false
tags:
- { name: twig.extension }
3) Then use it in twig like this
{{ myvar is instanceof('\\OC\\YourBundle\\Entity\\YourEntityOrWhatEver') }}
Source from Adrien Brault => https://gist.github.com/adrienbrault/7045544
My solution for Symfony 4.3
1) Create the AppExtension class in src/Twig folder. (The class is automatically detected).
2) Extend the AbstractExtension class:
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigTest;
class AppExtension extends AbstractExtension
{
public function getTests()
{
return [
new TwigTest('instanceof', [$this, 'isInstanceof'])
];
}
/**
* #param $var
* #param $instance
* #return bool
*/
public function isInstanceof($var, $instance) {
return $var instanceof $instance;
}
}
3) Then use same code of valdas.mistolis answer:
{% if value is instanceof('DateTime') %}
4) Thanks valdas.mistolis and symfony documentation i got my own solution:
Twig Extension templating
Since PHP 5.5.0 you can compare class names next way:
{{ constant('class', exception) is constant('\\Symfony\\Component\\HttpKernel\\Exception\\HttpException') }}
This snippet can help in particular cases when you need strict comparison of class names. If you need to check implementation of interface or to check inheritance would be better to create twig extension described above.
Another solution :
class A {
...
public function isInstanceOfB() {
return $this instanceof B;
}
}
class B extends A {}
class C extends A {}
then in your twig :
{{ a.isInstanceOfB ? ... something for B instance ... : ... something for C instance ... }}
OR
{% if a.isInstanceOfB %}
... do something for B instance ...
{% else %}
... do something for C instance ...
{% endif %}
Another example when iterating through Symfony forms:
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigTest;
use App\Document\Embeded\Image;
use App\Document\Embeded\Gallery;
use App\Document\Embeded\Article;
class AppExtension extends AbstractExtension
{
public function getTests()
{
return [
new TwigTest('image', [$this, 'isImage']),
new TwigTest('gallery', [$this, 'isGallery']),
new TwigTest('article', [$this, 'isArticle']),
];
}
public function isImage($var) {
return $var instanceof Image;
}
public function isGallery($var) {
return $var instanceof Gallery;
}
public function isArticle($var) {
return $var instanceof Article;
}
}
Twig
{% if form.vars.data is gallery %}
This is a Gallery
{% elseif form.vars.data is article %}
This is an Article
{% endif %}
Other solution without ReflectionClass with a twig filter :
First you need a TwigExtension class. Then, add a function as twig filter or twig function,
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class TwigExtension extends AbstractExtension
/**
* #return array|\Twig_Filter[]
*/
public function getFilters()
{
return [
new TwigFilter('is_instance_of', [$this, 'isInstanceOf'])
];
}
/**
* #param $object
* #param $class
* #return bool
*/
public function isInstanceOf($object, $class): bool
{
return is_a($object, $class, true);
}
}
And in twig template :
{% if true is same as (object|is_instance_of('ClassName')) %}
// do some stuff
{% endif %}
Check http://php.net/manual/fr/function.is-a.php
Use default filter in Twig like this:
{{ target.mobile|default(target) }}
Quite old, but I can't see here one more good possibility to achive this:
Enity:
public function __toString()
{
return 'NameOfYourEntity';
}
Twig:
{{ entity }}

Resources