Can't get my edit action (only on the ManyToOne side) to remove the link between my entities - php-8

Does anybody know what's wrong in my code ?
Im using EasyAdminBundle 4 and I've added a ManyToOne relation between two entities (Label & Organizer).
When I delete every Labels from an Organizer, it works, they are no more linked together.
But when I'm on the edit page of a Label and I remove the linked Organizer from the Tomselect and click on a save button, nothing changes.
My setOrganizer setter is effectively called with a null Organizer argument, and $this has no more Organizer attribute # the end of this method, it's null, but when I dump my Label object in an "EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityUpdatedEvent" subscriber, it still has an Organizer.
On the other hand, when I select an other Organizer in my Label edition page, it works as expected. (same when adding the 1st Organizer from there).
Here is some of my code, feel free to ask more if it potentially can help us understand what is happening.
Cheers !
/**
* #ORM\Entity(repositoryClass=LabelRepository::class)
*/
class Label extends AbstractOtherActor
{
/**
* #ORM\ManyToOne(targetEntity=Organizer::class, inversedBy="labels", fetch="EAGER")
* #ORM\JoinColumn(nullable=true)
*/
private $organizer;
public function getOrganizer(): ?Organizer
{
return $this->organizer;
}
public function setOrganizer(?Organizer $organizer): self
{
$this->organizer = $organizer;
return $this;
}
}
class Organizer extends AbstractOtherActor
{
public function __construct()
{
$this->labels = new ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity=Label::class, mappedBy="organizer", fetch="EAGER")
* #ORM\JoinColumn(nullable=true)
*/
private $labels;
/**
* #return Collection<int, Label>
*/
public function getLabels(): Collection
{
return $this->labels;
}
public function addLabel(Label $label): self
{
if (!$this->labels->contains($label)) {
$this->labels[] = $label;
$label->setOrganizer($this);
}
return $this;
}
public function removeLabel(Label $label): self
{
if ($this->labels->removeElement($label)) {
// set the owning side to null (unless already changed)
if ($label->getOrganizer() === $this) {
$label->setOrganizer(null);
}
}
return $this;
}
}
My AssociationField from LabelCrudController.php
AssociationField::new(
'organizer'
)->hideOnIndex(),
And my AssociationField from OrganizerCrudController.php
AssociationField::new(
'labels'
)
->setFormTypeOption('by_reference', false)
->setFormTypeOption("multiple", "true")->hideOnIndex(),

OK, finally found the answer, or at least one part of it :
Removing the two fetch="EAGER" solves my problem.
Now that I've got the what, I need the why, any idea ?
I was thinking there could be some kind of circular reference, that was making the same object persisted multiple times, but only one occurence has its child removed... Maybe the fact that getOrganizer is called multiple times when POSTing a label is a clue ?

Related

TYPO3 : How to determine child-objecttypes of specific parent domain-model?

