Same Doctrine ORM listener for multiple entitites - symfony

So I will be quick. Let's say you have a postPersist event. Can you make the same listener class respond to two different entities if they extend same interface?
<service id="app.entity.my_listener"
class="App\Entity\MyListener" lazy="true">
<tag name="doctrine.orm.entity_listener" entity="App\Entity\Entity1" event="postPersist"/>
<tag name="doctrine.orm.entity_listener" entity="App\Entity\Entity2" event="postPersist"/>
<tag name="kernel.event_subscriber" />
</service>
Keep in mind, both entities extend the same EntityInterface so my function signature takes:
public function postPersist(EntityInterface $entity, LifecycleEventArgs $event = null)
For "reasons" I can not validate this myself, but wondered if someone else is able to give an answer?

If you use doctrine lifecycle listener :
https://symfony.com/doc/current/doctrine/events.html#doctrine-lifecycle-listeners
You can make your case work. Since in this case they do not trigger on a specific entity. It is on your hand.
So if the listener should respond to only a specific interface.
As #Bossman said in his comment, you can do :
public function postPersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
// if this listener only applies to certain entity types,
// add some code to check the entity type as early as possible
if (!$entity instanceof EntityInterface) {
return;
}
$entityManager = $args->getObjectManager();
// ... do something with the Product entity
}

Related

Pull Certain Products to Front by Adding Custom SQL to Criteria

