symfony form one-to-one relation DELETE - forms

This has to be something really simple, but i just can't figure it out.
I got a Parent entity with a Child one-to-one unidirectional relation, the Parent entity being the owning side:
/**
* #ORM\OneToOne(targetEntity="Child", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\JoinColumn(name="child_id", referencedColumnName="id")}
*/
protected $Child;
The form has the ChildType added without it being required, but the ChildType has some required fields:
$builder->add('child', new ChildType(), [
'required' => false,
]);
In a create action there are no problems:
With all the Child fields being empty, the association stays null, the validation constraints for the Child relation are ignored, and the Child relation doesnt get created even though it has some required fields.
When some of the Child fields are filled in, the association is created and the Child entity gets persisted if validation passes.
So far working as expected..
When updating the data set, everything goes well exactly the same way as when creating. Perfect
Now a situation where i want the child entity removed, this should be as simple as clearing all child fields, since the child is not required.
Emtying all fields however, still triggers validation and results in doctrine updating the relation with all NULL fields.
What i tried:
Created a preUpdate listener for Doctrine to delete the relation if all fields are null. The problem is that you can't get this far without disabling validation. So this can't be the right way.
One way would be to just use a callback constraints that returns true if all fields are null, but this would render all the property constraints useless for all one-to-one cases. Cant be true..
So far no clue what now, nothing in the documentation either, only mass use-cases of x-to-many...
Is it possible to somehow use a different validation group or no validation PostSubmit inside the symfony form or some other way to not use any validation if all fields are NULL of the child entity?

One thing you can do is use a form listener, something like:
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
$entity = $event->getForm()->getData();
if (null === $entity->getChild->getField1() && null === $entity->getChild->getField2() ... and so on for each child field) {
$entity->setChild(null);
}
});

$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
if (
$form->getData()->getChild()
&& $this->isChildrenHaveEmptyData($form->get('child'))
) {
$entity->setChild(null);
}
});
.....
private function isChildrenHaveEmptyData(FormInterface $form): bool
{
foreach ($form as $child) {
if ($child->getData()) {
return false;
}
}
return true;
}

Related

Problems with Symfony embedded forms

