doctrine / data-fixtures

Doctrine2 ORM Data Fixtures Extensions
http://www.doctrine-project.org
MIT License
2.78k stars 224 forks source link

ArgumentCountError - Fixtures with DI and with DependentFixtureInterface #295

Closed guillaume-a closed 5 years ago

guillaume-a commented 6 years ago

Hello,

I have a project with some fixtures for functionnal testing. And one of my fixtures, for my User Entity, has a dependency injection

class UserFixtures extends Fixture {
    private $encoder;
        public function __construct(UserPasswordEncoderInterface $encoder)
    {
        $this->encoder = $encoder;
    }
    public function load( ObjectManager $manager ) {
        //.....
    }
}

Thanks to symfony 4 and autowiring, it works when I use command line. Then, I wrote my functionnal test with the famous

$this->loadFixtures(array('App\DataFixtures\UserFixtures'));

At this point, I had an issue witch I solved with service_test.yaml

services:
    _defaults:
        public: true

    App\DataFixtures\UserFixtures:
        arguments: ['@security.password_encoder']

But now, I have more fixtures with dependencies.

class PermissionFixtures extends Fixture implements DependentFixtureInterface {
    public function load( ObjectManager $manager ) {
        //...
    }

    public function getDependencies()
    {
        return array(
            UserFixtures::class,
            EtablissementFixtures::class,
        );
    }
}

And I have this error : (the same I solved with injecting in services_test.yaml)

ArgumentCountError : Too few arguments to function App\DataFixtures\UserFixtures::__construct(), 0 passed in /path/to/project/vendor/doctrine/data-fixtures/lib/Doctrine/Common/DataFixtures/Loader.php on line 193 and exactly 1 expected

After some breakpoints, I found this in doctrine code :

if ($fixture instanceof OrderedFixtureInterface) {
    $this->orderFixturesByNumber = true;
} elseif ($fixture instanceof DependentFixtureInterface) {
    $this->orderFixturesByDependencies = true;
    foreach($fixture->getDependencies() as $class) {
        if (class_exists($class)) {
            $this->addFixture($this->createFixture($class));
        }
    }
}

So, if my fixture implements DependentFixtureInterface it goes through createFixture method.

    protected function createFixture($class)
    {
        return new $class();
    }

And I see now, it just create the fixture with new UserFixture() and no argument is passed.

So my question is :

Is this considered a bug ? Is there some kind of workaround ?

Thank you for reading.

weaverryan commented 6 years ago

Hi there!

Where does the $this->loadFixtures() method come from? I’m not familiar with it.

This metho appears to be circumventing the normal way that the fixtures are loaded. I can’t give more directions because I’m not at a computer, but the way you are loading the fixtures is at the root of the problem.

Cheers!

guillaume-a commented 6 years ago

Hi,

$this->loadFixtures() comes from Liip\FunctionalTestBundle\Test\WebTestCase, the FunctionalTestBundle I use for functionnal testing on symfony.

And they have a chapter on fixtures which I followed.

Regards.

BonBonSlick commented 5 years ago

@guillaume-a tell me please how you solved that?

imports :
  - { resource: '../../parameters.xml' }

doctrine:
  dbal:
    default_connection: default
    connections       :
      default:
        driver :   pdo_sqlite
        path   :    '%kernel.cache_dir%/test.db'
        memory : false
        charset: UTF8

services test


  <services>
    <defaults autowire="true"
              autoconfigure="true"
              public="false"
    >
    </defaults>

    <service id="RoleFactoryInterface"
             alias="App\Factory\RoleFactoryInterface"
             public="true"
    />
    <service id="UserFactoryInterface"
             alias="App\Factory\UserFactoryInterface"
             public="true"
    />
  </services>

