I'm trying to get Entity Listeners work with ODM in Symfony 2.7 but to no avail.
a51.document.listener.store:
class: A51\FilesystemBundle\EventListener\StoreEntityListener
tags:
- { name: doctrine.odm.mongodb.document_manager, event: postLoad, method: onPostLoad }
arguments: [#a51.repo.file]
and:
<?php
namespace A51\FilesystemBundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use A51\FilesystemBundle\Document\Store;
use A51\FilesystemBundle\Repository\FileRepositoryInterface;
class StoreEntityListener
{
/**
* #var FileRepositoryInterface
*/
private $fileRepository;
public function __construct(FileRepositoryInterface $fileRepository)
{
$this->fileRepository = $fileRepository;
}
public function onPostLoad(LifecycleEventArgs $args)
{
$this->index($args);
}
public function index(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof Store)
{
$entity->setTotalByteSize($this->fileRepository->findSumFilesSizeByStore($entity));
}
}
}
I've tried pretty much everything I could find in docs but for some reason onPostLoad method does not get called.
Store document gets loaded with ParamConverter:
* #ParamConverter("store", class="A51FilesystemBundle:Store")
Any help would be welcome.
I have a MongoDB listener in my project, but my code it's so different of yours. There's a simpler way, all you must to do it's import DocumentManager and then you can call it from construct to use it on all you listener. I'm gonna show you my code and tell me if this help you ;)
namespace AppBundle\OdmListener;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ODM\MongoDB\DocumentManager;
class RedundancyListener
{
/**
* #var DocumentManager
*/
private $dm;
/**
* #param DocumentManager $odm
*/
function __construct(DocumentManager $dm)
{
$this->dm = $dm;
}
Then inside you can do any queries or updates as you do it in you controller. Also you can use ORM or ODM CycleEvents if you import them, like I do in the example.
/**
* #param LifecycleEventArgs $eventArgs
*/
public function preUpdate(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof \AppBundle\Entity\Example) {
$subscriptionHash = $this->getSubscription($entity);
$this->dm->createQueryBuilder('AppBundle\Document\Example')
->update()
//->multiple(true)
->field('name')->set($entity->getName())
->field('last_name')->set($entity->getLastName())
->field('mail')->set($entity->getMail())
->getQuery()
->execute();
}
}
}
Related
I will start saying I am using Symfony 4.3.4 and Api Platform (called AP from now on). Having said that this how my custom controller (used for AP) looks like:
declare(strict_types=1);
namespace App\Controller\CaseWork\Pend;
use App\Request\PendCaseRequest;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Routing\Annotation\Route;
class PendCaseController
{
/**
* #Route("/myroute/{id}/pend", name="routeName")
* #ParamConverter("case", class="App\Entity\Cases")
*/
public function __invoke(PendCaseRequest $request, int $id)
{
// do something with the $request
}
}
As you may notice I also have a Request Data Transformer Object and here is a code snippet for it:
declare(strict_types=1);
namespace App\Request;
use App\Interfaces\RequestDTOInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Constraints as Assert;
class PendCaseRequest implements RequestDTOInterface
{
/**
* #var int
*
* #Assert\NotBlank()
* #Assert\NotNull()
* #Assert\Type("integer")
*/
private $param;
public function __construct(Request $request)
{
$data = json_decode($request->getContent(), true);
$this->param = (int) $data['param'];
// ...
}
}
It's suppose (as per docs here) that when the request comes in and an id matching a App\Entity\Cases is found a new attribute named case should be append to my $request object but in my scenario is not happening and I am not sure why or what I am missing.
While debugging and setting a break point at this line $this->param = (int) $data['param']; in my DTO, if I print out $this->attributes I got the following output:
‌Symfony\Component\HttpFoundation\ParameterBag::__set_state(array(
'parameters' =>
array (
),
))
What I am missing here? What is wrong with my approach?
I have found a "solution" here. I end up using a Decorator as suggested by the answer on that post.
My main controller changed into this:
declare(strict_types=1);
namespace App\Controller\CaseWork\Pend;
use App\Request\PendCaseRequest;
use App\Entity\Cases;
class PendCaseController
{
public function __invoke(PendCaseRequest $request, Cases $case)
{
// do something with the $request
}
}
A decorator was created:
declare(strict_types=1);
namespace App\Decorator;
use App\Controller\CaseWork\Pend\PendCaseController;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Cases;
use App\Request\PendCaseRequest;
class PendCaseDecorator
{
/** #var PendCaseController */
protected $decoratedController;
/** #var EntityManagerInterface */
protected $entityManager;
public function __construct(PendCaseController $controller, EntityManagerInterface $entityManager)
{
$this->decoratedController = $controller;
$this->entityManager = $entityManager;
}
public function __invoke(PendCaseRequest $request, int $id)
{
$object = $this->entityManager->getRepository(Cases::class)->find($id);
if (!$object instanceof Cases) {
throw new NotFoundHttpException('Entity with '.$id.' not found');
}
return $this->decoratedController($request, $object);
}
}
And I had registered it at services.yml:
services:
App\Controller\CaseWork\Pend\PendCaseController: ~
App\Decorator\PendCaseDecorator:
decorates: App\Controller\CaseWork\Pend\PendCaseController
That way I keep using my DTO and pass back a Cases entity object.
I'm learning about Entities and Repositories in Symfony and I want to know if is possible access entity object in its repository.
I have the following code in Controller
$account = new Account($username, $password, $email);
$em = $this->getDoctrine()->getManager();
$result = $em->getRepository('AppBundle:Account')->registerAccount();
and then in Repository
public function registerAccount() {
// How access to $account here?
}
Should I just pass $account to repository function or is there another way?
Yes, you need to pass that class instance as an argument.
public function register(Account $account)
{
//$account is accessible here...
}
P.S. I believe you want a method to persist your Account entities right? Its fine to have a method for that in your repositories.
I would use something like
public function save(Account $account)
{
$em = $this->getEntityManager();
$em->persist($account);
$em->flush();
return $account;
}
Repository is not a place where you should have an access into your entity instance
.
Following the documentation http://symfony.com/doc/current/doctrine/repository.html
In repository you should create a custom function which return a result you need (based on DQL you create), and then call your function on repository in controller.
First you have to tell your entity you create repository
// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
*/
class Product
{
//...
}
This information is in annotation which describe your entity.
Then you create your custom function
// src/AppBundle/Repository/ProductRepository.php
namespace AppBundle\Repository;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
public function findAllOrderedByName()
{
return $this->getEntityManager()
->createQuery(
'SELECT p FROM AppBundle:Product p ORDER BY p.name ASC'
)
->getResult();
}
}
As you can see the function findAllOrderedByName() is finding all item in table with product and order asc result.
And finally in a place when you need to have your result (eg. in a Controller) you need to call your repository using created function:
$em = $this->getDoctrine()->getManager();
$products = $em->getRepository('AppBundle:Product')
->findAllOrderedByName();
I would like to suggest create object manager service like AccountManager which dependence EntityRepository and Entity Class name. This service can include create, update etc functions. After creating this service don't forgot to register it in services.
E.g.
in controller you can create object Account entity
$account = new Account();
$account->setUsername($username);
$account->setPassword($password);
$account->setEmail($email);
$accountManager = $this->get('account.manager');
$accountManager->registerAccount($account);
Object Manager service
<?php
//...
class AccountManager
{
/**
* #var EntityManager
*/
private $entityManager;
/*
* #var string
*/
private $entityClassName;
/**
* #param EntityManager $entityManager
* #param string $entityClassName
*/
public function __construct(EntityManager $entityManager, $entityClassName)
{
$this->entityManager = $entityManager;
$this->entityClassName = $entityClassName;
}
/**
* #param EntityManager $entityManager
* #return bool
*/
public function registerAccount(Account $account)
{
try {
$this->entityManager->persist($account);
$this->entityManager->flush();
return true;
} catch (\Exception $exception) {
// logging
}
return false;
}
}
`
I have a lifecycle event. As soon as an order is created the prePersist lifecycle event add a few more details to the order before it is persisted to the database.
This is my prePersist event class;
<?php
namespace Qi\Bss\BaseBundle\Lib\PurchaseModule;
use Qi\Bss\BaseBundle\Entity\Business\PmodOrder;
use Doctrine\ORM\Event\LifecycleEventArgs;
/**
* Listener class
* Handles events related to list prices
*/
class OrderUserListener
{
/**
* Service container
* #var type
*/
private $serviceContainer;
/**
* Performs tasks before destruction
* #ORM\PrePersist
*/
public function prePersist(LifecycleEventArgs $args)
{
$order = $args->getEntity();
if ($order instanceof PmodOrder) {
$user = $this->serviceContainer->get('security.token_storage')->getToken()->getUser();
if ($user) {
$order->setCreatedBy($user);
$order->setCreatedAt(new \DateTime(date('Y-m-d H:i:s')));
$order->setDepartment($user->getDepartment());
$order->setStatus(PmodOrder::STATUS_AWAITING_APPROVAL);
$this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created');
}
}
}
/**
* Sets the sales order exporter object
* #param type $serviceContainer
*/
public function setServiceContainer($serviceContainer)
{
$this->serviceContainer = $serviceContainer;
}
}
It works perfectly but this part $this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created'); doesn't want to work. I try to call a service inside it. I know the service works perfectly inside my controllers, but here I get an error;
A new entity was found through the relationship
'Qi\Bss\BaseBundle\Entity\Business\PmodLog#order' that was not
configured to cascade persist operations for entity: Nuwe Test vir
logger. To solve this issue: Either explicitly call
EntityManager#persist() on this unknown entity or configure cascade
persist this association in the mapping for example
#ManyToOne(..,cascade={"persist"}).
This is how my OrderLogger service class looks like;
<?php
namespace Qi\Bss\BaseBundle\Lib\PurchaseModule;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Doctrine\ORM\EntityManager;
use Qi\Bss\BaseBundle\Entity\Business\PmodLog;
/**
* Class AppLogger. Purchase Module logger.
* #package FcConnectBundle\Lib
*/
class OrderLogger {
private $em;
private $tokenStorage;
/**
* Constructor.
*
* #param EntityManager $em
* #param TokenStorage $securityTokenStorage
*/
public function __construct(EntityManager $em, TokenStorage $securityTokenStorage)
{
$this->em = $em;
$this->tokenStorage = $securityTokenStorage;
}
/**
* Log an order action.
*
* #param string $text
*/
public function log($order, $action)
{
$logRecord = new PmodLog();
if (is_object($this->tokenStorage->getToken())) {
$user = $this->tokenStorage->getToken()->getUser();
if (is_object($user)) {
$logRecord->setUser($user);
}
}
$logRecord->setOrder($order);
$logRecord->setAction($action);
$logRecord->setTime(new \DateTime());
$this->em->persist($logRecord);
$this->em->flush();
}
}
I have already tried changing the persist in my log to merge, but that also doesn't work. Can somebody please help and explain what I do wrong?
This is not the best architecture, but it will work:
On prePersist add all messages to some kind of private variable (like $logMessages), and add another event
/**
* #param PostFlushEventArgs $args
*/
public function postFlush(PostFlushEventArgs $args)
{
$logMessages = $this->logMessages;
$this->logMessages = array(); //clean to avoid double logging
if (!empty($logMessages)) {
foreach ($logMessages as $message) {
$this->serviceContainer->get('bss.pmod.order_logger')->log($message);
}
}
}
I fixed the problem by adding a postPersist and call the logger in there instead of inside my prePersist;
/**
* Performs tasks before destruction
* #ORM\PostPersist
*/
public function postPersist(LifecycleEventArgs $args)
{
$order = $args->getEntity();
if ($order instanceof PmodOrder) {
$this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created');
}
}
Because what I think is happening is that the logger tries to be executed but the order in the logger doesn't yet exists as it is not yet persisted. This way makes more sense to me, and I think this is the easiest fix. I could be wrong though, any comments and other opinions on my answer are welcome.
Hi I'm tring to do a Symfony event listener following this documentation:
http://symfony.com/doc/2.8/cookbook/doctrine/event_listeners_subscribers.html
<?php
namespace FM\AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use FM\AdminBundle\Entity\Address\BillingAddress;
class BillingAdressListener
{
/**
* #param LifecycleEventArgs $args
*/
public function listenBillingAdress(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if(!$entity instanceof BillingAddress){
return;
}
$this->postPersist($args);
}
/**
* #param LifecycleEventArgs $args
*/
public function postPersist(LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$billingAdress = $args->getEntity();
dump($billingAdress); die();
}
}
service.yml
billing_adress.listener:
class: FM\AppBundle\EventListener\BillingAdressListener
tags:
- { name: doctrine.event_listener, event: listenBillingAdress }
But nothing is happening when I'm submitting a form with the BillingAddress object.
Did I do something wrong?
It's called Entity Listeners in Doctrine. Here is documentation.
Don't forget to add your entity listener in mapping. For example xml mapping:
<entity name="AppBundle\Entity\AssignmentAttempt" table="app_assignment_attempt" >
<entity-listeners>
<entity-listener class="AppBundle\EntityListener\AssignmentAttemptListener">
<lifecycle-callback type="prePersist" method="prePersist"/>
<lifecycle-callback type="preUpdate" method="preUpdate"/>
</entity-listener>
</entity-listeners>
<!-- Rest mapping -->
</entity>
In config you can simply add
app.entity_listener.assignment_attempt:
class: AppBundle\EntityListener\AssignmentAttemptListener
tags:
- { name: doctrine.orm.entity_listener }
My bad, I was not doing a Persist but an Update.
It works with an Persist.
Here is my code now:
<?php
namespace FM\AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use FM\AdminBundle\Entity\Address\BillingAddress;
class BillingAddressListener
{
/**
* #param LifecycleEventArgs $args
*/
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if(!$entity instanceof BillingAddress){
return;
}
$this->addNameToUser($args);
}
/**
* #param LifecycleEventArgs $args
*/
public function addNameToUser(LifecycleEventArgs $args)
{
/** #var BillingAddress $billingAdress */
$billingAdress = $args->getEntity();
$user = $billingAdress->getOwner();
if(empty($user->getFirstName())) $user->setFirstName($billingAdress->getFirstName());
if(empty($user->getLastName())) $user->setLastName($billingAdress->getLastName());
}
}
And I have to call the prePersist method.
I was doing the contrary.
billing_address.listener:
class: FM\AppBundle\EventListener\BillingAddressListener
tags:
- { name: doctrine.event_listener, event: prePersist }
I have the entity (such as below). I want to set some default values while creating.
As you can see in __construct, it is easy to set the $name (string), but how can I set the $group? (for example I know that there is a group in database with id=122)
/**
* #ORM\Entity
*/
class Person {
private $id;
/** #ORM\Column(type="string") */
private $name;
/**
* #ORM\ManyToOne(targetEntity="Group", inversedBy="persons")
* #ORM\JoinColumn(referencedColumnName="id")
*/
private $group;
public function setGroup(Group $group)
{
$this->group = $group;
$group->addPerson($this);
}
// ... setters/getters
//construct default Person
public function __construct()
{
$this->setName("Mike");
$this->setGroup($EXISTING_GROUP_FROM_MY_DB); // <<--------------
}
}
I agree with moonwave99 that this is poor design. Here you are trying to access the database (through the Doctrine service) from a place that is not container-aware (i.e. it does not, and should not, know about Doctrine).
I had a similar issue recently... pretty much the same exact issue, actually. But I didn't want this logic to be inside the controller. So I wrote a service to take care of the User creation. And I gave that service access to the only other service it needed: Doctrine.
Here's an example, where a User is created with all available Roles:
namespace MyBundle\Entity;
class UserFactory
{
private $doctrine;
public function __construct($doctrine)
{
$this->doctrine = $doctrine;
}
public function generateNewUser($email, $password)
{
$user = new User();
// Since you have access to the Doctrine service, you can use $this->doctrine
// to do anything you would normally do in your controller with $this->getDoctrine()
$roles = $this->doctrine->getEntityManager()->getRepository("MyBundle:Role")->findAll();
foreach ($roles as $role)
{
$user->addRole($role);
}
return $user;
}
}
Now register that service in config.yml or services.yml, remembering to pass the Doctrine service to it:
services:
mybundle.factory.user:
class: MyBundle\Entity\UserFactory
arguments: ['#doctrine']
And that's it... Now, in your controller, you can create a new User by doing:
public function MyController()
{
$user = $this->get("mybundle.factory.user")->generateNewUser("someone#email.com", "password123");
}
The recommended method is to require the associated Entity object within the constructor arguments, optionally in combination with a Factory such as the Entity Repository, to supply the Group Entity during instantiation. This ensures the entity is always in a valid state.
src/Entity/Person.php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\PersonRepository")
*/
class Person
{
//...
public function __construct($name, Group $group)
{
$this->setName($name);
$this->setGroup($group);
}
//...
}
src/Repsotory/PersonRepository.php
namespace App\Repsotory;
use App\Entity\Group;
use App\Entity\Person;
class PersonRepository
{
const DEFAULT_GROUP = 122;
public function create($name, Group $group = null)
{
if (null === $group) {
$group = $this->_em->getReference(Group::class, self::DEFAULT_GROUP);
}
$person = new Person($name, $group);
$this->_em->persist($person);
return $person;
}
}
This allows you to rely solely on the Doctrine ORM Entity Manager to maintain the default Group association.
$person = $em->getRepository(Person::class)->create('Mike');
$group = $person->getGroup();
echo $group->getId(); //outputs: 122
$em->flush();
This approach can be extended upon in Symfony to use Query services instead of the doctrine entity repository, to provide a central location that handles the instantiation of the entities.
In Symfony 3.4+ you can use Repository
services
to provide dependency injection for the repository, instead of using
the EntityManagerInterface.
src/Service/PersonCreateQuery.php
namespace App\Service;
use App\Entity\Group;
use App\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
class PersonCreateQuery
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function __invoke($name)
{
$group = $this->em->getReference(Group::class, 122);
$person = new Person($name, $group);
$this->em->persist($person);
return $person;
}
}
Now you can use dependency injection to retrieve the Query service and use it as desired, such as with a Symfony Form or Controller.
namespace App\Controller;
use App\Service\PersonCreateQuery;
class PersonController
{
public function createAction(PersonCreateQuery $query)
{
$person = $query('Mike');
$this->getDoctrine()->getManager()->flush();
//...
}
}
Note: Usages of $em->getReference() can be replaced with $em->find(). Using $em->getReference() will prevent a query to the database but will throw an exception if the reference is invalid, while using $em->find() will return null instead.
Another approach is to use either Lifecycle Callbacks in the entity or an Event Listener to do more complex functionality. However, this will cause your entity to be instantiated in an invalid state until it is persisted.
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Person
{
const DEFAULT_GROUP = 122;
/** #ORM\Column(type="string") */
private $name = 'Mike';
/**
* #ORM\ManyToOne(targetEntity="Group", inversedBy="persons")
* #ORM\JoinColumn(referencedColumnName="id")
*/
private $group;
//....
public function setGroup(Group $group)
{
$this->group = $group;
$group->addPerson($this);
}
/**
* #param LifecycleEventArgs $event
* #ORM\PrePersist
*/
public function onPrePersist(LifecycleEventArgs $event)
{
if (!$this->group instanceof Group) {
/** set default group if not specified */
$group = $event->getEntityManager()->getReference(Group::class, self::DEFAULT_GROUP);
$this->setGroup($group);
}
}
}
Now when you persist a Person entity it will add the group if it was not explicitly set elsewhere.
$person = new Person();
$person->setName('Foo Bar');
$em->persist($person); //persist or do nothing if already persisted
$group = $person->getGroup();
echo $group->getId(); //outputs: 122
$groupPerson = $group->getPerson();
echo $groupPerson->getName(); //outputs: Foo Bar
$em->flush(); //save to database
For sanity here are the links to the docs for the doctrine events:
Doctrine 2 - Events
Doctrine 2 - Lifecycle Callbacks
Symfony - Doctrine Lifecycle Callbacks