Symfony #ParamConverter get entity through user relationship - php

I have several routes that start with /experiment/{id}/... and I'm tired of rewriting the same logic to retrieve the signed in user's experiment. I guess I could refactor my code but I'm guessing #ParamConverter would be a better solution.
How would I rewrite the following code to take advantage of Symfony's #ParamConverter functionality?
/**
* Displays details about an Experiment entity, including stats.
*
* #Route("/experiment/{id}/report", requirements={"id" = "\d+"}, name="experiment_report")
* #Method("GET")
* #Template()
* #Security("has_role('ROLE_USER')")
*/
public function reportAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$experiment = $em->getRepository('AppBundle:Experiment')
->findOneBy(array(
'id' => $id,
'user' => $this->getUser(),
));
if (!$experiment) {
throw $this->createNotFoundException('Unable to find Experiment entity.');
}
// ...
}
Experiment entities have composite primary keys as follows:
class Experiment
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
*/
protected $id;
/**
* #var integer
*
* #ORM\Column(name="user_id", type="integer")
* #ORM\Id
*/
protected $userId;
/**
* #ORM\ManyToOne(targetEntity="UserBundle\Entity\User", inversedBy="experiments", cascade={"persist"})
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
// ..
}
I want to retrieve a signed in user's experiment using their user id and an experiment id in the route.

You can achieve that by using custom ParamConverter. For example something like that:
namespace AppBundle\Request\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use AppBundle\Entity\Experiment;
class ExperimentConverter implements ParamConverterInterface
{
protected $em;
protected $user;
public function __construct(EntityManager $em, TokenStorage $tokenStorage)
{
$this->em = $em;
$this->user = $tokenStorage->getToken()->getUser();
}
public function apply(Request $request, ParamConverter $configuration)
{
$object = $this->em->getRepository(Experiment::class)->findOneBy([
'id' => $request->attributes->get('id'),
'user' => $this->user
]);
if (null === $object) {
throw new NotFoundHttpException(
sprintf('%s object not found.', $configuration->getClass())
);
}
$request->attributes->set($configuration->getName(), $object);
return true;
}
public function supports(ParamConverter $configuration)
{
return Experiment::class === $configuration->getClass();
}
}
You need to register your converter service and add a tag to it:
# app/config/config.yml
services:
experiment_converter:
class: AppBundle\Request\ParamConverter\ExperimentConverter
arguments:
- "#doctrine.orm.default_entity_manager"
- "#security.token_storage"
tags:
- { name: request.param_converter, priority: 1, converter: experiment_converter }

Unfortunately, you can't inject the currently logged in users id into the param converter, unless you actually pass it as a parameter in the url.
You could create your own converter, but I think your best bet would be to just create a protected method for fetching the experiment. It will be just as easy to use and maintain as an annotation:
protected function getCurrentUsersExperiment($experimentId)
{
return $this->getDoctrine()->getManager()->getRepository('AppBundle:Experiment')
->findOneBy(array(
'id' => $experimentId,
'user' => $this->getUser()
));
}
public function reportAction(Request $request, $id)
{
$experiment = $this->getCurrentUsersExperiment($id);
...
}
As it's noted in the symfony best practices: use ParamFetcher whenever appropriate, but don't overthink it.

While the question already has an answer, I'd like to add my two cents to it:
It looks like you may be solving a wrong problem: is your objective to prevent access to other user's experiments? If so, just load the Experiment object, and use a Security Voter to determine whether the current user can access the experiment or not.
Using the param converter the way you want will throw a 404 error instead of 401, with the same ultimate result: access to other people's experiments is denied. Unless you're trying to conceal the existence of a particular experiment ID, you might as well return a HTTP response reflecting the actual situation - 401, not 404. I would argue that obscuring the existence of some ID has probably no real benefit.
If nothing else, then using the security voter to deny access sounds like more fit-for-purpose and reusable approach in Symfony context, comparing to a homebrew param converter.
If you don't want to use a security voter, you could also get away with using an expression: if your Experiment's repository class was autowiring the Security object, you would be able to fetch the current user from the repository class. If that repository class has a method findCurrentUserExperiment(int $id), the usage would be close to:
/**
* #Route("/experiment/{experiment}/report", requirements={"experiment" = "\d+"}, name="experiment_report")
* #Entity("experiment", expr="repository.findCurrentUserExperiment(experiment)")
*/
public function reportAction(Request $request, ?Experiment $experiment)
I still think that a security voter is a better fit, but at least with the expression you wouldn't have to manage a narrow-use-case ParamConverter.

Related

Create Doctrine entity from JSON column

