Samuele Lilli - DonCallisto
FORM COMPONENT
(http://www.freepik.com/free-photos-vectors/smile - Smile vector designed by Freepik)
Samuele Lilli - DonCallisto
FORM COMPONENT
(http://www.freepik.com/free-photos-vectors/smile - Smile vector designed by Freepik)
Samuele Lilli - DonCallisto
FORM COMPONENT
(http://www.freepik.com/free-photos-vectors/smile - Smile vector designed by Freepik)
Samuele Lilli - DonCallisto
FORM COMPONENT
Standalone component (install it via composer/packagist or github)
Samuele Lilli - DonCallisto
FORM COMPONENT
Standalone component (install it via composer/packagist or github)
Provides twig facilities for render labels/fields/errors
Samuele Lilli - DonCallisto
FORM COMPONENT
Standalone component (install it via composer/packagist or github)
Provides twig facilities for render labels/fields/errors
Handling for you data submission (bind to entity if any, validation, data transformations, …)
Samuele Lilli - DonCallisto
FORM COMPONENT
Standalone component (install it via composer/packagist or github)
Provides twig facilities for render labels/fields/errors
Handling for you data submission (bind to entity if any, validation, data transformations, …)
Provides a bunch of built-in types
Samuele Lilli - DonCallisto
SUMMARY
EntityType
CollectionType
Form Data Filtering on Entity / Collection
Form Events
Form Data Types
Data Transformers
Value Objects
Property Path
Samuele Lilli - DonCallisto
That should not be the right answer
Never bend your needs to software limits (unless strictly necessary)
Doctrine looks only for changes ONLY on the owning side of association, so those
adds will not take place.
Take care yourself for data consistency (during objects lifecycle, ensure that all
references are setted well in both sides). This concept is not ORM-related.
We didn’t add by_reference => false into into FormType.
Samuele Lilli - DonCallisto
That should not be the right answer
Never bend your needs to software limits (unless strictly necessary)
Doctrine looks only for changes ONLY on the owning side of association, so those
adds will not take place.
Take care yourself for data consistency (during objects lifecycle, ensure that all
references are setted well in both sides). This concept is not ORM-related.
We didn’t add by_reference => false into into FormType.
Samuele Lilli - DonCallisto
Data consistency
Class Product
{
// ….
public function addCategory(Category $category)
{
if (!$this->categories->contains($category)) {
$this->categories[] = $category;
$category->addProduct($this);
}
return $this;
}
public function removeCategory(Category $category)
{
if ($this->categories->contains($category)) {
$this->categories->removeElement($category);
$category->removeProduct($this);
}
}
Class Category
{
// ….
public function addProduct(Product $product)
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
$product->addCategory($this);
}
return $this;
}
public function removeProduct(Product $product)
{
if ($this->products->contains($product)) {
$this->products->removeElement($product);
$product->removeCategory($this);
}
}
Samuele Lilli - DonCallisto
This should not be the right answer
Never bend your needs to software limits (unless strictly necessary)
Doctrine looks only for changes ONLY on the owning side of association, so those
adds will not take place. √
Take care yourself for data consistency (during objects lifecycle, ensure that all
references are setted well in both sides). This concept is not ORM-related. √
We didn’t add by_reference => false into into FormType.
Samuele Lilli - DonCallisto
WHY BY_REFERENCE => FALSE
It forces setter (adder) to be called on the parent element
As a rule of thumb, set ALWAYS by_reference to false when dealing
with objects (ArrayCollection included)
Samuele Lilli - DonCallisto
BY_REFERENCE => TRUE
Name
Cat.
Name
Name
Name
$cat->getProducts()->get{0}->setName(‘bar’);
$cat->getProducts()->get{1}->setName(‘foobar’);
$product3 = new Product();
$product3->setName();
$cat->getProducts()->add($product3);
$cat->setName(‘foo’);
Samuele Lilli - DonCallisto
BY_REFERENCE => FALSE
Name
Cat.
Name
Name
Name
$cat->getProducts()->get{0}->setName(‘bar’);
$cat->getProducts()->get{1}->setName(‘foobar’);
$product3 = new Product();
$product3>setName();
$cat->addProduct($product3);
$cat->setName(‘foo’);
Samuele Lilli - DonCallisto
Two possible solutions
“Manually” (programmatically) remove elements
Set orphanRemoval to true on the attribute
If the relationship was ManyToMany and User was
the owning side, no troubles
Samuele Lilli - DonCallisto
“Manually” (programmatically) remove elements
$originalTickets = new ArrayCollection();
foreach ($user->getTickets() as $ticket) {
$originalTickets->add($ticket);
}
if ($this->isFormSubmittedAndValid($form, $request)) {
foreach ($originalTickets as $originalTicket) {
if (!$user->getTickets()->contains($originalTicket)) {
$em->remove($originalTicket);
}
}
}
Samuele Lilli - DonCallisto
“Manually” (programmatically) remove elements
$originalTickets = new ArrayCollection();
foreach ($user->getTickets() as $ticket) {
$originalTickets->add($ticket);
}
if ($this->isFormSubmittedAndValid($form, $request)) {
foreach ($originalTickets as $originalTicket) {
if (!$user->getTickets()->contains($originalTicket)) {
$em->remove($originalTicket);
}
}
}
Samuele Lilli - DonCallisto
Set orphanRemoval to true on the attribute
/**
* @ORMOneToMany(targetEntity="Ticket", mappedBy="user", cascade={"persist"}, orphanRemoval=true)
*/
protected $tickets;
NOT RECOMMENDED
Samuele Lilli - DonCallisto
Set orphanRemoval to true on the attribute
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html
Samuele Lilli - DonCallisto
Set orphanRemoval to true on the attribute
public function passTicketsAction(User $yelding, User $beneficiary)
{
foreach ($yelding->getTickets() as $ticket) {
$yelding->removeTicket($ticket);
$beneficiary->addTicket($ticket);
}
}
Samuele Lilli - DonCallisto
Set orphanRemoval to true on the attribute
public function passTicketsAction(User $yelding, User $beneficiary)
{
foreach ($yelding->getTickets() as $ticket) {
$beneficiary->addTicket($ticket);
// OR → $ticket->setUser($beneficiary);
}
}
Samuele Lilli - DonCallisto
FORM DATA
FILTERING
(for entity and collection type)
Samuele Lilli - DonCallisto
EntityType
USE built-in queryBuilder option
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(‘foo’, EntityType::class, [
// ….
‘queryBuilder’ => function (FooRepo $fooRepo) {
return $fooRepo->filterFunction();
}
]);
}
Samuele Lilli - DonCallisto
CollectionType
CollectionType does not have any queryBuilder option
Declare form as a service an inject repository (entity manager)
This is the preferred way if you need the repo
Pass repository as an option
Usually, options are used for what you cannot inject into service
Samuele Lilli - DonCallisto
EntityManager injection
public function __construct(EntityManager $em)
{
$this->fooRepo = $em->getRepository(Foo::class);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(‘foo’, CollectionType::class, [
// ….
'data' => $this->fooRepo->filterFunction()
]);
}
Samuele Lilli - DonCallisto
EntityManager as an option
public function buildForm(FormBuilderInterface $builder, array $options)
{
$fooRepo = $options[‘fooRepo’];
$builder->add(‘foo’, CollectionType::class, [
// ….
'data' => $fooRepo>filterFunction()
]);
}
Samuele Lilli - DonCallisto
Is repository the only way to filter data?
Do I need to create a repository on purpose?
Samuele Lilli - DonCallisto
/**
* @ORMOneToMany(targetEntity="Ticket", mappedBy="user", cascade={"persist"}, orphanRemoval=true)
*/
protected $tickets;
● All tickets (entities) filtered out in the event will be
removed!
● Remove orphanRemoval option from the attribute and
handle collection yourself
FORM EVENTS
Samuele Lilli - DonCallisto
FORM EVENTS
$form = $this->createForm(UserType::class, $user);
$originalTickets = new ArrayCollection();
foreach ($form->get('tickets')->getData() as $ticket) {
$originalTickets->add($ticket);
}
if ($this->isFormSubmittedAndValid($form, $request)) {
foreach ($originalTickets as $originalTicket) {
if ($user->getTickets()->contains($originalTicket)) {
continue;
}
$em->remove($originalTicket);
}
Samuele Lilli - DonCallisto
FORM EVENTS
$form = $this->createForm(UserType::class, $user);
$originalTickets = new ArrayCollection();
foreach ($form->get('tickets')->getData() as $ticket) {
$originalTickets->add($ticket);
}
if ($this->isFormSubmittedAndValid($form, $request)) {
foreach ($originalTickets as $originalTicket) {
if ($user->getTickets()->contains($originalTicket)) {
continue;
}
$em->remove($originalTicket);
}
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
RENDER
FORM CREATION
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
RENDER
FORM CREATION
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
RENDER
FORM CREATION
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
RENDER
FORM CREATION
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
RENDER
FORM CREATION
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
RENDER
FORM CREATION
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
PRE_SET_DATA POST_SET_DATA
CONTROLLER
VIEW
NEW FORM
BIND
FORM POST
POST
PRE_SUBMIT SUBMIT POST_SUBMIT
IS VALID
PERSIST
Samuele Lilli - DonCallisto
TIP
Every group of events it’s called from START to
END on every FORM. This means that if you
have a chain of embedded form, all events are
called starting from innermost forms, going up
to parent form, ending on top form
Samuele Lilli - DonCallisto
DATA TYPES
MODEL DATA
NORM DATA
VIEW DATA
Samuele Lilli - DonCallisto
MODEL DATA
Main data type of PRE_SET_DATA and POST_SET_DATA
Reppresent data of underlying object. In previous example with
product form, product field model data is a Product object.
If field type is the same of underlying data, NORM DATA will be the
same of MODEL DATA
If field type is not the same of underlying data, you must use
ModelTransformer to transform MODEL DATA into NORM DATA
and vice versa (Don’t worry, we will talk about transformers next!)
Samuele Lilli - DonCallisto
NORM DATA
Main data type of SUBMIT event
Reppresent data after normalization. Commonly not used directly.
If on MODEL DATA is not present any ModelTransform, this is the
same of MODEL DATA and so the same of underlying object.
If NORM DATA isn’t the same of view reppresentation, you must use
ViewTransformer to transform NORM DATA into VIEW DATA and
vice versa
Samuele Lilli - DonCallisto
VIEW DATA
Main data type of POST_SUBMIT event
Reppresent data presented to the View. It’s the data type that you
get when you post the form.
If on VIEW DATA is not present any ViewTransformer, this is the
same of NORM DATA.
Samuele Lilli - DonCallisto
ENTITY
MODEL DATA NORM DATA
MODEL TRANSFORMER
NEW FORM BIND
TRANSFORM
REVERSE
TRANSFORM
Samuele Lilli - DonCallisto
ENTITY
MODEL DATA NORM DATA
NEW FORM BIND
TRANSFORM
REVERSE
TRANSFORM
MODEL TRANSFORMER
Samuele Lilli - DonCallisto
ENTITY - MODEL DATA
/**
* @ORMColumn(type="array", nullable=true)
*/
protected $tags;
Samuele Lilli - DonCallisto
ENTITY
MODEL DATA NORM DATA
NEW FORM BIND
TRANSFORM
REVERSE
TRANSFORM
MODEL TRANSFORMER
Samuele Lilli - DonCallisto
FORM FIELD
$builder->add('tags', TextType::class)
Samuele Lilli - DonCallisto
Since model data (array) is
different from norm data (text)
we need a model transformer
Samuele Lilli - DonCallisto
$builder->add('tags', TextType::class);
$builder->get(‘tags’)->addModelTransformer(...);
FORM FIELD
Samuele Lilli - DonCallisto
ENTITY
MODEL DATA NORM DATA
NEW FORM BIND
TRANSFORM
REVERSE
TRANSFORM
MODEL TRANSFORMER
Samuele Lilli - DonCallisto
TRANSFORM
public function transform($tagsArray)
{
// transform the array to string
if (null === $tagsArray) {
return '';
}
return implode(',', $tagsArray);
}
Samuele Lilli - DonCallisto
ENTITY
MODEL DATA NORM DATA
NEW FORM BIND
TRANSFORM
REVERSE
TRANSFORM
MODEL TRANSFORMER
Samuele Lilli - DonCallisto
public function reverseTransform($tagsString)
{
// transform the string back to an array
if (!$tagsString) {
return [];
}
return explode(',', $tagsString);
}
REVERSE TRANSFORM
Samuele Lilli - DonCallisto
VIEW
NORM DATA VIEW DATA
CREATE VIEWPOST
TRANSFORM
REVERSE
TRANSFORM
VIEW TRANSFORMER
Samuele Lilli - DonCallisto
DATE TYPE (TRANSFORM)
// Transforms a normalized date into a localized date string/array)
public function transform($dateTime)
{
if (null === $dateTime) {
return '';
}
if (!$dateTime instanceof DateTimeInterface) {
throw new TransformationFailedException('Expected a DateTimeInterface.');
}
$value = $this->getIntlDateFormatter()->format($dateTime->getTimestamp());
if (intl_get_error_code() != 0) {
throw new TransformationFailedException(intl_get_error_message());
}
return $value;
}
Samuele Lilli - DonCallisto
// Transforms a localized date string/array into a normalized date.
public function reverseTransform($value)
{
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
if ('' === $value) {
return;
}
$timestamp = $this->getIntlDateFormatter()->parse($value);
// ….
try {
$dateTime = new DateTime(sprintf('@%s', $timestamp));
} catch (Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}
// ….
return $dateTime;
}
DATE TYPE ( REVERSE TRANSFORM)
Samuele Lilli - DonCallisto
PRE SET DATA EVENT
Modify data given during pre-population.
Don’t modify form data directly but modify event data instead.
Add/Remove form fields
USED FOR
EVENT DATA
MODEL DATA
Samuele Lilli - DonCallisto
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $e) {
/** @var $data User */
$data = $e->getData(); // ← MODEL DATA
$form = $e->getForm();
$today = new DateTime();
$criteria = Criteria::create()
->where(Criteria::expr()->gte('date', $today));
$form->add(‘tickets’, CollectionType::class, [
// ….
'data' => $data->getTickets()->matching($criteria)
]);
});
});
PRE_SET_DATA
Samuele Lilli - DonCallisto
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $e) {
if ($e->getData()->canHandleMail()) { // ← MODEL DATA
$e->getForm()->add(‘email’, EmailType::class);
}
});
});
PRE_SET_DATA
Samuele Lilli - DonCallisto
Read pre-populated form data
Don’t remove fields that you’ve setted “statically” on form
building process. Use PRE_SET_DATA and implement the logic
about fields. One exception: you are extending from a parent
form where you cannot control yourself the logic.
USED FOR
EVENT DATA
MODEL DATA
POST SET DATA EVENT
Samuele Lilli - DonCallisto
class FooType extends BarType
{
// ….
// email field added in BarType
->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $e) {
if (!$e->getData()->canHandleMail()) { // ← MODEL DATA
$e->getForm()->remove(‘email’);
}
});
});
POST_SET_DATA
Samuele Lilli - DonCallisto
Change data from the request
Add/Remove form fields
USED FOR
EVENT DATA
REQUEST DATA (ARRAY)
PRE SUBMIT EVENT
Samuele Lilli - DonCallisto
Modify NormData
No real example will be showed
EVENT DATA
NORM DATA
SUBMIT EVENT
USED FOR
Samuele Lilli - DonCallisto
Fetch data after denormalization
Even if faster to read “final” data here, don’t implement any
business logic → hard to test and break SRP.
If you need to modify model data, do it elsewhere just after isValid
call.
USED FOR
EVENT DATA
VIEW DATA
POST SUBMIT EVENT
Samuele Lilli - DonCallisto
POST SUBMIT
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($handler) {
$delete = $event->getForm()->has('delete') ? $event->getForm()->get('delete')->getData() : false;
$entity = $event->getForm()->getParent()->getData();
if (!$delete) {
return;
}
$handler->remove($entity, $event->getForm()->getName());
});
Vichuploader-bundle
Samuele Lilli - DonCallisto
Class FooBar
{
private $foo;
private $bar;
public function __construct($foo, $bar)
{
$this->foo = $foo;
$this->bar = $bar;
}
public function getFoo(){ … }
public function getBar(){ … }
}
Samuele Lilli - DonCallisto
Class FooBarType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(‘foo’)
->add(‘bar’);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
‘empty_data’ => function (FormInterface $interface) {
return new FooBar($interface->get(‘foo’)->getData(), $interface->get(‘bar’)->getData());
},
]);
}
}