Best practice Symfony2 entity management - symfony

There is a lots of ways to manage entities in Symfony2, but I don't know which is the better
Solution 1: In the controller
public function myAction()
{
$myEntity = new MyEntity();
$form = $this->createForm($myEntityType, $myEntity);
...
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager()
$em->persist($myEntity);
$em->flush()
}
...
}
Solution 2: Using custom entityManager
public function myAction()
{
$myEntityManager = $this->get('manager.my_entity');
$myEntity = $myEntityManager->create();
$form = $this->createForm($myEntityType, $myEntity);
...
if ($form->isValid()) {
$myEntityManager->update($myEntity);
}
...
}
Solution 3: Using factory
public function myAction()
{
$myEntityFactory = $this->get('factory.my_entity');
$myEntity = $myEntityFactory->create();
$form = $this->createForm($myEntityType, $myEntity);
...
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager()
$em->persist($myEntity);
$em->flush()
}
...
}
I prefer the solution 2, but peoples told me this is not a single responsibility pattern because you have a factory and a method to update into it. The solution is maybe to use the factory in the manager, but it brings a lits of complexity.

Controller
public function myAction()
{
$myEntity = $this->get('entity_repository')->getBySomething();
$form = $this->createForm($myEntityType, $myEntity);
...
if ($form->isValid()) {
$this->get('entity_persister')->updateEntity($myEntity);
}
...
}
Your repository has the only responsability to give you model objects.
The domain service 'entity_persister' has the responsability to persist the given data to the model
Note that this solution is not the cleanest way because your form is directly mapped to an object representing the database table. I would recommend if you want to have a cleaner architecture to keep the model object and the object mapped by the form different.

Depends solely on a use case, IMO. There's no single "best way".
The question is: what do you actually do with this entity in your app? And how many times you need to access the same methods?
If only in one place, then creating a service or anything outside a controller could be totally uncessary.
Factory design pattern has its own usages, so it's a separate matter to be analysed (regardless the entity).
I'd go with 1st option for most of use cases, though, when you need to just create an Entity:
$myEntity = new MyEntity();
Why? Because code doesn't hide anything. Come on, MyEntity is just a Plain Object, it doesn't need any service or manager or (in most cases) factory. The other two seem to be a bad practice then, as they hide what is really going on there (unless Factory is needed).

Related

Symfony 4 - Compare two objects before persisting?

