Symfony 3 - PATCH Method doesn't remove/replace elements in a collection - php

In my entity file Collection.php I have self referencing many to many relationship:
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Collection")
* #ORM\JoinTable(name="collection_related_collections",
* joinColumns={#ORM\JoinColumn(name="collection_source", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="collection_target", referencedColumnName="id")}
* )
* #JMS\Groups({"collection_related_collections"})
* #JMS\Expose()
* #Assert\Count(
* max = 3,
* maxMessage = "You cannot specify more than {{ limit }} related collections"
* )
* #var Collection[]|ArrayCollection $relatedCollections
*/
protected $relatedCollections;
This is processed in a controller by a form handler:
$this->get('app_bundle.form.handler.api_form_handler')
->process($form);
Finnally my CollectionFormType.php has RelatedCollection field:
->add('relatedCollections', EntityType::class, [
'required' => false,
'choice_value' => 'hashId',
'choice_label' => function (Collection $collection) {
return $collection->getName();
},
'multiple' => true,
'expanded' => true,
'class' => Collection::class,
'query_builder' => function (CollectionRepository $collectionRepository) {
return $collectionRepository->getQueryBuilder();
}
])
Everything works fine when I am trying to PATCH an empty relatedCollection field:
[PATCH] http://symfony.dev/api/collections/{collectionHashId}
{
"collection": {
"relatedCollections": ["7D68076025", "196208D03D"]
}
}
But I cannot remove/replace elements trying
[PATCH] http://symfony.dev/api/collections/{collectionHashId}
{
"collection": {
"relatedCollections": ["7D68076025"]
}
}
This action has no effect. As a result value of relatedCollection field remains the same as I set it in the previous request:
"relatedCollections": ["7D68076025", "196208D03D"]
Also, I cannot remove relatedCollections values by sending an empty array [], because it makes no changes at all.
Is there a way to replace/remove ArrayCollection values using PATCH Method?

Have you tried this with PUT as well with a similar result? I suspect this is caused by the "owner" of the relationship being the child; in this case RelatedCollection as I understand it.
You'll need to update the owning side of the relationship if that is the case. Check this out for more information: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/unitofwork-associations.html

Related

How to include fractal transformed objects directly to collection meta without data key

