Using DateIntervalType class in form - php

How to implement new DateIntervalType field in Symfony project?
$builder->add('remindEvery', DateIntervalType::class, array(
'widget' => 'integer', // render a text field for each part
// 'input' => 'string', // if you want the field to return a ISO 8601 string back to you
// customize which text boxes are shown
'with_years' => false,
'with_months' => false,
'with_days' => true,
'with_hours' => true,
));
The DateIntervalType field type was introduced in Symfony 3.2.
This field allows the user to select an interval of time. For example, if you want to allow the user to choose how often they receive a status email, they could use this field to choose intervals like every "10 minutes" or "3 days".

Create field in entity WorkingTime:
class WorkingTime
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(
* name="duration_task", type="string", length=25
* )
*/
private $durationTask;
// ...more fields
/**
* ##################################################
* Getter & Setter
* ##################################################
*/
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Get durationTask
*
* #return string
*/
public function getDurationTask()
{
return $this->durationTask;
}
/**
* Set durationTask
*
* #param string $durationTask
*
* #return WorkingTime
*/
public function setDurationTask( $durationTask )
{
$this->durationTask = $durationTask;
return $this;
}
// ...
in Controller:
$newWorkingTime = new WorkingTime();
// you can set default duration: 0 hour 1 minute
$newWorkingTime
->setDurationZz( 'PT0H1M' );
$form = $this->createForm( WorkingTimeType::class, $newWorkingTime );
$form->handleRequest( $request )->getData( $newWorkingTime );
// save to database
if ( $form->isSubmitted() ) {
$em2 = $this->getDoctrine()->getManager();
$em2->persist( $newWorkingTime );
$em2->flush();
}
return $this->render(
'AppBundle:WorkingTime:new.html.twig', [
'form' => $form->createView(),
] ]
);
class WorkingTimeType:
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateIntervalType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
/**
* Class WorkingTimeType
*
* #package AppBundle\Form
*/
class WorkingTimeType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm( FormBuilderInterface $builder, array $options )
{
$builder
->add( 'durationTask', DateIntervalType::class, [
'input' => 'string',
'widget' => 'choice',
// choice fields to display
'with_years' => false,
'with_months' => false,
'with_days' => false,
'with_minutes' => true,
'with_hours' => true,
'with_seconds' => false,
]
)
->add( 'save', SubmitType::class );
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions( OptionsResolver $resolver )
{
$resolver->setDefaults( [ 'data_class' => 'AppBundle\Entity\WorkingTime' ] );
}
/**
* #return string
*/
public function getBlockPrefix()
{
return 'success_form';
}
}
in new.html.twig:
{{ form(form) }}

Related

Doctrine persisting Many-To-One entities

