SF4 + doctrine event - php

I'm playing new Symfony 4. I really like when service use autowire and the component use annotations. I don't must nothing registration in services.yml. Ofcourse I can't solve everything in this way, but I try.
I create simple and test project. When I have Page. Page has "slug" field. This value is generated with field "title". I created special service (App\Service\Slug).
I would like to generate slug in event when page is create or update. To the event I should be to injected the slug service.
I would like to create one listner for only one entity - Page. I found: https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html In this solution the event is run for all entities and I must create configuration in services.yml.
App\EventListener\PageListener:
tags:
- { name: doctrine.event_listener, event: prePersist }
class PageListener
{
/**
* #var Slug
*/
protected $slug;
/**
* PageListener constructor.
*
* #param Slug $slug
*/
public function __construct(Slug $slug)
{
$this->slug = $slug;
}
public function prePersist(
LifecycleEventArgs $args
) {
$page = $args->getObject();
/** ... */
$slug = $this->slug->get($page->getTitle());
$page->setSlug($slug);
/** ... */
}
}
In this solution the event is run in all enitites and I must check the instance.
I remeber that In sf2.8 I could add adnotation to entity (EntityListeners) -> https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html In this version symfony I see this adnotation but it doesn't work. I found only old cookbook: https://symfony.com/doc/master/bundles/DoctrineBundle/entity-listeners.html
How should I do it correctly?
In the conclusion I want to create a simple doctrine event, which work for only one entity and its configuration is minimal.
edit:
When I tested, I found solution which "works":
Entity:
/**
* Class Page
*
* #package App\Entity
*
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
* #ORM\EntityListeners("App\EventListener\PageListener")
* #ORM\Table(name="pages")
*/
class Page
services.yaml:
App\EventListener\PageListener:
tags:
- { name: doctrine.orm.entity_listener }
PageListener:
public function __construct(Slug $slug)
{
$this->slug = $slug;
}
/**
* #ORM\PrePersist
*/
public function prePersistHandler(Page $user, LifecycleEventArgs $event)
I don't know how or I can't removed configuration in "service.yaml".

Related

Symfony delete ManyToMany related object when one part is empty?

I'm trying to delete unused tags. Although the relationship between Post and Tag has been deleted, the post-linked tag is not deleted.
"orphanRemoval" does not work because it has deleted all. cascade "remove" does not delete.
Post Entity:
class Post implements \JsonSerializable
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Cms\PostTag", inversedBy="posts", cascade={"persist", "remove"})
* #ORM\JoinTable(name="post_tag_taxonomy")
* #Assert\Count(max="5")
*/
private $tags;
}
Tag Entity:
class PostTag {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Cms\Post", mappedBy="tags")
*/
private $posts;
}
Here's a similar example, but for Java. How to delete an ManyToMany related object when one part is empty?
I suggest you use preUpdate event from Doctrine life cycle callbacks. On the event of update a Post, you tell doctrine to check if there's a Tag change (in this case it's to NULL), if yes then query the Tag check if any posts still use it.
In short, you need to :
Add #ORM\HasLifecycleCallbacks before your class to enable life cycles.
Add preUpdate function in Post class :
/**
* #ORM\PreUpdate
* #param PreUpdateEventArgs $event
*/
public function clearChangeSet(PreUpdateEventArgs $event)
{
if ($event->hasChangedField('field_you_want_to_check')
) {
$em = $event->getEntityManager();
// Now use the entityManager to query the tag and check.
}
}
By doing this doctrine will do the check for you, in the logic code you just need to perform the unlinking, no need to care about delete tags there.
Update : as pointed out, for associations in entity you cannot get the changes by $event->hasChangedField, use the method in Symfony 3 / Doctrine - Get changes to associations in entity change set
Solution:
/**
* #ORM\PostUpdate()
*/
public function postUpdate(LifecycleEventArgs $args)
{
/** #var PersistentCollection $tags */
$tags = $args->getEntity()->getTags();
if ($tags->isDirty() && ($deleted = $tags->getDeleteDiff())) {
$em = $args->getEntityManager();
foreach ($deleted as $tag) {
if ($tag->getPosts()->count() === 1) {
$em->remove($tag);
$em->flush($tag);
}
}
}
}

Doctrine/Symfony: How to include EntityManager in EntityListener