I've a Symfony 4 project.
In my controller, I've an action to edit an object in my database :
/**
* Editer un groupe
*
* #Route("/admin/validation/{id}", name="admin_validation_edit")
*
* #param GroupeValidateurs $groupeValidateurs
* #return void
*/
public function edit(GroupeValidateurs $groupeValidateurs, Request $request)
{
$form = $this->createForm(GroupeValidateursType::class, $groupeValidateurs);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//persist and flush in database
}
But when my form is submitted, I would like to compare my old and my new object, to make some actions.
For that, I used a session vairable :
$session = $this->get('session');
if (!$session->has('groupe')) {
$session->set('groupe', $groupeValidateurs);
}
And I remove it when the form is submitted and valid.
It works, but it's not correct, because, if I go on an edit page with some groupeValidateur, and just after, I go on another edit page with another groupeValidateur, My session variable will contains my previous groupeValidateur.
Which solution can I use please ?
before $form->handleRequest($request) your object $groupeValidateurs is still the original.
If you want to keep some information there are several options, amongst those the very easy and straightforward:
handle outside object and outside the form component:
if I understood you correctly, you only want to prevent certain users to be added/removed. Since I don't know your entity, I will assume, that your object has a method getUsers(), that returns the current users.
$oldUsers = $groupeValidateurs->getUsers(); // I assume $oldUsers is an ARRAY***
$form = $this->createForm(...)
//...
if($form->isSubmitted() && $form->isValid()) {
$newUsers = $groupeValidateurs->getUsers();
// do whatever ...
}
***) if this is a OneToMany or ManyToMany relation, make sure to return the array instead of the collection:
public function getUsers() {
return $this->users instanceof \Doctrine\Common\Collections\Collection
? $this->users->toArray()
: $this->users;
}
if you manage to keep $this->users as a Collection always, you can just return $this->users->toArray();
other options:
add event listeners to the form, that capture the data before edits come in, add this to a constraint that gets an additional list of users to prevent from being added/removed
IF there is a property on the user which makes it clear, the user shall not be removed ever, you can bake this into your add/removeUser function:
function removeUser($user) {
if($user->isAdmin()) {
return;
}
$this->users->removeElement($user); // I assume users to be a doctrine Collection
}
function setUsers($users) {
foreach($this->users as $user) {
if($user->isAdmin() && !in_array($users)) {
$users[] = $user; // add user;
}
}
$this->users = $users;
}
note: depending on your form, you might have to set by_reference to false. which imho is not a real problem if a) your getUsers() returns the array instead of the collection (how it should be) or b) if you implement addUser/removeUser.
also, this approach has the obvious caveat, that nobody can remove that user without removing the admin privilege, so maybe this is overkill ;o)
setup a doctrine event listener for updates on your entity type that checks for removed users and re-add them accordingly. for this to work, you either have to check the changesets somehow (this is quite the overkill probably)
upon changing the users of an object, store the old version (if add/remove implementation, take care not to overwrite the backup) of user list
implement clone on your entity properly and actually produce a copy of your object before getting it changed (by handleRequest). compare at will.
get the original entity as Andrea Manzi described and compare with that.
In edit "action" try using:
if ($form->isSubmitted() && $form->isValid()) {
$currentdata = $form->getData();
/**/
}
to get current data submitted
Or write an "update" action like this:
public function update(Request $request, $id)
{
/* #var $em EntityManager */
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository(Entity::class)->find($id); //your GroupeValidateurs entity class
$form = $this->createForm(GroupeValidateursType::class, $groupeValidateurs);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//persist and flush in database
$currentdata = $form->getData();
$beforedata = $em->getUnitOfWork()->getOriginalEntityData($entity);
/*....*/
}
Clone object to $cloneGroupeValidateurs. Set entity to form. Next submitting your form and compare with previously cloned variable.

why does doctrine think a existed entity as a new entity?

