Custom proxy entities for fixture generation - symfony

So my question is very specific but I couldn't figure out how to make it even after several months of reflection. The following topic will be about Symfony, Doctrine and generating fixtures on-the-go for the tests.
I want to generate fixtures on the go from a test. The goal is to provide a very specific set of fixtures for each tests using helpers without sacrify the readability. That is the goal, so my idea was to create a tests/Resources/EntityProxy which is a mirror of the src/Entity folder, containing the same amount of classes with the exact same name. Each EntityProxy extends from its related Entity, use a custom trait to fill the properties easily.
You guessed it, I want to only use in tests the EntityProxy and use it directly into the functions to tests them. And there is a major issue with that, as Doctrine doesn't recognize the EntityProxy as an entity even if it extends from a real Entity.
Is there a way to say to Doctrine to persist an EntityProxy as its extended Entity?
__
The following code is an example of what I want as en EntityProxy:
namespace Tests\Resources\EntityProxy;
class User extends App\Entity\User
{
use FixtureGenerationTrait;
public function static makeDefault(): self
{
return static::generate([
'username' => self::getFaker()->username,
'email' => self::getFaker()->email,
...
]);
}
public function static make(array $data = []): self
{
$entity = static::makeDefault();
$entity = static::setValues($entity, $data);
return $entity;
}
}
And can be used in the tests like following: User::make(['name' => 'John Wick']);

Related

excluding tables with setExcludedDoctrineTables when loading fixtures with liip TestFixturesBundle

