How to Use Gaufrette and Symfony 3.0 - php

I have an issue from figuring out how to use the Symfony 3.0 with Gaufete in order to Upload into an s3 bucket.
According to the documentation: https://github.com/KnpLabs/KnpGaufretteBundle
I have set the config.yml:
knp_gaufrette:
adapters:
photostorage:
amazon_s3:
amazon_s3_id: amazonS3
bucket_name: '%save_location%'
options:
directory: 'symphotest'
And the services.yml:
services:
amazonS3:
class: Aws\S3\S3Client
factory_class: Aws\S3\S3Client
factory_method: 'factory'
arguments:
key: %amazon_s3.key%
secret: %amazon_s3.secret%
region: %amazon_s3.region%
And because I want to use custom enviromental variables any sort of configuration I pass it a file params.php:
$container->setParameter('save_type','s3');
$container->setParameter('save_location',getenv('BUCKET'));
$container->setParameter('aws_key',getenv('S3_ACCESS'));
$container->setParameter('aws_secret_key',getenv('S3_SECRET'));
Where I Include it on the top of the config.yml:
imports:
- { resource: params.php }
- { resource: security.yml }
- { resource: services.yml }
I have made an Entity names Images.php:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
use Gaufrette\Adapter\AwsS3 as AwsS3Adapter;
use Gaufrette\Filesystem;
/**
* #ORM\Entity
* #ORM\Table(name="images")
* #ORM\HasLifecycleCallbacks
*/
class Images
{
/**
* #ORM\Column(type="string", length=60)
* #ORM\Id
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="AppBundle\Doctrine\AutoIdGenerate")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="string", length=100)
*/
private $name_small;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ImageGroups", inversedBy="images")
*/
private $users;
/**
* #Assert\File(maxSize="6000000")
*/
private $file;
private $tmp;
private $path;
public function getFile()
{
return $file;
}
public function setFile(UploadedFile $file = null)
{
$this->file=$file;
};
public function __construct()
{
//IDK what to do here
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->getFile())
{
$filename = sha1(uniqid(gethostname(), true));
$this->name = $filename.'.'.$this->getFile()->guessExtension();
$this->$name_small='small'.$filename.'.'.$this->getFile()->guessExtension();
}
}
/**
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->getFile())
{
return;
}
// if there is an error when moving the file, an exception will
// be automatically thrown by move(). This will properly prevent
// the entity from being persisted to the database on error
$this->getFile()->move($this->getUploadRootDir(), $this->path);
// check if we have an old image
if (isset($this->temp))
{
// delete the old image
unlink($this->getUploadRootDir().'/'.$this->temp);
// clear the temp image path
$this->temp = null;
}
$this->file = null;
}
/**
* #ORM\PostRemove()
*/
public function removeUpload()
{
$file = $this->getAbsolutePath();
if ($file)
{
//Do stuff for Deleting
}
}
/**
* Get id
*
* #return string
*/
public function getId()
{
return $this->id;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Get nameSmall
*
* #return string
*/
public function getNameSmall()
{
return $this->name_small;
}
}
But I do not know how can I get the S3 Adapter and client because in this example:https://github.com/KnpLabs/Gaufrette/blob/master/doc/adapters/awsS3.md
In initiates the s3 client and filesystem. But on Symfony 3.0 I've already configured them on config.yml. Therefore there must be another way to get them.
All I want is an example of usage.

I recommend you read this article: https://blog.fortrabbit.com/new-app-cloud-storage-s3
Is is a quick start guide that talks about why you could use decentralized storage and covers the following topics:
How to sign up for an AWS account
How to create you first S3 bucket storage container — name space for your files
How to set up proper permissions — safe access with additional credentials
Alternatively, I have had good experiences with the LeaguePHP Adapter:
League\Flysystem\AwsS3v3
It provides a simple api for using the amazon web services for files and stuff! It's compatible standalone or using Symfony or laravel. Check out the documentation. You can see the methods from the source folder.