Our aim is to pull certain promoted products in the product listing to front.
Important: The promoted products differ by category / filter, so it would not work to just insert a custom field or use the "promoted products" flag which is already built in. We already have access to the the product IDs to pull to front, we just need to sort the list accordingly.
We subscribed to ProductListingCriteriaEvent::class and tried something - based on https://stackoverflow.com/a/6810827/288568 - like this:
$criteria = $event->getCriteria()
$sortings = $criteria->getSorting();
$criteria->resetSorting();
$criteria->addSorting(new FieldSorting('FIELD(id, 0x123456...)', FieldSorting::DESCENDING));
foreach($sortings as $sorting) {
$criteria->addSorting($sorting);
}
Where 0x123456... would be the UUID of the product to to pull to front.
This of course does not work, because Shopware expects a field.
Is it possible to create something like a "virtual" field for this reason or are there other ways to insert such a raw SQL part?
Adding a New Custom Sorting facility by decorating the QueryBuilder
We can implement a new Sorting class which takes specific ids to pull to front and then decorate the CriteriaQueryBuilder to add this new sorting type.
Implementation details (tested on Shopware 6.4.6.0)
First we define a class to hold the information for the new sorting method:
CustomSorting.php
<?php declare(strict_types=1);
namespace ExampleProductListing\Framework\DataAbstractionLayer\Search\Sorting;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
class CustomSorting extends FieldSorting
{
private array $ids = [];
public function addId(string $id)
{
$this->ids[] = $id;
}
public function getIds()
{
return $this->ids;
}
}
Next, we define a decorator for the CriteriaQueryBuilder:
services.xml
<service id="ExampleProductListing\Framework\DataAbstractionLayer\Dbal\CriteriaQueryBuilderDecorator"
decorates="Shopware\Core\Framework\DataAbstractionLayer\Dbal\CriteriaQueryBuilder">
<argument type="service" id="ExampleProductListing\Framework\DataAbstractionLayer\Dbal\CriteriaQueryBuilderDecorator.inner"/>
<argument type="service" id="Shopware\Core\Framework\DataAbstractionLayer\Search\Parser\SqlQueryParser"/>
<argument type="service" id="Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper"/>
<argument type="service" id="Shopware\Core\Framework\DataAbstractionLayer\Search\Term\SearchTermInterpreter"/>
<argument type="service" id="Shopware\Core\Framework\DataAbstractionLayer\Search\Term\EntityScoreQueryBuilder"/>
<argument type="service" id="Shopware\Core\Framework\DataAbstractionLayer\Dbal\JoinGroupBuilder"/>
<argument type="service"
id="Shopware\Core\Framework\DataAbstractionLayer\Dbal\FieldResolver\CriteriaPartResolver"/>
</service>
Next, we implement the decorator, which holds the new logic for generating the SQL with the FIELD() method.
CriteriaQueryBuilderDecorator.php
<?php declare(strict_types=1);
namespace ExampleProductListing\Framework\DataAbstractionLayer\Dbal;
use ExampleProductListing\Framework\DataAbstractionLayer\Search\Sorting\CustomSorting;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\CriteriaQueryBuilder;
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper;
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\FieldResolver\CriteriaPartResolver;
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\JoinGroupBuilder;
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\QueryBuilder;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\Filter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Parser\SqlQueryParser;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Term\EntityScoreQueryBuilder;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Term\SearchTermInterpreter;
class CriteriaQueryBuilderDecorator extends CriteriaQueryBuilder
{
private $decoratedService;
/***
* #var EntityDefinitionQueryHelper
*/
private $helper;
public function __construct(
CriteriaQueryBuilder $decoratedService,
SqlQueryParser $parser,
EntityDefinitionQueryHelper $helper,
SearchTermInterpreter $interpreter,
EntityScoreQueryBuilder $scoreBuilder,
JoinGroupBuilder $joinGrouper,
CriteriaPartResolver $criteriaPartResolver
)
{
$this->decoratedService = $decoratedService;
$this->helper = $helper;
parent::__construct($parser, $helper,$interpreter, $scoreBuilder, $joinGrouper, $criteriaPartResolver);
}
public function getDecorated(): CriteriaQueryBuilder
{
return $this->decoratedService;
}
public function addSortings(EntityDefinition $definition, Criteria $criteria, array $sortings, QueryBuilder $query, Context $context): void
{
foreach ($sortings as $sorting) {
if ($sorting instanceof CustomSorting) {
$accessor = $this->helper->getFieldAccessor($sorting->getField(), $definition, $definition->getEntityName(), $context);
$ids = implode(',', array_reverse($sorting->getIds()));
if (empty($ids)) {
continue;
}
$query->addOrderBy('FIELD(' . $accessor . ',' . $ids . ')', 'DESC');
} else {
$this->decoratedService->addSortings($definition, $criteria, [$sorting], $query, $context);
}
}
}
public function build(QueryBuilder $query, EntityDefinition $definition, Criteria $criteria, Context $context, array $paths = []): QueryBuilder
{
return parent::build($query, $definition, $criteria, $context, $paths);
}
public function addFilter(EntityDefinition $definition, ?Filter $filter, QueryBuilder $query, Context $context): void
{
parent::addFilter($definition, $filter, $query, $context);
}
}
How to use the new sorting method
Finally, when building the criteria (for example in ProductListingCriteriaEvent) we can pull specific products to front by specifying there IDs. (hard coded here, in real world they come from a different source, which depends on the chosen filters)
$customSorting = new CustomSorting('product.id');
$customSorting->addId('0x76f9a07e153645d7bd8ad62abd131234');
$customSorting->addId('0x76a890cb23ea433a97006e71cdb75678');
$event->getCriteria()
->addSorting($customSorting);
Compatibility
This works only for the SQL engine. If ElasticSearch should also be supported, this probably would work by decorating the ElasticSearch Query Buidler as well.
Raw SQL is by design not supported, as the query with that criteria can also be parsed and executed by ElasticSearch if you use the ElasticSearch integration.
Instead of handling it all within the read you could use a simple custom field that you sort for. You could add a myCustomSorting custom field to the products and set the value of that field to 1 for all products that you want to show up first. Then you extend the criteria to first sort by that field.

symfony2 get paramConverter value in custom annotation

