Symfony2 : Doctrine : PHPUnit : Set entity Id during flushing with mocked entity manager in unit tests - symfony

Symfony 2.8.13 / Doctrine ORM 2.5.5 / PHPUnit 5.7.5
I want to test a method of a class that makes use of the doctrine entity manager. This public method calls a private one that instantiates a Bookmark entity, flushes it and returns this entity. Then later, in the tested method I need to access the entity Id. Everything is mocked excepted the Bookmark entity itself. The main problem is that there is no setId() method in my entity. Here is the code and my main idea to solve this issue but I don't know if it is correct ?
Tested class and method
class BookmarkManager
{
//...
public function __construct(TokenStorageInterface $tokenStorage, ObjectManager $em, Session $session)
{
//...
}
public function manage($bookmarkAction, $bookmarkId, $bookmarkEntity, $bookmarkEntityId)
{
//...
$bookmark = $this->add($bookmarkEntity, $bookmarkEntityId);
//...
$bookmarkId = $bookmark->getId();
//...
}
private function add($entity, $entityId)
{
//...
$bookmark = new Bookmark();
//...
$this->em->persist($bookmark);
$this->em->flush();
return $bookmark;
}
}
Test
class BookmarkManagerTest extends \PHPUnit_Framework_TestCase
{
public function testThatRestaurantAdditionToBookmarksIsWellManaged()
{
//...
// THIS WON'T WORK AS NO setId() METHOD EXISTS
$entityManagerMock->expects($this->once())
->method('persist')
->will($this->returnCallback(function ($bookmark) {
if ($bookmark instanceof Bookmark) {
$bookmark->setId(1);
}
}));
//...
$bookManager = new BookmarkManager($tokenStorageMock, $entityManagerMock, $sessionMock);
//...
}
}
Solutions ?
1- Make usage of reflection class as proposed here :
$entityManagerMock->expects($this->once())
->method('persist')
->will($this->returnCallback(function ($bookmark) {
if ($bookmark instanceof Bookmark) {
$class = new \ReflectionClass($bookmark);
$property = $class->getProperty('id');
$property->setAccessible(true);
$property->setValue($bookmark, 1);
//$bookmark->setId(1);
}
}));
2- Create a test Boookmark entity that extends from the real one and add a setId() method. Then create a mock of this class and replace and customize the one got from the ReturnCallback method with this one ? It seems crappy...
Any thoughts ? Thanks for your help.

The reflection looks interesting but it decreases readability of tests (mixing with mocks makes the situation tough).
I would create a fake for entity manager and implements there setting id based on reflection:
class MyEntityManager implements ObjectManager
{
private $primaryIdForPersitingObject;
public function __construct($primaryIdForPersitingObject)
{
$this->primaryIdForPersitingObject = $primaryIdForPersitingObject;
}
...
public function persist($object)
{
$reflectionClass = new ReflectionClass(get_class($object));
$idProperty = $reflectionClass->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($object, $this->primaryIdForPersitingObject);
}
public function flush() { }
...
}
Once you implemented this, you can inject the instance of MyEntityManager and make your tests small and easier to maintain.
You test would look like
<?php
class BookmarkManagerTest extends \PHPUnit_Framework_TestCase
{
public function testThatRestaurantAdditionToBookmarksIsWellManaged()
{
// ...
$entityManager = MyEntityManager(1);
//...
$bookManager = new BookmarkManager($tokenStorageMock, $entityManager, $sessionMock);
//...
}
}
Of course, a situation may be harder if there is a need of setting different ids for many persisting objects. Then you can, for example, increase $primaryIdForPersitingObject on persist call
public function persist($object)
{
$reflectionClass = new ReflectionClass(get_class($object));
$idProperty = $reflectionClass->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($object, $this->primaryIdForPersitingObject);
$this->primaryIdForPersitingObject++;
}
It may be extended even further to have separate primaryIdForPersitingObject each entity class, and your tests will be still clean.

Related

Symfony 4: I decorated UrlGeneratorInterface, but it's not used, it uses CompiledUrlGenerator instead