I have two entities , user and store, they have a many-to-one relationship, before I create a user, I have to make sure a store is existed, it is not allowed to create a store while creating a user ,that means cascade={"persist"} can't be used.
Store class
public function addUser(User $user)
{
if (!$this->users->contains($user))
{
$this->users->add($user);
$user->setStore($this);
}
return $this;
}
before I create a user , I am pretty sure that a store is already existed.these code below is the way I used to create user
$store= $this->get('vmsp.store_provider')->getCurrentStore();
$store->addUser($user);
$userManager->updateUser($user);
code in updateUser method is not special:
$this->entityManager->persist($user);
$this->entityManager->flush();
code in getCurrentStore method:
public function getCurrentStore($throwException=true)
{
if (isset(self::$store)) {
return self::$store;
}
$request = $this->requestStack->getCurrentRequest();
$storeId = $request->attributes->get('storeId', '');
$store = $this->entityRepository->find($storeId);
if ($store === NULL&&$throwException) {
throw new NotFoundHttpException('Store is not found');
}
self::$store = $store;
return $store;
}
this gives me a error:
A new entity was found through the relationship
'VMSP\UserBundle\Entity\User#store' that was not configured to cascade
persist operations for entity: ~ #1. 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"})
thing is getting very interesting, why does a existed store become new entity? why does doctrine think that existed store entity as a new entity?
It seems like your Store-entity is detached from the EntityManager somehow. I can't really see where it happens. Finding that out will probably take a few debugging sessions by you.
A quick fix might be to merge the user's store back into the EntityManager using EntityManager::merge($entity), e.g. in your updateUser-method:
public function updateUser(User $user) {
$store = $user->getStore();
$this->entityManager->merge($store);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
You might also want to play around with Doctrine's UnitOfWork especially with getState($entity, $assumedState) to find out whether your store is still managed or not.

preUpdate() siblings manage into tree: how to break ->persist() recursion?

Let's say I've got an entity like this
class FooEntity
{
$id;
//foreign key with FooEntity itself
$parent_id;
//if no parent level =1, if have a parent without parent itself = 2 and so on...
$level;
//sorting index is relative to level
$sorting_index
}
Now I would like on delete and on edit to change level and sorting_index of this entity.
So I've decided to take advantage of Doctrine2 EntityListeners and I've done something similar to
class FooListener
{
public function preUpdate(Foo $entity, LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$this->handleEntityOrdering($entity, $em);
}
public function preRemove(Foo $entity, LifecycleEventArgs $args)
{
$level = $entity->getLevel();
$cur_sorting_index = $entity->getSortingIndex();
$em = $args->getEntityManager();
$this->handleSiblingOrdering($level, $cur_sorting_index, $em);
}
private function handleEntityOrdering($entity, $em)
{
error_log('entity to_update_category stop flag: '.$entity->getStopEventPropagationStatus());
error_log('entity splobj: '.spl_object_hash($entity));
//code to calculate new sorting_index and level for this entity (omitted)
$this->handleSiblingOrdering($old_level, $old_sorting_index, $em);
}
}
private function handleSiblingOrdering($level, $cur_sorting_index, $em)
{
$to_update_foos = //retrieve from db all siblings that needs an update
//some code to update sibling ordering (omitted)
foreach ($to_update_foos as $to_update_foo)
{
$em->persist($to_update_foo);
}
$em->flush();
}
}
The problem here is pretty clear: if I persist a Foo entity, preUpdate() (into handleSiblingOrdering function) trigger is raised and this cause an infinite loop.
My first idea was to insert a special variable inside my entity to prevent this loop: when I started a sibling update, that variable is setted and before executing the update code is checked. This works like a charm for preRemove() but not for preUpdate().
If you notice I'm logging spl_obj_hash to understand this behaviour. With a big surprise I can see that obj passed to preUpdate() after a preRemove() is the same (so setting a "status flag" is a fine) but the object passed to preUpdate() after a preUpdate() isn't the same.
So ...
First question
Someone could point me in the right direction to manage this situation?
Second question
Why doctrine needs to generate different objects if two similar events are raised?
I've founded a workaround
Best approach to this problem seem to create a custom EventSubscriber with a custom Event dispatched programmatically into controller update action.
That way I can "break" the loop and having a working code.
Just to make this answer complete I will report some snippet of code just to clarify che concept
Create custom events for your bundle
//src/path/to/your/bundle/YourBundleNameEvents.php
final class YourBundleNameEvents
{
const FOO_EVENT_UPDATE = 'bundle_name.foo.update';
}
this is a special class that will not do anything but provide some custom events for our bundle
Create a custom event for foo update
//src/path/to/your/bundle/Event/FooUpdateEvent
class FooUpdateEvent
{
//this is the class that will be dispatched so add properties useful for your own logic. In my example two properties could be $level and $sorting_index. This values are setted BEFORE dispatch the event
}
Create a custom event subscriber
//src/path/to/your/bundle/EventListener/FooSubscriber
class FooSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(YourBundleNameEvents::FooUpdate => 'handleSiblingsOrdering');
}
public function handleSiblingsOrdering(FooUpdateEvent $event)
{
//I can retrieve there, from $event, all data I setted into event itself. Now I can run all my own logic code to re-order siblings
}
}
Register your Subscriber as a service
//app/config/config.yml
services:
your_bundlename.foo_listener:
class: Your\Bundle\Name\EventListener\FooListener
tags:
- { name: kernel.event_subscriber }
Create and dispatch events into controller
//src/path/to/your/bundle/Controller/FooController
class FooController extends Controller
{
public function updateAction()
{
//some code here
$dispatcher = $this->get('event_dispatcher');
$foo_event = new FooEvent();
$foo_event->setLevel($level); //just an example
$foo_event->setOrderingIndex($ordering_index); //just an examle
$dispatcher->dispatch(YourBundleNameEvents::FooUpdate, $foo_event);
}
}
Alternative solution
Of course above solution is the best one but, if you have a property mapped into db that could be used as a flag, you could access it directly from LifecycleEventArgs of preUpdate() event by calling
$event->getNewValue('flag_name'); //$event is an object of LifecycleEventArgs type
By using that flag we could check for changes and stop the propagation
You are doing wrong approach by calling $em->flush() inside preUpdate, I even can say restricted by Doctrine action: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#reference-events-implementing-listeners
9.6.6. preUpdate
PreUpdate is the most restrictive to use event, since it is called
right before an update statement is called for an entity inside the
EntityManager#flush() method.
Changes to associations of the updated entity are never allowed in
this event, since Doctrine cannot guarantee to correctly handle
referential integrity at this point of the flush operation.