I'm using Zend Framework 3 with Doctrine and I'm trying to save an Entity "Cidade" related to another Entity "Estado" witch is already stored in the database. However, Doctrine is trying to persist Entity "Estado", and the only attribute I have from Estado is the primary key in a HTML combo.
My view forms are built under Zend forms and fieldsets, which means POST data is automatically converted to the target entities using ClassMethods hydrator.
The problem is that if I set the attribute $estado with cascade={"persist"} in Cidade Entity, Doctrine tries to persist Estado Entity missing all required attributes but the primary key ID, which comes from POST request (HTML combo). I also considered using cascade={"detach"} ir order to Doctrine ignore Estado Entity in the EntityManager. But I get this error:
A new entity was found through the relationship 'Application\Entity\Cidade#estado' that was not configured to cascade persist operations for entity: Application\Entity\Estado#000000007598ee720000000027904e61.
I found a similar doubt here and the only way I could find for this matter was first retrieving Estado Entity and setting it on Cidade Entity before saving. If this is the only way, can I tell my form structure won't work unless I retrieve all relationships before saving the dependant entities?
In other words, what is the best way of doing such thing in Doctrine (for example):
<?php
/*I'm simulating the creation of Estado Entity representing an
existing Estado in database, so "3" is the ID rendered in HTML combo*/
$estado = new Entity\Estado();
$estado->setId(3);
$cidade = new Entity\Cidade();
$cidade->setNome("City Test");
$cidade->setEstado($estado); //relationship here
$entityManager->persist($cidade);
$entityManager->flush();
How to do that without having to retrieve an Estado all the time I need to save a Cidade? Wouldn't affect performance?
My Cidade Entity:
<?php
namespace Application\Entity;
use Zend\InputFilter\Factory;
use Zend\InputFilter\InputFilterInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Cidade
* #package Application\Entity
* #ORM\Entity
*/
class Cidade extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
private $nome;
/**
* #var Estado
* #ORM\ManyToOne(targetEntity="Estado", cascade={"detach"})
* #ORM\JoinColumn(name="id_estado", referencedColumnName="id")
*/
private $estado;
/**
* Retrieve input filter
*
* #return InputFilterInterface
*/
public function getInputFilter()
{
if (!$this->inputFilter) {
$factory = new Factory();
$this->inputFilter = $factory->createInputFilter([
"nome" => ["required" => true]
]);
}
return $this->inputFilter;
}
/**
* #return string
*/
public function getNome()
{
return $this->nome;
}
/**
* #param string $nome
*/
public function setNome($nome)
{
$this->nome = $nome;
}
/**
* #return Estado
*/
public function getEstado()
{
return $this->estado;
}
/**
* #param Estado $estado
*/
public function setEstado($estado)
{
$this->estado = $estado;
}
}
My Estado Entity:
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\Factory;
use Zend\InputFilter\InputFilterInterface;
/**
* Class Estado
* #package Application\Entity
* #ORM\Entity
*/
class Estado extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
private $nome;
/**
* #var string
* #ORM\Column(length=3)
*/
private $sigla;
/**
* #return string
*/
public function getNome()
{
return $this->nome;
}
/**
* #param string $nome
*/
public function setNome($nome)
{
$this->nome = $nome;
}
/**
* #return string
*/
public function getSigla()
{
return $this->sigla;
}
/**
* #param string $sigla
*/
public function setSigla($sigla)
{
$this->sigla = $sigla;
}
/**
* Retrieve input filter
*
* #return InputFilterInterface
*/
public function getInputFilter()
{
if (!$this->inputFilter) {
$factory = new Factory();
$this->inputFilter = $factory->createInputFilter([
"nome" => ["required" => true],
"sigla" => ["required" => true]
]);
}
return $this->inputFilter;
}
}
Both entities extend my superclass AbstractEntity:
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping\MappedSuperclass;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
/**
* Class AbstractEntity
* #package Application\Entity
* #MappedSuperClass
*/
abstract class AbstractEntity implements InputFilterAwareInterface
{
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var InputFilterAwareInterface
*/
protected $inputFilter;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #param int $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #param InputFilterInterface $inputFilter
* #return InputFilterAwareInterface
* #throws \Exception
*/
public function setInputFilter(InputFilterInterface $inputFilter)
{
throw new \Exception("Método não utilizado");
}
}
My HTML inputs are rendered as it follows:
<input name="cidade[nome]" class="form-control" value="" type="text">
<select name="cidade[estado][id]" class="form-control">
<option value="3">Bahia</option>
<option value="2">Espírito Santo</option>
<option value="1">Minas Gerais</option>
<option value="9">Pará</option>
</select>
Each option above is an Estado Entity retrieved from database. My POST data comes as the following example:
[
"cidade" => [
"nome" => "Test",
"estado" => [
"id" => 3
]
]
]
On Zend Form's isValid() method, this POST data is automatically converted to the target Entities, which makes me crash on this Doctrine issue. How do I move on?
You should bind an object to your form and use the Doctrine Hydrator. In the form the field names should exactly match that of the Entity. So Entity#name is Form#name.
With Separation of Concerns I'm absolutely against placing the InputFilter for an Entity within the Entity itself. As such, I'll give you an example with everything separated, if you decide to mash it back together, that's up to you.
AbstractEntity for ID
/**
* #ORM\MappedSuperclass
*/
abstract class AbstractEntity
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
// getter/setter
}
Cicade Entity
/**
* #ORM\Entity
*/
class Cidade extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
protected $nome; // Changed to 'protected' so can be used in child classes - if any
/**
* #var Estado
* #ORM\ManyToOne(targetEntity="Estado", cascade={"persist", "detach"}) // persist added
* #ORM\JoinColumn(name="id_estado", referencedColumnName="id")
*/
protected $estado;
// getters/setters
}
Estado Entity
/**
* #ORM\Entity
*/
class Estado extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
protected $nome;
//getters/setters
}
So, above is the Entity setup for Many to One - Uni-direction relation.
You want to manage this, easily, with forms. So we need to create InputFilters for both.
Having InputFilters separately from the Entity allows us to nest them. This in turn allows us to create structured and nested forms.
For example, you could create a new Estado on-the-fly. If this were a bi-directional relation, you could create multiple Cicade Entity objects on-the-fly from/during the creation of Estado.
First: InputFilters. In the spirit of abstraction, which you started with your Entities, let's do that here as well:
AbstractDoctrineInputFilter
source AbstractDoctrineInputFilter & source AbstractDoctrineFormInputFilter
This gives a nice clean setup and a requirement to fulfill. I'm glossing over the more complex elements added in the source files, feel free to look those up though.
Both objects (Estado & Cicade) require an ObjectManager (they're Doctrine entities after all), so I'm assuming you might have more. The below should come in handy.
<?php
namespace Application\InputFilter;
use Doctrine\Common\Persistence\ObjectManager;
use Zend\InputFilter\InputFilter;
abstract class AbstractInputFilter extends InputFilter
{
/**
* #var ObjectManager
*/
protected $objectManager;
/**
* AbstractFormInputFilter constructor.
*
* #param array $options
*/
public function __construct(array $options)
{
// Check if ObjectManager|EntityManager for FormInputFilter is set
if (isset($options['object_manager']) && $options['object_manager'] instanceof ObjectManager) {
$this->setObjectManager($options['object_manager']);
}
}
/**
* Init function
*/
public function init()
{
$this->add(
[
'name' => 'id',
'required' => false, // Not required when adding - should also be in route when editing and bound in controller, so just additional
'filters' => [
['name' => ToInt::class],
],
'validators' => [
['name' => IsInt::class],
],
]
);
// If CSRF validation has not been added, add it here
if ( ! $this->has('csrf')) {
$this->add(
[
'name' => 'csrf',
'required' => true,
'filters' => [],
'validators' => [
['name' => Csrf::class],
],
]
);
}
}
// getters/setters for ObjectManager
}
Estado InputFilter
class EstadoInputFilter extends AbstractInputFilter
{
public function init()
{
parent::init();
$this->add(
[
'name' => 'nome', // <-- important, name matches entity property
'required' => true,
'allow_empty' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 2,
'max' => 255,
],
],
],
]
);
}
}
Cicade InputFilter
class EstadoInputFilter extends AbstractInputFilter
{
public function init()
{
parent::init(); // Adds the CSRF
$this->add(
[
'name' => 'nome', // <-- important, name matches entity property
'required' => true,
'allow_empty' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 2,
'max' => 255,
],
],
],
]
);
$this->add(
[
'name' => 'estado',
'required' => true,
]
);
}
}
So. Now we have 2 InputFilters, based on an AbstractInputFilter.
EstadoInputFilter filters just the nome property. Add additional if you want ;)
CicadeInputFilter filters the nome property and has a required estado field.
The names match those of the Entity definition in the respective Entity classes.
Just to be complete, below is the CicadeForm, take what you need to create the EstadoForm.
class CicadeForm extends Form
{
/**
* #var ObjectManager
*/
protected $objectManager;
/**
* AbstractFieldset constructor.
*
* #param ObjectManager $objectManager
* #param string $name Lower case short class name
* #param array $options
*/
public function __construct(ObjectManager $objectManager, string $name, array $options = [])
{
parent::__construct($name, $options);
$this->setObjectManager($objectManager);
}
public function init()
{
$this->add(
[
'name' => 'nome',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Nome',
],
]
);
// #link: https://github.com/doctrine/DoctrineModule/blob/master/docs/form-element.md
$this->add(
[
'type' => ObjectSelect::class,
'required' => true,
'name' => 'estado',
'options' => [
'object_manager' => $this->getObjectManager(),
'target_class' => Estado::class,
'property' => 'id',
'display_empty_item' => true,
'empty_item_label' => '---',
'label' => _('Estado'),
'label_attributes' => [
'title' => _('Estado'),
],
'label_generator' => function ($targetEntity) {
/** #var Estado $targetEntity */
return $targetEntity->getNome();
},
],
]
);
//Call parent initializer. Check in parent what it does.
parent::init();
}
/**
* #return ObjectManager
*/
public function getObjectManager() : ObjectManager
{
return $this->objectManager;
}
/**
* #param ObjectManager $objectManager
*
* #return AbstractDoctrineFieldset
*/
public function setObjectManager(ObjectManager $objectManager) : AbstractDoctrineFieldset
{
$this->objectManager = $objectManager;
return $this;
}
}
Config
Now that the classes are there, how to use them? Slap 'em together with module config!
In your module.config.php file, add this config:
'form_elements' => [
'factories' => [
CicadeForm::class => CicadeFormFactory::class,
EstadoForm::class => EstadoFormFactory::class,
// If you create separate Fieldset classes, this is where you register those
],
],
'input_filters' => [
'factories' => [
CicadeInputFilter::class => CicadeInputFilterFactory::class,
EstadoInputFilter::class => EstadoInputFilterFactory::class,
// If you register Fieldsets in form_elements, their InputFilter counterparts go here
],
],
From this config we read we need a Factory for both the Form and for the InputFilter of a set.
Below the CicadeInputFilterFactory
class CicadeInputFilterFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $requestedName
* #param array|null $options
*
* #return CicadeInputFilter
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var ObjectManager|EntityManager $objectManager */
$objectManager = $this->setObjectManager($container->get(EntityManager::class));
return new CicadeInputFilter(
[
'object_manager' => objectManager,
]
);
}
}
Matching CicadeFormFactory
class CicadeFormFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $requestedName
* #param array|null $options
*
* #return CicadeForm
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) : CicadeForm
{
$inputFilter = $container->get('InputFilterManager')->get(CicadeInputFilter::class);
// Here we creazte a new Form object. We set the InputFilter we created earlier and we set the DoctrineHydrator. This hydrator can work with Doctrine Entities and relations, so long as data is properly formatted when it comes in from front-end.
$form = $container->get(CicadeForm::class);
$form->setInputFilter($inputFilter);
$form->setHydrator(
new DoctrineObject($container->get(EntityManager::class))
);
$form->setObject(new Cicade());
return $form;
}
}
Massive preparation done, time to use it
Specific EditController to Edit an existing Cicade Entity
class EditController extends AbstractActionController // (Zend's AAC)
{
/**
* #var CicadeForm
*/
protected $cicadeForm;
/**
* #var ObjectManager|EntityManager
*/
protected $objectManager;
public function __construct(
ObjectManager $objectManager,
CicadeForm $cicadeForm
) {
$this->setObjectManager($objectManager);
$this->setCicadeForm($cicadeForm);
}
/**
* #return array|Response
* #throws ORMException|Exception
*/
public function editAction()
{
$id = $this->params()->fromRoute('id', null);
if (is_null($id)) {
$this->redirect()->toRoute('home'); // Do something more useful instead of this, like notify of id received from route
}
/** #var Cicade $entity */
$entity = $this->getObjectManager()->getRepository(Cicade::class)->find($id);
if (is_null($entity)) {
$this->redirect()->toRoute('home'); // Do something more useful instead of this, like notify of not found entity
}
/** #var CicadeForm $form */
$form = $this->getCicadeForm();
$form->bind($entity); // <-- This here is magic. Because we overwrite the object from the Factory with an existing one. This pre-populates the form with value and allows us to modify existing one. Assumes we got an entity above.
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
/** #var Cicade $cicade */
$cicade = $form->getObject();
$this->getObjectManager()->persist($cicade);
try {
$this->getObjectManager()->flush();
} catch (Exception $e) {
throw new Exception('Could not save. Error was thrown, details: ', $e->getMessage());
}
$this->redirect()->toRoute('cicade/view', ['id' => $entity->getId()]);
}
}
return [
'form' => $form,
'validationMessages' => $form->getMessages() ?: '',
];
}
/**
* #return CicadeForm
*/
public function getCicadeForm() : CicadeForm
{
return $this->cicadeForm;
}
/**
* #param CicadeForm $cicadeForm
*
* #return EditController
*/
public function setCicadeForm(CicadeForm $cicadeForm) : EditController
{
$this->cicadeForm = $cicadeForm;
return $this;
}
/**
* #return ObjectManager|EntityManager
*/
public function getObjectManager() : ObjectManager
{
return $this->objectManager;
}
/**
* #param ObjectManager|EntityManager $objectManager
*
* #return EditController
*/
public function setObjectManager(ObjectManager $objectManager) : EditController
{
$this->objectManager = $objectManager;
return $this;
}
}
So, felt like giving a really expanded answer. Covers the whole thing really.
If you have any questions about the above, let me know ;-)