I decorated UrlGeneratorInterface
app.decorator.url_generator:
class: App\CoreBundle\Routing\Extension\UrlGenerator
decorates: Symfony\Component\Routing\Generator\UrlGeneratorInterface
arguments: ['#app.decorator.url_generator.inner']
but it's not used in cases where some bundle in example executes $this->generator->generate(), and I tracked what Symfony does through XDebug and CompiledUrlGenerator is used instead. I can see where this happens, namely in Symfony\Component\Routing\Router in getGenerator it specifically checks for CompiledUrlGenerator::class. But I don't want to override vanilla Symfony code. How am I supposed to override/decorate/extend which class in order for mine to be chosen always, as I have special parameters I need to add to the path. Thank you in advance!
I found it.
app.decorator.router:
class: App\CoreBundle\Routing\Extension\Router
decorates: 'router.default'
arguments: ['#app.decorator.router.inner']
Decorating this actually makes all packages use your Router. And as the UrlGenerator it has the generate function which can be extended.
EDIT: On request I provide the router class as well:
class Router implements RouterInterface {
protected $innerRouter;
public function __construct(RouterInterface $innerRouter) {
$this->innerRouter = $innerRouter;
}
public function setContext(RequestContext $context)
{
$this->innerRouter->setContext($context);
}
public function getContext()
{
return $this->innerRouter->getContext();
}
public function getRouteCollection()
{
return $this->innerRouter->getRouteCollection();
}
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
//add here to $parameters...
return $this->innerRouter->generate($name, $parameters, $referenceType);
}
public function match($pathinfo)
{
$parameters = $this->innerRouter->match($pathinfo);
//add here to $parameters...
return $parameters;
}
}

Getter method for services in Symfony controller

Is it a good practice to have a service getter for frequently used services in a controller? For example I mean:
class SomeController Extends Contorller {
private function getSomethingManager()
{
return $this->get('myvendorname.something.manager');
}
}
Your example is a bit confusing because you can use the Doctrine service directly with your controller. You can inject it in your Action if you use the Autowire function.
public function test(EntityManagerInterface $em) {
}
Then you have the entity manager injected or you can load it over the controller with:
$this->getDoctrine()->getManager()
So this is not a real good example. When you use autowire all classes are registered as service and you can use it.
For database queries you have to use entities and repositories.
https://symfony.com/doc/current/doctrine.html
If you are above Symfony 3.3 you can use a Service Locater. You list all common services in Service Locator class. When you need to fetch a specific service from anywhere (from example, Controller, Command, Service so on), all you have to do is, inject ServiceLocator class and fetch required service via ServiceLocator:locate.
It is pretty simple and useful. It helps you to reduce dependency injection as well. Have a look at the full example in the link above.
class ServiceLocator implements ServiceLocatorInterface, ServiceSubscriberInterface
{
private $locator;
public function __construct(ContainerInterface $locator)
{
$this->locator = $locator;
}
public static function getSubscribedServices()
{
return [
ModelFactoryInterface::class,
CalculatorUtilInterface::class,
EntityManagerInterface::class,
AnotherClass::class,
AndAnother::class,
];
}
public function get(string $id)
{
if (!$this->locator->has($id)) {
throw new ServiceLocatorException(sprintf(
'The entry for the given "%s" identifier was not found.',
$id
));
}
try {
return $this->locator->get($id);
} catch (ContainerExceptionInterface $e) {
throw new ServiceLocatorException(sprintf(
'Failed to fetch the entry for the given "%s" identifier.',
$id
));
}
}
}
And this is how you use it: ServiceLocator->locate(AnotherClass::class);

Prevent the doctrine postLoad event for some cases