I am trying to achieve the following scenario:
Auction and Category entities (many-to-one). The Category entity has a one-to-many relationship to CategoryAttribute entity which allows an unlimited number of attributes of various types to be added to the category. Let's say a Cars category will have Make and Year attributes. The CategoryAttribute entity has widgetType property which defines how to render the attribute (input, select etc.), attributeValues which is the data for the attribute (populating the select etc.) and isRequired property to tell whether a property is required or not. So far so good. Managing the attributes was piece of cake BUT:
On the Auction side of things I want when the user selects a given category from the list to render all the attributes for that category to be filled in. This is translated to an related AuctionAttribute entity (many-to-one to the auction in attributes property in the class). The AuctionAttribute has reference to the CategoryAttribute, a attributeValue to hold the input or selected value.
Well, the whole AJAX request and fill in of the attributes for a selected category was not a problem. The problem(s) arise when I submit the form. Basically there are two issues.
How do we bind the attributes part of the form to the actual form for validation. Let's say we have Car category selected and Make attribute is required, how do we validate that this attribute?
How do we bind the attributes input to AuctionAttribute entity in that form?
I know that for embedded forms I need to hook up to the FormEvents::PRE_SUBMIT event but I am not sure how to transform the attribute to an Entity in there.
In terms of code, I have the following:
When getting the attributes for a category, I create a AuctionAttributeFormType and render it into a twig form helper and return the HTML back in the AJAX request:
$form = $this->createForm(new Type\AuctionAttributeFormType(), null, array('csrf_protection' => false));
foreach ($categoryAttributes as $attribute) {
$form->add('attribute_'.$attribute->getId(), $attribute->getWidgetType(), array('label' => $attribute->getName(), 'required' => $attribute->isRequired());
}
When the Auction form is submitted, I hook to the PRE_SUBMIT event and when whether there is a attribute submitted and it belongs to the set of attributes of the category but this is as far as I went before I got stuck:
$builder->addEventListener(
Form\FormEvents::PRE_SUBMIT, function (Form\FormEvent $event) {
$auction = $event->getData();
if (null !== $auction['category']) {
$categoryAttributes = $this
->repository
->findAttributesForCategory($auction['category'])
->getResult();
if (count($categoryAttributes) > 0) {
$attribute_values = array();
foreach ($categoryAttributes as $attribute) {
if (isset($auction['attribute_' . $attribute->getId()])) {
$attribute_values[$attribute->getId()] = $auction['attribute_' . $attribute->getId()];
}
}
}
}
}
);
I need to get the values from attribute_values array into AuctionAttribute entities bound to the Auction entity. Any idea how this could be achieved. I think it should be done through some kind of data transformer but I am not sure to what to transform that data - should it be a form->add field, or directly touch the Auction entity which is filled in with data.
Any suggestions?
EDIT:
I made it work with the use of Model transformer but now there is another problem, when editing the record, if there is more than one attribute, only the first one is populated with data. Here is a sample gist of the code:
https://gist.github.com/SvetlinStaev/86e066a865478e40718c
My suggestion is NOT to convert the submitted data via Event Listeners, but to use a Data Transformer, which you attach to a form field like so:
$formBuilder->add(
$formBuilder
->create('FIELD_NAME', 'FIELD_TYPE', [
... FIELD_OPTIONS ...
])
->addModelTransformer(new SomeModelTransformer())
)
And the "SomeModelTransformer" class should look like this:
class SeatingToNumberTransformer implements DataTransformerInterface
{
/**
* Transforms the object from the norm data to model data
* The norm data is the field value. Say you have an integer field, $normDataObject would be an int.
* In your case: you need to instantiate several new AuctionAttribute objects and persist them maybe
*/
public function transform($normDataObject)
{
$transformedObject = $this->someTransformAction($normDataObject);
return $transformedObject;
}
/**
* Reverts the transform
* in your case: from AuctionAttribute to int
*/
public function reverseTransform($modelDataObject)
{
$transformedObject = $this->someOtherTransformAction($modelDataObject);
return $transformedObject;
}
}
More info can be found here
If you need more help, just let me know.

Symfony2 (>= 2.3): How to listen to parent form event from child?

I have a custom FormType, which needs to add itself to the parent Entity when the parent Form persists.
In Symfony < 2.3 this could be done by doing the following:
class FooType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
...
$builder->getParent()->addEventSubscriber(new FooSubscriber)
}
}
class FooSubscriber implements EventSubscriberInterface
{
static function getSubscribedEvents()
{
return array(
FormEvents::POST_SUBMIT => 'postSubmit'
);
}
}
But after upgrading to Symfony 2.6 I've discovered that $builder->getParent() has been removed. But now I can't listen to the parent being submitted.
So I added the listener to my builder and referenced the parent from within the Subscriber. But this doesn't really work, since I do a check on the parent form being valid - which it isn't, since it's not submitted yet:
function postSubmit(FormEvent $e)
{
if ($e->getForm()->getParent()->getRoot()->isValid()) {
//this gives 'false'
This false is caused by the next piece of code:
// Symfony\Component\Form\Form.php # line 744
public function isValid()
{
if (!$this->submitted) {
return false;
}
And because the parent form first loops through all the childs and submits that, before setting $this->submitted = true on itself... I'm not sure if the parent is valid.
TL;DR
How can I add an Eventlistener to my parent Form, without having to adjust my parent Form? I want my FooType be something I can add to all forms, without having to know/remember to do some logic for that FooType specific.
I needed the same functionality because I have a custom form field that needs the parent entity after all mapped fields have been updated. Unfortunately the POST_SUBMIT of child forms is called before SUBMIT on the parent is run.
I ended up passing the eventDispatcher to the child, and tying my listener there. I needed two listeners to get the job done: one to get the processed value, and one to update the main entity. passing $generatedPassword to the closure by reference allows you to share data from the child event to the parent.
#Parent::buildForm
$builder->add('generate_password', GeneratePasswordType::class, [
'event_dispatcher' => $builder->getEventDispatcher(),
]);
#Child::buildForm
// first listen to submit event to get current field value
$generateNewPassword = false;
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) use (&generateNewPassword) {
$generateNewPassword = null !== $event->getData();
});
// then run updater after parent entity has been updated
$parentDispatcher = $options['event_dispatcher'];
$parentDispatcher->addListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use (&$generateNewPassword) {
$user = $event->getData();
if(true === $generateNewPassword){
// update password & email user new credentials
}
}
(The custom field is a checkbox marked 'generate new password on save' for a user management module. It emails the user the generated password, which is why I need the latest email address from the main entity)

Symfony 2 - Change entity's field from Admin while keeping validation using SonataAdminBundle

Using:
Symfony 2.5
SonataAdminBundle
I am trying to change one of the entity fields (title) when data is submitted / saved to database by using two fields from associated entites ex.
DocumentRevision <- Document -> CustomEntity [title] = Document[title]+DocumentRevision[number]
But title of CustomEntity has to be unique - this was the problem I was trying to solve and managed with Database constraints and UniqueEntity validation (not quite - more on this later).
Now the issue is that I change the title data on Doctrine preUpdate/Persist effectivly skipping validation for that field since it's empty at validation time. When user puts wrong data Database layer throws an error about duplicate for unique constraint.
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function setTitleFromDocumentName() {
$this->setTitle($this->getDocument()->getName() . " rev. " . $this->getDocumentRevision()->getRevisionNumber());
}
The entity itself is using UniqueEntity constraint on field title, so custom constraints or validation groups are pointles from my perspective as it would only duplicate the already used constraint.
/**
* #UniqueEntity(
* fields={"title"}
* )
**/
The simplest solution as it seems would be to get somewhere between post Submit before validation, but it would have to be done from Entity.
My question is how can (can it?) be done without overriding SonataCRUD Controller or it's other parts, is it even possible?
It can be done, but there are issues:
I was able to change the title using Form Events like this:
protected function configureFormFields(FormMapper $formMapper) {
...
$builder = $formMapper->getFormBuilder();
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (!$data) {
return;
}
$data['title'] = $data['document'] . ' rev. ' . $data['documentRevision'];
$event->setData($data);
}
...
formMapper
->add('title',null,array(
...
);
The current problem is that I am getting the IDs of 'document' and 'documentRevision' and I need their names or __toString() representation at least.
Another issue is that although I can set the title using the event it shows error from DB when it should show Form error since validation should be done on FormEvents::SUBMIT - this one I don't understand.
Last thing to note is that if I try to use callback function:
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this,'onPreSubmit'))
public function onPreSubmit() {
$entity = $this->getSubject();
$entity->setTitleFromDocumentName();
}
I will get null title and errors if Entity tries to get fields from related entites - Calling function on non object.
Regarding entity data maybe this will help you to get the subject:
https://gist.github.com/webdevilopers/fef9e296e77bb879d138
Then you could use getters to get the desired data for instance:
protected function configureFormFields(FormMapper $formMapper)
{
$subject = $this->getSubject();
$formMapper->getFormBuilder()->addEventListener(FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($subject) {
$document = $subject->getDocument();
// ...
});
}
I also posted this on your issue:
https://github.com/sonata-project/SonataAdminBundle/issues/2273
To solved this when I changed the unique entity validation constraints as ones used by me where not completely valid from conceptual perspective.
Also it's important to note that functions that are marked as #PrePersist, #PreUpdate etc. must be public if they are to be used like that, marking them private will make Doctrine fail.
Note that the methods set as lifecycle callbacks need to be public and, when using these annotations, you have to apply the #HasLifecycleCallbacks marker annotation on the entity class.
See: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#lifecycle-callbacks (first paragraph after the code sample).