Adding choices in entityType

I'm learning by myself the symfony framework (my job is not about developing, I'm not a developer) and I find out most of case the solution but here, is one what I didn't know how to manage.
I have 2 entity :
Product:
/**
* Product
*
* #ORM\Table(name="product")
* #ORM\Entity(repositoryClass="ProductBundle\Repository\ProductRepository")
* #UniqueEntity("productNumber")
*/
class Product
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="productNumber", type="string", length=255, unique=true)
* #Assert\Regex(
* pattern="/[0-9][.][0-9]{3}/",
* message="It should be like 1.234"
* )
*/
private $productNumber;
/**
* #ORM\ManyToOne(targetEntity="ProductGroup")
*/
private $productGroup;
/**
* Constructor
*/
public function __construct()
{
}
}
Camera :
/**
* Camera
*
* #ORM\Table(name="camera")
* #ORM\Entity(repositoryClass="ProductBundle\Repository\CameraRepository")
*/
class Camera
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="modele", type="string", length=255, unique=true)
*/
private $modele;
/**
* #var string
*
* #ORM\Column(name="description", type="text")
*/
private $description;
/**
*
* #ORM\ManyToOne(targetEntity="Product")
*/
private $product;
/**
* #ORM\ManyToMany(targetEntity="CustomField", inversedBy="camera", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $customFields;
/**
* Constructor
*/
public function __construct()
{
$this->customFields = new ArrayCollection();
}
}
My form :
namespace ProductBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
class CameraType extends AbstractType {
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('product', EntityType::class, [
'class' => 'ProductBundle:Product',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('p')
->select('p')
->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
->where('c.product IS NULL')
;
},
'attr' => [
'required' => true,
],
'choice_label' => 'productNumber',
])
->add('modele', TextType::class, [
'label' => "Modele",
])
->add('description', TextType::class, [
'label' => "Description",
])
->add('customFields', CollectionType::class, [
'entry_type' => CustomFieldType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'required' => false,
'attr' => [
'class' => 'customfield'
]
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'ProductBundle\Entity\Camera'
));
}
}
When I add a camera, I would like only the Product:productNumber where are available (not take by a camera), the querybuilder is working but my issue concern the edit form, it show only available productNumber so it's changing every time I need to edit this camera.
What can I handle this ? Should I try to found another way to add a productNumber ? do you have a "trick" ?
I hope you will understand the problem and my english because it's not my first language.
Have a nice day.
Edit : I'm on Symfony 3.1.4
I presume on new form your choice field shows only unused ProductBundle:Camera entity, and on edit form it should show saved ProductBundle:Camera entity and all unused ones.
You should look into Form Event Subscribers
You need to implement two event listeners PRE_SET_DATA and PRE_SUBMIT.
Here is one way to do it. Something like this works on SF 2.8
First you will have to create product entity form from custom ProductFieldSubscriber which becomes EventSubscriberInterface:
$builder->addEventSubscriber(new ProductFieldSubscriber('product', [])
Now ProductFieldSubscriber should look something like this (untested)
namespace ProductBundle\Form\EventListener;
use Symfony\Component\Form\FormInterface,
Symfony\Component\Form\FormEvent,
Symfony\Component\EventDispatcher\EventSubscriberInterface,
Symfony\Component\Form\FormEvents,
Doctrine\ORM\EntityRepository,
Symfony\Bridge\Doctrine\Form\Type as DoctrineTypes
;
class ProductFieldSubscriber implements EventSubscriberInterface
{
private $propertyPathToSelf;
public function __construct($propertyPathToSelf, array $formOptions=[]) {
$this->propertyPathToSelf = $propertyPathToSelf;
$this->formOptions = $formOptions;
}
public static function getSubscribedEvents() {
return [
FormEvents::PRE_SET_DATA => 'onPreSetData',
FormEvents::PRE_SUBMIT => 'onPreSubmit',
];
}
private function addForm(FormInterface $form, $selfId = null) {
$formOptions = array_replace_recursive ([
'class' => 'ProductBundle:Product',
'placeholder' => null,
'compound' => false,
'query_builder' => function (EntityRepository $er) use ($selfId) {
$qb = $er->createQueryBuilder('p')
->select('p')
->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
->where('c.product IS NULL')
;
if (null !== $selfId) {
$qb
->orWhere($qb->expr()->eq('p.product', ':existingId'))
->setParameter('existingId', $selfId->getId())
;
}
return $qb;
},
],
$this->formOptions
);
if ($selfId) {
$formOptions['data'] = $selfId;
}
$form->add($this->propertyPathToSelf, DoctrineTypes\EntityType::class, $formOptions);
}
public function onPreSetData(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$selfIdTypeMethod = "get{$this->propertyPathToSelf}";
$selfId = $data->$selfIdTypeMethod();
$this->addForm($form, $selfId);
}
public function onPreSubmit(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
$selfId = array_key_exists($this->propertyPathToSelf, $data) ? $data[$this->propertyPathToSelf] : null;
$this->addForm($form, $selfId);
}
}
Query builder would be simpler if you had mapped entity relations.
Bonus update:
form option 'placeholder' => null, takes care that no default 'empty' option is available.
form option 'required' => true, forces html5 form popup validation.
Then you should use something like entity #assert notations and use validator constraints on entity attribute:
use Symfony\Component\Validator\Constraints as Assert;
/**
* #var string
*
* #Assert\NotNull()
* #ORM\Column(name="modele", type="string", length=255, unique=true)
*/
private $modele;
You could also disallow edit form from opening via controller editAction (maybe some redirect) and twig, where you could hide edit button.

Symfony 2 Set Embedded Entity Form Data

Currently I have embedded entity forms in a form. And I would like the underlying data to auto populate the address fields when a user selects a project. However using the form events as shown below in the entities and the corresponding ajax code specified in the Symfony 2 documentation, the address fields are never populated in the corresponding ajax response.
Using var_dump I can see that the address is grabbed and sent to the data option when I add the address field. But it never shows up in the form response when the form is submitted with just the selected project.
Is this the proper way to do it or am I missing some vital piece somewhere? Or does symfony not do this and should I do it via jquery/ajax?
TransactionUser
class TransactionUser
{
/**
* #var string
*
* #ORM\Column(name="id", type="guid")
* #ORM\Id
*/
private $id;
/**
* The associated transaction to this TransactionUser.
*
* #var \Acme\BaseBundle\Entity\Transaction $transaction
*
* #ORM\ManyToOne(targetEntity="Acme\BaseBundle\Entity\Transaction", inversedBy="transactionUsers", cascade={"all"})
*/
private $transaction;
/**
* The Project that this TransactionUser is associated with.
*
* #var \Acme\BaseBundle\Entity\Project
*
* #ORM\ManyToOne(targetEntity="Acme\BaseBundle\Entity\Project", inversedBy="transactionUsers", cascade={"all"})
*/
private $project;
}
Transaction
class Transaction
{
/**
* #var string
*
* #ORM\Column(name="id", type="guid")
* #ORM\Id
*/
private $id;
/**
* #var \Acme\BaseBundle\Entity\Address
*
* #ORM\ManyToOne(targetEntity="Acme\BaseBundle\Entity\Address", cascade={"persist"}, inversedBy="propertyAddressTransactions")
*/
private $propertyAddress;
/**
* #ORM\OneToMany(targetEntity="Acme\BaseBundle\Entity\TransactionUser", mappedBy="transaction", cascade={"all"})
*/
private $transactionUsers;
}
Address
class Address
{
/**
* The database id for the Address.
*
* #var string
*
* #ORM\Column(name="id", type="guid")
* #ORM\Id
*/
private $id;
/**
* The first line of the street address.
*
* #var string
*
* #ORM\Column(name="streetAddressLine1", type="text")
*/
private $streetAddressLine1;
/**
* The Transactions that list this address as the property address.
*
* #ORM\OneToMany(targetEntity="Acme\BaseBundle\Entity\Transaction", mappedBy="propertyAddress")
*/
private $propertyAddressTransactions;
}
Project
class Project
{
/**
* #var string
*
* #ORM\Column(name="id", type="guid")
* #ORM\Id
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Acme\BaseBundle\Entity\TransactionUser", mappedBy="project", cascade={"all"})
* \Doctrine\Common\Collections\ArrayCollection
*/
private $transactionUsers;
}
NewTransactionFormType
class NewTransactionFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('project', EntityType::class, array(
'class' => 'AcmeBaseBundle:Project',
'required' => false,
'optional' => true,
'label' => 'Select Project',
'placeholder' => 'Select Project',
'property' => 'projectName',
'query_builder' => function (EntityRepository $er) {
$queryBuilder = $er->createQueryBuilder('p');
return $queryBuilder;
},
));
$builder->add('transaction', NewTransactionSubFormType::class);
$builder->get('transaction')->add('propertyAddress', AddressFormType::class);
//Listeners on project to set the address
$formModifier = function (FormInterface $form, Project $project = null) {
$address = null;
if (!is_null($project) && !is_null($project->getId())) {
$address = $project->getFirstTransactionAddress();
}
$form->get('transaction')->add('propertyAddress', AddressFormType::class, array(
'data' => $address
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// this would be your entity, i.e. Transaction
$data = $event->getData();
$formModifier($event->getForm(), $data->getProject());
}
);
$builder->get('project')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
// It's important here to fetch $event->getForm()->getData(), as
// $event->getData() will get you the client data (that is, the ID)
$project = $event->getForm()->getData();
// since we've added the listener to the child, we'll have to pass on
// the parent to the callback functions!
$formModifier($event->getForm()->getParent(), $project);
}
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\BaseBundle\Entity\TransactionUser',
));
}
}
NewTransactionSubFormType
class NewTransactionSubFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', TextType::class, array(
'label' => 'Name',
'attr' => array(
'class' => '',
'placeholder' => 'Name',
),
'required' => true,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\BaseBundle\Entity\Transaction',
));
}
}
AddressFormType
class AddressFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('streetAddressLine1', TextType::class, array(
'label' => 'Street Address Line 1',
'attr' => array(
'class' => '',
'placeholder' => 'Street Address Line 1',
),
'required' => true,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\BaseBundle\Entity\Address',
));
}
}