I'm trying to use Liip test bundle to load fixtures from code using the loadFixtures() method from FixturesTrait
However, I need to exclude the RESOURCE table that I don't want to be dropped in the process. If I understand correctly, this should be easy using the setExcludedDoctrineTables method according to the doc https://github.com/liip/LiipTestFixturesBundle/blob/master/doc/database.md
Unfortunately, when I run this code, the table RESOURCES gets dropped with all the others.
Can anybody see why ?
not sure if that's relevant but I'm using a mysql db in a separate docker container.
<?php
namespace App\Tests\Controller;
use Symfony\Component\Panther\PantherTestCase;
use Symfony\Component\Panther\Client;
use Facebook\WebDriver\WebDriverBy as By;
use Facebook\WebDriver\Exception\TimeoutException;
use Liip\FunctionalTestBundle\Test\WebTestCase;
use Symfony\Component\Panther\PantherTestCaseTrait;
use Liip\TestFixturesBundle\Test\FixturesTrait;
use App\Repository\UserRepository;
use App\DataFixtures\UserFixtures;
use App\DataFixtures\AccountFixtures;
abstract class AbstractPantherTest extends WebTestCase{
// use trait so we can combine Liip and Panther features
use PantherTestCaseTrait; // this is the magic. Panther is now available.
// provide fixtures loading feature
use FixturesTrait;
// #var Symfony\Component\Panther\Client
protected static $client;
//Initialize the test case
function setUp():void
{
static::bootKernel();
if(self::$client === null){
self::$client = self::createPantherClient(['browser' => PantherTestCase::FIREFOX]);
$this->setExcludedDoctrineTables(["RESOURCES"]);
$this->loadFixtures([
UserFixtures::class,
]);
// retrieve the test user
$userRepository = static::$container->get(UserRepository::class);
// retrieve the test user
$testUser = $userRepository->findOneByUsername('Administrator');
// simulate $testUser being logged in
self::doLogin($testUser->getUsername(), 'xxx');
}
}
}
Answering my own question as I just found out the issue. Sometimes a good night of sleep is all you need :)
So this code is actually working. The table RESOURCES got dropped because the setUp method was redefined in the concrete class implementing the test and that redefinition included another call to loadFixtures, but for a different data class and without the exclusion :
<?php
namespace App\Tests\Controller;
use App\DataFixtures\DiscountFixtures;
class DiscountControllerWebTest extends AbstractPantherTest
{
function setUp():void
{
parent::setUp();
$this->loadFixtures([
DiscountFixtures::class,
]);
}

What is the best way to create a singleton entity in Symfony 4?

I want to create a settings page, which only has a form in it. If the form is submitted it only updates settings entity but never creates another one. Currently, I achieved this like:
/**
* #param SettingsRepository $settingsRepository
* #return Settings
*/
public function getEntity(SettingsRepository $settingsRepository): Settings
{
$settings = $settingsRepository->find(1);
if($settings == null)
{
$settings = new Settings();
}
return $settings;
}
In SettingsController I call getEntity() method which returns new Settings entity (if the setting were not set yet) or already existing Settings entity (if setting were set at least once).
However my solution is quite ugly and it has hardcoded entity id "1", so I'm looking for a better solution.
Settings controller:
public function index(
Request $request,
SettingsRepository $settingsRepository,
FlashBagInterface $flashBag,
TranslatorInterface $translator,
SettingsService $settingsService
): Response
{
// getEntity() method above
$settings = $settingsService->getEntity($settingsRepository);
$settingsForm = $this->createForm(SettingsType::class, $settings);
$settingsForm->handleRequest($request);
if ($settingsForm->isSubmitted() && $settingsForm->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($settings);
$em->flush();
return $this->redirectToRoute('app_admin_settings_index');
}
return $this->render(
'admin/settings/index.html.twig',
[
'settings_form' => $settingsForm->createView(),
]
);
}
You could use Doctrine Embeddables here.
Settings, strictly speaking, should not be mapped to entities, since they are not identifiable, nor meant to be. That is, of course, a matter of debate. Really, a Settings object is more of a value object than an entity. Read here for more info.
So, in cases like these better than having a one to one relationship and all that fuzz, you probably will be fine with a simple Value Object called settings, that will be mapped to the database as a Doctrine Embeddable.
You can make this object a singleton by creating instances of it only in factory methods, making the constructor private, preventing cloning and all that. Usually, it is enough only making it immutable, meaning, no behavior can alter it's state. If you need to mutate it, then the method responsible for that should create a new instance of it.
You can have a a method like this Settings::createFromArray() and antoher called Settings::createDefaults() that you will use when you new up an entity: always default config.
Then, the setSettings method on your entity receieves only a settings object as an argument.
If you don't like inmutablity, you can also make setter methods for the Settings object.

Symfony2 - How to Dynamically get a Doctrine entity's Entity Manager

We are building a CMS and website building platform with a lot of different vendors and clients sites, so there are a lot of different content type entities, which are edited by generic controllers and helper services that don't necessarily (easily) know what the entity manager is for a given entity.
NOTE: We have several entityManagers to separate access to different databases, e.g. Global, Billing, Local, etc
There are many cases where we need to detect what is the entity's EntityManager. For example, we have a MediaHelper that dynamically associates media from a database with matching fields on the entity (this doesn't work with associations because the Media has to connect with literally any entity and you can't have that kind of dynamic association and we don't want a hundred different associations).
The media is in a bundle managed by the 'Local' EntityManager. But the entity may be in a 'Global' EntityManager (you can't assume it's in the same entity manager). So we need to detect and persist the right entity manager for the right entity.
So how do you recommend dynamically detecting the entityManager for an entity?
Original Custom Method
NOTE: the accepted answer is a much better solution. This is just here for archival purposes.
Here is a simple solution that works. But I don't know enough about Symfony and Doctrine to know if it's a bad idea? Does anyone else know? If not, I don't know why this wouldn't be in the core, as a Doctrine Utility.
I created an EntityHelper service that injects the Doctrine service into it:
gutensite_cms.entity_helper:
class: Gutensite\CmsBundle\Service\EntityHelper
arguments:
- "#doctrine"
Then in the entity helper is one simple function to get the Entity Manager for an entity (the config.yml registers the bundles for entity managers already):
/**
* Automagically find the entityManager for an entity.
* #param $entity
* #return mixed
*/
public function getManagerForEntity($entity) {
$className = \Doctrine\Common\Util\ClassUtils::getRealClass(get_class($entity));
foreach (array_keys($this->doctrine->getManagers()) as $name) {
if(in_array($className, $this->doctrine->getManager($name)->getConfiguration()->getMetadataDriverImpl()->getAllClassNames())) return $em;
}
}
NOTE: Doctrine Registry#getAliasNamespace already does something nearly identical to this foreach loop, I just modified the idea to return the entity manager instead of the namespace, e.g.
public function getAliasNamespace($alias) {
foreach (array_keys($this->getManagers()) as $name) {
try {
return $this->getManager($name)->getConfiguration()->getEntityNamespace($alias);
} catch (ORMException $e) {
}
}
throw ORMException::unknownEntityNamespace($alias);
}
Update 10/21/15: Entity Detection Code updated per #Cerad's suggestion.
Per the suggestion of #qooplmao, there is an easy method already in the Doctrine core.
// 1) get the real class for the entity with the Doctrine Utility.
$class = \Doctrine\Common\Util\ClassUtils::getRealClass(get_class($entity))
// 2) get the manager for that class.
$entityManager = $this->container->get('doctrine')->getManagerForClass($class);
Updated 10/22/15 After suggestions from Cerad and Qooplmao
Can you not give the entity manager a alias that suits the context it is for? You talk about Global, Billing, Local, so for example:
'service_manager' => array(
'aliases' => array(
'global_entity_manager' => 'My\Global\EntityManager',
'billing_entity_manager' => 'My\Billing\EntityManager',
'local_entity_manager' => 'My\Local\EntityManager',
),
)
You could also map the entity manager to the namespace of the entity.
So let's say you have a folder for your Global entities that is Global\Entity, then you could alias the entity manager for those entities Global\Entity\EntityManager. The advantage of this solution is that you can map several namespaces to the same entity manager, so your Billing and Global entities can easily share the same entity manager:
'service_manager' => array(
'aliases' => array(
'Global\Entity\EntityManager' => 'My\Global\EntityManager',
'Billing\Entity\EntityManager' => 'My\Global\EntityManager', // <-same name
'Local\Entity\EntityManager' => 'My\Local\EntityManager',
),
)
This only works if your entities in one namespace are manager by the same EntityManager instance. I can hardly believe this would not be the case in any project, but otherwise you should maybe reorganize a bit? :D