Symfony2 Entity validation — one has to be true

I have a problem with the validation of Symfony. I have a form which is of the type User and the user maps some other stuff (like Addresses, Phones etc.)
Now I want to force the creator of the user that he makes one of the Addresses/Phones the primary one (the entity has a field for that).
How can I solve this? So only one of the OneToMany Entitys (one of the Addresses) needs to be a primary one. And assure that it will be always at least one.
One way is to add a field to the User entity pointing to a primary address in a one-to-one manner and make it required.
Another way is to create a custom validator that will loop through the user addresses and validate that at least one of them is marked as primary.
Or you could just use the True constraint:
/**
* #True
*/
public function isThereOnePrimaryAddress()
{
$primes = 0;
foreach ($this->getAddresses() as $address) {
if ($address->isPrimary()) {
$primes++;
}
}
if (1 === $primes) {
return true;
}
return false;
}

Symfony: How to hide form fields from display and then set values for them in the action class

I am fairly new to symfony and I have 2 fields relating to my table "Pages"; created_by and updated_by. These are related to the users table (sfGuardUser) as foreign keys. I want these to be hidden from the edit/new forms so I have set up the generator.yml file to not display these fields:
form:
display:
General: [name, template_id]
Meta: [meta_title, meta_description, meta_keywords]
Now I need to set the fields on the save. I have been searching for how to do this all day and tried a hundred methods. The method I have got working is this, in the actions class:
protected function processForm(sfWebRequest $request, sfForm $form)
{
$form_params = $request->getParameter($form->getName());
$form_params['updated_by'] = $this->getUser()->getGuardUser()->getId();
if ($form->getObject()->isNew()) $form_params['created_by'] = $this->getUser()->getGuardUser()->getId();
$form->bind($form_params, $request->getFiles($form->getName()));
So this works. But I get the feeling that ideally I shouldnt be modifying the web request, but instead modifying the form/object directly. However I havent had any success with things like:
$form->getObject()->setUpdatedBy($this->getUser()->getGuardUser());
If anyone could offer any advice on the best ways about solving this type of problem I would be very grateful.
Thanks,
Tom
After processing and saving the form you could set those fields on the object and re-save:
protected function processForm(sfWebRequest $request, sfForm $form)
{
$form->bind($request->getParameter($form->getName()));
if ($form->isValid())
{
$page = $form->save();
$user = $this->getUser()->getGuardUser();
$page->setUpdatedBy($user);
if (empty($page->created_by))
{
$page->setCreatedBy($user);
}
$page->save();
$this->getUser()->setFlash('notice', 'Successfully saved page.');
$this->redirect('#homepage');
}
}
There's also a Doctrine extension called Blameable that automatically sets edited_by and created_by fields on specified models. The Doctrine website is undergoing some reorganization but here is the cached page for the extension.
To process your form create a new object, set the fields then save.
$article = new Article();
$article->setName($request->getParameter($form->getName());
$article->setDescription($request->getParameter($form->getDescription());
$article->setMetaKeywords($request->getParameter($form->getMetaKeywords());
$article->save();
What you want to do is customize your form and unset the 'created_at' and 'updated_at' pieces of the form in configure
class SampleForm extends BaseSampleForm
{
public function configure()
{
unset(
$this['created_at'],
$this['updated_at']
);
}
}
Then they won't show up in the form and will get the values setup by the "Timestampable" behavior before being saved
http://stereointeractive.com/blog/2010/04/07/symfony-forms-hide-created_at-updated_at-columns/