New fields not persisted in database after overriding FormType when register a new user

I am overriding the form type to register a user. All looks ok, but when I submit my form the new fields are not persisted in database.
I followed the documentation.
My ProfileType:
<?php
namespace Application\Sonata\UserBundle\Form\Type;
//use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
//use Sonata\UserBundle\Model\UserInterface;
use Sonata\UserBundle\Form\Type\ProfileType as BaseType;
class ProfileType extends BaseType
{
private $class;
/**
* #param string $class The User class name
*/
public function __construct($class)
{
$this->class = $class;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('username', null, array(
'label' => 'Pseudo',
'required' => false
))
->add('firstname', null, array(
'label' => 'Prénom'
))
->add('lastname', null, array(
'label' => 'Nom'
))
->add('email', 'email', array(
'label' => 'Email'
))
->add('dateOfBirth', 'birthday', array(
'label' => 'Date d\'anniversaire',
'required' => false,
'data' => new \DateTime("01/01/1980")
))
->add('plainPassword', 'password', array(
'label' => 'Password'
))
->add('phone', null, array(
'label' => 'Téléphone',
'required' => false
))
->add('adress', null, array(
'label' => 'Adresse',
'required' => false
))
->add('zip', null, array(
'label' => 'Code postale',
'required' => false
))
->add('city', null, array(
'label' => 'Ville',
'required' => false
))
->add('newsletter', 'checkbox', array(
'label' => 'newsletter',
'required' => false
))
#hidden
->add('website', 'hidden', array(
'label' => 'website',
'required' => false
))
->add('biography', 'hidden', array(
'label' => 'biography',
'required' => false
))
->add('locale', 'hidden', array(
'label' => 'locale',
'required' => false
))
->add('timezone', 'hidden', array(
'label' => 'Timezone',
'required' => false
))
->add('gender', 'hidden', array(
'label' => 'Civilité',
'required' => false
))
;
// var_dump($builder);
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Application\Sonata\UserBundle\Entity\User',
'intention' => 'profile',
'label' => 'Edit Profile'
));
}
// public function getParent()
// {
// return 'fos_user_registration';
// }
/**
* {#inheritdoc}
*/
public function getName()
{
return 'application_sonata_user_profile';
}
}
My user class:
<?php
namespace Application\Sonata\UserBundle\Entity;
use Sonata\UserBundle\Entity\BaseUser as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints as DoctrineAssert;
/**
* Application\Sonata\UserBundle\Entity\User
*
* #ORM\Table(name="fos_user_user", indexes={#ORM\Index(name="search_idx", columns={"username", "email"})}))
* #ORM\Entity()
* #DoctrineAssert\UniqueEntity(fields={"username"}, message="username.already.exist" )
* #DoctrineAssert\UniqueEntity(fields={"email"}, message="email.already.exist" )
*/
class User extends BaseUser
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
*
* #ORM\Column(name="zip", type="string", length=255, nullable=true)
*/
protected $zip;
/**
* #var string
*
* #ORM\Column(name="adress", type="text", nullable=true)
*/
protected $adress;
/**
* #var string
*
* #ORM\Column(name="city", type="string", length=255, nullable=true)
*/
protected $city;
/**
* #var boolean
*
* #ORM\Column(name="newsletter", type="boolean", nullable=true)
*/
private $newsletter;
/**
* Get id
*
* #return integer $id
*/
public function getId()
{
return $this->id;
}
/**
* Set zip
*
* #param string $zip
* #return FosUserUser
*/
public function setZip($zip)
{
$this->zip = $zip;
return $this;
}
/**
* Get zip
*
* #return string
*/
public function getZip()
{
return $this->zip;
}
/**
* Set adress
*
* #param string $adress
* #return FosUserUser
*/
public function setAdress($adress)
{
$this->adress = $adress;
return $this;
}
/**
* Get adress
*
* #return string
*/
public function getAdress()
{
return $this->adress;
}
/**
* Set city
*
* #param string $city
* #return FosUserUser
*/
public function setCity($city)
{
$this->city = $city;
return $this;
}
/**
* Get city
*
* #return string
*/
public function getCity()
{
return $this->city;
}
/**
* Set Newsletter
*
* #param boolean $newsletter
* #return FosUserUser
*/
public function setNewsletter($newsletter)
{
$this->newsletter = $newsletter;
return $this;
}
/**
* Get Newsletter
*
* #return boolean
*/
public function getNewsletter()
{
return $this->newsletter;
}
}
Thank you for your help.
If you have a just created properties of the entity and you are using the metadata cache, the doctrine still doesn't aware about these new properties. Just try to clear the metadata cache.

