I'm using Symfony for make a web site and I installed sonata admin Bundle.
I have an entity with a boolean variable (enable).
I would like when this variable change state to True the other one for the same table go to False. In fact I would like only one variable (enable) for the same table is at True.
So I thought to change setEnable directly in my Entity but I can't get the repository from my Entity class.
How can I get my repository from my Entity Class ?
You should use lifecycleCallbacks to do this (with preUpdate), check doc for a good configuration.
And do something like this :
public function preUpload()
{
if ($this->getVariable1())
$this->setVariable2(false);
}
I think you might change prospective.
If this behavior is a logic of your application it's better to do a doctrine subscriber. So when you persist or update your entity it's will be checked.
Take a look at http://symfony.com/doc/current/cookbook/doctrine/event_listeners_subscribers.html
In your specific case this will be done with this simple code:
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
class ObjectSubscriber implements EventSubscriber
{
public function __construct($objectManager)
{
}
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
);
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->manage($args);
}
public function postPersist(LifecycleEventArgs $args)
{
$this->manage($args);
}
public function manage(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof YourEntity) {
if ($entity->getEnable()) {
$em = $args->getEntityManager();
$entities = $em->getRepository('YourEntityRepository')->findByEnable(true);
foreach ($entities as $e) {
if ($e->getId() == $entity->getId()) {
continue;
}
$e->setEnable(false);
}
$em->flush();
}
}
}
}
Related
I override (custom operation and service) the DELETE operation of my app to avoid deleting data from DB. What I do is I update a field value: isDeleted === true.
Here is my controller :
class ConferenceDeleteAction extends BaseAction
{
public function __invoke(EntityService $entityService, Conference $data)
{
$entityService->markAsDeleted($data, Conference::class);
}
...
My service :
class EntityService extends BaseService
{
public function markAsDeleted(ApiBaseEntity $data, string $className)
{
/**
* #var ApiBaseEntity $entity
*/
$entity = $this->em->getRepository($className)
->findOneBy(["id" => $data->getId()]);
if ($entity === null || $entity->getDeleted()) {
throw new NotFoundHttpException('Unable to find this resource.');
}
$entity->setDeleted(true);
if ($this->dataPersister->supports($entity)) {
$this->dataPersister->persist($entity);
} else {
throw new BadRequestHttpException('An error occurs. Please do try later.');
}
}
}
How can I hide the "deleted" items from collection on GET verb (filter them from the result so that they aren't visible) ?
Here is my operation for GET verb, I don't know how to handle this :
class ConferenceListAction extends BaseAction
{
public function __invoke(Request $request, $data)
{
return $data;
}
}
I did something; I'm not sure it's a best pratice.
Since when we do :
return $data;
in our controller, API Platform has already fetch data and fill $data with.
So I decided to add my logic before the return; like :
public function __invoke(Request $request, $data)
{
$cleanDatas = [];
/**
* #var Conference $conf
*/
foreach ($data as $conf) {
if (!$conf->getDeleted()) {
$cleanDatas[] = $conf;
}
}
return $cleanDatas;
}
So now I only have undeleted items. Feel free to let me know if there is something better.
Thanks.
Custom controllers are discouraged in the docs. You are using Doctrine ORM so you can use a Custom Doctrine ORM Extension:
// api/src/Doctrine/ConferenceCollectionExtension.php
namespace App\Doctrine;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Conference;
use Doctrine\ORM\QueryBuilder;
final class CarCollectionExtension implements QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
{
if ($resourceClass != Conference::class) return;
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("$rootAlias.isDeleted = false OR $rootAlias.isDeleted IS NULL);
}
}
This will automatically be combined with any filters, sorting and pagination of collection operations with method GET.
You can make this Extension specific to an operation by adding to the if statement something like:
|| $operationName == 'conference_list'
If you're not using the autoconfiguration, you have to register the custom extension:
# api/config/services.yaml
services:
# ...
'App\Doctrine\ConferenceCollectionExtension':
tags:
- { name: api_platform.doctrine.orm.query_extension.collection }
If you also want to add a criterium for item operations, see the docs on Extensions
As I said in the title, I want to convert an object to an ID (int/string) and backwards from ID to an object. Usually I would work with entity relations, but in this case I do not know the other entity/bundle and it should work independant.
I guess, I could use doctrine mapping types for that, but how can I inject my custom entity loader? So maybe I can use a callback for fetching the data.
Thats my idea (pseudocode):
class User {
public function getId() { return 'IAmUserBobAndThisIdMyId'; }
}
class Meta {
private $user; // <== HERE I NEED THE MAGIC
public function setUser($user) { $this->user = user; }
}
$user = new User();
$meta = new Meta();
$meta->setUser($user);
$em->persist($meta); // <== HERE THE MAPPING TYPE SHOULD CONVERT THE ENTITY
Know I want the entity in my database like that: user:IAmUserBobAndThisIdMyId
And backwards:
$meta = $repository->findOneById(1); // HERE I NEED THE MAGIC AGAIN
$user = $meta->getUser();
echo $user->getId();
// output: IAmUserBobAndThisIdMyId
So far, so easy... But now I need some logic and database access to restore that entity. The loading is easy, but how can I inject that into my mapping type class?
I read the doctrine documentation and I was wondering, if I could use the event manager I get from AbstractPlatform $platform via parameter. Or is there maybe a better way?
You can try something like this, but i did not test this. Also you can use doctrine postLoad and postPersist/postUpdate events to transform your User entity to integer and back.
doctrine.yaml
doctrine:
dbal:
...
types:
user: App\Doctrine\DBAL\Type\UserType
...
UserType.php
<?php
namespace App\Doctrine\DBAL\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\IntegerType;
use App\Repository\UserRepository;
use App\Entity\User;
class UserType extends IntegerType
{
const NAME = 'user';
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return $this->userRepository->find($value);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if (!$value instanceof User) {
throw new \InvalidArgumentException("Invalid value");
}
return $value->getId();
}
public function getName()
{
return self::NAME;
}
}
I found a proper solution without hacking any classes to inject some service. The type mapping class fires an event and the conversion is handled outside.
class EntityString extends Type
{
// ...
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return $this->dispatchConverterCall($platform, TypeMapperEventArgs::TO_PHP_VALUE, $value);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return $this->dispatchConverterCall($platform, TypeMapperEventArgs::TO_DB_VALUE, $value);
}
protected function dispatchConverterCall(AbstractPlatform $platform, $name, $value)
{
$event = new TypeMapperEventArgs($name, $value);
$platform->getEventManager()->dispatchEvent(TypeMapperEventArgs::NAME, $event);
return $event->getResult();
}
// ...
}
Probably there are some better solutions, but for the moment that code does what I need. ;-)
My website is running Symfony 3.4 and I made my own user member system.
My User entity contains a Datetime field 'lastLogin' and I can't find a solution to update it every time a user logged in.
I created a custom UserChecker then I tried to update the field in it :
<?php
namespace CoreBundle\Security;
use CoreBundle\Entity\User as AppUser;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() )
{
throw new AuthenticationException();
}
else
{
// BELOW IS WHAT I TRY, BUT FAIL.
$entityManager = $this->get('doctrine')->getManager();
$user->setLastLogin(new \DateTime());
$entityManager->persist($user);
$entityManager->flush();
}
}
public function checkPostAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
}
}
But it doesn't work. Maybe I can't use the doctrine entity manager in this file ?
If I use $this->get('doctrine')->getManager(); I get :
Fatal Error: Call to undefined method
CoreBundle\Security\UserChecker::get()
Dunno why #doncallisto removed his post. It was (IMHO) the right thing.
Take a look at http://symfony.com/doc/current/components/security/authentication.html#authentication-success-and-failure-events
So you have several options.
SecurityEvents::INTERACTIVE_LOGIN - triggers every time the user
full out the login form and submit credentials. Will work, but you
won't get last_login updates if you have remember_me cookie or similar
AuthenticationEvents::AUTHENTICATION_SUCCESS - triggers each time
(every request) when authentication was successful. It means your last_login will be updated each time on every request unless user logged out
so you'll need a EventSubscriber. Take a look at this article. https://thisdata.com/blog/subscribing-to-symfonys-security-events/
MAybe you'll need a simplified version.
public static function getSubscribedEvents()
{
return array(
// AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure', // no need for this at that moment
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin', // this ist what you want
);
}
and then the onSecurityInteractiveLogin method itself.
public function onSecurityInteractiveLogin( InteractiveLoginEvent $event )
{
$user = $this->tokenStorage->getToken()->getUser();
if( $user instanceof User )
{
$user->setLastLogin( new \DateTime() );
$this->entityManager->flush();
}
}
P.S.
FosUserBundle uses interactive_login and a custom event to set last_login on entity
look at: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/EventListener/LastLoginListener.php#L63
Friend you can use this to inject the entityManager by a constructor
use Doctrine\ORM\EntityManagerInterface;
public function __construct(EntityManagerInterface $userManager){
$this->userManager = $userManager;
}
And in the checkPreAuth you call it
public function checkPreAuth(UserInterface $user){
if (!$user instanceof AppUser) {
return;
}
if ( $user->getDeleted() || !$user->getEnabled() ){
throw new AuthenticationException();
}else{
// BELOW IS WHAT I TRY, BUT FAIL.
$user->setLastLogin(new \DateTime());
$this->userManager->persist($user);
$this->userManager->flush();
}
}
I have one question I don't seem to find an answer to.
I have my User entity with a "Status" field.
What I want to do is store in another table "StatusEvent" a new line each time the status of a user is changed to keep track of the history of statuses of my users.
I tried to work with the PreUpdate method but it doesn't allow the creation of new Entities in this step.
I was maybe thinking that it might be possible with other events (onFlush maybe?) but these do not have the methods of the LifecycleEventArgs from PreUpdate (which allows to know if a field has been changed).
Anyone has already came across a same pattern or has an idea on how I could implement it?
Thanks by advance,
This is a nice case to use a custom event and listener.
Create a class UserEvents to hold a constant with the event name like
class UserEvents
{
const STATUS_CHANGED = 'user.status.changed';
}
Create a UserStatusChangedEvent that extends Event and takes the user as a parameter.
class UserChangedEvent extends Event
{
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function getUser(): User
{
return $this->user;
}
}
And then create and register a listener to capture/handle that event and create the entry that you need using the data from the user object that was passed in the event when it was dispatched.
class UserListener
{
public function onStatusChanged(UserChangedEvent $event)
{
$user = $event->getUser();
//TODO: Create your new status change entry. If you need the entity manager, just inject it in the constructor, like with any other service
}
}
You then need to register you listener as a service and tag it
AppBundle\Event\Listener\UserListener:
tags:
- { name: kernel.event_listener, event: user.status.changed, method: onStatusChanged }
And now all you have to do is dispatch a new instance of the event every time the status changes, passing it the user that you just persisted.
$eventDispatcher->dispatch(
UserEvents::STATUS_CHANGED,
$user
);
Edit: To defend the manual dispatching of the custom event VS the automated dispatch of onFlush, the custom event code is far easier to read even from a newbie that has no knowledge of how/when doctrine lifecycle events are triggered or how the entity manager works internally. The cherry at the top is that the dispatching works as a nice reminder that you have a listener there, which will be useful when you revisit your code in a few months.
The solution by #Dimitris would work, but requires you to dispatch the event manually.
I would use the onFlush method like you mentioned. (If you are writing a library, you are better off with the custom event)
You can use UnitOfWork to get the change sets.
public function onFlush(OnFlushEventArgs $event)
{
$em = $event->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
$this->newEntities[] = $entity;
if ($entity instanceof User) {
$changeSet = $uow->getEntityChangeSet($entity);
// if the $changeSet contains the status, log the change
$log = new Log();
$em->persist($log);
$uow->computeChangeSet($em->getClassMetadata(Log::class), $log);
}
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
// same here, create a private method to avoid duplication
}
}
The trade off of this listener is it will only log things on flush.
If your entity changes state multiple times before flush, only the last state will be logged. Eg state1 -> 2 -> 3 will only be logged as state1 -> 3
If you plan on creating a complex status field with many states and transitions have a look at the workflow component and use the listeners from there. It is a bit more work, but well worth it.
So what I did following the advice of both Dimitris and Padam67.
Define a DoctrineListener that listens on the onFlush event and register it
Dispatch a custom event in the DoctrineListener
Define an EventSubscriber listening on my custom event
Define a handler to manage the logic
Call the handler from the EventSubscriber
I know it makes a lot of files, but I like to separate everything as much as possible for a cleaner and simpler code to read :)
Define a DoctrineListener that listens on the onFlush event:
config/services.yaml
App\EventListener\Doctrine\DoctrineListener:
tags:
- { name: doctrine.event_listener, event: onFlush }
App\EventListener\Doctrine\DoctrineListener.php
<?php
namespace App\EventListener\Doctrine;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\UnitOfWork;
use App\Event\TalentStatusChangedEvent;
use App\Entity\Talent;
use App\Event\Constants\TalentEvents;
class DoctrineListener
{
private $logger;
private $dispatcher;
public function __construct(
LoggerInterface $logger,
EventDispatcherInterface $dispatcher
) {
$this->logger = $logger;
$this->dispatcher = $dispatcher;
}
public function onFlush(OnFlushEventArgs $event)
{
$entityManager = $event->getEntityManager();
$uow = $entityManager->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof Talent) {
$this->createTalentStatusChangedEvent($entity, $uow);
}
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof Talent) {
$this->createTalentStatusChangedEvent($entity, $uow);
}
}
}
private function createTalentStatusChangedEvent(Talent $entity, UnitOfWork $uow)
{
$this->logger->info(self::class . ' - talentStatusChanged: ' . $entity . ' - start');
$changeSet = $uow->getEntityChangeSet($entity);
if (array_key_exists('status', $changeSet)) {
$talentStatusChangedEvent = new TalentStatusChangedEvent($entity, new \DateTime());
$this->dispatcher->dispatch(TalentEvents::STATUS_CHANGED, $talentStatusChangedEvent);
$this->logger->info(self::class . ' - talentStatusChanged: ' . $entity . ' - success');
} else {
$this->logger->info(self::class . ' - talentStatusChanged: ' . $entity . ' - fail');
}
}
}
Define a TalentStatusChangedEvent
App\Event\TalentStatusChangedEvent.php
<?php
namespace App\Event;
use App\Entity\Talent;
use Symfony\Component\EventDispatcher\Event;
class TalentStatusChangedEvent extends Event
{
private $talent;
private $statusChangedDate;
public function __construct(Talent $talent, \DateTime $date)
{
$this->talent = $talent;
$this->statusChangedDate = $date;
}
public function getTalent()
{
return $this->talent;
}
public function getStatus()
{
return $this->talent->getStatus();
}
public function getStatusChangedDate()
{
return $this->statusChangedDate;
}
}
Define an EventSubscriber for my event (defined a separate file containing all my events per type)
App\EventListener\Admin\User\TalentSubscriber.php
<?php
namespace App\EventListener\Admin\User;
use App\Domain\User\StatusChanged\StatusChangedHandler;
use App\Entity\Talent;
use App\Event\Constants\TalentEvents;
use App\Event\TalentStatusChangedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class TalentSubscriber implements EventSubscriberInterface
{
private $statusChangedHandler;
public function __construct(
StatusChangedHandler $statusChangedHandler
) {
$this->statusChangedHandler = $statusChangedHandler;
}
public static function getSubscribedEvents()
{
return array(
TalentEvents::STATUS_CHANGED => 'statusChanged',
);
}
public function statusChanged(TalentStatusChangedEvent $event) {
$this->statusChangedHandler->handle($event);
}
}
Define a handler to actually manage the creation of the linked entity
App\Domain\User\StatusChanged.php
<?php
namespace App\Domain\User\StatusChanged;
use App\Entity\Talent;
use App\Entity\TalentStatusEvent;
use App\Event\TalentStatusChangedEvent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
class StatusChangedHandler
{
private $entityManager;
private $logger;
public function __construct(
EntityManagerInterface $entityManager,
LoggerInterface $logger
) {
$this->entityManager = $entityManager;
$this->logger = $logger;
}
public function handle(TalentStatusChangedEvent $event)
{
$this->logger->info(self::class . ' - Talent ' . $event->getTalent() . ' - start');
$talentStatusEvent = new TalentStatusEvent();
$talentStatusEvent->setTalent($event->getTalent());
$talentStatusEvent->setStatus($event->getStatus());
$this->entityManager->persist($talentStatusEvent);
// Calling ComputeChangeSet and not flush because we are during the onFlush cycle
$this->entityManager->getUnitOfWork()->computeChangeSet(
$this->entityManager->getClassMetadata(TalentStatusEvent::class),
$talentStatusEvent
);
$this->logger->info(self::class . ' - Talent ' . $event->getTalent() . ' - success');
}
}
I'm trying to find a good way for handling my access controls in Symfony2.
My requirements:
90% of my application can only be accessed by authenticated users
in many controllers I need to check if the user is the owner
there are also some differences for different user roles
What I've done already:
installed JMSSecurityExtraBundle to check permissions via annotation
defined global ace's for my entity classes
I create an ace for the owner for every object during the create process
The check for owner and roles is no Problem. I only want to define in a global way that a user has to be authenticated and for exceptions (sites that can be accessed anonymous) I want to define it separated (best via annotations).
I don't want to do this via routing pattern.
I'm not sure it be what you're looking for, but did you try with Event Listener ?
You can make your verification in the onKernelController method. Then, you will can create different Interfaces and check the type of your controller in the listener.
class AceBuilderListener implements EventSubscriber{
private $container;
public function setContainer($container){
$his->container = $container;
}
public function getSubscribedEvents()
{
return array(
Events::prePersist,
Events::preUpdate,
Events::preRemove,
Events::postPersist,
Events::postUpdate,
Events::postRemove,
Events::loadClassMetadata,
);
}
public function prePersist(){ echo( get_class($entity) ); }
public function preUpdate(){ echo( get_class($entity) ); }
public function preRemove(){ echo( get_class($entity) ); }
public function postPersist(){ echo( get_class($entity) ); }
public function postUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
echo get_class($entity);
// perhaps you only want to act on some "Product" entity
if ($entity instanceof Product | x) {
// ... do something with the Product
}
}
public function postRemove(){ die( get_class($entity) ); }
public function loadClassMetadata( LoadClassMetadataEventArgs $args ){
$classMetadata = $args->getClassMetadata();
$entityManager = $args->getEntityManager();
$user = $this->container->get('security.context')->getToken()->getUser();
// you can check here if isGranted();
// and get the entity from the object $classMetadata
$this->container->get('security.context')->isGranted('EDIT', $entity);
}
}