Setting a default value for a one-on-one association - symfony

Doctrine is a beast in combination with Symfony, but I can't seem to find good examples on how to achieve what I need.
My user schema is pretty standard. I want to set a one-to-one association that points to the forum permission a user has.
For this to work, I need to create a "default entity" that holds the default permissions given to the user upon creation.
Here is the User#forumPermission association
/**
* #var $forumPermissions ?ForumPermission
* One User instance has One Forum Permission instance.
* #ORM\OneToOne(targetEntity="ForumPermission")
* #JoinColumn(name="forum_permission_id", referencedColumnName="id")
*/
private $forumPermission;
/**
* #return ForumPermission
*/
public function getForumPermission() : ?ForumPermission
{
return $this->forumPermission;
}
/**
* #param ForumPermission $forumPermission
*/
public function setForumPermission(ForumPermission $forumPermission): void
{
$this->forumPermission = $forumPermission;
}
This means the forum_permission table is empty at this time.
After that, I read in the doctrine docs that you can listen to all kinds of events related to flushing and persisting. Here is the docs sections for onFlush
This is what I came up with
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
/* #var $entity User */
if (true === $entity instanceof User
&& null === $entity->getForumPermission()) {
$entity->setForumPermission($this->getDefaultForumPermission($em, $uow));
}
}
}
private function getDefaultForumPermission(
\Doctrine\ORM\EntityManagerInterface $em,
\Doctrine\ORM\UnitOfWork $uow)
{
// Get the default permissions form the database
$defaultForumPermission = $em->getRepository(ForumPermission::class)->find(1);
// I will create a new entity if the default permissions do not exist
if (null === $defaultForumPermission) {
$defaultForumPermission = new ForumPermission();
$uow->persist($defaultForumPermission);
$uow->computeChangeSet($em->getClassMetadata('\App\Entity\ForumPermission'), $defaultForumPermission);
}
return $defaultForumPermission;
}
The docs aren't very clear on what you can do and where you should do it.
I figure I could do this all in the controller, but I like to keep everything where it belongs as intended by design.
So I'm wondering if this is the best way to do it, should I be doing this during prePersist or another event? Any help is greatly appreciated.

If I get it right, each time a User is created you want to create a new ForumPermission associated to this User. The best way to do it is to listen to the prePersist Doctrine event.
See https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html for additional information.

Related

Trigger UniqueEntity after persisting and before flushing data