I have some different domain-models, each being parent of different sub-models.
All of those domain-models extend themselves out of a basic model class and I want to write a general function in the base-model, that deals with subclasses of the current model. Therefore, I need to find a way, to dynamically get all child-model-classes of a given domain-model.
Can this be done somehow ? Perhaps via Object-Storage-Definitions or similar ?!
Update : as mentioned in the comment section, mny question had nothing to do with TYPO3, it was a general php-question .. solution for my question are reflection-classes.
I guess your question has nothing to do with TYPO3, so take a look at this general PHP question thread and possible solutions here.
You are talking about Database Relationships. Yes, this can be done in TYPO3.
Each model should be mapped to a table. So, let's take for example the Category domain model and parent property
class Category extends AbstractEntity
{
/**
* #var \TYPO3\CMS\Extbase\Domain\Model\Category
*/
protected $parent = null;
/**
* #return \TYPO3\CMS\Extbase\Domain\Model\Category
*/
public function getParent()
{
if ($this->parent instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
$this->parent->_loadRealInstance();
}
return $this->parent;
}
/**
* #param \TYPO3\CMS\Extbase\Domain\Model\Category $parent
*/
public function setParent(\TYPO3\CMS\Extbase\Domain\Model\Category $parent)
{
$this->parent = $parent;
}
The parent property will return the parent category. The same logic is when you want to get the childs.

Edit hidden records in frontend

I am building an extension to edit tt_news records in frontend.
I set setIgnoreEnableFields(TRUE) in my repository.
But if I try edit a hidden record, I get the error
Object with identity „12345" not found.
Any solution for this?
I am guessing you are using an action like
/**
* Single view of a news record
*
* #param \Vendor\Ext\Domain\Model\News $news news item
*/
public function detailAction(\Vendor\Ext\Domain\Model\News $news = null)
Your problem is, that the Repository is not used to fetch the record.
As a solution, remove the argument, clear the caches and try something like that
/**
* Single view of a news record
*
* #param \Vendor\Ext\Domain\Model\News $news news item
*/
public function detailAction() {
$id = (int)$this->request->getArgument('news');
if ($id) {
$news = $this->newsRepository->findByUid($previewNewsId);
}
}
Now you can manipulate the QuerySettings and use those.
The problem is the PropertyMapping. If extbase try to assign an uid (12345) to an Domain Object (tt_news) the "setEnableFields" setting of the Repository isn't respected. So you must fetch the object by yourself.
the simple solution is to do this in an initialize*Action for each "show" action. For editAction an example:
public function initializeEditAction() {
if ($this->request->hasArgument('news')) {
$newsUid = $this->request->getArgument('news');
if (!$this->newsRepository->findByUid($newsUid)) {
$defaultQuerySettings = $this->newsRepository->createQuery()->getQuerySettings();
$defaultQuerySettings->setIgnoreEnableFields(TRUE);
$this->newsRepository->setDefaultQuerySettings($defaultQuerySettings);
if ($news = $this->newsRepository->findByUid($newsUid)) {
$this->request->setArgument('news', $news);
}
}
}
}
The Hard Part is to get the object to update. As I never try this I have found an TypeConverter to fetch also hidden Records at https://gist.github.com/helhum/58a406fbb846b56a8b50
Maybe Instead to register the TypeConverter for everything (like the example in ext_localconf.php) you can try to assign it only in the initializeUpdateAction
public function initializeUpdateAction() {
if ($this->arguments->hasArgument('news')) {
$this->arguments->getArgument('news')->getPropertyMappingConfiguration()
->setTypeConverter('MyVendor\\MyExtension\\Property\\TypeConverters\\MyPersistenObjectConverter')
}
}

How to dynamically control the validation of a form?

I've got some issues with Symfony's form validation handling. I'd like to validate a form bound to an entity based on its data. There are quite a bunch of information how to dynamically modify the form fields using FormEvents. What I'm missing on this topic is how to control/modify the validation.
My simplified use case is:
A user can add an event to a calendar.
The validation checks if there's already an event.
If there's a collision, the validation will throw an error.
The user should now be able to ignore this error/warning.
The validation is implemented as a Validator with Constraint::CLASS_CONSTRAINT as the target (as it's taking some more stuff into account).
I tried to:
Hack around the validation groups, but couldn't find access to the entity wide validators.
Hack around the FormEvents and add an extra field like "Ignore date warning".
Hack around the submit button to change it to something like "Force submit".
... but never found a working solution. Even simpler hacks with a single property based validator didn't work out. :(
Is there a Symfony way to dynamically control the validation?
Edit: My code looks like this:
use Doctrine\ORM\Mapping as ORM;
use Acme\Bundle\Validator\Constraints as AcmeAssert;
/**
* Appointment
*
* #ORM\Entity
* #AcmeAssert\DateIsValid
*/
class Appointment
{
/**
* #ORM\Column(name="title", type="string", length=255)
*
* #var string
*/
protected $title;
/**
* #ORM\Column(name="date", type="date")
*
* #var \DateTime
*/
protected $date;
}
The validator used as a service:
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the date of an appointment.
*/
class DateIsValidValidator extends ConstraintValidator
{
/**
* {#inheritdoc}
*/
public function validate($appointment, Constraint $constraint)
{
if (null === $date = $appointment->getDate()) {
return;
}
/* Do some magic to validate date */
if (!$valid) {
$this->context->addViolationAt('date', $constraint->message);
}
}
}
The corresponding Constraint class is set to target the entity class.
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class DateIsValid extends Constraint
{
public $message = 'The date is not valid!';
/**
* {#inheritdoc}
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
/**
* {#inheritdoc}
*/
public function validatedBy()
{
return 'acme.validator.appointment.date';
}
}
Edit 2: Try with FormEvents... I also tried all the different events.
$form = $formFactory->createBuilder()
->add('title', 'text')
->add('date', 'date')
->addEventListener(FormEvents::WHICHONE?, function(FormEvent $event) {
$form = $event->getForm();
// WHAT TO DO HERE?
$form->getErrors(); // Is always empty as all events run before validation?
// I need something like
if (!$dateIsValid) {
$form->setValidationGroup('ignoreWarning');
}
});
Edit 3: Constraint are correctly declared. That's not the issue:
services:
validator.acme.date:
class: AcmeBundle\Validator\Constraints\DateValidator
arguments: ["#acme.other_service"]
tags:
- { name: validator.constraint_validator, alias: acme.validator.appointment.date }
Validation is done on the entity, all Forms does is execute the Object's validations.
You can choose groups based on submitted data
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function(FormInterface $form) {
$data = $form->getData();
if (Entity\Client::TYPE_PERSON == $data->getType()) {
return array('person');
} else {
return array('company');
}
},
));
}
I have had issues when using this approach on embedded forms && cascade-validation
Edit: using flash to determine if validation must take place.
// service definition
<service id="app.form.type.callendar" class="%app.form.type.callendar.class%">
<argument type="service" id="session" />
<tag name="form.type" alias="my_callendar" />
</service>
// some controller
public function somAvtion()
{
$form = $this->get('app.form.type.callendar');
...
}
// In the form
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function(FormInterface $form) {
$session = $form->getSession();
if ($session->getFlashBag()->get('callendar_warning', false)) {
return array(false);
} else {
return array('Validate_callendar');
}
},
));
}
How does your user interact with the application to tell it to ignore the warning? Is there some kind of additional button?
In that case you could simply check the button used for submitting the form or add some kind of hidden field (ignore_validation) etc.
Wherever you end up getting that user input from (flash and dependency injection, based on submitted data etc.), I would then use validation groups and a closure to determine what to validate (just like juanmf explained in his answer).
RE your second approach (Form Events), you can add a priority to event listeners: As you can see in Symfony's Form Validation Event Listener, they use FormEvents::POST_SUBMIT for starting the validation process. So if you just add an event listener, it gets called before the validation listener and so no validation has happened yet.
If you add a negative priority to your listener, you should be able to also access the form validation errors:
$builder->addEventListener(FormEvents::POST_SUBMIT, function(){...}, -900);
Old question but...
I would first add a field (acceptCollision) in the form as suggested by you and other answers above.
Then you validator can do something like:
public function validate($appointment, Constraint $constraint)
{
if (null === $date = $appointment->getDate()) {
return;
}
if ($appointment->getAcceptCollision()) {
$valid = true;
} elseif (
// Check Unicity of the date (no collision)
) {
$valid = true;
} else {
$valid = false;
}
if (!$valid) {
$this->context->addViolationAt('date', $constraint->message);
}
}
I think you run into a problem because you are using the wrong concept. The decision which validation should be running belongs to the controller, not the validator.
So I would simply check in the controller which submit button is pressed (or weither there is a checkbox checked) and switch validation groups. However the form should be visually different, so I would probably create 2 forms for both states (both extend a base one or one form type that use options).