Don't inject a service into an entity: it is bad practice.
Use Doctrine event subscribers instead: as described on Symfony docs
# app/config/services.yml
services:
# ...
AppBundle\EventListener\SearchIndexerSubscriber:
tags:
- { name: doctrine.event_subscriber }
Event subscriber:
// src/AppBundle/EventListener/SearchIndexerSubscriber.php
namespace AppBundle\EventListener;
use AppBundle\Entity\Product;
use Doctrine\Common\EventSubscriber;
// for Doctrine < 2.4: use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
class SearchIndexerSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return [
Events::postPersist,
Events::postUpdate,
];
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->index($args);
}
public function postPersist(LifecycleEventArgs $args)
{
$this->index($args);
}
public function index(LifecycleEventArgs $args)
{
$entity = $args->getObject();
// perhaps you only want to act on some "Product" entity
if ($entity instanceof Product) {
$entityManager = $args->getObjectManager();
// ... do something with the Product
}
}
}

Related

Symfony4 extends controller with route annotation

I'm building a webapp with Symfony and since now I had to repeat a specific pattern for each new controller I built.
For example I have this AdminController :
/**
* #Route("/pro/{uniqid}")
* #ParamConverter("company", options={"mapping":{"uniqid" = "uniqid"}})
* #Security("is_granted(constant('App\\Security\\Voter\\CompanyVoter::VIEW'), company)")
* #package App\Controller
*/
class AdminController extends Controller
{
/**
* #Route("/admin/users/", name="users")
* #return \Symfony\Component\HttpFoundation\Response
*/
public function users(Company $company){}
}
So, each controller must redefine #Route, #ParamConverter and #Security that is extremely redundant.
I tried to create a LoggedController that define every annotation, then make Controller extends that LoggedController, but that does not work.
Is there a solution or should I continue to copy/paste these Annotation each time I create a new Controller that needs to implement it ?
EDIT :
I add the declaration of Company entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\CompanyRepository")
*/
class Company
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
Long story short, you can but it will be a lot easier to duplicate your annotations in every controller.
But if you wan't to do this anyway, here are some solutions.
Routing
This is the easy one. You can define a global prefix in the config/routes/annotations.yaml file.
If you're using the default config, you can try something like this:
# Default controllers
controllers:
resource: ../../src/Controller/
type: annotation
# Company controllers
company_controllers:
resource: ../../src/Controller/Company/
type: annotation
prefix: /pro/{uniqid}
All your routes will now start with /pro/{uniqid} and you can remove the #Route annotation from your controller.
ParamConverter
You can create your own ParamConverter. Everytime you'll use a Company type in an action method, it'll be converted to the matching entity using the uniqid attribute.
Something like this:
// src/ParamConverter/CompanyConverter.php
<?php
namespace App\ParamConverter;
use App\Entity\Company;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;
class CompanyConverter implements ParamConverterInterface
{
const CONVERTER_ATTRIBUTE = 'uniqid';
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* CompanyConverter constructor.
*
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #inheritdoc
*/
public function apply(Request $request, ParamConverter $configuration)
{
$uniqid = $request->attributes->get(self::CONVERTER_ATTRIBUTE);
$company = $this->entityManager->getRepository(Company::class)->findOneBy(['uniqid' => $uniqid]);
$request->attributes->set($configuration->getName(), $company);
}
/**
* #inheritdoc
*/
function supports(ParamConverter $configuration)
{
return $configuration->getClass() === Company::class;
}
}
With this, you can remove the #ParamConverter annotation from your controller.
Security
You can't use the access_control section of the security.yaml file since custom functions are not yet supported.
Otherwise, something like this could have been nice:
security:
...
access_control:
-
path: ^/pro
allow_if: "is_granted(constant('App\\Security\\Voter\\CompanyVoter::VIEW'), company)"
(Note: It was fixed in Symfony 4.1 but i don't know yet how it will work).
Instead, you can use a subscriber listening on the kernel.request kernel event:
<?php
namespace App\Subscriber;
use App\Entity\Company;
use App\Security\CompanyVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class SecurityListener implements EventSubscriberInterface
{
/**
* #var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* #param AuthorizationCheckerInterface $authorizationChecker
* #param EntityManagerInterface $entityManagerInterface
*/
public function __construct(AuthorizationCheckerInterface $authorizationChecker, EntityManagerInterface $entityManager)
{
$this->authorizationChecker = $authorizationChecker;
$this->entityManager = $entityManager;
}
/**
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$uniqid = $request->attributes->get('uniqid')) {
return;
}
$company = $this->entityManager->getRepository(Company::class)->findOneBy(['titre' => $uniqid]);
if (!$this->authorizationChecker->isGranted(CompanyVoter::VIEW, $company)) {
throw new AccessDeniedHttpException();
}
}
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'onKernelRequest',
);
}
}

Add Twig global variable after kernel.request event

I am currently working on developing a project in SAAS, each client can access his platform by a personalized url (site1.com, site2.com, etc.).
For each domain name a set of template customization data is defined in the back office and I must be able to access it from my Twig files. So I defined a listener on the kernerl.request event that adds a global variable to Twig based on the current domain name. Everything works fine in most cases, except when a page is first displayed, Twig must be run upstream and I get the following error:
Unable to add global "site" as the runtime or the extensions have
already been initialized.
Listener class
class SiteListener
{
public function __construct(
SiteHelper $siteHelper,
\Twig_Environment $twig
) {
$this->siteHelper = $siteHelper;
$this->twig = $twig;
}
/**
* Add current contexts to twig global.
*/
public function addContextsToTwigGlobal(GetResponseEvent $event)
{
$this->twig->addGlobal('site', $this->siteHelper);
}
}
Listener service declaration
multisite.listener.site:
class: MultisiteBundle\Listener\SiteListener
arguments:
- "#multisite.helper.site"
- "#twig"
tags:
- { name: kernel.event_listener, event: kernel.request, method: addContextsToTwigGlobal }
SiteHelper service
class SiteHelper
{
/**
* #var RequestStack
*/
protected $requestStack;
/**
* #var ContextConfigManager;
*/
protected $contextConfigManager;
/**
* #var ContextConfig
*/
protected $contextConfig;
public function __construct(
RequestStack $requestStack,
ContextConfigManager $contextConfigManager
) {
$this->requestStack = $requestStack;
$this->contextConfigManager = $contextConfigManager;
$this->contextConfig = $this->contextConfigManager
->findByHostOrStandard($this->getHost());
}
/**
* Get host from current request.
*
* #return string|null
*/
public function getHost()
{
$request = $this->requestStack->getCurrentRequest();
return ($request) ? $request->getHost() : null;
}
/**
* Get current context config
*
* #return ContextConfig
*/
public function getContextConfig()
{
return $this->contextConfig;
}
}
Any idea ?
I decided to write a Twig function to avoid this kind of problem. This seem to be a good solution.
class SiteExtension extends \Twig_Extension
{
/**
* #var SiteHelper
*/
private $siteHelper;
/**
* Constructor.
*
* #param SiteHelper $siteHelper
*/
public function __construct(SiteHelper $siteHelper)
{
$this->siteHelper = $siteHelper;
}
/**
* {#inheritdoc}
*/
public function getFunctions()
{
return array(
new \Twig_Function('site', array($this->siteHelper, 'getContext')),
);
}
}

Symfony VichUploaderBundle: File name could not be generated

I am using VichUploader to upload files within a symfony project. In configuration i use (copied from documentation):
service: vich_uploader.namer_property
options: { property: 'slug'}
In my entity i generate the slugs automatically with Gedmo/Sluggable:
/**
* #Gedmo\Slug(fields={"title"}, updatable=false)
* #ORM\Column(type="string", length=100, nullable=false)
*/
protected $slug;
But when trying to save the entity i get the following error 500:
File name could not be generated: property slug is empty.
If i set the property to 'title' it works. Did i forget a configuration parameter or something else to get it working with the Gedmo slug?
I'm having the same issue at the moment, as a workaround, I've slightly changed the slug getter in the entity class:
use Gedmo\Sluggable\Util\Urlizer;
class Event
{
// ...
/**
* #var string
*
* #Gedmo\Slug(fields={"name"})
* #ORM\Column(name="slug", type="string", length=128, unique=true)
*/
private $slug;
// ...
public function getSlug()
{
if (!$this->slug) {
return Urlizer::urlize($this->getName());
}
return $this->slug;
}
}
That did the trick.
Unfortunately, there're a couple of drawbacks:
If you ever want to update sluggable behaviour in the annotation to include additional properties, you'll have to update the getter as well.
This method lacks a check against the database: if there's already a record in the database with the same name urlizer in the getter won't be able to add an increment to the file name, previously saved file may be overwritten! As a workaround, you can add unique=true to sluggable properties.
VichUploader listens to the prePersist and preUpdate events, whereas Sluggable listens to the onFlush event. Because prePersist and preUpdate are called before onFlush, it isn't possible to do this purely using configuration.
However, if your file field is nullable, you can work around it by changing your controller code. When you receive the submitted form with the file, remove the file, save the entity, then re-add the file and save the entity again. On the second save, the slug will already be set, so VichUploader will be able to save the file fine.
if ($form->isSubmitted() && $form->isValid()) {
if ($file = $entity->getFile()) {
$entity->setFile(null);
}
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
if ($file) {
$entity->setFile($file);
$em->persist($entity);
$em->flush();
}
// ...
}
This only works when adding a new file. If you subsequently change the slug and resave the entity without uploading a new file, the filename is not updated.
I was having the same problem uploading a document for which I needed the fileName to be the slug.
I was using Gedmo annotations to generate the slug, however this only triggers on flush and the vichUploader namer is triggered upon persist.
The easiest way for me to get this working was to not use the Gedmo\Sluggable annotation but rather create a prePersist listener on my Document object and use the Cocur\Slugify library.
So here is the code.
My Document Entity:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* #ORM\Entity(repositoryClass="App\Repository\DocumentRepository")
* #Vich\Uploadable
* #ORM\EntityListeners({"App\Listeners\DocumentListener"})
*/
class Document
{
use TimestampableEntity;
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=100, nullable=false)
*/
private $fileName;
/**
* #Vich\UploadableField(mapping="document", fileNameProperty="fileName")
* #var File
*/
private $documentFile;
/**
* #ORM\Column(type="string", length=100, unique=true)
*/
private $slug;
/**
*/
public function getDocumentFile(): ?File
{
return $this->documentFile;
}
/**
* #param File $documentFile
* #return Document
* #throws \Exception
*/
public function setDocumentFile(File $documentFile = null): Document
{
$this->documentFile = $documentFile;
if($documentFile){
$this->updatedAt = new \DateTimeImmutable();
}
return $this;
}
public function getId(): ?int
{
return $this->id;
}
public function getSlug(): ?string
{
return $this->slug;
}
public function setSlug(string $slug): self
{
$this->slug = $slug;
return $this;
}
/**
* #return mixed
*/
public function getFileName()
{
return $this->fileName;
}
/**
* #param mixed $fileName
*/
public function setFileName($fileName): void
{
$this->fileName = $fileName;
}
}
And the listener :
namespace App\Listeners;
use App\Entity\Document;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Cocur\Slugify\Slugify;
class DocumentListener
{
public function prePersist(Document $document, LifecycleEventArgs $args)
{
$slugify = new Slugify();
if(!empty($document->getDocumentFile())){
$originalName = pathinfo($document->getDocumentFile()->getClientOriginalName(), PATHINFO_FILENAME);
$slug = $slugify->slugify($originalName);
$document->setSlug($slug);
}
}
}
So far I have not had any problems.
Let me know if this works for you
By default the doctrine extensions bundle does not attach any listener:
http://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html#activate-the-extensions-you-want
You should configure it to get sluggable working:
stof_doctrine_extensions:
orm:
default:
sluggable: true