I'm importing a kind of csv data via doctrine entity manger, however, I have a loop that performs a batch processing as it is mentioned at the level of the doc.
https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/batch-processing.html#bulk-inserts
$validationErrors = [];
foreach($data as $itrationNumber => $item) {
/** #var User|null $user **/
$user = $this->em->getRepository(User::class)->findOnBy(['email' => $item['email']]);
$user = ($user) ? $user : new User();
$user->setName('test');
$errors = $this->validator->validate($user);
if ($errors->count() === 0) {
$this->persist($user);
} else {
$validationErrors[] = $errors;
}
if ($itratioNumber% 100 === 0) {
$this->em->flush();
}
}
return $validationErrors;
Here is my User class, which has a unique constraint on the email field:
/**
* #ORM\Entity
* #UniqueEntity("email")
*/
class User
{
/**
* #ORM\Column(name="email", type="string", length=255, unique=true)
* #Assert\Email
*/
protected $email;
}
Unfortunately, if there is more than one row in my data that has an identical email address, UniqueEntity validation will not be triggered, simply, because users are persisted but are not flushed into the database.
Solution 1: Avoid the batch and do a flush at each iteration which is very violating and may provoke connection closed doctrine or kind of memory leak.
Solution 2: It was to create a custom constraint, which is inspired by UniqueEntity("email") and then check for each item's email address if there's a user already persisted with the same mail.
The problem that if the user already exists in the database, and we call em-> persist(), I cannot find any persisted object neither in $entityManager->getUnitOfWork()->getScheduledEntityInsertions() nor in $entityManager->getUnitOfWork()->getScheduledEntityUpdates().
Only during a new insertion that I hole the object persisted in the response of the function ->getScheduledEntityInsertions()
I would be very grateful if anyone has any idea how I could recover the entities persisted after the $entityManager->persist() step.
Or simply a 3rd solution which allows me to trigger a validation on the uniqueness of the email even in a batch context.
You should keep a second bit of information to see if an e-mail address was already processed:
$emails = [];
foreach($data as $itrationNumber => $item) {
if (isset($emails[$item['email']])) {
continue;
}
/** #var User|null $user **/
$user = $this->em->getRepository(User::class)->findOnBy(['email' => $item['email']]);
$user = ($user) ? $user : new User();
$user->setName('test');
if ($this->validator->validate($user)->count() === 0) {
$this->persist($user);
$emails[$item['email']] = true;
}
if ($itratioNumber% 100 === 0) {
$this->em->flush();
}
}
This code snippet does not normalize the e-mail address, this should be done first (strtolower and maybe removal of +foo gmail local parts for example).
Except if yo are WILLING to load the whole emails in a memory cache and play around that with REDIS or direct arrays, I can't see a second way but using the DB constraint of uniqueness (i.e doctrine flush)
Regarding your fear of memory overload, you can simply detach the persisted entity from the managed pool by calling the clear method. So your code would look something like this:
foreach($data as $itrationNumber => $item) {
/** #var User|null $user **/
$user = $this->em->getRepository(User::class)->findOnBy(['email' => $item['email']]);
$user = ($user) ? $user : new User();
$user->setName('test');
if ($this->validator->validate($user)->count() === 0) {
$this->persist($user);
$this->em->flush();
}
$this->em->clear();
}
I personnaly used this solution for a batch processing of huge CSV flow, and got inspired from this doctrine documentation section.
P.S I would think about DB transactions but might need more tricks.

Set logged user in another entity with FOSUserBundle

I want to set the logged user in a set method for another entity. For example, I have the entities Article and User (User is an FOSUser entity). I want to set the current user as author for an article.
In the Article entity, I write:
/**
* #ORM\ManyToOne(targetEntity="UserBundle\Entity\User", inversedBy="articles")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
/**
* Set user
*
* #param \UserBundle\Entity\User $user
* #return Article
*/
public function setUser(\UserBundle\Entity\User $user = null)
{
//This line is the problem
$this->user = $this->container->get('security.context')->getToken()->getUser();
return $this;
}
I'm trying to get the user from the container, but when I perform a save, the set value is null. I test the line in a controller and I get successfully the current user. Maybe inside an entity the method for get the logged user is different?
An Entity cannot access the service container, also you cannot use any service inside.
To make it working, call the Article::setUser() from a controller or another context that can use the container. e.g. :
$currentUser = $this->container->get('security.token_storage')->getToken()->getUser();
$article = new Article();
$article->setUser($currentUser);
// ...
$em = $this->getDoctrine()->getManager();
$em->persist($article);
$em->flush();
And the field will be correctly filled.
You have to set the user with the function chalasr said
$article->setUser($this->getUser());
persist it to db and you are done :)

How to disable Blameable-behaviour programmatically in Symfony2