I'm using league/fractal with JsonApiSerializer,
I've got users collection for json output.
Now I want to add some filters data to this json response (like users count for current filters).
I got this:
$resource = new Collection($dataProvider->getData(), new UserTransformer());
//the only way to include some not directly linked data i found is using setMeta():
$resource->setMetaValue('projects', $dataProvider->getProjects());
$resource->setMetaValue('somes', $dataProvider->getTasks());
But! 'projects' & 'somes' collections (yes, they are collection too) also included with 'data' key in it.
So, I've got this structure:
{
'data' => [
{//user1},{//user2},...
],
'meta' => {
'projects' => {
'data' => {...}
},
'somes' => {
'data' => {...}
}
}
}
but I want something like:
{
'data' => [
{//user1},{//user2},...
],
'meta' => {
'projects' => {...}, //there is no 'data' key
'somes' => {...} //there is no 'data' key
}
}
What should I do?
This is kinda hack but works fine without refactor Scope class which hardcoded in fractal's League\Fractal\Manager::createData() and is only way to use your own Scope class realization is to overload this method in Manager's extension.
<?php
use League\Fractal\Serializer\JsonApiSerializer;
/**
* Class EmbedSerializer
*/
class EmbedSerializer extends JsonApiSerializer
{
const RESOURCE_EMBEDDED_KEY = 'embedded';
/**
* Serialize a collection.
*
* #param string $resourceKey
* #param array $data
* #return array
*/
public function collection($resourceKey, array $data)
{
return $resourceKey === self::RESOURCE_EMBEDDED_KEY ? $data : [$resourceKey ?: 'data' => $data];
}
/**
* Serialize an item.
*
* #param string $resourceKey
* #param array $data
* #return array
*/
public function item($resourceKey, array $data)
{
return $resourceKey === self::RESOURCE_EMBEDDED_KEY ? $data : [$resourceKey ?: 'data' => [$data]];
}
}
So, now i could use it like:
/** #var $this->fractal League\Fractal\Manager */
$this->fractal->setSerializer(new EmbedSerializer());
$projectsCollection = $this->fractal->createData(
new Collection($projects, new UserProjectTransformer(), 'embedded')
)->toArray();
$resource = new Collection($users, new UserTransformer());
$resource->setMetaValue('projects', $projectsCollection);
That's all u need. Hope this will be helpful.

Symfony form not saving entity with ManyToMany relation

I have problem saving entity trough form with ManyToMany relations.
I can not save fields that are on "mappedBy" side of relation.
Code below is not saving anything to database and not trowing any errors:
// Entity/Pet
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Customer", mappedBy="pet", cascade={"persist"})
*/
private $customer;
/**
* Set customer
*
* #param \AppBundle\Entity\Customer $customer
* #return Pet
*/
public function setCustomer($customer)
{
$this->customer = $customer;
return $this;
}
// Entity/Customer
/**
* #var Pet
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Pet", inversedBy="customer", cascade={"persist"})
* #ORM\JoinTable(name="customer_pet",
* joinColumns={
* #ORM\JoinColumn(name="customer_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="pet_id", referencedColumnName="id")
* }
* )
*/
private $pet;
// PetType.php
$builder->add('customer', 'entity',
array(
'class' => 'AppBundle:Customer',
'property' => 'firstname',
'empty_value' => 'Choose owner',
'multiple' => true
));
It is working the other way around. So if I am saving something from CustomerType everything works.
EDIT:
Solution below worked for me but after couple days I found a problem with that solution. If form will be submitted with value that has been already saved in the database then Symfony will trow an error. To prevent that I had to check if given customer has been already assigned to the pet.
Checking of currently assigned customers had to be done on the beginning of function and not after form submission because for some reason after submission Pet() object contains submitted values not only those present in the db.
So on the beginning I've putted all already assigned customers in to the array
$em = $this->getDoctrine()->getManager();
$pet = $em->getRepository('AppBundle:Pet')->find($id);
$petOriginalOwners = array();
foreach ($pet->getCustomer() as $petCustomer)
{
$petOriginalOwners[] = $petCustomer->getId();
}
And after form submission I've checked if submitted ID's are in the array
if ($form->isValid())
{
foreach ($form['customer']->getData()->getValues() as $v)
{
$customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
if ($customer && !in_array($v->getId(), $petOriginalOwners) )
{
$customer->addPet($pet);
}
}
$em->persist($pet);
$em->flush();
return $this->redirect($this->generateUrl('path'));
}
In Symfony2 the entity with the property with the inversedBy doctrine comment is the one that is supposed to EDIT THE EXTRA TABLE CREATED BY THE MANYTOMANY RELATION. That is why when you create a customer it inserts the corresponding rows in that extra table, saving the corresponding pets.
If you want the same behavior to happen the other way around, I recommend:
//PetController.php
public function createAction(Request $request) {
$entity = new Pet();
$form = $this->createCreateForm($entity);
$form->submit($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach ($form['customer']->getData()->getValues() as $v) {
$customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
if ($customer) {
$customer->addPet($entity);
}
}
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('pet_show', array('id' => $entity->getId())));
}
return $this->render('AppBundle:pet:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
private function createCreateForm(Pet $entity) {
$form = $this->createForm(new PetType(), $entity, array(
'action' => $this->generateUrl('pet_create'),
'method' => 'POST',
));
return $form;
}
These two are but standard Symfony2 CRUD-generated actions in the controller corresponding to Pet entity.
The only tweak is the foreach structure inserted in the first action, that way you forcibly add the same pet to each customer you select in the form, thus getting the desired behavior.
Look, it is highly probable THIS is not the RIGHT WAY, or the PROPER WAY, but is A WAY and it works. Hope it helps.
In my case with a services <-> projects scenario, where services has "inversedBy" and projects has "mappedBy" I had to do this in my project controller's edit action so that when editing a project the services you checked would be persisted.
public function editAction(Request $request, Project $project = null)
{
// Check entity exists blurb, and get it from the repository, if you're inputting an entity ID instead of object ...
// << Many-to-many mappedBy hack
$servicesOriginal = new ArrayCollection();
foreach ($project->getServices() as $service) {
$servicesOriginal->add($service);
}
// >> Many-to-many mappedBy hack
$form = $this->createForm(ProjectType::class, $project);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
// << Many-to-many mappedBy hack
foreach ($servicesOriginal as $service) {
if (!$project->getServices()->contains($service)) {
$service->removeProject($project);
$em->persist($service);
}
}
foreach ($project->getServices() as $service) {
$service->addProject($project);
$em->persist($service);
}
// >> Many-to-many mappedBy hack
$em->persist($project);
$em->flush();
return; // I have a custom `redirectWithMessage()` here, use what you like ...
}
return $this->render("Your-template", [
$form => $form->createView(),
$project => $project,
]);
}
This works for both adding and removing entities in the many-to-many from the "mappedBy" side, so EntityType inputs should work as intended.
What's going on here is we're first building an "original" collection containing all of the service entities already linked to for this project. Then when the form is saving we're ensuring:
First that any unchecked services (those in the original collection but not the project object) have the project removed from their internal collection, then persisted.
Second that any newly checked services each add the project to their internal collection, then persisted.
Important: This depends on your entity's addService() and addProject() methods respectively check that each others' collections do not contain duplications. If you don't do this you'll end up with an SQL level error about a duplicate record insertion.
In the service entity I have:
/**
* Add project
*
* #param Project $project
*
* #return Service
*/
public function addProject(Project $project)
{
if (!$this->projects->contains($project)) {
$this->projects->add($project);
}
if (!$project->getServices()->contains($this)) {
$project->getServices()->add($this);
}
return $this;
}
In the project entity I have:
/**
* Add service
*
* #param Service $service
*
* #return Project
*/
public function addService(Service $service)
{
if (!$this->services->contains($service)) {
$this->services->add($service);
}
if (!$service->getProjects()->contains($this)) {
$service->getProjects()->add($this);
}
return $this;
}
You could alternatively check this in your controller instead, but makes sense if the model validates this itself when possible, as the model would break anyway if there were duplicates from any source.
Finally in your controller's create action you'll likely need this bit too just before $em->persist($project). (You won't need to work with an "original" collection as none will exist yet.)
// << Many-to-many mappedBy hack
foreach ($project->getServices() as $service) {
$service->addProject($project);
$em->persist($service);
}
// >> Many-to-many mappedBy hack
I just had the same problem and I solved it differently.
Changing the code in the controller is not the better way to do it.
In my case I have a GenericController that handle all my CRUDs so I can't put in it specific code.
The best way to do it is by adding in your PetType a listener like this :
// PetType.php
$builder->add('customer', 'entity',
array(
'class' => 'AppBundle:Customer',
'property' => 'firstname',
'empty_value' => 'Choose owner',
'multiple' => true
))
->addEventListener( FormEvents::SUBMIT, function( FormEvent $event ) {
/** #var Pet $pet */
$pet = $event->getData();
foreach ( $pet->getCustomers() as $customer ) {
$customer->addPet( $pet );
}
} );
That way you'll keep the mapping logic in the same place.

Symfony Embedded Form Conditional Validation

I have a form which contains three objects:
$builder
->add('customer', new CustomerType())
->add('shippingAddress', new AddressType())
->add('billingAddress', new AddressType())
->add('sameAsShipping', 'checkbox', ['mapped' => false])
;
Each of the embedded forms has their own validation constraints and they work. In my main form, I have cascade_validation => true so that all of the embedded form validation constraints are applied. This also works.
I am having trouble 'disabling' the validation on the billingAddress form if the sameAsShipping checkbox is enabled. I can't make the validation in the AddressType form conditional because it always needs to be enforced for the shippingAddress form.
I've solved this same problem by using validation groups.
First, this is important: use the validation_groups option in your AddressType to set the validation groups of every constraint of each field in the type:
<?php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Form\FormBuilderInterface;
class AddressType extends \Symfony\Component\Form\AbstractType
{
function buildForm(FormBuilderInterface $builder, array $options)
{
$groups = $options['validation_groups'];
$builder->add('firstName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
$builder->add('lastName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
}
}
Then, in the parent form pass different validation groups to the two fields:
<?php
$formBuilder = $this->get('form.factory')
->createNamedBuilder('checkout', 'form', null, [
'cascade_validation' => true,
])
->add('billingAddress', 'address', [
'validation_groups' => 'billingAddress'
])
->add('shippingAddress', 'address', [
'validation_groups' => 'shippingAddress'
]);
Then, determine determine your validation groups by looking at the value of the checkbox.
if ($request->request->get('sameAsShipping')) {
$checkoutValidationGroups = ['Default', 'billingAddress'];
} else {
$checkoutValidationGroups = ['Default', 'billingAddress', 'shippingAddress'];
}
You can then validate only either the billingAddress or the shippingAddress, or both using the validation group mechanism.
I chose to use a button:
$formBuilder->add('submitButton', 'submit', ['validation_groups' => $checkoutValidationGroups]);
Create a form model (I use it in nearly every form, but this code here is not tested):
/**
* #Assert\GroupSequenceProvider()
*/
class YourForm implements GroupSequenceProviderInterface {
/**
* #Assert\Valid()
*/
private $customer;
/**
* #Assert\Valid()
*/
private $shippingAddress;
/**
* #Assert\Valid(groups={'BillingAddressRequired'})
*/
private $billingAddress;
private $billingSameAsShipping;
public function getGroupSequence() {
$groups = ['YourForm'];
if(!$this->billingSameAsShipping) {
$groups[] = 'BillingAddressRequired';
}
return $groups;
}
}
Try to use meaningful names. sameAsShipping is hard to understand. Read the if-condition in getGroupSequence: if not billing (address) same as shipping (address) then billing address required.
That's all, clear code in my opinion.

Symfony2 - simplecms : how add extras data?

I've a problem in my form for the entity Page from simplecms
I want to add an item in the array Extras, so i added it in my formtype :
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Cmf\Bundle\SimpleCmsBundle\Doctrine\Phpcr\Page;
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', 'text', array(
'label' => 'Titre',
'attr' => array('placeholder' => 'Titre complet de la page')
))
->add('name', 'text', array(
'label' => 'Label',
'attr' => array('placeholder' => 'nom-simplifie-de-la-page')
))
->add('body', 'ckeditor')
->add('locale', 'hidden')
->add('publishable')
->add('extras_link','text', array(
'property_path' =>"extras['link']",
));
}
The vars are in class Page (i didnt had to override it) and functions removeExtra() and addExtra() too (necessary to my form alimentation)
/**
* Add a single key - value pair to extras
*
* #param string $key
* #param string $value - if this is not a string it is cast to one
*/
public function addExtra($key, $value)
{
$this->extras[$key] = (string) $value;
}
/**
* Remove a single key - value pair from extras, if it was set.
*
* #param string $key
*/
public function removeExtra($key)
{
if (array_key_exists($key, $this->extras)) {
unset($this->extras[$key]);
}
}
form is working, but when i submit, it find removeExtra() but not addExtra()
"Found the public method "removeExtra()", but did not find a public "addExtra()" on class Symfony\Cmf\Bundle\SimpleCmsBundle\Doctrine\Phpcr\Page"
Somebody already had this problem? or know how to add data in extras?
THX (sorry for my english)
This is a limitation of the form layer unfortunately. The logic that looks for the right method only finds a adder with one parameter. What we ended up doing is using the burgov/key-value-form-bundle : https://github.com/Burgov/KeyValueFormBundle/
This required a PR like this to enabled in SeoBundle: https://github.com/symfony-cmf/SeoBundle/pull/158
from the code in Page::setExtras i think this should already work, though a similar PR to the one on SeoBundle would make it slightly more efficient.

Symfony forms, error bubbling

I have a problem with forms' error bubbling.
One field in my form is defined like this:
$formBuilder->add('title','text',
'required' => true,
'error_bubbling' => false,
)
)
I would like to add a validator like this to this field:
/**
* #Assert\True(message = "Bad title.")
*/
public function getTitleCorrect()
{
/* ... */
return false;
}
It works ok, but the error message shows up on top of the form, not in the field row.
In the Twig template this error message is rendered by {{form_errors(form)}} as a global error. When I use {{form_errors(form.title)}}, it does not print my error.
What can I do to change the assignment of the error?
Messages are attached to a field only when validator is attached to corresponding property.
Your validator is attached to a method of the class so error is indeed global.
You should to something like that:
use ...\TitleValidator as AssertTitleValid;
class MyEntity
{
/**
* #AssertTitleValid
*/
private $title;
}
And create your own TitleValidator class.
'error_bubbling' is false by default unless it is set to true or form is compound.
Besides error might be bubbled up if there is some mismatch between form field and validated property/method, like you have in your example or if different case is used (snake_case for form field and $camelCase for validated property).
In this case you may use 'error_mapping' in your form class:
/**
* #param OptionsResolver $resolver
*
* #throws AccessException
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => YourEntity::class,
'error_mapping' => [
'entityProperty' => 'form_field_1',
'entityMethod' => 'form_field_2',
],
]);
}

Categories