Environment specific data fixtures with Symfony+Doctrine - symfony

With Smyfony2 and Doctrin2, data fixtures can be created using the following example: http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
What I would like is to be able to use this concept for testing so that setup/teardown can create a pure test data environment for functional testing. How might I go about having a specific set of test-only fixtures run during functional tests and how do I separate these fixtures from my standard fixtures so that the console command ignores them?
It seems that the way to do it would be to replicate the functionality of the doctrine:fixtures console command and store the test fixtures elsewhere. Does anyone have a better solution?

An alternative to breaking out fixtures by directory is to use a custom fixture class. Your fixture classes would then extend this class and specify the environments it will actually be loaded in.
<?php
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
/**
* Provides support for environment specific fixtures.
*
* This container aware, abstract data fixture is used to only allow loading in
* specific environments. The environments the data fixture will be loaded in is
* determined by the list of environment names returned by `getEnvironments()`.
*
* > The fixture will still be shown as having been loaded by the Doctrine
* > command, `doctrine:fixtures:load`, despite not having been actually
* > loaded.
*
* #author Kevin Herrera <kevin#herrera.io>
*/
abstract class AbstractDataFixture implements ContainerAwareInterface, FixtureInterface
{
/**
* The dependency injection container.
*
* #var ContainerInterface
*/
protected $container;
/**
* {#inheritDoc}
*/
public function load(ObjectManager $manager)
{
/** #var KernelInterface $kernel */
$kernel = $this->container->get('kernel');
if (in_array($kernel->getEnvironment(), $this->getEnvironments())) {
$this->doLoad($manager);
}
}
/**
* {#inheritDoc}
*/
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
/**
* Performs the actual fixtures loading.
*
* #see \Doctrine\Common\DataFixtures\FixtureInterface::load()
*
* #param ObjectManager $manager The object manager.
*/
abstract protected function doLoad(ObjectManager $manager);
/**
* Returns the environments the fixtures may be loaded in.
*
* #return array The name of the environments.
*/
abstract protected function getEnvironments();
}
Your fixtures would end up looking like this:
<?php
namespace Vendor\Bundle\ExampleBundle\DataFixtures\ORM;
use AbstractDataFixture;
use Doctrine\Common\Persistence\ObjectManager;
/**
* Loads data only on "prod".
*/
class ExampleData extends AbstractDataFixture
{
/**
* #override
*/
protected function doLoad(ObjectManager $manager)
{
// ... snip ...
}
/**
* #override
*/
protected function getEnvironments()
{
return array('prod');
}
}
I believe that this should work with both ORM an ODM data fixtures.

The easiest way is to put your fixtures into different folders and then load them with the php app/console doctrine:fixtures:load --fixtures=../src/Acme/TestBundle/DataFixtures/ORM/test command. The fixtures option must point to the relative path from where you app folder!
You can then split up your data into initial, test and so on or create dev, test, staging, prod fixtures, just as you like.
If you want to mix them up, I don't know any better solution than what I did: I createt a "templates" folder where all fixtures reside in. In my dev folder, I create one class which extends the proper fixture class from template and adjusts what is needed to adjust (like overriding the getOrder method). It's not perfect and I guess one could think about extending the fixtures:load command to take multiple paths, but it works for me.

Related

Use Action class instead of Controller in Symfony

I am adherent of Action Class approach using instead of Controller. The explanation is very simple: very often Controller includes many actions, when following the Dependency Injection principle we must pass all required dependencies to a constructor and this makes a situation when the Controller has a huge number of dependencies, but in the certain moment of time (e.g. request) we use only some dependencies. It's hard to maintain and test that spaghetti code.
To clarify, I've already used to work with that approach in Zend Framework 2, but there it's named Middleware. I've found something similar in API-Platform, where they also use Action class instead of Controller, but the problem is that I don't know how to cook it.
UPD:
How can I obtain the next Action Class and replace standard Controller and which configuration I should add in regular Symfony project?
<?php
declare(strict_types=1);
namespace App\Action\Product;
use App\Entity\Product;
use Doctrine\ORM\EntityManager;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SoftDeleteAction
{
/**
* #var EntityManager
*/
private $entityManager;
/**
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #Route(
* name="app_product_delete",
* path="products/{id}/delete"
* )
*
* #Method("DELETE")
*
* #param Product $product
*
* #return Response
*/
public function __invoke(Request $request, $id): Response
{
$product = $this->entityManager->find(Product::class, $id);
$product->delete();
$this->entityManager->flush();
return new Response('', 204);
}
}
The question is a bit vague for stackoverflow though it's also a bit interesting. So here are some configure details.
Start with an out of the box S4 skeleton project:
symfony new --version=lts s4api
cd s4api
bin/console --version # 4.4.11
composer require orm-pack
Add the SoftDeleteAction
namespace App\Action\Product;
class SoftDeleteAction
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function __invoke(Request $request, int $id) : Response
{
return new Response('Product ' . $id);
}
}
And define the route:
# config/routes.yaml
app_product_delete:
path: /products/{id}/delete
controller: App\Action\Product\SoftDeleteAction
At this point the wiring is almost complete. If you go to the url you get:
The controller for URI "/products/42/delete" is not callable:
The reason is that services are private by default. Normally you would extend from AbstractController which takes care of making the service public but in this case the quickest approach is to just tag the action as a controller:
# config/services.yaml
App\Action\Product\SoftDeleteAction:
tags: ['controller.service_arguments']
At this point you should have a working wired up action.
There of course many variations and a few more details. You will want to restrict the route to POST or fake DELETE.
You might also consider adding an empty ControllerServiceArgumentsInterface and then using the services instanceof functionality to apply the controller tag so you no longer need to manually define your controller services.
But this should be enough to get you started.
The approach I was trying to implement is named as ADR pattern (Action-Domain-Responder) and Symfony has already supported this started from 3.3 version. You can refer to it as Invokable Controllers.
From official docs:
Controllers can also define a single action using the __invoke() method, which is a common practice when following the ADR pattern (Action-Domain-Responder):
// src/Controller/Hello.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* #Route("/hello/{name}", name="hello")
*/
class Hello
{
public function __invoke($name = 'World')
{
return new Response(sprintf('Hello %s!', $name));
}
}