I'm trying to run a console command in symfony2 in which some properties of a certain class are being updated. One of the properties has got a corresponding reviewedBy-property which is being set by the blameable-behaviour like so:
/**
* #var bool
* #ORM\Column(name="public_cmt", type="boolean", nullable=true)
*/
private $publicCmt;
/**
* #var User $publicCmtReviewedBy
*
* #Gedmo\Blameable(on="change", field="public_cmt")
* #ORM\ManyToOne(targetEntity="My\Bundle\EntityBundle\Entity\User")
* #ORM\JoinColumn(name="public_cmt_reviewed_by", referencedColumnName="id", nullable=true)
*/
private $publicCmtReviewedBy;
When i run the task there's no user which can be 'blamed' so I get the following exception:
[Doctrine\ORM\ORMInvalidArgumentException]
EntityManager#persist() expects parameter 1 to be an entity object, NULL given.
However I can also not disable blameable because it's not registered as a filter by the time i start the task and programmatically trying to set the user through:
// create the authentication token
$token = new UsernamePasswordToken(
$user,
null,
'main',
$user->getRoles());
// give it to the security context
$this->getService('security.context')->setToken($token);
doesn't work. Anyone got an idea?
If you use the StofDoctrineExtensionsBundle you can simply do :
$this->container->get('stof_doctrine_extensions.listener.blameable')
->setUserValue('task-user');
see : https://github.com/stof/StofDoctrineExtensionsBundle/issues/197
First of all, I'm not sure if 'field' cares if you use the database column or the property, but you might need to change it to field="publicCmt".
What you should do is override the Blameable Listener. I'm going to assume you are using the StofDoctrineExtensionsBundle. First override in your config:
# app/config/config.yml
stof_doctrine_extensions:
class:
blameable: MyBundle\BlameableListener
Now just extend the existing listener. You have a couple options - either you want to allow for NULL values (no blame), or, you want to have a default user. Say for example you want to just skip the persist and allow a null, you would override as such:
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
public function getUserValue($meta, $field)
{
try {
$user = parent::getUserValue($meta, $field);
}
catch (\Exception $e) {
$user = null;
return $user;
}
protected function updateField($object, $ea, $meta, $field)
{
if (!$user) {
return;
}
parent::updateField($object, $ea, $meta, $field);
}
}
So it tries to use the parent getUserValue() function first to grab the user, and if not it returns null. We must put in a try/catch because it throws an Exception if there is no current user. Now in our updateField() function, we simply don't do anything if there is no user.
Disclaimer - there may be parts of that updateField() function that you still need...I haven't tested this.
This is just an example. Another idea would be to have a default database user. You could put that in your config file with a particular username. Then instead of returning null if there is no user from the security token, you could instead grab the default user from the database and use that (naturally you'd have to inject the entity manager in the service as well).
Slight modification of the above answer with identical config.yml-entry: we can check if a user is set and if not: since we have access to the object-manager in the updateField-method, get a default-user, set it and then execute the parent-method.
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
protected function updateField($object, $ea, $meta, $field)
{
// If we don't have a user, we are in a task and set a default-user
if (null === $this->getUserValue($meta, $field)) {
/* #var $ur UserRepository */
$ur = $ea->getObjectManager()->getRepository('MyBundle:User');
$taskUser = $ur->findOneBy(array('name' => 'task-user'));
$this->setUserValue($taskUser);
}
parent::updateField($object, $ea, $meta, $field);
}
}

Link 1 entity with many others kind of entities