I'm trying to create my own symfony2 annotations.
What I'm trying to achieve is get the paramConverter object in my annotation (in my controller), like
/**
* #ParamConverter("member", class="AppBundle:Member")
* #Route("my/route/{member}", name="my_route")
* #MyCustomAnnotation("member", some_other_stuff="...")
*/
public function myAction(Member $member) {...}
The purpose here is to get the "member" in my annotation, so I can work on it before it is passed to the controller action
Currently, my annotation "reader" is working as a service
MyCustomAnnotationDriver:
class: Vendor\Bundle\Driver\CustomAnnotationDriver
tags: [{name: kernel.event_listener, event: kernel.controller, method: onKernelController}]
arguments: [#annotation_reader]
How can I achieve this ?
I have done it few months ago but I choose much simpler approach. My use-case was to inject object based on currently logged user (either Profile or Teacher).
Check this GIST out:
GIST: https://gist.github.com/24d3b1778bc86429c7b3.git
PASTEBIN (gist currently doesn't work): http://pastebin.com/CBjrHvbM
Then, register the converter as:
<service id="my_param_converter" class="AcmeBundle\Services\RoleParamConverter">
<argument type="service" id="security.context"/>
<argument type="service" id="doctrine.orm.entity_manager"/>
<tag name="request.param_converter" converter="role_converter"/>
</service>
Finally, use it:
/**
* #Route("/news")
* #ParamConverter("profile", class="AcmeBundle:Profile", converter="role_converter")
*/
public function indexAction(Profile $profile){
// action's body
}
You can also, apply this custom ParamConverter to controller's class.
So I found a solution lurking in the doc. In fact, the event is working with the Request, so for every params in my route, I check the corresponding ParamConverter, and get the entity.
Here is what I get :
public function onKernelController(FilterControllerEvent $event)
{
if (!is_array($controller = $event->getController()))
return; //Annotation only available in controllers
$object = new \ReflectionObject($controller[0]);
$method = $object->getMethod($controller[1]);
$annotations = new ArrayCollection();
$params = new ArrayCollection();
foreach ($this->reader->getMethodAnnotations($method) as $configuration) {
if($configuration instanceof SecureResource) //SecureResource is my annotation
$annotations->add($configuration);
else if($configuration instanceof \Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter)
$params->add($configuration);
}
foreach($annotations as $ann) {
$name = $ann->resource; // member in my case
$param = $params->filter(
function($entry) use ($name){
if($entry->getName() == $name) return $entry;
} //Get the corresponding paramConverter to get the repo
)[0];
$entityId = $event->getRequest()->attributes->get('_route_params')[$name];
$entity = $this->em->getRepository($param->getClass())->find($entityId);
//.. do stuff with your entity
}
// ...
}

Adding new FOSUserBundle users to a default group on creation

I'm building my first serious Symfony2 project. I'm extending the FOSUserBundle for my user/group management, and I'd like new users to be automatically added to a default group.
I guess you just have to extend the User entity constructor like this :
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->groups = new \Doctrine\Common\Collections\ArrayCollection();
// Get $defaultGroup entity somehow ???
...
// Add that group entity to my new user :
$this->addGroup($defaultGroup);
}
But my question is how do I get my $defaultGroup entity in the first place?
I tried using the entity manager from within the entity, but then I realized it was stupid, and Symfony was throwing an error. I googled for this, but found no real solution except maybe setting up a service for that... although this seems quite unclear for me.
OK, I started working on implementing artworkad's idea.
First thing I did was updating FOSUserBundle to 2.0.*#dev in composer.json, because I was using v1.3.1, which doesn't implement the FOSUserEvents class. This is required to subscribe to my registration event.
// composer.json
"friendsofsymfony/user-bundle": "2.0.*#dev",
Then I added a new service :
<!-- Moskito/Bundle/UserBundle/Resources/config/services.xml -->
<service id="moskito_bundle_user.user_creation" class="Moskito\Bundle\UserBundle\EventListener\UserCreationListener">
<tag name="kernel.event_subscriber" alias="moskito_user_creation_listener" />
<argument type="service" id="doctrine.orm.entity_manager"/>
</service>
In the XML, I told the service I needed access to Doctrine through an argument doctrine.orm.entity_manager. Then, I created the Listener :
// Moskito/Bundle/UserBundle/EventListener/UserCreationListener.php
<?php
namespace Moskito\Bundle\UserBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityManager;
/**
* Listener responsible to change the redirection at the end of the password resetting
*/
class UserCreationListener implements EventSubscriberInterface
{
protected $em;
protected $user;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess',
);
}
public function onRegistrationSuccess(FormEvent $event)
{
$this->user = $event->getForm()->getData();
$group_name = 'my_default_group_name';
$entity = $this->em->getRepository('MoskitoUserBundle:Group')->findOneByName($group_name); // You could do that by Id, too
$this->user->addGroup($entity);
$this->em->flush();
}
}
And basically, that's it !
After each registration success, onRegistrationSuccess() is called, so I get the user through the FormEvent $event and add it to my default group, which I get through Doctrine.
You did not say how your users are created. When some admin creates the users or you have a custom registration action, you can set the group in the controller's action.
$user->addGroup($em->getRepository('...')->find($group_id));
However if you use fosuserbundles build in registration you have to hook into the controllers: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/controller_events.md and use a event listener.