How to Inject Services in another Service using Factory to create them

I have a little problem with the creation of my services in symfony 4.1
I use Factories to create my services, and to force the Factory to have the expected method I made an Interface
<?php
namespace App\Service\Factory\Interfaces;
use App\Service\Interfaces\BaseModelServiceInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
/**
* Interface ModelServiceFactoryInterfaces
* #package App\Service\Factory\Interfaces
*/
interface ModelServiceFactoryInterfaces
{
/**
* Create the Model related Service
*
* #return BaseModelServiceInterface
*/
public function createService(DocumentManager $dm);
}
I get the DocumentManager from autowired services to create the Repository in the Factory and pass it to the service, like this
/**
* Class ChapterServiceFactory
* #package App\Service\Factory
*/
class ChapterServiceFactory implements ModelServiceFactoryInterfaces
{
/**
* #param DocumentManager $dm
* #return ChapterService|BaseModelServiceInterface
*/
public function createService(DocumentManager $dm)
{
$chapterRepository = $dm->getRepository(Chapter::class);
/**
* #var $chapterRepository ChapterRepository
*/
return new ChapterService($chapterRepository);
}
}
The problem with that is, if I want to have another service in my ChapterService I can't autowire it in the Factory because of the Interface, but I don't want to delete the Interface either.
Is there a way to have "dynamic arguments" with an Interface, or another way than the Interface to force the Factories to have the createService method ?
There's no workaround for this: if you declare an interface as the argument, then you need to provide an explicit implementation for that interface. This means that you cannot declare two default implementations for the same interface. Only thing you can do in this case is to declare explicitly the service and all its arguments.
By the way, as you're writing about Doctrine Repositories, I suggest you to take a look at ServiceEntityRepository: extending this class from one of your repos will make automatically the report a service that you can inject where needed.

Multiple files for Doctrine Fixtures for Symfony 3

I'm using the bundle Doctrine:Fixtures to load an example of bbdd throw the Entitys and since I work alone in the project it's ok.
But now, I got a colleague in my project and we were wondering if it's possible to set up a different files for the loading.
I got my own file of fixtures associated in git and I don't want him modifying this file. I would like to have an special file just for him that will allows him to modify whenever he wants this file of fixtures. So, anyone can have his owns records in the init of the bbdd.
If it's not possible with multiple files, could be possible in another way?
http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
You can load specific fixture files using the --fixture flag:
php app/console doctrine:fixtures:load --fixture=/src/BundleName/DataFixtures/ORM/Fixture.php
Or, you could add a Fixtures.php.dist file with some working examples, then use .gitignore to ignore Fixtures.php.
Then add a command into your build (or in composer scripts), and/or your documentation to copy this .dist file to Fixtures.php when checking out the project.
Another method if you're doing BDD is to create a Helper class that can be used in your Context to create and persist entities as you need them in tests. This would allow you to create only specifics needed for the test. All it really needs is the EntityManager so it may be simpler than pre-defining all the fixtures up front.
You can use Faker to generate realistic entities.
class AbstractFixtureHelper implements ContainerAwareInterface
{
/**
* #var Generator
*/
protected $faker;
/**
* #var ContainerInterface
*/
protected $container;
public function __construct()
{
$this->faker = Factory::create();
}
/**
* #param ContainerInterface|null $container
* #return void
*/
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
/**
* #return EntityManager
*/
protected function getEntityManager()
{
return $this->container->get('doctrine.orm.entity_manager');
}
}
Then for different entities - in this example, a user:
class UserFixtureHelper extends AbstractFixtureHelper
{
public function createUser()
{
$user = new User();
$user->setEmail($this->faker->email);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
return $user;
}
}
Then in your Context, inject the UserFixtureHelper and create directly in the scenario steps.
/**
* #Given there is a User who XXX
*/
public function thereIsAUser()
{
$user = $this->userFixtureHelper->createUser();
}