Extend UserProvider forSymfony 2 FOS UserBundle

Iam using FOS User bundle to login in my site. For a requirement I need to make changes in the Fos user bundle user provider(
FOS\UserBundle\Security\UserProvider).
I have created a userProvider that extends the original userProvider in
FOS and have overridden the loadUserByUsername method to make my changes. It is given below
<?php
/*
* This file is part of the FOSUserBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Common\UtilityBundle\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface as SecurityUserInterface;
use FOS\UserBundle\Model\User;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Propel\User as PropelUser;
use FOS\UserBundle\Security\UserProvider as FOSProvider;
use Symfony\Component\DependencyInjection\ContainerInterface;
class UserProvider extends FOSProvider
{
/**
*
* #var ContainerInterface
*/
protected $container;
protected $userManager;
public function __construct(UserManagerInterface $userManager, ContainerInterface $container) {
$this->userManager = $userManager;
$this->container = $container;
}
/**
* {#inheritDoc}
*/
public function loadUserByUsername($username)
{
$user = $this->findUserUsername($username);
if (!$user) {
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
return $user;
}
/**
* {#inheritDoc}
*/
public function refreshUser(SecurityUserInterface $user)
{
if (!$user instanceof User && !$user instanceof PropelUser) {
throw new UnsupportedUserException(sprintf('Expected an instance of FOS\UserBundle\Model\User, but got "%s".', get_class($user)));
}
if (!$this->supportsClass(get_class($user))) {
throw new UnsupportedUserException(sprintf('Expected an instance of %s, but got "%s".', $this->userManager->getClass(), get_class($user)));
}
if (null === $reloadedUser = $this->userManager->findUserBy(array('id' => $user->getId()))) {
throw new UsernameNotFoundException(sprintf('User with ID "%d" could not be reloaded.', $user->getId()));
}
return $reloadedUser;
}
/**
* {#inheritDoc}
*/
public function supportsClass($class)
{
$userClass = $this->userManager->getClass();
return $userClass === $class || is_subclass_of($class, $userClass);
}
/**
* Finds a user by username.
*
* This method is meant to be an extension point for child classes.
*
* #param string $username
*
* #return UserInterface|null
*/
protected function findUserUsername($username)
{
return $this->userManager->findUserByUsername($username);
}
/**
* Finds a user by username.
*
* This method is meant to be an extension point for child classes.
*
* #param string $username
*
* #return UserInterface|null
*/
protected function findUserClub($username, $club)
{
return $this->userManager->findUserByClub($club, $username);
}
}
I have configured the service with the following.
fg.security.authentication.userprovider:
class: %Common_utility.security.user_provider.class%
arguments:
- #fos_user.user_manager
- #service_container
My security.yml looks like this:
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
providers:
fg_security_userprovider:
id: fg.security.authentication.userprovider
In my parameter.yml, I set the argument as:
Common_utility.security.user_provider.class: Common\UtilityBundle\Security\UserProvider
But all the time Iam getting the error like below:
Uncaught exception
'Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException'
with message 'The service "security.authentication.manager" has a
dependency on a non-existent service
"security.user.provider.concrete.fos_userbundle".' in
/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php:59
Does anybody know how to get this working?