Symfony2: creating a new SecurityIdentity

I'm using ACL in Symfony 2.1, and I need to create a new SecurityIdentity, so that my ACL can be set in function of some sort of groups.
Picture the following situation: there are groups with users (with different roles) that each have user information. In group 1, users with the ROLE_ADMIN can't edit other users from the same group's information, but in group 2, users with ROLE_ADMIN can edit others information.
So basically my ACL will vary in function of what group the user is in.
I thought I'd start solving this problem with the creation of a new "GroupSecurityIdentity". However the class itself doesn't suffice, as I get this exception when I use it:
$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.
My question is: how do I "register" my new SecurityIdentity so I can use it as RoleSecurityIdentity and UserSecurityIdentity?
What better ways are there to implement a system similar to this I want to do?
2 years ago I went down that path, it turned out to be a bad decision. Modifying the ACL system is difficult and might cause problems when updating Symfony. There are at least 2 better solutions. I'll list them all so you can decide which best suits your needs.
New security identity
I'm using the GroupInterface from FOSUserBundle, but I guess you could use your own too. The following files need to be added:
AclProvider.php
The method to change is private - the whole file has to be copied, but the only change has to be made to hydrateObjectIdentities
GroupSecurityIdentity.php
MutableAclProvider.php
We have to duplicate the whole file as it must extend AclProvider, but we're using a custom one and can't therefore extend the stock MutableAclProvider. The methods changed are getInsertSecurityIdentitySql and getSelectSecurityIdentityIdSql.
SecurityIdentityRetrievalStrategy.php
Next up: rewire the dependency injection container by providing the following parameters:
<parameter key="security.acl.dbal.provider.class">
Acme\Bundle\DemoBundle\Security\Acl\Dbal\MutableAclProvider
</parameter>
<parameter key="security.acl.security_identity_retrieval_strategy.class">
Acme\Bundle\DemoBundle\Security\Acl\Domain\SecurityIdentityRetrievalStrategy
</parameter>
Time to cross fingers and see whether it works. Since this is old code I might have forgotten something.
Use roles for groups
The idea is to have group names correspond to roles.
A simple way is to have your User entity re-implement UserInterface::getRoles:
public function getRoles()
{
$roles = parent::getRoles();
// This can be cached should there be any performance issues
// which I highly doubt there would be.
foreach ($this->getGroups() as $group) {
// GroupInterface::getRole() would probably have to use its
// canonical name to get something like `ROLE_GROUP_NAME_OF_GROUP`
$roles[] = $group->getRole();
}
return $roles;
}
A possible implementation of GroupInterface::getRole():
public function getRole()
{
$name = $this->getNameCanonical();
return 'ROLE_GROUP_'.mb_convert_case($name, MB_CASE_UPPER, 'UTF-8');
}
It's now just a matter of creating the required ACE-s as written in the cookbook article.
Create a voter
Finally, you could use custom voters that check for the presence of specific groups and whether the user has access to said object. A possible implementation:
<?php
namespace Acme\Bundle\DemoBundle\Authorization\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
class MySecureObjectVoter implements VoterInterface
{
/**
* {#inheritDoc}
*/
public function supportsAttribute($attribute)
{
$supported = array('VIEW');
return in_array($attribute, $supported);
}
/**
* {#inheritDoc}
*/
public function supportsClass($class)
{
return $class instanceof GroupableInterface;
}
/**
* {#inheritDoc}
*/
public function vote(TokenInterface $token, $object, array $attributes)
{
$result = VoterInterface::ACCESS_ABSTAIN;
if (!$object instanceof MySecureObject) {
return VoterInterface::ACCESS_ABSTAIN;
}
foreach ($attributes as $attribute) {
if (!$this->supportsAttribute($attribute)) {
continue;
}
// Access is granted, if the user and object have at least 1
// group in common.
if ('VIEW' === $attribute) {
$objGroups = $object->getGroups();
$userGroups = $token->getUser()->getGroups();
foreach ($userGroups as $userGroup) {
foreach ($objGroups as $objGroup) {
if ($userGroup->equals($objGroup)) {
return VoterInterface::ACCESS_GRANTED;
}
}
}
return voterInterface::ACCESS_DENIED;
}
}
}
}
For more details on voters please refer to the cookbook example.
I would avoid creating a custom security identity. Use the two other methods provided. The second solution works best, if you will be having lots of records and each of them must have different access settings. Voters could be used for setting up simple access granting logic (which most smaller systems seem to fall under) or when flexibility is necessary.
I write my answer here to keep a track of this error message.
I implemented group support with ACL and i had to hack a bit the symfony core "MutableAclProvider.php"
protected function getSelectSecurityIdentityIdSql(SecurityIdentityInterface $sid)
{
if ($sid instanceof UserSecurityIdentity) {
$identifier = $sid->getClass().'-'.$sid->getUsername();
$username = true;
} elseif ($sid instanceof RoleSecurityIdentity) {
$identifier = $sid->getRole();
$username = false;
}else {
//throw new \InvalidArgumentException('$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.');
$identifier = $sid->getClass().'-'.$sid->getGroupname();
$username = true;
}
return sprintf(
'SELECT id FROM %s WHERE identifier = %s AND username = %s',
$this->options['sid_table_name'],
$this->connection->quote($identifier),
$this->connection->getDatabasePlatform()->convertBooleans($username)
);
}
Even if the provided object is not an instance of UserSecurityIdentity or RoleSecurityIdentity it return a value. So now i can use a custom "GroupSecurityIdentity"
It's not easy to put in place but was much adapted to my system.