Parameters in behat.yml

I want to make
behat.yml -
default:
extensions:
Behat\MinkExtension\Extension:
base_url: 'my-url'
a parameter pulled from parameters.yml... Is this possible? I made a mink_base_url parameter in parameters.yml and then added
imports:
- { resource: parameters.yml }
to behat.yml. No matter what I do, I get this
[Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException]
The service "behat.mink.context.initializer" has a dependency on a non-existent parameter "mink_base_url"
Behat configuration is in no way related to Symfony's. It's true that Behat uses Symfony's DI container, but it's a separate instance.
If wanted to implement it, you'd probably need to create your own Behat extension to support the imports section.
This worked for me with Symfony 3. Just omit base_url from behat.yml, and set it from the container parameters. Thanks to #DanielM for providing the hint.
<?php
use Behat\MinkExtension\Context\MinkContext;
use Symfony\Component\DependencyInjection\ContainerInterface;
class FeatureContext extends MinkContext {
/**
* FeatureContext constructor.
* #param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* #BeforeScenario
*/
public function setUpTestEnvironment()
{
$this->setMinkParameter('base_url', $this->container->getParameter('my_url'));
}
}
It is possible to access the symfony parameters within behat yaml as using
- '%%name_of_the_parameter%%'
Double percentage sign (%%) does the trick.
If you just want to access base_url, you can get it once mink has been started.
$this->getMinkParameter('base_url');
Here's an example :
class AbstractBehatContext extends MinkContext {
/**
* The base url as set behat.yml
* #var bool
*/
protected $baseUrl;
/**
* #BeforeScenario
*/
public function getBaseUrl() {
$this->baseUrl = $this->getMinkParameter('base_url');
}
}
Note, this needs to be able to access Mink, so it won't work in __construct or in #BeforeSuite. Additionally #BeforeScenario will be called at the start of every scenario which is going to set it pointlessly a lot.

FOSRestBundle's serializer throws recursion error using inherited entities

I’m developing an application that inherits abstract classes. These abstract classes have their own mapping for the serializer as shown in the example bellow
Hezten\CoreBundle\Model\Enroled:
exclusion_policy: ALL
And the abstract class:
<?php
namespace Hezten\CoreBundle\Model;
abstract class Enroled implements EnroledInterface
{
protected $student;
protected $subject;
//Some functions...
}
The class that inherits the previous class looks as follows
<?php
namespace XXX\XXXBundle\Entity;
use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Exclude;
use Doctrine\ORM\Mapping as ORM;
use Hezten\CoreBundle\Model\Enroled as BaseEnroled;
/**
* #ORM\Entity
* #ExclusionPolicy("NONE")
*/
class Enroled extends BaseEnroled
{
/** #ORM\Id
* #ORM\ManyToOne(targetEntity="XXX\XXXBundle\Entity\Student", inversedBy="enroled")
* #Exclude
*/
protected $student;
/** #ORM\Id
* #ORM\ManyToOne(targetEntity="XXX\XXXBundle\Entity\Subject", inversedBy="enroled")
* #Exclude
*/
protected $subject;
/** #ORM\Column(type="boolean") */
private $field0;
/** #ORM\Column(type="boolean")
*/
private $field1;
/** #ORM\Column(type="boolean") */
private $field2;
}
The error thrown says this:
Warning: json_encode() [<a href='function.json-encode'>function.json-encode</a>]: recursion detected in C:\xampp\htdocs\Project\vendor\jms\serializer\src\JMS\Serializer\JsonSerializationVisitor.php line 29
For sure, I'm doing something wrong as no entities are exposed, just three fields of "Enroled" entity according to the mappings but I have no clue. I spent a couple of days trying to figure out what's the mistake without success.
What is the proper way to do the mapping of inhertied properties?
Update
Code used to serialize JSON using FOSRestBundle:
$students = $this->get('hezten_core.manager.enroled')->findEnroledBySubject($subject);
$view = View::create()
->setStatusCode(200)
->setFormat('json')
->setData($students);
return $this->get('fos_rest.view_handler')->handle($view);
Finally the API is working... I had to override the metadata of the classes I was inheriting adding the following lines to the config.yml
jms_serializer:
metadata:
directories:
HeztenCoreBundle:
namespace_prefix: "Hezten\\CoreBundle"
path: "%kernel.root_dir%/serializer/HeztenCoreBundle"
In the path that is selected above I added one yml file for each Model setting exclusion policy to ALL:
Hezten\CoreBundle\Model\Enroled:
exclusion_policy: ALL
And I used annotations on the entities that were inheriting those models to expose required info.
I don't know if this is the best approach but works well for me

Resources