Extbase update deleted=1 object in Controller fails (Object with identity x not found)

I want do give the function to 'restore' deleted Object in my FE-Ext. It seems, that it does not find any deleted records an so i cannot update them set deleted = 0.
What you be you sugestion to handle that from the controller?:
$query->getQuerySettings()->setIgnoreEnableFields(TRUE);
$query->getQuerySettings()->setIncludeDeleted(TRUE);
Thank you.
Im not quite sure what you mean by "from the controller". Normally you implement this in your repository and just call the method from the controller.
In your repo:
public function findRecordEvenIfItIsDeleted($uid) {
$query = $this->createQuery();
$settings = $query->getQuerySettings();
settings->setIgnoreEnableFields(TRUE);
settings->setIncludeDeleted(TRUE);
$query->matching($query->equals('uid', $uid));
return $query->execute();
}
In your controller:
$myObject = $this->myRepsository->findRecordEvenIfItIsDeleted($uid);
Done. (Of course your storage pid must be set (or disable respectStoragePage as well)
You're adding does not throw any error because you are setting the querySettings to include deleted records. But maybe, this setting has to be enabled even when you are updating, as the repository should find the object you are updating.
I haven't tested it but give this a try.
In your repository(just a pseudo code)
public function update($modifiedObject) {
settings->setIncludeDeleted(TRUE);
parent::update($modifiedObject);
}
I know this question was asked long time ago but today i had a similar problem with a hidden object. My solution was this one:
add this to your Model (in your case exchange "hidden" by "deleted"):
/**
* #var boolean
*/
protected $hidden;
/**
* #return boolean $hidden
*/
public function getHidden() {
return $this->hidden;
}
/**
* #return boolean $hidden
*/
public function isHidden() {
return $this->getHidden();
}
/**
* #param boolean $hidden
* #return void
*/
public function setHidden($hidden) {
$this->hidden = $hidden;
}
in your repository add this function to find the deleted/hidden object:
public function findHiddenByUid($uid) {
$query = $this->createQuery();
$query->getQuerySettings()->setIgnoreEnableFields(TRUE);
$query->getQuerySettings()->setEnableFieldsToBeIgnored(array('disabled','hidden'));
return $query
->matching($query->equals('uid', $uid))
->execute()
->getFirst();
}
now in your Controller you can read the object, set the "hidden" option and update it:
$yourobject = $this->yourobjectRepository->findHiddenByUid($uid);
$yourobject->setHidden(1);
$this->yourobjectRepository->update($yourobject);
Maybe not interesting for your task but for others:
In my case i additionally had the problem posting a hidden object in a form to an action. So note that if you want to post an object by a form, it is better (or probably necessary) to first set the objects deleted/hidden option to 0.

Akeneo 2.1.4 : How can I change a products' parent (used product model)

I have the requirement where upon importing I need to be able to change to products' product model. I tried to do this by changing the parent in the CSV file I'm importing, but this will show the following message:
WARNING
parent: Property "parent" cannot be modified, "new_parent_code" given.
What is the proper way to make this work? I tried 'hacking' the database by manually assigning a different parent to the product by editing the parent directly in the pim_catalog_product-table, and this seemed to work, but when editing the product unexpected results occur.
Could anyone point me in the right direction how I can change a product parent upon importing?
update:
I now came up with the following solution:
In my own bundle, I added Resources/config/updaters.yml (using DependencyInjecten Extension) with the following:
parameters:
# Rewrite parent field setter so we can allow the importer to update the parent:
pim_catalog.updater.setter.parent_field.class: Vendor\Bundle\InstallerBundle\Updater\Setter\ParentFieldSetter
And my custom ParentFieldSetter.php:
namespace Vendor\Bundle\InstallerBundle\Updater\Setter;
use Akeneo\Component\StorageUtils\Exception\ImmutablePropertyException;
use Akeneo\Component\StorageUtils\Repository\IdentifiableObjectRepositoryInterface;
/**
* Class ParentFieldSetter
*/
class ParentFieldSetter extends \Pim\Component\Catalog\Updater\Setter\ParentFieldSetter
{
/**
* #var IdentifiableObjectRepositoryInterface
*/
private $productModelRepository;
/**
* ParentFieldSetter constructor.
* #param IdentifiableObjectRepositoryInterface $productModelRepository
* #param array $supportedFields
*/
public function __construct(
IdentifiableObjectRepositoryInterface $productModelRepository,
array $supportedFields
) {
$this->productModelRepository = $productModelRepository;
parent::__construct($productModelRepository, $supportedFields);
}
/**
* #param \Pim\Component\Catalog\Model\ProductInterface|\Pim\Component\Catalog\Model\ProductModelInterface $product
* #param string $field
* #param mixed $data
* #param array $options
*/
public function setFieldData($product, $field, $data, array $options = []): void
{
try {
parent::setFieldData($product, $field, $data, $options);
} catch (ImmutablePropertyException $exception) {
if ($exception->getPropertyName() === 'parent') {
// Allow us to change the product parent:
if ($parent = $this->productModelRepository->findOneByIdentifier($data)) {
$familyVariant = $parent->getFamilyVariant();
$product->setParent($parent);
$product->setFamilyVariant($familyVariant);
if (null === $product->getFamily()) {
$product->setFamily($familyVariant->getFamily());
}
}
} else {
throw $exception;
}
}
}
}
This works. Now, upon importing the parent gets saved properly. I'm only wondering if:
a). This implementation is correct.
b). I'm not causing some other major issues by changing the parent.
I also noted the following TODO-statement in the original Akeneo-code above the code that throws the error when attempting to change the parent:
// TODO: This is to be removed in PIM-6350.
Anyone from Akeneo care to shed some light on this?