How to make symfony 2 auto select the drop down field in edit action

I have a category table and product table. The categories are listed in product form in a drop down select box. When i edit a product then category saved in product table should be auto selected in category drop down at product edit form (selected="selected").
I have already tried empty_data and empty_value but they does not work.
I am searching for the solution since 2 days but could not find any tutorial or such example. I will be really thankful if someone can refer me a tutorial or can see below code to sort out the problem:
Controller
<?php
// src/Scsp/CmsBundle/Controller/ProductController.php
namespace Scsp\CmsBundle\Controller;
use Scsp\CmsBundle\Entity\CategoryEntity;
use Scsp\CmsBundle\Entity\ProductEntity;
use Scsp\CmsBundle\Form\ProductForm;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\HttpFoundation\Session\Session;
class ProductController extends Controller {
public function editAction($id, $request) {
$em = $this->getDoctrine()->getManager();
$product = $em->getRepository('ScspCmsBundle:ProductEntity')->find($id);
if (!$product){
throw $this->createNotFoundException('No product found for id ' . $id);
}
$productForm=new ProductForm($id);
$form = $this->createForm($productForm, $product, array(
'attr' => array('class' => 'form-horizontal')
));
$form->handleRequest($request);
if ($request->isMethod('POST') && $form->isValid()) {
$product->setCategoryid(3);
$em->persist($product);
$em->flush();
$this->session->getFlashBag()->add('success', $product->getProducts() . ' successfully edited!');
return $this->redirect($this->generateUrl('scsp_cms_products', array('action' => 'list', 'id' => 0)));
}
return $this->render('ScspCmsBundle:Default:form.html.twig', array(
'form' => $form->createView(),
'page_title' => 'Edit product',
'type' => 'products',
'id' => $id
));
}
}
Product Entity:
<?php
// src/Scsp/CmsBundle/Entity/ProductEntity.php
namespace Scsp\CmsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="Scsp\CmsBundle\Repository\ProductRepository")
* #ORM\Table(name="products")
*/
class ProductEntity {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
*/
protected $products;
/**
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="CategoryEntity", inversedBy="products")
* #ORM\JoinColumn(name="categoryid", referencedColumnName="id")
*/
protected $categoryid;
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set id
*
* #param string $id
* #return Id
*/
public function setId($id) {
$this->id = $id;
return $this;
}
/**
* Set products
*
* #param string $products
* #return ProductEntity
*/
public function setProducts($products)
{
$this->products = $products;
return $this;
}
/**
* Get products
*
* #return string
*/
public function getProducts()
{
return $this->products;
}
/**
* Set categoryid
*
* #param integer $categoryid
* #return ProductEntity
*/
public function setCategoryid($categoryid)
{
$this->categoryid = $categoryid;
return $this;
}
/**
* Get categoryid
*
* #return integer
*/
public function getCategoryid()
{
return $this->categoryid;
}
}
Category Entity:
<?php
// src/Scsp/CmsBundle/Entity/CategoryEntity.php
namespace Scsp\CmsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="Scsp\CmsBundle\Repository\CategoryRepository")
* #ORM\Table(name="categories")
*/
class CategoryEntity {
/**
* #ORM\OneToMany(targetEntity="ProductEntity", mappedBy="categorid")
*/
protected $products;
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
*/
protected $categories;
public function __construct(){
$this->products = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set id
*
* #param string $id
* #return Id
*/
public function setId($id) {
$this->id = $id;
return $this;
}
/**
* Set categories
*
* #param string $categories
* #return Categories
*/
public function setCategories($categories) {
$this->categories = $categories;
return $this;
}
/**
* Get categories
*
* #return string
*/
public function getCategories() {
return $this->categories;
}
public function __toString() {
return $this->getCategories();
}
/**
* Add products
*
* #param \Scsp\CmsBundle\Entity\ProductEntity $products
* #return CategoryEntity
*/
public function addProduct(\Scsp\CmsBundle\Entity\ProductEntity $products)
{
$this->products[] = $products;
return $this;
}
/**
* Remove products
*
* #param \Scsp\CmsBundle\Entity\ProductEntity $products
*/
public function removeProduct(\Scsp\CmsBundle\Entity\ProductEntity $products)
{
$this->products->removeElement($products);
}
/**
* Get products
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getProducts()
{
return $this->products;
}
}
Product Form:
<?php
// src/Scsp/CmsBundle/Form/ProductForm.php
namespace Scsp\CmsBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use \Scsp\CmsBundle\Entity\ProductEntity;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormInterface;
class ProductForm extends AbstractType
{
private $id;
public function __construct($id) {
$this->id=$id;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if($this->id>0){
$builder->add('id', 'text',
array(
'label' => false,
'attr' => array(
'class' => 'form-control',
'readonly' => 'readonly'
)
)
);
}
$builder
->add('products', 'text',
array(
'label' => false,
'max_length' => 100,
'attr' => array(
'class' => 'form-control',
'autocomplete' => 'off'
)
)
)
->add('categoryid', 'entity', array(
'label' => false,
'class' => 'ScspCmsBundle:CategoryEntity',
'property' => 'categories',
'data'=>$builder->getData()->getCategoryId(),
'attr' => array(
)
)
)
->add('save', 'submit',array(
'attr' => array(
'formnovalidate' => 'formnovalidate',
'class' => 'btn btn-primary',
'label' => 'Save Changes'
)
)
)
->getForm();
}
public function getName()
{
return 'products';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'empty_data' => 'Scsp\CmsBundle\Entity\CategoryEntity'
));
}
}
categoryid
is not an entity, so you must to add a property called category in ProductEntity and stablish the relation ManyToOne there, the code:
/**
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="CategoryEntity", inversedBy="products")
* #ORM\JoinColumn(name="categoryid", referencedColumnName="id")
*/
protected $categoryid;
must be replaced by:
/**
* #ORM\Column(name="categoryid", type="integer")
*/
protected $categoryid;
/**
* #var CategoryEntity
* #ORM\ManyToOne(targetEntity="CategoryEntity", inversedBy="products")
* #ORM\JoinColumn(name="categoryid", referencedColumnName="id")
*/
protected $category;
//Here generate the get and the set customized
/**
* #return CategoryEntity
*/
public function getCategory()
{
return $this->category;
}
/**
* #param CategoryEntity $category
* #return $this
*/
public function setCategory($category)
{
//this is the owning side
//all changes will be detected
//in this side by doctrine
$category->addProduct($this);
$this->category= $category;
//The line below is important to set the categoryid
//field in the product table
$this->setCategoryid($category->getId());
return $this;
}
Now in the Product Form replace:
->add('categoryid', 'entity', array(
'label' => false,
'class' => 'ScspCmsBundle:CategoryEntity',
'property' => 'categories',
'data'=>$builder->getData()->getCategoryId(),
'attr' => array(
)
)
)
by:
->add('category', 'entity', array(
'label' => false,
'class' => 'ScspCmsBundle:CategoryEntity',
'property' => 'categories',
'attr' => array(
)
)
)
That's all, enjoy it!
I see many things wrong in your form type, nonetheless, what are you looking for is the data option in the field definition:
->add('categoryid', 'entity',
array(
'label' => false,
'class' => 'ScspCmsBundle:CategoryEntity',
'property' => 'categories',
'data' => $builder->getData()->getCategoryId()
)
)
This data field must contain the select value property, which is the categoryId, at least when using an entity field type as normal. I don't know if this will work with you current setup!
I have a solution for Symfony 1.4.18.
The key is use jQuery.
You can view a detail explanation (including images) in my blog (in Spanish) here: http://symfonyandme.com/2013/10/09/como-usar-select-dependientes-en-symfony-1-4-con-tres-tablas/
And the same solution here: Symfony 1.4 with AJAX jQuery. How can I improve the AJAX's function that contains a select box which depends on another select box?
Then you would work for adjust this example for Symfony 2

Categories