I have an entity BaseInformation
/**
* #ORM\Entity
* #ORM\EntityListeners({"AppBundle\EntityListener\BaseInformationListener"})
* #ORM\Table(name="BaseInformation")
*/
class BaseInformation
{ ...
Therefore I have an EntityListener
/**
* Class BaseInformationListener
* #package AppBundle\EventListener
*/
class BaseInformationListener
{
/**
* #ORM\PreUpdate
*
* #param BaseInformation $baseInformation
* #param PreUpdateEventArgs $event
*/
public function preUpdateHandler(BaseInformation $baseInformation, PreUpdateEventArgs $event)
{
dump($baseInformation);
dump($event->getEntityChangeSet());
}
}
I need to save the ChangeSet into the database. But I have no access to an EntityManager. I can make a service out of it, but the listener is automatically called over the annotation in the entity. So how do I have access to the EntityManager to save my ChangeSet for example?
You can define the listener a service and tag it as an EntityListener so you can use the dependency you need as usually:
services:
base_information_listener:
class: AppBundle\EntityListener\BaseInformationListener
arguments: ['#doctrine.orm.entity_manager']
tags:
- { name: doctrine.orm.entity_listener }
Prior to doctrine 2.5 you need to use the annotation in the related entity also (as described in the doc).
Hope this help

Sonata Admin - Unable to get sonata_type_collection working with modal

In my admin I have a OneToMany defined as it:
/**
* #ORM\OneToMany(targetEntity="Module", mappedBy="sequence", cascade={"persist", "remove"})
*/
private $modules;
And the inversed side:
/**
* #ORM\ManyToOne(targetEntity="ModuleSequence", inversedBy="modules", cascade={"persist"}, fetch="LAZY")
* #ORM\JoinColumn(name="sequence_id", referencedColumnName="id")
*/
protected $sequence;
In my admin class I defined the 'modules' field as it:
->add('modules', 'sonata_type_collection',array(
'by_reference' => false
))
Finally in the ModuleSequence Entity here's the addModule method:
/**
* Add modules
*
* #param \AppBundle\Entity\Module $module
* #return ModuleSequence
*/
public function addModule(\AppBundle\Entity\Module $module)
{
$module->setSequence($this);
$this->modules[] = $module;
return $this;
}
I have the "add" button, I get the modal, I fill it and validate. The Ajax request is sent into the profiler but no new row appear.
The 'sequence_id' is not set in the database and I don't know why... Any idea please?
When I use the 'inline' & 'table' options, the id is well set.
Had the same issue and solved it with overriding the prePersist and preUpdate methods and then persist the associations.
public function prePersist($object)
{
$this->persistBlocks($object);
$content->mergeNewTranslations();
}
public function preUpdate($object)
{
$this->prePersist($object);
}
private function persistModules($object)
{
$em = $this->getConfigurationPool()->getContainer()->get('doctrine.orm.entity_manager');
foreach($object->getModules() as $module)
{
$module->setObject($object); // change Object to the name of your Entity!!
$em->persist($module);
}
}
After a long discussion on Sonata Admin GitHub here:
https://github.com/sonata-project/SonataAdminBundle/issues/4236
Problem partially solved...

Attach entity history/changes to the entity as OneToMany association

I've an entity Order, with a property events which should contain a list of all changes made to this entity.
The Order class:
<?php
/**
* #ORM\Entity
*/
class Order
{
// more stuff...
/**
* #ORM\OneToMany(
* targetEntity="OrderEvent",
* mappedBy="order",
* cascade={"persist", "merge"}
* )
*/
protected $events;
// more stuff...
}
The OrderEvent class:
/**
* #ORM\Entity
*/
class OrderEvent
{
// more stuff...
/**
* #ORM\ManyToOne(targetEntity="Order", inversedBy="events")
* #ORM\JoinColumn(nullable=false)
*/
protected $order;
// more stuff...
}
class OrderLifecycle
{
public function preUpdate(Order $order, PreUpdateEventArgs $args)
{
$changes = $args->getEntityChangeSet();
if (!empty($changes)) {
$event = new OrderEvent();
$event->setOrder($order)
->setChanges($changes);
$order->addEvent($event);
return $event;
}
}
}
But according to the doctrine documentation, the preUpdate method should not be used to change associations.
What is the recommended way to do things like this one?
I am using Zend Framework 2, but I think that's not relevant.
I think in this case you could use PostUpdate event. In that case you are sure that the update action was successful and you can do what you want; add the new OrderEvent instance to your $events collection.
EDIT
You are not the first one implementing such thing. Maybe you should check existing examples and see how they deal with this (or even consider using it). For example the Gedmo Loggable solution.
With this extension you can mark entities as loggable with a simple #annotiation:
/**
* #Entity
* #Gedmo\Loggable
*/
class Order
{
// Your class definition
}

Doctrine 2 association overwrite

I have a question about Doctrine 2 and the ability (or not?) to extend an association between to classes.
Best explained with an example :)
Let's say I have this model (code is not complete):
/**
* #Entity
*/
class User {
/**
* #ManyToMany(targetEntity="Group")
* #var Group[]
*/
protected $groups;
}
/**
* #Entity
*/
class Group {
/**
* #ManyToMany(targetEntity="Role")
* #var Role[]
*/
protected $roles;
}
/**
* #Entity
*/
class Role {
/**
* #ManyToOne(targetEntity="RoleType")
* #var RoleType
*/
protected $type;
}
/**
* #Entity
*/
class RoleType {
public function setCustomDatas(array $params) {
// do some stuff. Default to nothing
}
}
Now I use this model in some projects. Suddenly, in a new project, I need to have a RoleType slightly different, with some other fields in DB and other methods. Of course, it was totally unexpected.
What I do in the "view-controller-but-not-model" code is using services:
// basic configuration
$services['RoleType'] = function() {
return new RoleType();
};
// and then in the script that create a role
$role_type = $services['RoleType'];
$role_type->setCustomDatas($some_params);
During application initialization, I simply add this line to overwrite the default RoleType
$services['RoleType'] = function() {
return new GreatRoleType();
};
Ok, great! I can customize the RoleType call and then load some custom classes that do custom things.
But... now I have my model. The model says that a Role targets a RoleType. And this is hard-written. Right now, to have my custom changes working, I need to extend the Role class this way:
/**
* #Entity
*/
class GreatRole extends Role {
/**
* Changing the targetEntity to load my custom type for the role
* #ManyToOne(targetEntity="GreatRoleType")
* #var RoleType
*/
protected $type;
}
But then, I need to extend the Group class to target GreatRole instead of Role.
And in the end, I need to extend User to target GreatGroup (which targets GreatRole, which targets GreatRoleType).
Is there a way to avoid this cascade of extends? Or is there a best practice out there that is totally different from what I did?
Do I need to use MappedSuperClasses? The doc isn't very explicit...
Thanks for your help!
--------------------------- EDIT ---------------------------
If I try to fetch all the hierarchy from User, that's when I encounter problems:
$query
->from('User', 'u')
->leftJoin('u.groups', 'g')
->leftJoin('g.roles', 'r')
->leftJoin('r.type', 't');
If I want to have a "r.type" = GreatRoleType, I need to redefine each classes.

Categories