I have this form in my symfony application:
namespace MyNamespace\EntityBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class OrganizationType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// profession checkboxes imbrication
->add('professions', 'collection', array(
'type' => new ProfessionType(),
'allow_add' => true,// if unrecognized items are submitted to the collection, they will be added as new items
'allow_delete' => false,
'by_reference' => false, //in order that the adders are called.
'mapped' => true,
))
->add('name')
->add('siret')
->add('corporation')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyNamespace\EntityBundle\Entity\Organization',
'csrf_protection' => true,
'csrf_field_name' => '_token_',
// a unique key to help generate the secret token
'intention' => 'organization_stuff',
));
}
/**
* #return string
*/
public function getName()
{
return 'organization';
}
}
And this how I render the form in my twig view:
<div>
{{ form_start(form, {'action': path('path_action'), 'method': 'POST'}) }}
{{ form_errors(form) }}
{{ form_row(form.professions.vars.prototype) }}
{{ form_row(form.name) }}
{{ form_row(form.siret) }}
{{ form_row(form.corporation) }}
{{ form_end(form) }}
</div>
It renders me this in my html view on my browser:
As you can see I have a required label named __name__label__ (at the top of the form) and the embedded form label Professions above the submit button.
How can I fix that, or customize this behavior ?
Note: in my twig if I only use {{ form_row(form.professions) }}, my professionType does not display the fields.
This is the code of ProfessionType.php :
$builder
->add('production', 'checkbox', array('required' => false ))
->add('transport', 'checkbox', array('required' => false ))
->add('pumping', 'checkbox', array('required' => false ))
;
I think you are having those labels because you have used the default view format predefined by symfony you need to customize it , the other reason is that you have displayed the embedded form prototype, you need to set this prototype as data type attribute :
<ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e }}">
...
See http://symfony.com/doc/current/cookbook/form/form_collections.html
Related
I'm doing an app with Symfony 5 and there is a problem i don't manage to find a solution, I have no idea.
I want to make a form of an entity "Person".
A Person can add in his family other Person.
So in my entity I made a Many-To-Many self referencing to Person.
class Person
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=50)
*/
private $name;
/**
* #ORM\Column(type="string", length=50)
*/
private $firstname;
/**
* #ORM\Column(type="string", length=255)
*/
private $birthdaydate;
/**
* #ORM\Column(type="string", length=255)
*/
private $gender;
/**
* #ManyToMany(targetEntity="Person")
* #JoinTable(name="family",
* joinColumns={#JoinColumn(name="person__id", referencedColumnName="person__id")},
* inverseJoinColumns={#JoinColumn(name="family_id", referencedColumnName="person__id")}
* )
*/
private $myFamily;
And now, I want to make a form in which I can add new Person, in a person.
I did a CollectionType, like symfony said, but when i want to print it to the page, I get a timeout because of an infinite loop.
It's the "allow_add" which causes the problem.
And i need the prototype variable returned by "allow_add" to add new field in the front.
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['attr' => ['class' => 'form_textfield']])
->add('firstname')
->add('birthdayDate', TextType::class, ['attr' => ['class' => 'form_datetime']])
->add('gender', GenderType::class)
->add('submit', SubmitType::class)
->add('myFamily', CollectionType::class, array('entry_type' => PersonType::class, 'mapped' => false, 'allow_add' => true, 'by_reference' => false, 'allow_delete' => true));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Person::class,
]);
}
}
Here is my form, but there is nothing interesting, I will add the js necessary when i will be resolve this problem.
{% extends 'base.html.twig' %}
{% block title %}Hello PersonController!
{% endblock %}
{% block body %}
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.firstname) }}
{{ form_row(form.birthdayDate) }}
{{ form_row(form.gender) }}
{{ form_row(form.myFamily) }}
<button type="button" class="add_item_link" data-collection-holder-class="tags">Add a tag</but
{{ form_end(form) }}
{% endblock %}
Thanks everyone in advance.
The answer from Dylan Kas was good, just by adding a new form, it's good.
The Person Form
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['attr' => ['class' => 'form_textfield']])
->add('firstname')
->add('birthdayDate', TextType::class, ['attr' => ['class' => 'form_datetime']])
->add('gender', GenderType::class)
->add('submit', SubmitType::class)
->add('myFamily', CollectionType::class, array('entry_type' => ChildType::class, 'by_reference' => false, 'allow_add' => true, 'allow_delete' => true));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Person::class,
]);
}
}
The child, referenced by myFamily :
class ChildType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['attr' => ['class' => 'form_textfield']])
->add('firstname')
->add('birthdayDate', TextType::class, ['attr' => ['class' => 'form_datetime']])
->add('gender', GenderType::class)
->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Person::class,
]);
}
}
And the view :
{% block body %}
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.firstname) }}
{{ form_row(form.birthdayDate) }}
{{ form_row(form.gender) }}
<button type="button" class="add_item_link" data-collection-holder-class="myFamily">Add a tag</button>
<ul class="myFamily" data-index="{{ form.myFamily|length > 0 ? form.myFamily|last.vars.name + 1 : 0 }}" data-prototype="{{ form_widget(form.myFamily.vars.prototype)|e('html_attr') }}"></ul>
{{ form_end(form) }}
{% endblock %}
With the js associated
const addFormToCollection = (e) => {
const collectionHolder = document.querySelector(
"." + e.currentTarget.dataset.collectionHolderClass
);
const item = document.createElement("li");
item.innerHTML = collectionHolder.dataset.prototype.replace(
/__name__/g,
collectionHolder.dataset.index
);
collectionHolder.appendChild(item);
collectionHolder.dataset.index++;
};
document
.querySelectorAll(".add_item_link")
.forEach((btn) => btn.addEventListener("click", addFormToCollection));
It still need some work, maybe I can make the child form extending the person form. The front need some work too. But the next people facing this problem will have the solution here.
I am still asking myself how could I do if I've needed to have a form including itself the same form, including itself the same form etc...
The form would be recursivable.
There is an infinite loop because the myFamily property references a Person entity which itself references a myFamily property ...
To keep things simple, one way to manage the family of a person would be to create a separate Family entity.
From the Person point of view, it seems more coherent to have a ManyToOne relationship with a family.
After that, you can add the family of a Person by using the EntityType:class inside the PersonFormType.
Here is the documentation for EntityType : https://symfony.com/doc/current/reference/forms/types/entity.html
I'm new to Symfony 4 and I'm trying to render a form with a ChoiceType Field with numeric choices in order to generate the exact Number of tags Chosen by the user.
This is my controller:
class ContactController extends AbstractController
{
/**
* #Route("/matrix", name="matrix")
*/
public function index(Request $request)
{
$contact = new Contact();
// i've already added some tags
$tag3 = new Tag();
$tag3->setName('tag3');
$contact->getTags()->add($tag3);
$tag4=new Tag();
$tag4->setName('ciao');
$contact->getTags()->add($tag4);
$form = $this->createForm(ContactType::class, $contact);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$contactFormData = $form->getData();
dump($contactFormData);
}
return $this->render('contact/index.html.twig', array(
//'our_form' => $form,
'form' => $form->createView(),
));
}
At this Point of my code, the form seems to be filled, I've checked with some dumps.
This is my twig
{% block body %}
<div>
{{ form_start(form) }}
{{ form_widget(form) }}
<ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}">
{% for tag in form.tags %}
<li> {{ form_row(tag.name) }}
</li>
{% endfor %}
</ul>
<input type="submit" value="Send" class="btn btn-success" />
{{ form_end(form) }}
</div>
{% endblock %}
It seems there's no visibility between this two Files, in fact, he is not able to enter into the for a loop. I've dumped some stuff and I've seen that tags have no children at this Point but it should.
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('motto')
->add('expectations', ChoiceType::class, array(
'choices' => array(
'1' => '1',
'2' => '2',
'3' => '3',
'4' => '4',
'5' => '5',
),
));
$builder->add('tags', CollectionType::class, array(
'entry_type' => TagType::class,
'entry_options' => array('label' => false),
'allow_add' => true,
'by_reference' => false,
'mapped' => false,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// Configure your form options here
]);
}
}
I'm perplex about this code => $contact->getTags()->add($tag3);. It seems Tags to be an entity and Contact another one so your Contact entity should have adders/removers and/or setters/removers (event if cumulate both is not necessary).
So your entity should like:
class Contact
{
// ...
/** #var Collection */
protected $tag;
// ...
public function __construct()
{
$this->tags = new ArrayCollection();
}
// ...
public function addTag(Tag $tag)
{
$this->tags->add($tag);
}
public function removeTag(Tag $tag)
{
// ...
}
}
A good example to implement your case: How to Embed a Collection of Forms
Then i don't know what's your TagType form looks like but even if it's well develop your twig is not ok.
First form_widget(form) render the entire form
From Symfony Doc
Renders the HTML widget of a given field. If you apply this to an entire form or collection of fields, each underlying form row will be rendered.
So re-render the collection will have no effect. And even if your twig code is not te good one to render a collection.
I try to build a huge form in symfony 3 with the use of the CollectionType. I have to define multiple sub-forms, some multiple, some single.
This is my FormType for that:
public function buildRegistrationForm(FormBuilderInterface $builder, array $options) {
$builder->add('userRegistration', CollectionType::class, [
'entry_type' => UserRegistrationType::class,
'entry_options' => ['label' => true],
]);
$builder->add('meters', CollectionType::class, [
'entry_type' => MeterType::class,
'entry_options' => ['label' => true],
'allow_add' => true,
]);
...
}
Now I try to access the CollectionType fields in the view. The code for this is:
{{ form_label(registrationForm.email, null, {'label_attr': {'class': 'form-label'}}) }}
{{ form_widget(registrationForm.email, {'attr': {'class': 'form-control'}}) }}
but I get the error:
Neither the property "email" nor one of the methods "email()", "getemail()"/"isemail()"/"hasemail()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
I know that Symfony tries to get the email field directly out of the main form (registrationForm), but I don't know how to access the subform. In the documentation (http://symfony.com/doc/current/form/form_collections.html) it is described that I can simply access the sub form by using registrationForm.userRegistration.email. But this gives me the error:
Neither the property "userRegistration" nor one of the methods ...
How can I access the subfields in the view?
First step is to understand why we want to use collectionType?
In case if you have One-To-Many or Many-To-Many relationship you should use CollectionType.
Example:
/**
* Class Team
*
* #ORM\Entity
* #ORM\Table(name="example_project_team")
*/
class Team
{
// ...
/**
* Unidirectional Many-To-Many
*
* Many Teams has many users accounts.
*
* #var ArrayCollection $users
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\User",
* cascade={"persist", "remove"},
* orphanRemoval=true
* )
*
* #ORM\JoinTable(name="teams_to_users",
* joinColumns={#ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")}
* )
*/
protected $users;
// ...
}
In this example we have some entity called team. Each team has many users (this is just example related to you). I guess, you have already created User entity.
Imagine that for you have UserRegistrationType for your user entity.
/**
* Class UserRegistrationType
*/
class UserRegistrationType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username', 'Symfony\Component\Form\Extension\Core\Type\TextType', [
'label' => false,
'translation_domain' => 'messages'
])
->add('email', 'Symfony\Component\Form\Extension\Core\Type\TextType', [
'label' => false,
'translation_domain' => 'messages'
])
// ... the other fields
;
}
/**
* #return string
*/
public function getName()
{
return 'app_user_registration_type';
}
/**
* #return null|string
*/
public function getBlockPrefix()
{
return 'app_user_registration';
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => User::class,
'csrf_protection' => true,
'validation' => true,
));
}
}
Please pay attention !!!
We used here 'data_class' => User::class,
As you can see we use User object in userRegistrationType. It means, that we can use this form for every object, which has field of type User or field with type CollectionType (your case!) but collection of Users!
Our Team entity has field users.
Now, as we have already created the userRegistrationType we can add it to TeamFormType.
public function teamRegistrationFormType(FormBuilderInterface $builder, array $options) {
$builder->add('users', CollectionType::class, [
'entry_type' => UserRegistrationType::class,
'entry_options' => [
'label' => false,
],
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
]);
// ...
}
Finally. Your form in twig:
{# you can add this form in your twig file #}
<div class="box">
{% block team_form %}
{{ form_start(team_form, { 'attr': {'class': 'form-horizontal', 'role' : 'form'}}) }}
<div class="box-body">
<div class="form-group">
{% block users_collection %}
<label for="" class="col-sm-2 control-label">
{{ 'admin.label.phones'|trans }}
</label>
<div class="col-sm-10 users" data-prototype="{{ form_widget(team_form.users.vars.prototype)|e('html_attr') }}">
{% for user in users %}
<div>
{{ form_widget(user) }}
</div>
{% endfor %}
</div>
<span class="help-block">
{{ form_errors(users) }}
</span>
{% endblock users_collection %}
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-danger">
{{ 'admin.button.submit'|trans }}
</button>
</div>
</div>
{{ form_end(team_form) }}
</div>
{% endblock team_form %}
</div>
Here we have strange widget {{ form_widget(user) }}. I guess, you want to edit the style of this widget. You can do it with Symfony built-in form styles. Create file (if you haven't created it yet) path_to_your_project/src/AppBundle/Resources/views/Form/fields.html.twig and add to it the style of your userRegistrationType. Notice, that name of this widget is created from name of you form block prefix and string "_widget" (public function getBlockPrefix())
{% file fields.html.twig %}
{% trans_default_domain 'messages' %}
{% block app_user_registration_widget %}
{% spaceless %}
<div class="form-group" {{ block('widget_container_attributes') }}>
<div class="col-sm-2">
{{ form_widget(form.username, {'attr' : {'class' : 'form-control' }}) }}
</div>
<div class="col-sm-4">
{{ form_widget(form.email, {'attr' : {'class' : 'form-control' }}) }}
</div>
</div>
{% endspaceless %}
{% endblock %}
And of course you will have to set the add and delete buttons. For this purposes I suggest you use official documentation: How to Embed a Collection of Forms or you can use bundle (but not official): ninsuo/symfony-collection
I am currently working on a user creation form.
A user has a profile attribute:
/**
* Many Users have One profile
* #ORM\ManyToOne(targetEntity="ProjectBundle\Entity\User\Profile", inversedBy="users")
* #ORM\JoinColumn(name="profile_id", referencedColumnName="id")
*/
private $profile;
This profile is chosen according to another select (on change action jquery) :
{% autoescape 'html'%}
{{ '<script id="tmpl_user_profile" type="text/x-jquery-tmpl">
<option value="${id}">${libelle}</option>
</script>'|raw }}
{% endautoescape %}
<script>
$('select#user_organisationMember').on('change',function(){
var value = this.value;
if (value == '') {
$('#user_profile').empty();
}
var urlAjax = "{{ path('admin_user_get_profile', { 'entity': "value" }) }}";
$.ajax({
url: urlAjax.replace("value",value),
method: "post"
}).done(function(msg){
$('#user_profile').empty();
$('#tmpl_user_profile').tmpl(JSON.parse(msg)).appendTo('#user_profile');
}) ;
});
</script>
Until then everything worked correctly !
The different profiles in the select tag changes well according to the other select.
Upon arrival on the page, I want the list of profiles to be empty.
So I adapted my form using Symfony's FormEvent.
This is my first use of FormEvent, I may have made a mistake!
My FormType :
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('lastname')
->add('firstname')
->add('gender',null,array('required'=>true))
->add('organisationMember',null,array(
'required' => true,
'choices' => $options['organisation'],
'group_by' => 'type_organisation',
'placeholder' => 'Choisissez votre organisation'
))
->add('job')
->add('mobile')
->add('phone')
->add('alert_failure')
->add('alert_full')
->add('alert_other')
->add('plainPassword', TextType::class, array('required' => true))
->add('email')
->add('profile', null, array(
'required' => true,
));
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$entity = $event->getData();
$form = $event->getForm();
if (!$entity || null === $entity->getId()) {
$form->remove('profile');
$form->add('profile', ChoiceType::class);
}
});
}
By default all the profiles of my database are loaded in the select to not get the error 'This value is incorrect'.
But I do not want the user to see all the profiles, so I remove it in the event and return the field empty.
But I still get the error 'This value is incorrect' because actually, since the base select is empty, the form does not found the value entered.
I would like to have a select which by default is empty, which is filled in Ajax and which does not show me the error 'This value is incorrect'.
How can I please do it?
Thanks
Add field not only on PRE_SET_DATA, but also on PRE_SUBMIT event. Check my answer on similar question.
I found a very simple solution, I do not know if it's a good idea, but it takes not a lot of lines.
I have delivered the default form
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('lastname')
->add('firstname')
->add('gender',null,array('required'=>true))
->add('organisationMember',null,array(
'required' => true,
'choices' => $options['organisation'],
'group_by' => 'type_organisation',
'placeholder' => 'Choisissez votre organisation'
))
->add('job')
->add('mobile')
->add('phone')
->add('alert_failure')
->add('alert_full')
->add('alert_other')
->add('plainPassword', TextType::class, array('required' => true))
->add('email')
->add('profile', null, array(
'required' => true,
'placeholder' => 'Choose an organisation !',
));
}
And on my view:
<div class="form-group{% if form.profile.vars.errors|length %} has-error{% endif %}">
<label for="{{ form.profile.vars.id }}" class="col-sm-3 control-label no-padding-right required">Profile(s) </label>
<div class="col-sm-9">
{% do form.profile.setRendered %}
<select name="user[profile]" id="user_profile" required class="form-control">
//We can do a loop if needed
</select>
{{ form_errors(form.profile) }}
</div>
</div>
This way I fully manage whether or not I want information in my select.
And it works for me.
I have created a form to invite new users. The form has no problem submitting and being handled correctly, until isValid() method returns errors after a submission. When this happens, the page is re-rendered correctly with the appropriate errors being shown. Unfortunately, the submit button then becomes un-responsive: It is still styled correctly (the style changes when it is disabled via jQuery, so that doesn't seem to be the problem), the correct submission URL still appears at the bottom-left of the navigator, but nothing happens when it is clicked.
The UserInviteType form building class:
class UserInviteType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('userBackground', ChoiceType::class, array(
'choices' => array(
'Professeur' => 'professor',
'Étudiant' => 'student',
'Employé de laboratoire' => 'labEmployee',
'Employé administratif' => 'adminEmployee',
'Autre' => 'other'
)))
->add('firstName', TextType::class)
->add('lastName', TextType::class)
->add('email', EmailType::class)
->add('misc', TextType::class, array(
'required' => false,
))
->add('level', EntityType::class, array(
'required' => false,
'class' => 'AspProfessorProfileBundle:Level',
'choice_label' => 'value',
'multiple' => false,
))
->add('canModify', CollectionType::class, array(
'entry_type' => CanModifyInitType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'required' => true,
))
->add('save', SubmitType::class, array(
'disabled' => 'false',
))
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Asp\UserBundle\Entity\UserInvite',
'validation_groups' => array(
'Asp\UserBundle\Entity\UserInvite',
'determineValidationGroups'
),
));
}
}
The controller which handles page rendering and form submission:
public function usersAction(Request $request)
{
$user = new UserInvite();
$form = $this->get('form.factory')->create(UserInviteType::class, $user);
/* retreive all users - for user list rendering*/
$userManager = $this->get('fos_user.user_manager');
$users = $userManager->findUsers();
if ($request->isMethod('POST') && $form->handleRequest($request)->isValid()) {
$em = $this->getDoctrine()->getManager();
$data = $form->getData();
if ($data->getUserBackground() == 'professor' || $data->getUserBackground() == 'adminEmployee' || $data->getUserBackground() == 'other') {
$canMod = $user->getCanModify();
foreach ($canMod as $cM) {
$user->removeCanModify($cM);
}
}
$em->persist($user);
$em->flush();
$request->getSession()->getFlashBag()->add('notice', 'Invitation envoyé à '.$user->getFirstName().' '.$user->getLastName().'('.$user->getEmail().').');
return $this->redirectToRoute('asp_core_admin_users');
}
return $this->render('AspCoreBundle:Admin:users.html.twig', array(
'users' => $users,
'form' => $form->createView()
));
}
The Twig view rendering of the form:
<div class="user-init-form-container">
{{ form_start(form, {'attr': {'class': 'form-horizontal'}}) }}
{{ form_errors(form) }}
{# ....... Declare other form elements ....... #}
<div class="form-group">
<div class="col-sm-offset-3 col-sm-8 col-lg-offset-2 col-lg-9">
{{ form_widget(form.save, { 'id': 'submit_button', 'label': 'Inviter', 'attr': {'class': 'btn btn-primary'}}) }}
</div>
</div>
{{ form_end(form) }}
</div>
I do not do any jQuery interaction with the submit button directly (except when I wanted to see the disabled button styling to confirm this wasn't the problem).
I have looked around quite a bit and can't seem to find any topic anywhere discussing this. Hope one of you can spot what I am doing wrong, because I am really stumped right now!
Thank you.
Alex S.
Try to specify form action ?
{{ form_start(form, {'attr': {'id': 'myId'}, 'action': path('my_route')}) }}
Other methods if you prefer http://symfony.com/doc/current/form/action_method.html
I Figured it out.
It was due to a hidden field which was in-properly rendered after error generation. This hidden field suddenly became required and would prevent the submission of the form.
Thank you.
Alex