Doctrine blameable extension 'on change' doesn't work

I'm on symfony 2.6.3 with stof Doctrine extension.
TimeStampable and SoftDeletable work well.
Also Blameable "on create" and "on update" are working well too:
/**
* #var User $createdBy
*
* #Gedmo\Blameable(on="create")
* #ORM\ManyToOne(targetEntity="my\TestBundle\Entity\User")
* #ORM\JoinColumn(name="createdBy", referencedColumnName="id")
*/
protected $createdBy;
/**
* #var User $updatedBy
*
* #Gedmo\Blameable(on="update")
* #ORM\ManyToOne(targetEntity="my\TestBundle\Entity\User")
* #ORM\JoinColumn(name="updatedBy", referencedColumnName="id")
*/
protected $updatedBy;
But "on change" seems not to be working.
/**
* #var User $deletedBy
*
* #Gedmo\Blameable(on="change", field="deletedAt")
* #ORM\ManyToOne(targetEntity="my\UserBundle\Entity\User")
* #ORM\JoinColumn(name="deletedBy", referencedColumnName="id")
*/
protected $deletedBy;
I've got SoftDeletable configured on "deletedAt" field. SoftDeletable works fine, but deletedBy is never filled.
How can I manage to make it work? I just want to set user id who deleted the entity.
Here my solution :
mybundle.soft_delete:
class: Listener\SoftDeleteListener
arguments:
- #security.token_storage
tags:
- { name: doctrine_mongodb.odm.event_listener, event: preSoftDelete }
class SoftDeleteListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* Method called before "soft delete" system happened.
*
* #param LifecycleEventArgs $lifeCycleEvent Event details.
*/
public function preSoftDelete(LifecycleEventArgs $lifeCycleEvent)
{
$document = $lifeCycleEvent->getDocument();
if ($document instanceof SoftDeletedByInterface) {
$token = $this->tokenStorage->getToken();
if (is_object($token)) {
$oldValue = $document->getDeletedBy();
$user = $token->getUser();
$document->setDeletedBy($user);
$uow = $lifeCycleEvent->getObjectManager()->getUnitOfWork();
$uow->propertyChanged($document, 'deletedBy', $oldValue, $user);
$uow->scheduleExtraUpdate($document, array('deletedBy' => array($oldValue, $user)));
}
}
}
}
The problem is you want to update entity (set user) when you call remove method on it.
Currently there may not be a perfect solution for registering user who soft-deleted an object using Softdeleteable + Blameable extensions.
Some idea might be to overwrite SoftDeleteableListener (https://github.com/Atlantic18/DoctrineExtensions/blob/master/lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php) but I had a problem doing it.
My current working solution is to use Entity Listener Resolver.
MyEntity.php
/**
* #ORM\EntityListeners({„Acme\MyBundle\Entity\Listener\MyEntityListener" })
*/
class MyEntity {
/**
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User")
* #ORM\JoinColumn(name="deleted_by", referencedColumnName="id")
*/
private $deletedBy;
public function getDeletedBy()
{
return $this->deletedBy;
}
public function setDeletedBy($deletedBy)
{
$this->deletedBy = $deletedBy;
}
MyEntityListener.php
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Acme\MyBundle\Entity\MyEntity;
class MyEntityListener
{
/**
* #var TokenStorageInterface
*/
private $token_storage;
public function __construct(TokenStorageInterface $token_storage)
{
$this->token_storage = $token_storage;
}
public function preRemove(MyEntity $myentity, LifecycleEventArgs $event)
{
$token = $this->token_storage->getToken();
if (null !== $token) {
$entityManager = $event->getObjectManager();
$myentity->setDeletedBy($token->getUser());
$entityManager->persist($myentity);
$entityManager->flush();
}
}
}
An imperfection here is calling flush method.
Register service:
services:
myentity.listener.resolver:
class: Acme\MyBundle\Entity\Listener\MyEntityListener
arguments:
- #security.token_storage
tags:
- { name: doctrine.orm.entity_listener, event: preRemove }
Update doctrine/doctrine-bundle in composer.json:
"doctrine/doctrine-bundle": "1.3.x-dev"
If you have any other solutions, especially if it is about SoftDeleteableListener, please post it here.
This is my solution, I use preSoftDelete event:
app.event.entity_delete:
class: AppBundle\EventListener\EntityDeleteListener
arguments:
- #security.token_storage
tags:
- { name: doctrine.event_listener, event: preSoftDelete, connection: default }
and service:
<?php
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class EntityDeleteListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function preSoftDelete(LifecycleEventArgs $args)
{
$token = $this->tokenStorage->getToken();
$object = $args->getEntity();
$om = $args->getEntityManager();
$uow = $om->getUnitOfWork();
if (!method_exists($object, 'setDeletedBy')) {
return;
}
if (null == $token) {
throw new AccessDeniedException('Only authorized users can delete entities');
}
$meta = $om->getClassMetadata(get_class($object));
$reflProp = $meta->getReflectionProperty('deletedBy');
$oldValue = $reflProp->getValue($object);
$reflProp->setValue($object, $token->getUser()->getUsername());
$om->persist($object);
$uow->propertyChanged($object, 'deletedBy', $oldValue, $token->getUser()->getUsername());
$uow->scheduleExtraUpdate($object, array(
'deletedBy' => array($oldValue, $token->getUser()->getUsername()),
));
}
}
It's not consistence because I check setDeletedBy method exists and set deletedBy property, but it work for me, and you can upgrade this code for your needs
Here is another solution I found :
Register a service:
softdeleteable.listener:
class: AppBundle\EventListener\SoftDeleteableListener
arguments:
- '#security.token_storage'
tags:
- { name: doctrine.event_listener, event: preFlush, method: preFlush }
SoftDeleteableListener:
/**
* #var TokenStorageInterface|null
*/
private $tokenStorage;
/**
* DoctrineListener constructor.
*
* #param TokenStorageInterface|null $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #param PreFlushEventArgs $event
*/
public function preFlush(PreFlushEventArgs $event)
{
$user = $this->getUser();
$em = $event->getEntityManager();
foreach ($em->getUnitOfWork()->getScheduledEntityDeletions() as $object) {
/** #var SoftDeleteableEntity|BlameableEntity $object */
if (method_exists($object, 'getDeletedBy') && $user instanceof User) {
$object->setDeletedBy($user);
$em->merge($object);
// Persist and Flush allready managed by other doctrine extensions.
}
}
}
/**
* #return User|void
*/
public function getUser()
{
if (!$this->tokenStorage || !$this->tokenStorage instanceof TokenStorageInterface) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
$token = $this->tokenStorage->getToken();
if (!$token) {
/** #noinspection PhpInconsistentReturnPointsInspection */
return;
}
$user = $token->getUser();
if (!$user instanceof User) {
/** #noinspection PhpInconsistentReturnPointsInspection */
return;
}
return $user;
}

Categories