I'm new to using Doctrine and am struggling to find the best way to handle the following scenario.
I have a table payment_gateways, which stores payment gateway config for users. Most of the data is common between payment gateways but there is also a JSON column config, the purpose of this column is to store configuration which is unique to specific payment gateways since I can't guarantee all payment gateways will share the same configuration fields.
I want to create a Doctrine ORM entity for my payment_gateways table, but I also want the config property to be its own entity where I can use its own getters and setters instead of accessing and setting properties directly from the config array.
Is single table inheritance a good way to approach this? I have tried this by creating a separate entity for each payment gateway I have integrated with and have extended the base entity PaymentGateway. In these entities I define the properties that I expect to be in the config property. Then I get/set the properties like so:
/**
* #ORM\Entity
*/
class PaypalGateway extends PaymentGateway
{
/**
* #return string|null
*/
public function getApiKey(): ?string
{
return $this->getConfig()['apiKey'] ?? null;
}
/**
* #param string $apiKey
*/
public function setApiKey(string $apiKey)
{
$data = $this->getConfig();
$data['apiKey'] = $apiKey;
$this->setConfig($data);
}
The parent PaymentGateway class looks like the following:
/**
* #ORM\Entity
* #ORM\Table(name="payment_gateways")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="provider_type", type="string")
* #ORM\DiscriminatorMap({
* "paypal" = "PaypalGateway",
* "stripe" = "StripeGateway"
* })
*/
abstract class PaymentGateway
{
**
* #ORM\Column(type="json")
*/
private ?array $config = null;
public function getConfig()
{
return $this->config;
}
public function setConfig($config)
{
$this->config = $config;
}
As far as I can tell, this is working correctly for me but I'm not sure if it's a good way to go about it. I was wondering if this is the correct approach or if there's something I'm missing completely?

How does Doctrine call a function that does not exist?

I'm trying to find out why a field is not saving. It's submitted fine and the page reloads with the same value, but it's not stored in the database. I have this in my project
$contact = new Contact($_POST);
$contact->Save();
However, the Save() function doesn't exist in the Contact class, and it doesn't extend anything! How is it able to call a function that doesn't exist? I don't believe PHP has monkeypatching. I know the function is called because I added print_r($_POST) right above it.
<?php
namespace ArcaSolutions\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Contact
*
* #ORM\Table(name="Contact")
* #ORM\Entity(repositoryClass="ArcaSolutions\CoreBundle\Repository\ContactRepository")
* #ORM\HasLifecycleCallbacks
*/
class Contact
{
My new field is defined as
/**
* #var string
*
* #ORM\Column(name="timezone", type="string", length=50, nullable=true)
*/
private $timezone;
/**
* Set timezone
*
* #param string $timezone
* #return Contact
*/
public function setTimezone($timezone)
{
$this->timezone = $timezone;
return $this;
}
/**
* Get timezone
*
* #return string
*/
public function getTimezone()
{
return $this->timezone;
}
It's an eDirectory project based on Symfony 2.8 and PHP 5.6.27.
As Padam87 suggests in the comments, you seem to mix Doctrine 1 with Doctrine 2 syntax. Doctrine 1 uses the Active Record pattern, whereas Doctrine 2 uses the Data Mapper pattern. A nice explaination of the differences can be found here: https://www.culttt.com/2014/06/18/whats-difference-active-record-data-mapper/
A Doctrine 2 entity will never have a save() or similar method. To persist or retrieve entities, you will have to use the entity manager. If you use Symfony, the entity manager can be injected as a service into your controllers or other services.
Read the Doctrine docs on how to work with an entity manager to learn more.
By the way, whatever you’re trying by injecting the $_POST data into your entity – this is a very bad idea. You should first process this data in your controller or some validator and only fill entities with validated content. Also, when using Symfony, you’ll want to get the POST data through the Request object which is passed to your action if you reference it in the signature.
Here’s a stub for a controller which gets the entity manager injected as a dependency (you should also register it the controller as a service in your bundle’s services.yml) and gets the POST data from the Request object:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Doctrine\ORM\EntityManagerInterface;
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function someAction(Request $request)
{
$data = $request->request->all();
// validate $data!!!
$contact = new Contact();
$contact->setSomeValue($data["value"]);
$this->entityManager->persist($contact);
$this->entityManager->flush();
return new Response("Ok", 200);
}
In Symfony, again, you could also inject an instance of Symfony\Component\Validator\Validator\ValidatorInterface and use Symfony’s validation component.
I found out why. There were two Contact classes and eDirectory was using the one in edirectory\web\classes\class_Contact.php. The other one I found was in edirectory\src\ArcaSolutions\CoreBundle\Entity\Contact.php. The one it really uses is defined as
class Contact extends Handle {
var $account_id;
...
function Save() {
$this->prepareToSave();
$dbObj = db_getDBObject(DEFAULT_DB,true);;
$sql = "UPDATE Contact SET"
. " updated = NOW(),"
. " first_name = $this->first_name,"
...
So it does have a Save() function.

Use custom annotations in Symfony User Provider

I'm trying to make a custom User Provider which uses a custom Doctrine-like bundle. This bundles uses entities, pretty much like Doctrine does :
/**
* #EntityMeta(table="MY_TABLE")
*/
class MyTable extends AbstractEntity
{
/**
* #var int
* #EntityColumnMeta(column="Code", isKey=true)
*/
protected $code;
/**
* #var int
* #EntityColumnMeta(column="Name")
*/
protected $name;
Those annotations work well when I use the doctrine-like manager provided by my bundle. This code works well :
public function indexAction(DoctrineLikeManager $manager)
{
$lines = $manager->getRepository('MyTable')->findBy(array(
'email' => 'test#test.com'
));
// do something with these
}
So I know annotations work. But when I use the same code, with the same entity, in the User Provider Class, I get the following error :
[Semantical Error] The annotation "#NameSpace\DoctrineLikeBundle\EntityColumnMeta" in property AppBundle\MyTable::$code does not exist, or could not be auto-loaded.
The UserProvider :
class HanaUserProvider implements UserProviderInterface
{
private $manager;
public function __construct(DoctrineLikeManager $manager)
{
$this->manager = $manager;
}
public function loadUserByUsername($username)
{
// this is where it fails :(
$lines = $this->manager->getRepository('MyTable')->findBy(array(
'email' => 'test#test.com'
));
// return user or throw UsernameNotFoundException
}
}
Is it possible to use custom annotations in that context ? Maybe I should do something in particular so custom annotations can be successfully loaded ?
Thanks in advance !
Ok, I found a solution, which might not be the best but works.
The thing is that annotations don't seem to use classic autoloading, as explained here.
I had to register a loader in my User Provider :
public function loadUserByUsername($username)
{
AnnotationRegistry::registerLoader('class_exists'); // THIS LINE HERE
$lines = $this->manager->getRepository('MyTable')->findBy(array(
'email' => 'test#test.com'
));
// return user or throw UsernameNotFoundException
}
However, the problem is that this method is deprecated and will be removed in doctrine/annotations 2.0.

Extend Entity with custom method to filter relations in Symfony2

I'm looking for a way to extend my Symfony2 (i currently use 2.3) Entity class with a method to effectively filter its relations on demand. So, imaging i have such 2 classes with OneToMany relation:
/**
* ME\MyBundle\Entity\Kindergarten
*/
class Kindergarten
{
/**
* #var integer $id
*/
private $id;
/**
* #var ME\MyBundle\Entity\Kinder
*/
private $kinders;
public function __construct()
{
$this->kinders = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get kinders
*
* #return Doctrine\Common\Collections\Collection
*/
public function getKinders()
{
return $this->kinders;
}
}
/**
* ME\MyBundle\Entity\Kinder
*/
class Kinder
{
/**
* #var integer $id
*/
private $id;
/**
* #var string $name
*/
private $name;
/**
* #var integer $age
*/
private $age;
}
My goal is to have a method on Kindergarten class to get on demand all kinders with age, for instance, between 10 and 12:
$myKindergarten->getKindersByAgeInInterval(10,12);
Of course, i can do something like:
class Kindergarten
{
...
public function getKindersByAgeInInterval($start, $end)
{
return $this->getKinders()->filter(
function($kinder) use ($start, $end)
{
$kinderAge = $kinder->getAge();
if($kinderAge < $start || $kinderAge > $end)
{
return false;
}
return true;
}
);
}
...
}
The solution above will work, but it's very inefficient, since I need to iterate across ALL kinders which can be a really big list and have no way to cache such filters. I have in mind usage of Criteria class or some proxy patterns, but not sure about a way to do it nice in Symfony2 especially since they probably will need access to EntityManager.
Any ideas?
I would suggest extracting this responsibility into an EntityRepository:
<?php
class KinderRepository extends \Doctrine\ORM\EntityRepository
{
public function findByKindergartenAndAge(Kindergarten $entity, $minAge = 10, $maxAge = 20)
{
return $this->createQueryBuilder()
->... // your query logic here
}
}
All the lookups should really happen in classes where you have access to the entity manager.
This is actually the way suggested by the Doctrine architecture. You can never have access to any services from your entities, and if you ever think you need it, well, then something is wrong with your architecture.
Of course, it may occur to you that the repository method could become pretty ugly if you later decide on adding more criteria (imagine you'll be searching by kindergarten, age, weight and height too, see http://www.whitewashing.de/2013/03/04/doctrine_repositories.html). Then you should consider implementing more logic, but again, that should not be that necessary.

Doctrine doesn't load associations from session correctly

I'm having this behavior with Doctrine 2.1 where I'm looking for a nice 'workaround'. The problem is as follows:
I have a user Entity:
/**
* #Entity(repositoryClass="Application\Entity\Repository\UserRepository")
* #HasLifecycleCallbacks
*/
class User extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var \DateTime
* #Column(type="datetime",nullable=false)
*/
protected $insertDate;
/**
*
* #var string
* #Column(type="string", nullable=false)
*/
protected $username;
/**
*
* #ManyToOne(targetEntity="UserGroup", cascade={"merge"})
*/
protected $userGroup;
}
And a usergroup entity:
/**
* #Entity
*/
class UserGroup extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var string
* #Column(type="string",nullable=false)
*/
protected $name;
}
If I instantiate a user object (doing this with Zend_Auth) and Zend_Auth puts it automatically the session.
The problem is however, that is I pull it back from the session at a next page then the data in the user class is perfectly loaded but not in the userGroup association. If I add cascade={"merge"} into the annotation in the user object the userGroup object IS loaded but the data is empty. If you dump something like:
$user->userGroup->name
You will get NULL back. The problem is no data of the usergroup entity is accesed before the user object is saved in the session so a empty initialized object will be returned. If I do something like:
echo $user->userGroup->name;
Before I store the user object in the session all data of the assocication userGroup is succesfully saved and won't return NULL on the next page if I try to access the $user->userGroup->name variable.
Is there a simple way to fix this? Can I manually load the userGroup object/association with a lifecycle callback #onLoad in the user class maybe? Any suggestions?
Your problem is a combination of what mjh_ca answered and a problem with your AbstractEntity implementation.
Since you show that you access entity fields in this fashion:
$user->userGroup->name;
I assume your AbstractEntity base class is using __get() and __set() magic methods instead of proper getters and setters:
function getUserGroup()
{
return $this->userGroup;
}
function setUserGroup(UserGroup $userGroup)
{
$this->userGroup = $userGroup;
}
You are essentially breaking lazy loading:
"... whenever you access a public property of a proxy object that hasn’t been initialized yet the return value will be null. Doctrine cannot hook into this process and magically make the entity lazy load."
Source: Doctrine Best Practices: Don't Use Public Properties on Entities
You should instead be accessing fields this way:
$user->getUserGroup()->getName();
The second part of your problem is exactly as mjh_ca wrote - Zend_Auth detaches your entity from the entity manager when it serializes it for storage in the session. Setting cascade={"merge"} on your association will not work because it is the actual entity that is detached. You have to merge the deserialized User entity into the entity manager.
$detachedIdentity = Zend_Auth::getInstance()->getIdentity();
$identity = $em->merge($detachedIdentity);
The question, is how to do this cleanly. You could look into implementing a __wakeup() magic method for your User entity, but that is also against doctrine best practices...
Source: Implementing Wakeup or Clone
Since we are talking about Zend_Auth, you could extend Zend_Auth and override the getIdentity() function so that it is entity aware.
use Doctrine\ORM\EntityManager,
Doctrine\ORM\UnitOfWork;
class My_Auth extends \Zend_Auth
{
protected $_entityManager;
/**
* override otherwise self::$_instance
* will still create an instance of Zend_Auth
*/
public static function getInstance()
{
if (null === self::$_instance) {
self::$_instance = new self();
}
return self::$_instance;
}
public function getEntityManager()
{
return $this->_entityManager;
}
public function setEntityManager(EntityManager $entityManager)
{
$this->_entityManager = $entityManager;
}
public function getIdentity()
{
$storage = $this->getStorage();
if ($storage->isEmpty()) {
return null;
}
$identity = $storage->read();
$em = $this->getEntityManager();
if(UnitOfWork::STATE_DETACHED === $em->getUnitOfWork()->getEntityState($identity))
{
$identity = $em->merge($identity);
}
return $identity;
}
}
And than add an _init function to your Bootstrap:
public function _initAuth()
{
$this->bootstrap('doctrine');
$em = $this->getResource('doctrine')->getEntityManager();
$auth = My_Auth::getInstance();
$auth->setEntityManager($em);
}
At this point calling $user->getUserGroup()->getName(); should work as intended.
When you store the entity to a session (via Zend_Auth or otherwise), the object is serialized and no longer maintained by Doctrine when subsequently retrieved and unserialized. Try merging the entity back into the EntityManager. See http://www.doctrine-project.org/docs/orm/2.1/en/reference/working-with-objects.html

Categories