Create entity on entity flush

How can I achieve this:
For example, I have an entity called Issue. I need to log changes of a field of this entity.
If a user changes the field "status" on the Issue entity I need to create a database record about it with the user, who changed the field, the previous status and the new status.
Using: Symfony2 + doctrine2.
You can use an event subscriber for that, and attach it to the ORM event listener (in symfony 2, there's docs about that):
namespace YourApp\Subscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use YourApp\Entity\Issue;
use YourApp\Entity\IssueLog;
class IssueUpdateSubscriber implements EventSubscriber
{
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $updated) {
if ($updated instanceof Issue) {
$em->persist(new IssueLog($updated));
}
}
$uow->computeChangeSets();
}
public function getSubscribedEvents()
{
return array(Events::onFlush);
}
}
You can eventually check the changeset as I've explained at Is there a built-in way to get all of the changed/updated fields in a Doctrine 2 entity.
I left the implementation of IssueLog out of the example, since that is up to your own requirements.

FOSUserBundle: Get EntityManager instance overriding Form Handler

I am starting with Symfony2 and I am trying to override FOS\UserBundle\Form\Handler\RegistrationFormHandler of FOSUserBundle.
My code is:
<?php
namespace Testing\CoreBundle\Form\Handler;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Form\Handler\RegistrationFormHandler as BaseHandler;
use Testing\CoreBundle\Entity\User as UserDetails;
class RegistrationFormHandler extends BaseHandler
{
protected function onSuccess(UserInterface $user, $confirmation)
{
// I need an instance of Entity Manager but I don't know where get it!
$em = $this->container->get('doctrine')->getEntityManager();
// or something like: $em = $this->getDoctrine()->getEntityManager
$userDetails = new UserDetails;
$em->persist($userDetails);
$user->setId($userDetails->getId());
parent::onSuccess($user, $confirmation);
}
}
So, the point is that I need an instance of Doctrine's Entity Manager but I don't know where/how get it in this case!
Any idea?
Thanks in advance!
You should not use EntityManager directly in most of the cases. Use a proper manager/provider service instead.
In case of FOSUserBundle service implementing UserManagerInterface is such a manager. It is accessible through fos_user.user_manager key in the service container (which is an allias to fos_user.user_manager.default). Of course registration form handler uses that service, it is accessible through userManager property.
You should not treat your domain-model (i.a. Doctrine's entities) as if it was exact representation of the database-model. This means, that you should assign objects to other objects (not their ids).
Doctrine is capable of handling nested objects within your entities (UserDetails and User objects have a direct relationship). Eventually you will have to configure cascade options for User entity.
Finally, UserDetails seems to be a mandatory dependency for each User. Therefore you should override UserManagerInterface::createUser() not the form handler - you are not dealing with user's details there anyway.
Create your own UserManagerInterface implementation:
class MyUserManager extends \FOS\UserBundle\Entity\UserManager {
/**
* {#inheritdoc}
*/
public function createUser() {
$user = parent::createUser();
$user->setUserDetails(new UserDetails());
// some optional code required for a proper
// initialization of User/UserDetails object
// that might require access to other objects
// not available inside the entity
return $user;
}
}
Register your own manager as a serive inside DIC:
<service id="my_project.user_manager" class="\MyProject\UserManager" parent="fos_user.user_manager.default" />
Configure FOSUserBundle to use your own implementation:
# /app/config/config.yml
fos_user:
...
service:
user_manager: my_project.user_manager

Resources