I have an entity BlogPost with a status property. This status property depends on an external API call which is handled via the doctrine postLoad event. All other properties are stored in the local database.
public function postLoad(BlogPost $post)
{
$this->postHandler->calculateStatus($post);
}
The problem is, in some cases i don't want to calculate the status at all. For example if i want to get only the description of all blogposts.
With the code above, all blog entities being loaded will trigger the postLoad event even if i just want to have values from a local database. That is very expensive and not acceptable.
So for example in my repository class i want to get all BlogPosts having a website without invoking the postLoad event.
public function findBlogPosts()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('bp')
->from('AppBundle:BlogPosts', 'bp')
->innerJoin('bp.website', 'w');
return $qb->getQuery()->getResult();
}
Is there a way to say "Yes, load the BlogPost collection, but do not fire event!" ???
Any other approaches? Custom event?
Thanks
Why don't just move this logic outside the post entity and event listener? If you know when you need to calculate the status you can do it explicitly.
For example
$post = $this->entityManager->find(BlogPost::class, $postId);
$status = $this->postHandler->calculateStatus($post);
The other approach I could suggest is not good but works. You could use lazy calculation and instead of calling $this->postHandler->calculateStatus($this) in postLoad event listener you could inject postHandler service into entity and perform the calculation in the moment you actually need it.
For example if you need calculation when calling $blogPost->getStatus() method, you could do it this way:
interface PostHandlerAwareInterface
{
public function setPostHandler(PostHandlerInterface $postHandler): void;
}
class EntityServiceInjectorEventSubscriber implements EventSubscriber
{
/** #var PostHandlerInterface */
private $postHandler;
public function postLoad($entity): void
{
$this->injectServices($entity);
}
public function postPersist($entity): void
{
$this->injectServices($entity);
}
private function injectServices($entity): void
{
if ($entity instanceof PostHandlerAwareInterface) {
$entity->setPostHandler($this->postHandler);
}
}
}
class BlogPost extends PostHandlerAwareInterface
{
/** #var PostHandlerInterface */
private $postHandler;
private $status;
public function setPostHandler(PostHandlerInterface $postHandler): void
{
$this->postHandler = $postHandler;
}
public function getStatus()
{
if (null === $this->status) {
$this->postHandler->calculateStatus($this);
}
return $this->status;
}
}
If you don't like this idea you still could manage it via (BUT I STRONGLY DO NOT RECOMMEND DO THIS DIRTY HACK) setting the flag to your entity event listener.
You could inject your entity event listener to the code and set flag before fetching data:
class BlogPostCalculateStatusListener
{
/** #var bool */
private $calculationEnabled = true;
public function suspendCalculation(): void
{
$this->calculationEnabled = false;
}
public function resumeCalculation(): void
{
$this->calculationEnabled = true;
}
public function postLoad(BlogPost $post): void
{
if ($this->calculationEnabled) {
$this->postHandler->calculateStatus($post);
}
}
}
$this->calculateStatusListener->suspendCalculation();
$blogPosts = $blogPostRepository->findBlogPosts();
$this->calculateStatusListener->resumeCalculation();
Hope this helps.
PS. If you want to get only the descriptions of all blog posts you can do this way:
class BlogPostRepository
{
public function findBlogPosts()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('bp.description')
->from('AppBundle:BlogPosts', 'bp')
->innerJoin('bp.website', 'w');
return $qb->getQuery()->getArrayResult();
}
}
getArrayResult does not invoke lifecycle callbacks.
Since i haven't found a real similar use case on the internet, i'll go for the following solution which seems the easiest and most acceptable cleanest to me. Maybe someone else could find this useful.
Implement a TransientLoadable Interface
interface TransientLoadable
{
public function isLoaded() : bool;
public function setLoaded(bool $loaded) : TransientLoadable;
public function setTransientLoadingFunction(\Closure $loadingFunction) :
TransientLoadable;
}
Implement the entity
class BlogPost implements TransientLoadable
{
...
}
Setup Loading function on postLoad Event
public function postLoad(BlogPost $post)
{
$func = function() use ($postHandler, $post)
{
//Since there may be another fields being loaded from the same API, catch them also since data is anyway in the same request
$postHandler->setAllDataFromAPI($post)
//Set the loading state to true to prevent calling the API again for the next property which may also be transient
$post->setLoaded(true);
}
$post->setTransientLoadingFunction($func)
}
Use the built-in lazy loading mechanism to get the property from the API only when it's needed
class BlogPost implements TransientLoadable
{
private function getStatus() : int
{
if (!$this->isLoaded) {
call_user_function($this->loadingFunction)
}
return $this->status;
}
private function getVisitorCount() : int
{
if (!$this->isLoaded) {
call_user_function($this->loadingFunction)
}
return $this->visitorCount;
}
}
So what's happening? Let's imagine we want to get the status and the visitor count, both are loaded via a single external API call.
If some api-dependent property of the entity is needed, all other properties gets loaded too (since we don't want to have for each property another call). This in ensured through the loaded function of the TransientLoadable interface. All data gets loaded by the setAllDataFromAPI function which is injected as a closure function.
I think that is not the cleanest solution. The loading stuf should be done by an extra layer on top of the entity class. Since sonata admin does not deal with such an layer, i think that this solution is cleaner than writing the loading mechanism directly to the entity class.
I am open to another suggestions or feedback
Thanks

How to inject dependency using Factory Pattern in Symfony2

I have this situation
abstract class Importer {
const NW = 1;
public static function getInstance($type)
{
switch($type)
{
case(self::NW):
return new NWImporter();
break;
}
}
protected function saveObject(myObject $myObject)
{
//here I need to use doctrine to save on mongodb
}
abstract function import($nid);
}
and
class NWImporter extends Importer
{
public function import($nid)
{
//do some staff, create myObject and call the parent method to save it
parent::saveObject($myObject);
}
}
and I want to use them like this
$importer = Importer::getInstance(Importer::NW);
$importer->import($nid);
my question is: how to inject doctrine to be used in saveObject method?
thanks
You need to configure your importer as a symfony service :
services:
test.common.exporter:
# put the name space of your class
class: Test\CommonBundle\NWImporter
arguments: [ "#doctrine" ]
then in NWImporter define a constructor with a parameter that will have the doctrine instance
public function __construct($doctrine)
{
$this->doctrine= $doctrine;
}
with this solution you can avoid using a factory method as symfony does it for you but if you wanna to keep it, When you call $importer = Importer::getInstance(Importer::NW); from your controller you can inject the doctrine argument in your factory method :
abstract class Importer {
const NW = 1;
public static function getInstance($type, $doctrine)
{
switch($type)
{
case(self::NW):
return new NWImporter($doctrine);
break;
}
}
protected function saveObject(myObject $myObject)
{
//here I need to use doctrine to save on mongodb
}
abstract function import($nid);
}
then in your controller you should to do something like that :
$doctrine = $this->container->get('doctrine');
$importer = Importer::getInstance(Importer::NW, $doctrine);
$importer->import($nid);

Where to transform raw data?

I'm starting a new bundle. Its goal is to display some statistics arrays and charts. The problem is I don' t know where to transform raw data into usable data in view's arrays and charts. I read lot of articles about keeping the controllers as thin as possible. And as far as I know, repositories are meant to extract data, not transform them.
Where am I supposed to transform my raw data, according to Symfony2 best practices?
it depends on your application but based on what you described looks like you need to define a Service and write all your logic there so your controller would look something like this
$customService = $this->get('my_custom_service');
$data = $customService->loadMyData();
read more about Services in Symfony: http://symfony.com/doc/current/book/service_container.html
Simply create your own, custom service that uses some repository/ies to extract the data and transform it into usable form.
Sample:
// repository
interface MyRepository {
public function findBySomething($something);
}
class MyRepositoryImpl extends EntityRepository implements MyRepository {
public function findBySomething($something) {
return $this->createQueryBuilder('a')
->where('a.sth = :sth')
->setParameter('std', $something)
->getQuery()
->getResult();
}
}
// service
interface MyService {
public function fetchSomeData();
}
class MyServiceImpl implements MyService {
/** #var MyRespostiory */
private $repo;
public function __construct(MyRepository $repo) {
$this->repo = $repo;
}
public function fetchSomeData() {
$rawData = $this->repo->findBySomething(123);
$data = [];
// do sth
return $data;
}
}
// final usage, eg. within a constructor
class MyConstructor extends Controller {
/** #var MyService */
private $myService;
public function __construct(MyService $myService) {
$this->myService = $myService;
}
public function someAction() {
// you could also get access to the service using $this->get('...')
$data = $this->myService->fetchSomeData();
return $this->render('SomeTemplate', [
'data' => $data
]);
}
}
// service declaration
<service id="myService" class="MyServiceImpl">
<argument type="service" id="doctrine.repository.my_repository" />
</service>

Resources