Let say I have a Company for which I manage Employees, Cars, Contracts, Buildings, Sites, Products, etc. As you can guess, these are quite independant things, so no inheritance is possible.
For each of these elements (i.e. Entities), I want to be able to attach one or several Documents (click on a button, form opens, select one/several Document or upload a new one).
Linking Document to one kind of entity is not a problem, my problem is that there are many kinds of entities. How should I manage that? I have 2 ideas which have their own problems...:
Create a ManyToMany relationship between Document and Employee, another between Document and Car, etc.
Problem: I have to duplicate the Controller code to attach Document, duplicate the forms, etc.
Create a single join table containing the Document's ID, the related entity's ID and the related entity's class name.
Problem: it doesn't look really clean to me, I didn't really dig in this way but I feel I'll have a lot of "entity mapping" problems.
Any suggestion?
[EDIT]
In fact I have to do the same for Event as well: I need to link some Events to some Employees and/or to some Cars, etc. And in my real case, I have more than 10 Entities to be linked to Event and/or Document, which means duplicating more tha 20 times the code if I go with the solution 1!
Assuming you're using Doctrine ORM, i think you're searching for the Mapped Superclasses inheritance.
The docs are better than words :
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#mapped-superclasses
So finally I managed to solve my problem, following #Rpg600 idea about Mapped Superclasses.
This is probably not the best and cleanest solution ever, I'm not really proud of it but it does the job and it is still better than my first ideas.
I create a BaseEntity which is my a mapped superclass (Employee, Car, etc. Entities have to extend this Class):
/**
* BaseEntity
* #ORM\MappedSuperclass
*/
class BaseEntity {
/**
* #ORM\OneToOne(targetEntity="MyProject\MediaBundle\Entity\Folder")
*/
private $folder;
/**
* Set folder
* #param \Webobs\MediaBundle\Entity\Folder $folder
* #return BaseEntity
*/
public function setFolder(\Webobs\MediaBundle\Entity\Folder $folder = null){
$this->folder = $folder;
return $this;
}
/**
* Get folder
* #return \Webobs\MediaBundle\Entity\Folder
*/
public function getFolder(){
return $this->folder;
}
}
As it is not possible to have a Many-to-Many relationship in a superclass, I use a Folder which will contain one or several Document. This is the dirty part of the solution ; the folder table basically contain only one field which is the id...
class Folder
{
private $id;
/**
* Note : Proprietary side
* #ORM\ManyToMany(targetEntity="MyProject\MediaBundle\Entity\Document", inversedBy="folders", cascade={"persist"})
* #ORM\JoinTable(name="document_in_folder")
*/
private $documents;
// setters and getters
Then I create a helper class (which is declared as a service) to manage the link between any Entity and the Document:
class DocumentHelper extends Controller
{
protected $container;
/** ************************************************************************
* Constructor
* #param type $container
**************************************************************************/
public function __construct($container = null)
{
$this->container = $container;
}
/** ************************************************************************
* Attach Document(s) to an $entity according to the information given in the
* form.
* #param Entity $entity
* #param string $redirectRouteName Name of the route for the redirection after successfull atachment
* #param string $redirectParameters Parameters for the redirect route
* #return Response
**************************************************************************/
public function attachToEntity($entity, $redirectRouteName, $redirectParameters)
{
$folder = $entity->getFolder();
if($folder == NULL){
$folder = new Folder();
$entity->setFolder($folder);
}
$form = $this->createForm(new FolderType(), $folder);
// ------------- Request Management ------------------------------------
$request = $this->get('request');
if ($request->getMethod() == 'POST') {
$form->bind($request); // Link Request and Form
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($folder);
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl($redirectRouteName, $redirectParameters));
}
}
return $this->render('MyProjectMediaBundle:Folder:addDocument.html.twig', array(
'form' => $form->createView(),
'entity' => $entity,
));
}
Doing that way, I just have to add one small action in each relevant controller, let say EmployeeController.php:
public function addDocumentAction(Employee $employee)
{
$redirectRouteName = 'MyProjectCore_Employee_see';
$redirectParameters = array('employee_id' => $employee->getId());
return $this->get('myprojectmedia.documenthelper')->attachToEntity($employee,$redirectRouteName,$redirectParameters);
}
Same principle for the display, in the helper I have the common function which I call in my already-existing seeAction() and in the TWIG file I import the common "Document list" display.
That's all folks!
I hope this can help :)

Symfony - Efficient access control for (dynamic) hierarchical roles