How to inject parameter in a set method at form submit?

When form is submitted, object's set method (League#setInformation) is called with corresponding data. All is working correctly. (See code below as an example)
I need to pass additional parameters to setInformation, namely current user id which is stored in session data.
That trick would help keeping session and model separate. Maybe useful in different situations too.
Do you know a way to deal with it?
class LeagueFormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('name');
$builder->add('information', 'collection', [
'type' => new LeagueInformationFormType(),
]);
}
public function setDefaultOptions(\Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver) {
$resolver->setDefaults([
'data_class' => 'xxx\Models\League',
]);
}
public function getName() {
return 'league';
}
}
class League {
public function getInformation() {
//...
}
public function setInformation($data) {
...
}
}
What I would do is declare form as a service, and inject the data from session. If you can, try to refactor your setInformation() function to two functions for example, so you dont have to provide all information through that one. However I think form events will help you set everything as you like.
If you are using Doctrine2 and the League class is actually a Doctrine2 Entity, I would recommend using a Doctrine2 subscriber/listener.
You can configure the subscriber/listener to do something either just before sending the data to the databse (onFlush), just after telling doctrine about a new entity (persist) or just before updating an existing record (update), whichever is the most appropiate in your case.
Inject the SecurityContext (#security.context in your DIC) into the subscriber/listener to pull out the current user information. (Make sure you check there is a user, because the subscriber wil also be run when nobody is logged in and a League object is saved)
The main advantage of this is that is does not pollute your form or controller. And if for some reason you create a League entity some other way the current user wil also be set.
Some docs:
http://doctrine-orm.readthedocs.org/en/latest/reference/events.html
It's a different story if you are not using Doctrine2 though.

Symfony2: Using Doctrine outside controller

I'm a bit of noob when it comes to OOP PHP, so please forgive me if I make this sound more complicated then it is.
Basically I am trying to clean up my controller as it's starting to get too cluttered.
I have my entities set up and I have also created a repository to add methods for some db queries to a sqlite database.
But now I also have to manipulate this data before outputting it, I've created a separate connector class that fetches additional info (from an XML web source) for each item being queried and then this gets added to the doctrine query data before being outputted.
I could manipulate this data in the repository but the data I am adding obviously doesn't originate from my entity. So I have therefore created a separate model class to add this data.
Please tell me if I'm on the right track.
In my entity repository I will have a custom method like this:
public function queryTop10All()
{
$query = $this->getEntityManager($this->em)
->createQueryBuilder('u')
->select('u.ratingkey, u.origTitle, u.origTitleEp, u.episode, u.season, u.year, u.xml, count(u.title) as playCount')
->from($this->class, 'u')
->groupBy('u.title')
->orderBy('playCount', 'desc')
->addOrderBy('u.ratingkey', 'desc')
->setMaxResults(10)
->getQuery();
return $query->getResult();
}
Now I created a new class in \Model\ChartsDataModel.php and I am injecting doctrine into it using a service and calling the custom method, getting the results and then adding the additional data from the web connector to it, like so:
namespace PWW\DataFactoryBundle\Model;
use Doctrine\ORM\EntityManager;
use PWW\DataFactoryBundle\Connector\XMLExtractor;
use PWW\DataFactoryBundle\Connector\WebConnector;
use PWW\ContentBundle\Entity\Settings;
class ChartsDataModel {
private $settings;
private $repository;
private $em;
public function __construct(EntityManager $em)
{
$this->settings = new Settings();
$this->repository = $this->settings->getGroupingCharts() ? 'PWWDataFactoryBundle:Grouped' : 'PWWDataFactoryBundle:Processed';
$this->em = $em;
}
public function getChartsTop10All()
{
$xmlExtractor = new XMLExtractor();
$webConnector = new WebConnector();
$results = $this->em->getRepository($this->repository)->queryTop10All();
$xml = $xmlExtractor->unXmlArray($results);
$outputArray = array();
foreach($xml as $item) {
$outputArray[] = array(
"ratingKey" => $item['ratingkey'],
"origTitle" => $item['origTitle'],
"origTitleEp" => $item['origTitleEp'],
"playCount" => $item['playCount'],
"episode" => $item['episode'],
"season" => $item['season'],
"year" => $item['year'],
"type" => $item['media']['type'],
"parent" => $webConnector->getMetaData($webConnector->getMetaDataParentKey($item['ratingkey'])),
"metadata" => $webConnector->getMetaData($item['ratingkey'])
);
}
return $outputArray;
}
}
The xmlExtractor class is used to pull out certain xml fields stored in a database field as a raw xml dump.
My config.yml:
services:
pww.datafactorybundle.model.charts_data_model:
class: PWW\DataFactoryBundle\Model\ChartsDataModel
arguments: [ #doctrine.orm.entity_manager ]
Then in my controller, I just instantiate a new ChartsDataModel and call the method like so:
namespace PWW\ContentBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
...
use PWW\DataFactoryBundle\Model\ChartsDataModel;
public function chartsAction()
{
$charts = new ChartsDataModel($this->getDoctrine()->getManager());
$top10Array = $charts->getChartsTop10All();
return $this->render('PWWContentBundle:Default:charts.html.twig', array('page' => 'charts', 'top10' => $top10Array));
}
I just want to know if I am doing this correctly and is there a better way of doing this (or right way)?
I'm also very new to Symfony and still getting my head around it. I just don't want to get into bad habits so I'm trying to do things right from the start.
I hope I explained this well enough :)
TIA
Just detected two things that are in the top of my head.
1 If you define the service like:
services:
pww.datafactorybundle.model.charts_data_model:
class: PWW\DataFactoryBundle\Model\ChartsDataModel
arguments: [ #doctrine.orm.entity_manager ]
Then you can inject it in the controller like described here, so you keep the service arguments out of the Controller:
public function chartsAction()
{
$myservice = $this->get('pww.datafactorybundle.model.charts_data_model');
$top10Array = $myservice->getChartsTop10All();
}
Secondly, I would not put this standard queries in the Model, I think is better to keep the models clean with their setters, getters and put this custom queries elsewhere like in a service that will handle all related Chart queries and you can instance from anywhere else.

Resources