Symfony Service setting NULL data

I've been having several issues with creating a service in Symfony 2.0, but have found workarounds for everything but the service setting null data.
public function logAction($request, $entityManager)
{
$logs = new Logs();
$logs->setRoute = $request->get('_route');
$logs->setController = $request->get('_controller');
$logs->setRequest = json_encode($request->attributes->all());
$logs->setPath = $request->server->get('PATH_INFO');
$logs->setIp = $request->server->get('REMOTE_ADDR');
$em = $entityManager;
$em->persist($logs);
$em->flush();
}
I'm passing the EntityManager when calling the service in another controller, I'll post that code just in case:
public function pageAction($id = null, Request $request)
{
$log = $this->get('logging');
$log->logAction($request, $this->getDoctrine()->getEntityManager());
return $this->render('AcmeDemoBundle:Demo:viewPage.html.twig', array('name' => $id));
}
I have created a services.yml file following the official docs (in the Log Bundle) and an actual row is inserted, but all fields are set to NULL. If I try to do a die in one of the setters in the entity file it doesn't die, so it seems like something isn't making it. (I have both the EntityManager and Entity files used at the top of the log service file before anyone asks)
Any help would be greatly appreciated.
$logs->setRoute = $request->get('_route');
Should be:
$logs->setRoute($request->get('_route'));
For D2 all the accessors need to be methods.

Resources