I need some advice on how to handle access control for the following scenario:
Corporation
Has one or many companies
Has one or many ROLE_CORP_ADMIN
Company
Has one or many regions.
Has one or many ROLE_COMPANY_ADMIN.
Region:
Has zero or many stores.
Has one or many ROLE_REGION_ADMIN.
Store:
Has zero or many assets.
Has one or many ROLE_STORE_ADMIN.
Has zero or many ROLE_STORE_EMPLOYEE.
Has zero or many ROLE_STORE_CUSTOMER (many is better).
The application should support many corporations.
My instinct is to create either a many-to-many relationship per entity for their admins (eg region_id, user_id). Depending on performance, I could go with a more denormalized table with user_id, corporation_id, company_id, region_id, and store_id. Then I'd create a voter class (unanimous strategy):
public function vote(TokenInterface $token, $object, array $attributes)
{
// If SUPER_ADMIN, return ACCESS_GRANTED
// If User in $object->getAdmins(), return ACCESS_GRANTED
// Else, return ACCESS_DENIED
}
Since the permissions are hierarchical, the getAdmins() function will check all owners for admins as well. For instance:
$region->getAdmins() will also return admins for the owning company, and corporation.
I feel like I'm missing something obvious. Depending on how I implement the getAdmins() function, this approach will require at least one hit to the db every vote. Is there a "better" way to go about this?
Thanks in advance for your help.
I did just what I posed above, and it is working well. The voter was easy to implement per the Symfony cookbook. The many-to-many <entity>_owners tables work fine.
To handle the hierarchical permissions, I used cascading calls in the entities. Not elegant, not efficient, but not to bad in terms of speed. I'm sure refactor this to use a single DQL query soon, but cascading calls work for now:
class Store implements OwnableInterface
{
....
/**
* #ORM\ManyToMany(targetEntity="Person")
* #ORM\JoinTable(name="stores_owners",
* joinColumns={#ORM\JoinColumn(name="store_id", referencedColumnName="id", nullable=true)},
* inverseJoinColumns={#ORM\JoinColumn(name="person_id", referencedColumnName="id")}
* )
*
* #var ArrayCollection|Person[]
*/
protected $owners;
...
public function __construct()
{
$this->owners = new ArrayCollection();
}
...
/**
* Returns all people who are owners of the object
* #return ArrayCollection|Person[]
*/
function getOwners()
{
$effectiveOwners = new ArrayCollection();
foreach($this->owners as $owner){
$effectiveOwners->add($owner);
}
foreach($this->getRegion()->getOwners() as $owner){
$effectiveOwners->add($owner);
}
return $effectiveOwners;
}
/**
* Returns true if the person is an owner.
* #param Person $person
* #return boolean
*/
function isOwner(Person $person)
{
return ($this->getOwners()->contains($person));
}
...
}
The Region entity would also implement OwnableInterface and its getOwners() would then call getCompany()->getOwners(), etc.
There were problems with array_merge if there were no owners (null), so the new $effectiveOwners ArrayCollection seems to work well.
Here is the voter. I stole most of the voter code and OwnableInterface and OwnerInterface from KnpRadBundle:
use Acme\AcmeBundle\Security\OwnableInterface;
use Acme\AcmeBundle\Security\OwnerInterface;
use Acme\AcmeUserBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
class IsOwnerVoter implements VoterInterface
{
const IS_OWNER = 'IS_OWNER';
private $container;
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container) {
$this->container = $container;
}
public function supportsAttribute($attribute)
{
return self::IS_OWNER === $attribute;
}
public function supportsClass($class)
{
if (is_object($class)) {
$ref = new \ReflectionObject($class);
return $ref->implementsInterface('Acme\AcmeBundle\Security\OwnableInterface');
}
return false;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
foreach ($attributes as $attribute) {
if (!$this->supportsAttribute($attribute)) {
continue;
}
if (!$this->supportsClass($object)) {
return self::ACCESS_ABSTAIN;
}
// Is the token a super user? This will check roles, not user.
if ( $this->container->get('security.context')->isGranted('ROLE_SUPER_ADMIN') ) {
return VoterInterface::ACCESS_GRANTED;
}
if (!$token->getUser() instanceof User) {
return self::ACCESS_ABSTAIN;
}
// check to see if this token is a user.
if (!$token->getUser()->getPerson() instanceof OwnerInterface) {
return self::ACCESS_ABSTAIN;
}
// Is this person an owner?
if ($this->isOwner($token->getUser()->getPerson(), $object)) {
return self::ACCESS_GRANTED;
}
return self::ACCESS_DENIED;
}
return self::ACCESS_ABSTAIN;
}
private function isOwner(OwnerInterface $owner, OwnableInterface $ownable)
{
return $ownable->isOwner($owner);
}
}

Resources