diff --git a/fixtures/Entity/InitializationOrder/Address.php b/fixtures/Entity/InitializationOrder/Address.php new file mode 100644 index 000000000..490653c22 --- /dev/null +++ b/fixtures/Entity/InitializationOrder/Address.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Nelmio\Alice\Entity\InitializationOrder; + +final class Address +{ + private $country; + private $city; + + public function getCountry() + { + return $this->country; + } + + public function setCountry(string $country) + { + $this->country = $country; + } + + public function getCity(): string + { + return $this->city; + } + + public function setCity(string $city) + { + $this->city = $city; + } +} diff --git a/fixtures/Entity/InitializationOrder/Person.php b/fixtures/Entity/InitializationOrder/Person.php new file mode 100644 index 000000000..6d822e86b --- /dev/null +++ b/fixtures/Entity/InitializationOrder/Person.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Nelmio\Alice\Entity\InitializationOrder; + +final class Person +{ + private $city; + private $country; + + public static function createWithAddress($address) + { + $instance = new self(); + + $instance->city = $address->getCity(); + $instance->country = $address->getCountry(); + + return $instance; + } +} diff --git a/src/Generator/Resolver/Value/Chainable/FixtureReferenceResolver.php b/src/Generator/Resolver/Value/Chainable/FixtureReferenceResolver.php index f4ebf2678..f00d99317 100644 --- a/src/Generator/Resolver/Value/Chainable/FixtureReferenceResolver.php +++ b/src/Generator/Resolver/Value/Chainable/FixtureReferenceResolver.php @@ -27,6 +27,7 @@ use Nelmio\Alice\Generator\Resolver\Value\ChainableValueResolverInterface; use Nelmio\Alice\IsAServiceTrait; use Nelmio\Alice\Throwable\Exception\Generator\ObjectGenerator\ObjectGeneratorNotFoundExceptionFactory; +use Nelmio\Alice\Throwable\Exception\Generator\Resolver\CircularReferenceException; use Nelmio\Alice\Throwable\Exception\Generator\Resolver\FixtureNotFoundExceptionFactory; use Nelmio\Alice\Throwable\Exception\Generator\Resolver\UnresolvableValueException; use Nelmio\Alice\Throwable\Exception\Generator\Resolver\UnresolvableValueExceptionFactory; @@ -40,6 +41,11 @@ final class FixtureReferenceResolver implements ChainableValueResolverInterface, */ private $generator; + /** + * @var array + */ + private $incompleteObjects = []; + public function __construct(ObjectGeneratorInterface $generator = null) { $this->generator = $generator; @@ -104,6 +110,7 @@ private function getReferredFixture(string $id, ResolvedFixtureSet $set): Fixtur * @param string $referredFixtureId * @param ResolvedFixtureSet $fixtureSet * @param GenerationContext $context + * @param bool|null $passIncompleteObject * * @return ResolvedValueWithFixtureSet */ @@ -111,12 +118,18 @@ private function resolveReferredFixture( FixtureIdInterface $referredFixture, string $referredFixtureId, ResolvedFixtureSet $fixtureSet, - GenerationContext $context + GenerationContext $context, + bool $passIncompleteObject = null ): ResolvedValueWithFixtureSet { if ($fixtureSet->getObjects()->has($referredFixture)) { $referredObject = $fixtureSet->getObjects()->get($referredFixture); - if ($referredObject instanceof CompleteObject || false === $context->needsCompleteGeneration()) { + if ($referredObject instanceof CompleteObject + || $passIncompleteObject + || array_key_exists($referredFixtureId, $this->incompleteObjects) + ) { + $this->incompleteObjects[$referredFixtureId] = true; + return new ResolvedValueWithFixtureSet( $referredObject->getInstance(), $fixtureSet @@ -125,17 +138,38 @@ private function resolveReferredFixture( } // Object is either not completely generated or has not been generated at all yet + // Attempts to generate the fixture completely if (false === $referredFixture instanceof FixtureInterface) { throw FixtureNotFoundExceptionFactory::create($referredFixtureId); } - $context->markIsResolvingFixture($referredFixtureId); - $objects = $this->generator->generate($referredFixture, $fixtureSet, $context); - $fixtureSet = $fixtureSet->withObjects($objects); + try { + $needsCompleteGeneration = $context->needsCompleteGeneration(); + + if (!$passIncompleteObject) { + $context->markAsNeedsCompleteGeneration(); + } + $context->markIsResolvingFixture($referredFixtureId); + $objects = $this->generator->generate($referredFixture, $fixtureSet, $context); + $fixtureSet = $fixtureSet->withObjects($objects); - return new ResolvedValueWithFixtureSet( - $fixtureSet->getObjects()->get($referredFixture)->getInstance(), - $fixtureSet - ); + if (false === $needsCompleteGeneration) { + $context->unmarkAsNeedsCompleteGeneration(); + } + + return new ResolvedValueWithFixtureSet( + $fixtureSet->getObjects()->get($referredFixture)->getInstance(), + $fixtureSet + ); + } catch (CircularReferenceException $exception) { + if (false === $needsCompleteGeneration && null !== $passIncompleteObject) { + throw $exception; + } + + $context->unmarkAsNeedsCompleteGeneration(); + + // Could not completely generate the fixtures, fallback to generating an incomplete object + return $this->resolveReferredFixture($referredFixture, $referredFixtureId, $fixtureSet, $context, true); + } } } diff --git a/tests/Generator/Resolver/Value/Chainable/FixtureReferenceResolverTest.php b/tests/Generator/Resolver/Value/Chainable/FixtureReferenceResolverTest.php index 6817671ae..48c3ada77 100644 --- a/tests/Generator/Resolver/Value/Chainable/FixtureReferenceResolverTest.php +++ b/tests/Generator/Resolver/Value/Chainable/FixtureReferenceResolverTest.php @@ -121,30 +121,6 @@ public function testIfTheReferenceRefersToACompletelyGeneratedFixtureThenReturns $this->assertEquals($expected, $actual); } - public function testIfTheReferenceRefersToAnInstantiatedFixtureThatDoesNotRequireToBeCompleteThenReturnsItsInstance() - { - $value = new FixtureReferenceValue('dummy'); - $fixture = new FakeFixture(); - $set = ResolvedFixtureSetFactory::create( - null, - null, - (new ObjectBag())->with( - new SimpleObject( - 'dummy', - $expectedInstance = new \stdClass()) - ) - ); - $scope = []; - $context = new GenerationContext(); - - $expected = new ResolvedValueWithFixtureSet($expectedInstance, $set); - - $resolver = new FixtureReferenceResolver(new FakeObjectGenerator()); - $actual = $resolver->resolve($value, $fixture, $set, $scope, $context); - - $this->assertEquals($expected, $actual); - } - public function testIfTheReferenceRefersToAnInstantiatedFixtureAndRequiresToBeCompleteThenGenerateIt() { $value = new FixtureReferenceValue('dummy'); @@ -216,6 +192,7 @@ public function testIfTheReferenceRefersToANonInstantiatedFixtureThenGenerateItB $generatorContext = new GenerationContext(); $generatorContext->markIsResolvingFixture('dummy'); + $generatorContext->markAsNeedsCompleteGeneration(); $generatorProphecy = $this->prophesize(ObjectGeneratorInterface::class); $generatorProphecy @@ -239,8 +216,10 @@ public function testIfTheReferenceRefersToANonInstantiatedFixtureThenGenerateItB $resolver = new FixtureReferenceResolver($generator); $actual = $resolver->resolve($value, $fixture, $set, $scope, $context); + $generatorContext->unmarkAsNeedsCompleteGeneration(); + $this->assertEquals($expected, $actual); - $this->assertEquals($context, $generatorContext); + $this->assertEquals($generatorContext, $context); $generatorProphecy->generate(Argument::cetera())->shouldHaveBeenCalledTimes(1); } diff --git a/tests/Loader/LoaderIntegrationTest.php b/tests/Loader/LoaderIntegrationTest.php index 757316066..f3aae8a76 100644 --- a/tests/Loader/LoaderIntegrationTest.php +++ b/tests/Loader/LoaderIntegrationTest.php @@ -3339,5 +3339,42 @@ public function provideFixturesToGenerate() ], ], ]; + + // https://github.com/nelmio/alice/issues/752 + yield 'calls and factory order' => (function () { + return [ + [ + \Nelmio\Alice\Entity\InitializationOrder\Address::class => [ + 'address' => [ + 'country' => 'France', + 'city' => 'Paris', + ], + ], + \Nelmio\Alice\Entity\InitializationOrder\Person::class => [ + 'person' => [ + '__factory' => [ + 'createWithAddress' => [ + '@address', + ], + ], + ], + ], + ], + [ + 'parameters' => [], + 'objects' => [ + 'address' => $address = (function () { + $address = new \Nelmio\Alice\Entity\InitializationOrder\Address(); + + $address->setCountry('France'); + $address->setCity('Paris'); + + return $address; + })(), + 'person' => \Nelmio\Alice\Entity\InitializationOrder\Person::createWithAddress($address) + ], + ], + ]; + })(); } }