tests setUp()

    $container = static::$container;
    $loader                      = new ContainerAwareLoader($container);
    $entityManager               = $this->getEntityManager();
    $referenceFixturesRepository = new ReferenceRepository($entityManager);
    $roleFactory                 = $container->get(RoleFactoryInterface::class);
    $roleFixtures                = new RoleFixtures($roleFactory);
    $roleFixtures->setReferenceRepository($referenceFixturesRepository);
    $roleFixtures->setContainer($container);
    $loader->addFixture($roleFixtures);
    $userFactory  = $container->get(UserFactoryInterface::class);
    $userFixtures = new UserFixtures($userFactory);
    $userFixtures->setReferenceRepository($referenceFixturesRepository);
    $userFixtures->setContainer($container);
    $loader->addFixture($userFixtures);

ERROR ArgumentCountError: Too few arguments to function App\DataFixtures\RoleFixtures::__construct(), 0 passed in /var/www/hint/vendor/doctrine/data-fixtures/lib/Doctrine/Common/DataFixtures/Loader.php on line 193 and exactly 1 expected

Because of

abstract class AbstractDataFixture extends Fixture implements ContainerAwareInterface{

  private $container;

  public function load(ObjectManager $manager) : void {
    if (null === $this->container) {
      throw new InvalidArgumentException(\sprintf('Expected %s got %s', ContainerInterface::class, 'null'));
    }

    $kernel = $this->container->get('kernel');
    if (true === \in_array($kernel->getEnvironment(), $this->environments(), true)) {
      $this->doLoad($manager);
    }
  }
....

final class UserFixtures extends AbstractDataFixture implements DependentFixtureInterface{
  private $userFactory;

  public function __construct(UserFactoryInterface $userFactory) {
    $this->userFactory = $userFactory;
  }

  public function getDependencies() : array {
    return [
      RoleFixtures::class, // this
    ];
  }
....

Same for other fixtures

Information for Service "App\DataFixtures\RoleFixtures"
==================================================================================

 ---------------- ---------------------------------------------------------- 
  Option           Value                                                     
 ---------------- ---------------------------------------------------------- 
  Service ID       App\DataFixtures\RoleFixtures  
  Class            App\DataFixtures\RoleFixtures  
  Tags             doctrine.fixture.orm                                      
  Public           no                                                        
  Synthetic        no                                                        
  Lazy             no                                                        
  Shared           yes                                                       
  Abstract         no                                                        
  Autowired        yes                                                       
  Autoconfigured   yes    

Error even when loaded from container

 $roleFixtures = $container->get(RoleFixtures::class);
    $loader->addFixture($roleFixtures);

    $userFixtures = $container->get(UserFixtures::class);
    $loader->addFixture($userFixtures);

This is package issue, can be solved by implementing ContaierAwareInterface and get dependent classes from assigned container in Loader

  foreach($fixture->getDependencies() as $class) {
                    if (class_exists($class)) {
//                        $this->addFixture($this->createFixture($class)); 
// if fixture implements ContainerAwareInterface, load dpendencies from container
                        $this->addFixture($fixture->container()->get($class));
                    }
                }

Of course we may add some additional checks like try to create class, if ArgumentCountError: error load from container. We may cache fixtures when they loaded at least once, this way if any dependent fixture requires other fixture, load it from cache, not in cache? Create, error? Load from container.

Also in ContainerAwareLoader we set container, but never use it in Loader.

Please fix it, or anyone has other solution? I dont understand why all related issues closed without solutions.

alcaeus commented 5 years ago

After taking another look, this looks like an issue not with this library but rather with the LiipFunctionalTestBundle not supporting dependency injection in fixtures which is provided using the Doctrine\Bundle\FixturesBundle\Loader\SymfonyFixturesLoader and compiler passes to ensure fixture services are injected into the fixture loader.

Relevant discussion can be fixed in https://github.com/liip/LiipFunctionalTestBundle/pull/432 and https://github.com/liip/LiipFunctionalTestBundle/issues/411. This will apparently be fixed in the upcoming 2.0 release. See https://github.com/liip/LiipFunctionalTestBundle/issues/381 for relevant discussion on this.

As this is not an issue with this library or our bundle, I'm closing this issue.

BonBonSlick commented 5 years ago

@alcaeus finally it worked with SymfonyFixturesLoader, thank you very much!