├── .gitattributes ├── tests └── Provider │ └── Doctrine │ ├── Fixtures │ ├── TestProxy │ │ └── .gitignore │ ├── TestEntity │ │ ├── NotAnEntity.php │ │ ├── Person │ │ │ └── User.php │ │ ├── SpaceStation.php │ │ ├── Name.php │ │ ├── Commander.php │ │ ├── Badge.php │ │ ├── Person.php │ │ └── SpaceShip.php │ ├── TestAnotherEntity │ │ └── Artist.php │ ├── ReferenceTest.php │ ├── TestCase.php │ ├── ExtraConfigurationTest.php │ ├── TransitiveReferencesTest.php │ ├── TimeTest.php │ ├── BidirectionalReferencesTest.php │ ├── SequenceTest.php │ ├── IncorrectUsageTest.php │ ├── SingletonTest.php │ ├── ReferencesTest.php │ ├── PersistingTest.php │ └── BasicUsageTest.php │ ├── ORM │ ├── TestEntity │ │ └── User.php │ ├── TestCase.php │ ├── RepositoryTest.php │ └── DateIntervalHelperTest.php │ ├── FixtureFactoryTest.php │ ├── EntityDefinitionUnavailableTest.php │ ├── TestDb.php │ └── Types │ └── StatusArrayTest.php ├── .gitignore ├── .editorconfig ├── src └── Provider │ └── Doctrine │ ├── Exception.php │ ├── ORM │ ├── Locking │ │ ├── LockException.php │ │ ├── VersionLockable.php │ │ ├── TableLockMode.php │ │ └── TableLock.php │ ├── QueryBuilder.php │ └── Repository.php │ ├── EntityDefinitionUnavailable.php │ ├── DBAL │ └── Types │ │ └── StatusArrayType.php │ ├── DateIntervalHelper.php │ ├── FieldDef.php │ ├── EntityDef.php │ └── FixtureFactory.php ├── phpunit.xml.dist ├── .php_cs.dist ├── composer.json └── README.markdown /.gitattributes: -------------------------------------------------------------------------------- 1 | /.github/ export-ignore 2 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/TestProxy/.gitignore: -------------------------------------------------------------------------------- 1 | *.php 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .php_cs 3 | .php_cs.cache 4 | composer.lock 5 | phpunit.xml 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 4 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.yml] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /src/Provider/Doctrine/Exception.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | } 25 | 26 | public function getId() 27 | { 28 | return $this->id; 29 | } 30 | 31 | public function getName() 32 | { 33 | return $this->name; 34 | } 35 | 36 | public function setName($name) 37 | { 38 | $this->name = $name; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/FixtureFactoryTest.php: -------------------------------------------------------------------------------- 1 | prophesize(EntityManager::class)->reveal(); 20 | 21 | $fixtureFactory = new FixtureFactory($entityManager); 22 | 23 | $this->expectException(EntityDefinitionUnavailable::class); 24 | 25 | $fixtureFactory->get('foo'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | ./tests/ 18 | 19 | 20 | 21 | 22 | 23 | ./src/ 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/TestEntity/Name.php: -------------------------------------------------------------------------------- 1 | first; 37 | } 38 | 39 | public function last(): ?string 40 | { 41 | return $this->last; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/EntityDefinitionUnavailableTest.php: -------------------------------------------------------------------------------- 1 | getMessage()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/TestEntity/Commander.php: -------------------------------------------------------------------------------- 1 | name = new Name(); 35 | } 36 | 37 | public function id(): string 38 | { 39 | return $this->id; 40 | } 41 | 42 | public function name(): Name 43 | { 44 | return $this->name; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/TestEntity/Badge.php: -------------------------------------------------------------------------------- 1 | label = $label; 28 | $this->owner = $owner; 29 | } 30 | 31 | public function getId() 32 | { 33 | return $this->id; 34 | } 35 | 36 | public function getLabel() 37 | { 38 | return $this->label; 39 | } 40 | 41 | public function getOwner() 42 | { 43 | return $this->owner; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/TestEntity/Person.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | $this->spaceShip = $spaceShip; 29 | } 30 | 31 | public function getId() 32 | { 33 | return $this->id; 34 | } 35 | 36 | public function getName() 37 | { 38 | return $this->name; 39 | } 40 | 41 | public function getSpaceShip() 42 | { 43 | return $this->spaceShip; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/ORM/TestCase.php: -------------------------------------------------------------------------------- 1 | 13 | * @license http://www.opensource.org/licenses/BSD-3-Clause New BSD License 14 | */ 15 | abstract class TestCase extends Framework\TestCase 16 | { 17 | /** 18 | * @var TestDb 19 | */ 20 | protected $testDb; 21 | 22 | /** 23 | * @var \Doctrine\ORM\EntityManager 24 | */ 25 | protected $em; 26 | 27 | protected function setUp() 28 | { 29 | parent::setUp(); 30 | 31 | $here = dirname(__FILE__); 32 | 33 | $this->testDb = new TestDb( 34 | $here . '/TestEntity', 35 | $here . '/TestProxy', 36 | 'FactoryGirl\Tests\Provider\Doctrine\ORM\TestProxy' 37 | ); 38 | 39 | $this->em = $this->testDb->createEntityManager(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | in(__DIR__); 4 | 5 | return PhpCsFixer\Config::create() 6 | ->setRiskyAllowed(true) 7 | ->setRules([ 8 | '@PSR2' => true, 9 | 'array_syntax' => [ 10 | 'syntax' => 'short', 11 | ], 12 | 'no_extra_blank_lines' => [ 13 | 'tokens' => [ 14 | 'break', 15 | 'case', 16 | 'continue', 17 | 'curly_brace_block', 18 | 'default', 19 | 'extra', 20 | 'parenthesis_brace_block', 21 | 'return', 22 | 'square_brace_block', 23 | 'switch', 24 | 'throw', 25 | 'use', 26 | 'use_trait', 27 | ], 28 | ], 29 | 'no_whitespace_in_blank_line' => true, 30 | 'php_unit_set_up_tear_down_visibility' => true, 31 | 'visibility_required' => [ 32 | 'elements' => [ 33 | 'const', 34 | 'method', 35 | 'property', 36 | ], 37 | ], 38 | ]) 39 | ->setFinder($finder); 40 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/ReferenceTest.php: -------------------------------------------------------------------------------- 1 | factory->defineEntity('SpaceShip'); 13 | $this->factory->defineEntity('Person', [ 14 | 'name' => 'Eve', 15 | 'spaceShip' => FieldDef::reference('SpaceShip') 16 | ]); 17 | } 18 | 19 | /** 20 | * @test 21 | */ 22 | public function referencedObjectShouldBeCreatedAutomatically() 23 | { 24 | $ss1 = $this->factory->get('Person')->getSpaceShip(); 25 | $ss2 = $this->factory->get('Person')->getSpaceShip(); 26 | 27 | $this->assertNotNull($ss1); 28 | $this->assertNotNull($ss2); 29 | $this->assertNotSame($ss1, $ss2); 30 | } 31 | 32 | /** 33 | * @test 34 | */ 35 | public function referencedObjectsShouldBeNullable() 36 | { 37 | $person = $this->factory->get('Person', ['spaceShip' => null]); 38 | 39 | $this->assertNull($person->getSpaceShip()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/TestCase.php: -------------------------------------------------------------------------------- 1 | testDb = new TestDb( 37 | $here . '/TestEntity', 38 | $here . '/TestProxy', 39 | 'FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestProxy' 40 | ); 41 | 42 | $this->em = $this->testDb->createEntityManager(); 43 | 44 | $this->factory = new FixtureFactory($this->em); 45 | $this->factory->setEntityNamespace('FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestEntity'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/ORM/RepositoryTest.php: -------------------------------------------------------------------------------- 1 | 13 | * @license http://www.opensource.org/licenses/BSD-3-Clause New BSD License 14 | */ 15 | class RepositoryTest extends TestCase 16 | { 17 | /** 18 | * @var Repository 19 | */ 20 | private $repository; 21 | 22 | protected function setUp() 23 | { 24 | parent::setUp(); 25 | 26 | $this->repository = new Repository( 27 | $this->em, 28 | $this->em->getClassMetadata('FactoryGirl\Tests\Provider\Doctrine\ORM\TestEntity\User') 29 | ); 30 | } 31 | 32 | /** 33 | * @test 34 | */ 35 | public function getsReference() 36 | { 37 | $user = new User(); 38 | $this->em->persist($user); 39 | $this->em->flush(); 40 | 41 | $this->assertInstanceOf( 42 | 'FactoryGirl\Tests\Provider\Doctrine\ORM\TestEntity\User', 43 | $this->repository->getReference($user->id) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/TestEntity/SpaceShip.php: -------------------------------------------------------------------------------- 1 | name = $name; 34 | $this->crew = new ArrayCollection(); 35 | $this->constructorWasCalled = true; 36 | } 37 | 38 | public function getId() 39 | { 40 | return $this->id; 41 | } 42 | 43 | public function getName() 44 | { 45 | return $this->name; 46 | } 47 | 48 | public function setName($name) 49 | { 50 | $this->name = $name; 51 | } 52 | 53 | public function getCrew() 54 | { 55 | return $this->crew; 56 | } 57 | 58 | public function constructorWasCalled() 59 | { 60 | return $this->constructorWasCalled; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/ExtraConfigurationTest.php: -------------------------------------------------------------------------------- 1 | factory->defineEntity('SpaceShip', [ 12 | 'name' => 'Foo' 13 | ], [ 14 | 'afterCreate' => function (TestEntity\SpaceShip $ss, array $fieldValues) { 15 | $ss->setName($ss->getName() . '-' . $fieldValues['name']); 16 | } 17 | ]); 18 | $ss = $this->factory->get('SpaceShip'); 19 | 20 | $this->assertSame("Foo-Foo", $ss->getName()); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function theAfterCreateCallbackCanBeUsedToCallTheConstructor() 27 | { 28 | $this->factory->defineEntity('SpaceShip', [ 29 | 'name' => 'Foo' 30 | ], [ 31 | 'afterCreate' => function (TestEntity\SpaceShip $ss, array $fieldValues) { 32 | $ss->__construct($fieldValues['name'] . 'Master'); 33 | } 34 | ]); 35 | $ss = $this->factory->get('SpaceShip', ['name' => 'Xoo']); 36 | 37 | $this->assertTrue($ss->constructorWasCalled()); 38 | $this->assertSame('XooMaster', $ss->getName()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/TransitiveReferencesTest.php: -------------------------------------------------------------------------------- 1 | factory->defineEntity('Person', [ 12 | 'spaceShip' => FieldDef::reference('SpaceShip'), 13 | ]); 14 | $this->factory->defineEntity('Badge', [ 15 | 'owner' => FieldDef::reference('Person') 16 | ]); 17 | $this->factory->defineEntity('SpaceShip'); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function referencesGetInstantiatedTransitively() 24 | { 25 | $this->simpleSetup(); 26 | 27 | $badge = $this->factory->get('Badge'); 28 | 29 | $this->assertNotNull($badge->getOwner()->getSpaceShip()); 30 | } 31 | 32 | /** 33 | * @test 34 | */ 35 | public function transitiveReferencesWorkWithSingletons() 36 | { 37 | $this->simpleSetup(); 38 | 39 | $this->factory->getAsSingleton('SpaceShip'); 40 | $badge1 = $this->factory->get('Badge'); 41 | $badge2 = $this->factory->get('Badge'); 42 | 43 | $this->assertNotSame($badge1->getOwner(), $badge2->getOwner()); 44 | $this->assertSame($badge1->getOwner()->getSpaceShip(), $badge2->getOwner()->getSpaceShip()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "breerly/factory-girl-php", 3 | "type": "library", 4 | "description": "Fixture replacement for focused and readable tests - A PHP port of Thoughtbot's Ruby Factory Girl", 5 | "keywords": [ 6 | "factory girl", 7 | "fixture factory", 8 | "fixture replacement", 9 | "object mother", 10 | "testing", 11 | "fixture", 12 | "tdd" 13 | ], 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Grayson Koonce", 18 | "email": "breerly@gmail.com" 19 | } 20 | ], 21 | "config": { 22 | "preferred-install": "dist", 23 | "sort-packages": true 24 | }, 25 | "require": { 26 | "php": "^7.1", 27 | "doctrine/annotations": "^1.7.0", 28 | "doctrine/common": "^2.2.1", 29 | "doctrine/dbal": "^2.2.1", 30 | "doctrine/orm": "^2.6.3" 31 | }, 32 | "require-dev": { 33 | "friendsofphp/php-cs-fixer": "^2.14.0", 34 | "phpunit/phpunit": "^7.5.1" 35 | }, 36 | "suggest": { 37 | "fzaninotto/faker": "For generating fake data in entity definitions" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "FactoryGirl\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "FactoryGirl\\Tests\\": "tests/" 47 | } 48 | }, 49 | "scripts": { 50 | "cs": "php-cs-fixer fix --diff --verbose" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/TimeTest.php: -------------------------------------------------------------------------------- 1 | invert = 1; 19 | $time->add($interval); 20 | $this->assertSame( 21 | $time->getTimestamp(), 22 | FieldDef::past()->years(3)->months(1)->days(2)->get(), 23 | 'Error getting unix timestamp' 24 | ); 25 | $this->assertSame( 26 | $time->format('d-m-y'), 27 | FieldDef::past()->years(3)->months(1)->days(2)->get(DateIntervalHelper::DATE_STRING), 28 | 'Error getting string' 29 | ); 30 | } 31 | 32 | public function testGetTimeFuture() 33 | { 34 | $time = new \DateTime(); 35 | $interval = new \DateInterval('P3Y1M2D'); 36 | $time->add($interval); 37 | $this->assertSame( 38 | $time->getTimestamp(), 39 | FieldDef::future()->years(3)->months(1)->days(2)->get() 40 | ); 41 | $this->assertSame( 42 | $time->format('d-m-y'), 43 | FieldDef::future()->years(3)->months(1)->days(2)->get(DateIntervalHelper::DATE_STRING) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/BidirectionalReferencesTest.php: -------------------------------------------------------------------------------- 1 | factory->defineEntity('SpaceShip'); 14 | $this->factory->defineEntity('Person', [ 15 | 'spaceShip' => FieldDef::reference('SpaceShip') 16 | ]); 17 | 18 | $person = $this->factory->get('Person'); 19 | $ship = $person->getSpaceShip(); 20 | 21 | $this->assertContains($person, $ship->getCrew()); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function unidirectionalReferencesWorkAsUsual() 28 | { 29 | $this->factory->defineEntity('Badge', [ 30 | 'owner' => FieldDef::reference('Person') 31 | ]); 32 | $this->factory->defineEntity('Person'); 33 | 34 | $this->assertInstanceOf(TestEntity\Person::class, $this->factory->get('Badge')->getOwner()); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function whenTheOneSideIsASingletonItMayGetSeveralChildObjects() 41 | { 42 | $this->factory->defineEntity('SpaceShip'); 43 | $this->factory->defineEntity('Person', [ 44 | 'spaceShip' => FieldDef::reference('SpaceShip') 45 | ]); 46 | 47 | $ship = $this->factory->getAsSingleton('SpaceShip'); 48 | $p1 = $this->factory->get('Person'); 49 | $p2 = $this->factory->get('Person'); 50 | 51 | $this->assertContains($p1, $ship->getCrew()); 52 | $this->assertContains($p2, $ship->getCrew()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Provider/Doctrine/DBAL/Types/StatusArrayType.php: -------------------------------------------------------------------------------- 1 | getVarcharTypeDeclarationSQL($fieldDeclaration); 25 | } 26 | 27 | public function convertToDatabaseValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) 28 | { 29 | if ($value === null) { 30 | return null; 31 | } 32 | 33 | if (!is_array($value)) { 34 | throw new ConversionException('Value must be an array'); 35 | } 36 | 37 | foreach ($value as $val) { 38 | if (!preg_match($this->acceptedPattern, $val)) { 39 | throw new ConversionException("'{$val}' does not match pattern '{$this->acceptedPattern}'"); 40 | } 41 | } 42 | 43 | array_walk($value, function (&$walker) { 44 | $walker = '[' . $walker . ']'; 45 | }); 46 | 47 | return implode(';', $value); 48 | } 49 | 50 | public function convertToPHPValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) 51 | { 52 | if ($value === null) { 53 | return null; 54 | } 55 | 56 | $ret = explode(';', $value); 57 | 58 | array_walk($ret, function (&$unwashed) { 59 | $unwashed = trim($unwashed, '[]'); 60 | }); 61 | 62 | return $ret; 63 | } 64 | 65 | public function getName() 66 | { 67 | return self::STATUSARRAY; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/SequenceTest.php: -------------------------------------------------------------------------------- 1 | factory->defineEntity('SpaceShip', [ 15 | 'name' => FieldDef::sequence(function ($n) { 16 | return "Alpha $n"; 17 | }) 18 | ]); 19 | $this->assertSame('Alpha 1', $this->factory->get('SpaceShip')->getName()); 20 | $this->assertSame('Alpha 2', $this->factory->get('SpaceShip')->getName()); 21 | $this->assertSame('Alpha 3', $this->factory->get('SpaceShip')->getName()); 22 | $this->assertSame('Alpha 4', $this->factory->get('SpaceShip')->getName()); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function sequenceGeneratorCanTakeAPlaceholderString() 29 | { 30 | $this->factory->defineEntity('SpaceShip', [ 31 | 'name' => FieldDef::sequence("Beta %d") 32 | ]); 33 | $this->assertSame('Beta 1', $this->factory->get('SpaceShip')->getName()); 34 | $this->assertSame('Beta 2', $this->factory->get('SpaceShip')->getName()); 35 | $this->assertSame('Beta 3', $this->factory->get('SpaceShip')->getName()); 36 | $this->assertSame('Beta 4', $this->factory->get('SpaceShip')->getName()); 37 | } 38 | 39 | /** 40 | * @test 41 | */ 42 | public function sequenceGeneratorCanTakeAStringToAppendTo() 43 | { 44 | $this->factory->defineEntity('SpaceShip', [ 45 | 'name' => FieldDef::sequence("Gamma ") 46 | ]); 47 | $this->assertSame('Gamma 1', $this->factory->get('SpaceShip')->getName()); 48 | $this->assertSame('Gamma 2', $this->factory->get('SpaceShip')->getName()); 49 | $this->assertSame('Gamma 3', $this->factory->get('SpaceShip')->getName()); 50 | $this->assertSame('Gamma 4', $this->factory->get('SpaceShip')->getName()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/IncorrectUsageTest.php: -------------------------------------------------------------------------------- 1 | factory->defineEntity('SpaceShip'); 12 | 13 | $this->expectException(\Exception::class); 14 | 15 | $factory->defineEntity('SpaceShip'); 16 | } 17 | 18 | /** 19 | * @test 20 | */ 21 | public function throwsWhenTryingToDefineEntitiesThatAreNotEvenClasses() 22 | { 23 | $this->expectException(\Exception::class); 24 | 25 | $this->factory->defineEntity('NotAClass'); 26 | } 27 | 28 | /** 29 | * @test 30 | */ 31 | public function throwsWhenTryingToDefineEntitiesThatAreNotEntities() 32 | { 33 | $this->assertTrue(class_exists('FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestEntity\NotAnEntity', true)); 34 | 35 | $this->expectException(\Exception::class); 36 | 37 | $this->factory->defineEntity('NotAnEntity'); 38 | } 39 | 40 | /** 41 | * @test 42 | */ 43 | public function throwsWhenTryingToDefineNonexistentFields() 44 | { 45 | $this->expectException(\Exception::class); 46 | 47 | $this->factory->defineEntity('SpaceShip', [ 48 | 'pieType' => 'blueberry' 49 | ]); 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function throwsWhenTryingToGiveNonexistentFieldsWhileConstructing() 56 | { 57 | $this->factory->defineEntity('SpaceShip', ['name' => 'Alpha']); 58 | 59 | $this->expectException(\Exception::class); 60 | 61 | $this->factory->get('SpaceShip', [ 62 | 'pieType' => 'blueberry' 63 | ]); 64 | } 65 | 66 | /** 67 | * @test 68 | */ 69 | public function throwsWhenTryingToGetLessThanOneInstance() 70 | { 71 | $this->factory->defineEntity('SpaceShip'); 72 | 73 | $this->expectException(\Exception::class); 74 | 75 | $this->factory->getList('SpaceShip', [], 0); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/TestDb.php: -------------------------------------------------------------------------------- 1 | 15 | * @license http://www.opensource.org/licenses/BSD-3-Clause New BSD License 16 | */ 17 | class TestDb 18 | { 19 | /** 20 | * @var \Doctrine\ORM\Configuration 21 | */ 22 | private $doctrineConfig; 23 | 24 | /** 25 | * @var array 26 | */ 27 | private $connectionOptions; 28 | 29 | /** 30 | * @param string $annotationPath 31 | * @param string $proxyDir 32 | * @param string $proxyNamespace 33 | */ 34 | public function __construct($annotationPath, $proxyDir, $proxyNamespace) 35 | { 36 | $cache = new ArrayCache(); 37 | 38 | $config = new Configuration(); 39 | $config->setMetadataCacheImpl($cache); 40 | $config->setQueryCacheImpl($cache); 41 | $config->setMetadataDriverImpl( 42 | $config->newDefaultAnnotationDriver($annotationPath) 43 | ); 44 | $config->setProxyDir($proxyDir); 45 | $config->setProxyNamespace($proxyNamespace); 46 | $config->setAutoGenerateProxyClasses(true); 47 | 48 | $this->connectionOptions = [ 49 | 'driver' => 'pdo_sqlite', 50 | 'path' => ':memory:' 51 | ]; 52 | 53 | $this->doctrineConfig = $config; 54 | } 55 | 56 | /** 57 | * @return EntityManager 58 | */ 59 | public function createEntityManager() 60 | { 61 | $em = EntityManager::create( 62 | $this->connectionOptions, 63 | $this->doctrineConfig 64 | ); 65 | $this->createSchema($em); 66 | 67 | return $em; 68 | } 69 | 70 | /** 71 | * @param EntityManager $em 72 | */ 73 | private function createSchema(EntityManager $em) 74 | { 75 | $tool = new SchemaTool($em); 76 | $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/SingletonTest.php: -------------------------------------------------------------------------------- 1 | factory->defineEntity('SpaceShip'); 12 | 13 | $ss = $this->factory->getAsSingleton('SpaceShip'); 14 | 15 | $this->assertSame($ss, $this->factory->get('SpaceShip')); 16 | $this->assertSame($ss, $this->factory->get('SpaceShip')); 17 | } 18 | 19 | /** 20 | * @test 21 | */ 22 | public function getAsSingletonMethodAcceptsFieldOverridesLikeGet() 23 | { 24 | $this->factory->defineEntity('SpaceShip'); 25 | 26 | $ss = $this->factory->getAsSingleton('SpaceShip', ['name' => 'Beta']); 27 | $this->assertSame('Beta', $ss->getName()); 28 | $this->assertSame('Beta', $this->factory->get('SpaceShip')->getName()); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function throwsAnErrorWhenCallingGetSingletonTwiceOnTheSameEntity() 35 | { 36 | $this->factory->defineEntity('SpaceShip', ['name' => 'Alpha']); 37 | $this->factory->getAsSingleton('SpaceShip'); 38 | 39 | $this->expectException(\Exception::class); 40 | 41 | $this->factory->getAsSingleton('SpaceShip'); 42 | } 43 | 44 | //TODO: should it be an error to get() a singleton with overrides? 45 | 46 | /** 47 | * @test 48 | */ 49 | public function allowsSettingSingletons() 50 | { 51 | $this->factory->defineEntity('SpaceShip'); 52 | $ss = new TestEntity\SpaceShip("The mothership"); 53 | 54 | $this->factory->setSingleton('SpaceShip', $ss); 55 | 56 | $this->assertSame($ss, $this->factory->get('SpaceShip')); 57 | } 58 | 59 | /** 60 | * @test 61 | */ 62 | public function allowsUnsettingSingletons() 63 | { 64 | $this->factory->defineEntity('SpaceShip'); 65 | $ss = new TestEntity\SpaceShip("The mothership"); 66 | 67 | $this->factory->setSingleton('SpaceShip', $ss); 68 | $this->factory->unsetSingleton('SpaceShip'); 69 | 70 | $this->assertNotSame($ss, $this->factory->get('SpaceShip')); 71 | } 72 | 73 | /** 74 | * @test 75 | */ 76 | public function allowsOverwritingExistingSingletons() 77 | { 78 | $this->factory->defineEntity('SpaceShip'); 79 | $ss1 = new TestEntity\SpaceShip("The mothership"); 80 | $ss2 = new TestEntity\SpaceShip("The battlecruiser"); 81 | 82 | $this->factory->setSingleton('SpaceShip', $ss1); 83 | $this->factory->setSingleton('SpaceShip', $ss2); 84 | 85 | $this->assertSame($ss2, $this->factory->get('SpaceShip')); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/ORM/DateIntervalHelperTest.php: -------------------------------------------------------------------------------- 1 | expectException(\InvalidArgumentException::class); 20 | $this->expectExceptionMessage(sprintf( 21 | 'Expected integer or integerish string, got "%s" instead.', 22 | is_object($years) ? get_class($years) : gettype($years) 23 | )); 24 | 25 | $helper->years($years); 26 | } 27 | 28 | /** 29 | * @dataProvider providerInvalidIntegerish 30 | * 31 | * @param mixed $months 32 | */ 33 | public function testMonthsRejectsInvalidValue($months) 34 | { 35 | $helper = new DateIntervalHelper(new \DateTime()); 36 | 37 | $this->expectException(\InvalidArgumentException::class); 38 | $this->expectExceptionMessage(sprintf( 39 | 'Expected integer or integerish string, got "%s" instead.', 40 | is_object($months) ? get_class($months) : gettype($months) 41 | )); 42 | 43 | $helper->months($months); 44 | } 45 | 46 | /** 47 | * @dataProvider providerInvalidIntegerish 48 | * 49 | * @param mixed $days 50 | */ 51 | public function testDaysRejectsInvalidValue($days) 52 | { 53 | $helper = new DateIntervalHelper(new \DateTime()); 54 | 55 | $this->expectException(\InvalidArgumentException::class); 56 | $this->expectExceptionMessage(sprintf( 57 | 'Expected integer or integerish string, got "%s" instead.', 58 | is_object($days) ? get_class($days) : gettype($days) 59 | )); 60 | 61 | $helper->days($days); 62 | } 63 | 64 | /** 65 | * @return array 66 | */ 67 | public function providerInvalidIntegerish() 68 | { 69 | $values = [ 70 | 'array' => [], 71 | 'boolean-false' => false, 72 | 'boolean-true' => true, 73 | 'float-negative' => -3.14, 74 | 'float-negative-casted-to-string' => (string) -3.14, 75 | 'float-positive' => 3.14, 76 | 'float-positive-casted-to-string' => (string) 3.14, 77 | 'null' => null, 78 | 'object' => new \stdClass(), 79 | 'resource' => fopen(__FILE__, 'r'), 80 | 'string' => 'foo', 81 | ]; 82 | 83 | return \array_map(function ($value) { 84 | return [ 85 | $value, 86 | ]; 87 | }, $values); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/ReferencesTest.php: -------------------------------------------------------------------------------- 1 | factory->defineEntity('SpaceShip', [ 15 | 'crew' => FieldDef::references('Person') 16 | ]); 17 | 18 | $this->factory->defineEntity('Person', [ 19 | 'name' => 'Eve', 20 | ]); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function referencedObjectsShouldBeCreatedAutomatically() 27 | { 28 | /** @var TestEntity\SpaceShip $spaceShip */ 29 | $spaceShip = $this->factory->get('SpaceShip'); 30 | 31 | $crew = $spaceShip->getCrew(); 32 | 33 | $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $crew); 34 | $this->assertContainsOnly('FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestEntity\Person', $crew); 35 | $this->assertCount(1, $crew); 36 | } 37 | 38 | /** 39 | * @test 40 | */ 41 | public function referencedObjectsShouldBeOverrideable() 42 | { 43 | $count = 5; 44 | 45 | /** @var TestEntity\SpaceShip $spaceShip */ 46 | $spaceShip = $this->factory->get('SpaceShip', [ 47 | 'crew' => $this->factory->getList('Person', [], $count), 48 | ]); 49 | 50 | $crew = $spaceShip->getCrew(); 51 | 52 | $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $crew); 53 | $this->assertContainsOnly('FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestEntity\Person', $crew); 54 | $this->assertCount($count, $crew); 55 | } 56 | 57 | /** 58 | * @test 59 | */ 60 | public function referencedObjectsShouldBeNullable() 61 | { 62 | /** @var TestEntity\SpaceShip $spaceShip */ 63 | $spaceShip = $this->factory->get('SpaceShip', [ 64 | 'crew' => null, 65 | ]); 66 | 67 | $crew = $spaceShip->getCrew(); 68 | 69 | $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $crew); 70 | $this->assertEmpty($crew); 71 | } 72 | 73 | /** 74 | * @test 75 | */ 76 | public function referencedObjectsCanBeSingletons() 77 | { 78 | /** @var TestEntity\Person $person*/ 79 | $person = $this->factory->getAsSingleton('Person'); 80 | 81 | /** @var TestEntity\SpaceShip $spaceShip */ 82 | $spaceShip = $this->factory->get('SpaceShip'); 83 | 84 | $crew = $spaceShip->getCrew(); 85 | 86 | $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $crew); 87 | $this->assertContains($person, $crew); 88 | $this->assertCount(1, $crew); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/PersistingTest.php: -------------------------------------------------------------------------------- 1 | factory->defineEntity('SpaceShip', ['name' => 'Zeta']); 15 | 16 | $this->factory->persistOnGet(); 17 | $ss = $this->factory->get('SpaceShip'); 18 | $this->em->flush(); 19 | 20 | $this->assertNotNull($ss->getId()); 21 | $this->assertSame($ss, $this->em->find('FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestEntity\SpaceShip', $ss->getId())); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function doesNotPersistByDefault() 28 | { 29 | $this->factory->defineEntity('SpaceShip', ['name' => 'Zeta']); 30 | $ss = $this->factory->get('SpaceShip'); 31 | $this->em->flush(); 32 | 33 | $this->assertNull($ss->getId()); 34 | $q = $this->em 35 | ->createQueryBuilder() 36 | ->select('ss') 37 | ->from('FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestEntity\SpaceShip', 'ss') 38 | ->getQuery(); 39 | $this->assertEmpty($q->getResult()); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function doesNotPersistEmbeddableWhenAutomaticPersistingIsTurnedOn() 46 | { 47 | $mappingClasses = [ 48 | Mapping\Embeddable::class, 49 | Mapping\Embedded::class, 50 | ]; 51 | 52 | foreach ($mappingClasses as $mappingClass) { 53 | if (!class_exists($mappingClass)) { 54 | $this->markTestSkipped('Doctrine Embeddable feature not available'); 55 | } 56 | } 57 | 58 | $this->factory->defineEntity('Name', [ 59 | 'first' => FieldDef::sequence(static function () { 60 | $values = [ 61 | null, 62 | 'Doe', 63 | 'Smith', 64 | ]; 65 | 66 | return $values[array_rand($values)]; 67 | }), 68 | 'last' => FieldDef::sequence(static function () { 69 | $values = [ 70 | null, 71 | 'Jane', 72 | 'John', 73 | ]; 74 | 75 | return $values[array_rand($values)]; 76 | }), 77 | ]); 78 | 79 | $this->factory->defineEntity('Commander', [ 80 | 'name' => FieldDef::reference('Name'), 81 | ]); 82 | 83 | $this->factory->persistOnGet(); 84 | 85 | /** @var TestEntity\Commander $commander */ 86 | $commander = $this->factory->get('Commander'); 87 | 88 | $this->assertInstanceOf(TestEntity\Commander::class, $commander); 89 | $this->assertInstanceOf(TestEntity\Name::class, $commander->name()); 90 | 91 | $this->em->flush(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Provider/Doctrine/DateIntervalHelper.php: -------------------------------------------------------------------------------- 1 | time = $time; 27 | $this->negative = $negative; 28 | } 29 | 30 | /** 31 | * @param int $years 32 | * @throws \InvalidArgumentException 33 | * @return $this 34 | */ 35 | public function years($years) 36 | { 37 | $this->assertIntegerish($years); 38 | 39 | $this->modify(new \DateInterval('P'.$years.'Y')); 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * @param int $months 46 | * @throws \InvalidArgumentException 47 | * @return $this 48 | */ 49 | public function months($months) 50 | { 51 | $this->assertIntegerish($months); 52 | 53 | $this->modify(new \DateInterval('P'.$months.'M')); 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * @param int $days 60 | * @throws \InvalidArgumentException 61 | * @return $this 62 | */ 63 | public function days($days) 64 | { 65 | $this->assertIntegerish($days); 66 | 67 | $this->modify(new \DateInterval('P'.$days.'D')); 68 | 69 | return $this; 70 | } 71 | 72 | private function modify(\DateInterval $interval) 73 | { 74 | $interval->invert = (int) $this->negative; 75 | 76 | $this->time->add($interval); 77 | } 78 | 79 | /** 80 | * @param int $format 81 | * @return \DateTime|int|string 82 | */ 83 | public function get($format = self::TIMESTAMP) 84 | { 85 | if ($format == self::DATE_TIME) { 86 | return $this->time; 87 | } 88 | 89 | if ($format == self::TIMESTAMP) { 90 | return $this->time->getTimestamp(); 91 | } 92 | 93 | if ($format == self::DATE_STRING) { 94 | return $this->time->format('d-m-y'); 95 | } 96 | 97 | throw new \InvalidArgumentException("Unknown time format '". $format ."'"); 98 | } 99 | 100 | /** 101 | * @param mixed $value 102 | * @throws \InvalidArgumentException 103 | */ 104 | private function assertIntegerish($value) 105 | { 106 | if (!is_numeric($value) || $value != (int)$value) { 107 | throw new \InvalidArgumentException(sprintf( 108 | 'Expected integer or integerish string, got "%s" instead.', 109 | is_object($value) ? get_class($value) : gettype($value) 110 | )); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Types/StatusArrayTest.php: -------------------------------------------------------------------------------- 1 | platform = $this->getMockForAbstractClass('\Doctrine\DBAL\Platforms\AbstractPlatform'); 23 | $this->type = Type::getType('statusarray'); 24 | } 25 | 26 | /** 27 | * @test 28 | */ 29 | public function getNameShouldReturnExpectedName() 30 | { 31 | $this->assertSame('statusarray', $this->type->getName()); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function nullShouldAlwaysConvertToNull() 38 | { 39 | $this->assertNull($this->type->convertToDatabaseValue(null, $this->platform)); 40 | $this->assertNull($this->type->convertToPHPValue(null, $this->platform)); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function nonArrayOrNotNullShouldFailDatabaseConversion() 47 | { 48 | $this->expectException(ConversionException::class); 49 | 50 | $value = 'lussenhof'; 51 | $this->type->convertToDatabaseValue($value, $this->platform); 52 | } 53 | 54 | public function provideStupidValues() 55 | { 56 | return [ 57 | ['//'], 58 | ['###'], 59 | ['lussenhofen%meister'], 60 | ]; 61 | } 62 | 63 | public function provideAcceptableValues() 64 | { 65 | return [ 66 | [ 67 | '[lussen.hofer];[lussen:meister];[1];[563]', 68 | ['lussen.hofer', 'lussen:meister', 1, 563], 69 | ], 70 | [ 71 | '[lussen.hofer]', 72 | ['lussen.hofer'], 73 | ], 74 | ]; 75 | } 76 | 77 | /** 78 | * @test 79 | * @dataProvider provideStupidValues 80 | */ 81 | public function invalidCharactersShouldFailDatabaseConversion($stupidValue) 82 | { 83 | $this->expectException(ConversionException::class); 84 | 85 | $value = [$stupidValue]; 86 | $this->type->convertToDatabaseValue($value, $this->platform); 87 | } 88 | 89 | /** 90 | * @test 91 | * @dataProvider provideAcceptableValues 92 | */ 93 | public function acceptableCharactersShouldPassDatabaseConversionAndReturnExpectedSerialization($expectedSerialization, $acceptableValue) 94 | { 95 | $serialization = $this->type->convertToDatabaseValue($acceptableValue, $this->platform); 96 | $this->assertSame($expectedSerialization, $serialization); 97 | } 98 | 99 | public function provideSerializedValues() 100 | { 101 | return [ 102 | [ 103 | ['lussen', 'hofer', '645', 'meisten:lusdre', 'larva.lussutab.tussi'], 104 | '[lussen];[hofer];[645];[meisten:lusdre];[larva.lussutab.tussi]', 105 | ] 106 | ]; 107 | } 108 | 109 | /** 110 | * @test 111 | * @dataProvider provideSerializedValues 112 | */ 113 | public function valuesShouldDeserializeProperly($expected, $serialized) 114 | { 115 | $deserialized = $this->type->convertToPHPValue($serialized, $this->platform); 116 | $this->assertSame($expected, $deserialized); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Provider/Doctrine/FieldDef.php: -------------------------------------------------------------------------------- 1 | get($name); 79 | }; 80 | } 81 | 82 | /** 83 | * Defines a field to `get()` a collection of named entities from the factory. 84 | * 85 | * The normal semantics of `get()` apply. 86 | * 87 | * Normally this means that the field gets a fresh instance of the named 88 | * entity. If a singleton has been defined, a collection with a single instance will be returned. 89 | * 90 | * @param string $name 91 | * @param int $numberOfInstances 92 | * 93 | * @throws \InvalidArgumentException 94 | * @return callable 95 | */ 96 | public static function references($name, $numberOfInstances = 1) 97 | { 98 | if ($numberOfInstances < 1) { 99 | throw new \InvalidArgumentException('Can only get >= 1 instances'); 100 | } 101 | 102 | return function (FixtureFactory $factory) use ($name, $numberOfInstances) { 103 | return $factory->getList( 104 | $name, 105 | [], 106 | $numberOfInstances 107 | ); 108 | }; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Provider/Doctrine/EntityDef.php: -------------------------------------------------------------------------------- 1 | name = $name; 29 | $this->entityType = $type; 30 | $this->metadata = $em->getClassMetadata($type); 31 | $this->fieldDefs = []; 32 | $this->config = $config; 33 | 34 | $this->readFieldDefs($fieldDefs); 35 | $this->defaultDefsFromMetadata(); 36 | } 37 | 38 | private function readFieldDefs(array $params) 39 | { 40 | foreach ($params as $key => $def) { 41 | if ($this->metadata->hasField($key) || 42 | $this->metadata->hasAssociation($key)) { 43 | $this->fieldDefs[$key] = $this->normalizeFieldDef($def); 44 | } else { 45 | throw new Exception('No such field in ' . $this->entityType . ': ' . $key); 46 | } 47 | } 48 | } 49 | 50 | private function defaultDefsFromMetadata() 51 | { 52 | $defaultEntity = $this->getEntityMetadata()->newInstance(); 53 | 54 | $allFields = array_merge($this->metadata->getFieldNames(), $this->metadata->getAssociationNames()); 55 | foreach ($allFields as $fieldName) { 56 | if (!isset($this->fieldDefs[$fieldName])) { 57 | $defaultFieldValue = $this->metadata->getFieldValue($defaultEntity, $fieldName); 58 | 59 | if ($defaultFieldValue !== null) { 60 | $this->fieldDefs[$fieldName] = function () use ($defaultFieldValue) { 61 | return $defaultFieldValue; 62 | }; 63 | } else { 64 | $this->fieldDefs[$fieldName] = function () { 65 | return null; 66 | }; 67 | } 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Returns the name of the entity definition. 74 | * @return string 75 | */ 76 | public function getName() 77 | { 78 | return $this->name; 79 | } 80 | 81 | /** 82 | * Returns the fully qualified name of the entity class. 83 | * @return string 84 | */ 85 | public function getEntityType() 86 | { 87 | return $this->entityType; 88 | } 89 | 90 | /** 91 | * Returns the fielde definition callbacks. 92 | */ 93 | public function getFieldDefs() 94 | { 95 | return $this->fieldDefs; 96 | } 97 | 98 | /** 99 | * Returns the Doctrine metadata for the entity to be created. 100 | * @return ClassMetadata 101 | */ 102 | public function getEntityMetadata() 103 | { 104 | return $this->metadata; 105 | } 106 | 107 | /** 108 | * Returns the extra configuration array of the entity definition. 109 | * @return array 110 | */ 111 | public function getConfig() 112 | { 113 | return $this->config; 114 | } 115 | 116 | private function normalizeFieldDef($def) 117 | { 118 | if (is_callable($def)) { 119 | return $this->ensureInvokable($def); 120 | } 121 | 122 | return function () use ($def) { 123 | return $def; 124 | }; 125 | } 126 | 127 | private function ensureInvokable($f) 128 | { 129 | if (method_exists($f, '__invoke')) { 130 | return $f; 131 | } 132 | 133 | return function () use ($f) { 134 | return call_user_func_array($f, func_get_args()); 135 | }; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Provider/Doctrine/ORM/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | boolean> 15 | */ 16 | protected $_statuses = []; 17 | 18 | /** 19 | * @var array 20 | */ 21 | protected $_queryConfigurers = []; 22 | 23 | /** 24 | * Entity name associated with this query builder (if any) 25 | * 26 | * @var string 27 | */ 28 | protected $entityName; 29 | 30 | /** 31 | * Entity alias associated with this query builder (if any) 32 | * 33 | * @var string 34 | */ 35 | protected $entityAlias; 36 | 37 | /** 38 | * Create a new QueryBuilder of the type that receives this static call. 39 | * 40 | * @param EntityManager $em 41 | * @return QueryBuilder 42 | */ 43 | public static function create(EntityManager $em) 44 | { 45 | return new static($em); 46 | } 47 | 48 | /** 49 | * @param EntityManager $em 50 | * @param string $entityName 51 | * @param string $entityAlias 52 | */ 53 | public function __construct(EntityManager $em, $entityName = null, $entityAlias = null) 54 | { 55 | parent::__construct($em); 56 | 57 | $this->entityName = $entityName; 58 | $this->entityAlias = $entityAlias; 59 | $this->init(); 60 | } 61 | 62 | /** 63 | * Post-construction template method 64 | * 65 | * Prepopulates with entity if both entity name and alias are set. 66 | */ 67 | public function init() 68 | { 69 | if (is_string($this->entityName) && is_string($this->entityAlias)) { 70 | $this->select($this->entityAlias) 71 | ->from($this->entityName, $this->entityAlias); 72 | } 73 | } 74 | 75 | /** 76 | * Configures the created Query using the configurers added to this builder 77 | * 78 | * @return \Doctrine\ORM\Query 79 | */ 80 | public function getQuery() 81 | { 82 | $query = parent::getQuery(); 83 | foreach ($this->_queryConfigurers as $configurer) { 84 | $configurer($query); 85 | } 86 | return $query; 87 | } 88 | 89 | /** 90 | * NOTE: Should be protected 91 | * 92 | * @param callback(Doctrine\ORM\Query) $configurer 93 | * @return QueryBuilder 94 | */ 95 | public function _configureQuery($configurer) 96 | { 97 | $this->_queryConfigurers[] = $configurer; 98 | return $this; 99 | } 100 | 101 | /** 102 | * Ensure that a certain operation has been performed on this object. 103 | * $status is a string that describes state the operation should affect and 104 | * $operation is a callback which is executed if and only if the status is 105 | * not already in effect. 106 | * 107 | * Example: 108 | * 109 | * public function withProduct() { 110 | * return $this->ensure('product is joined', function($qb) { 111 | * $qb->join('pv.product', 'p'); 112 | * }); 113 | * } 114 | * 115 | * Should be used to allow the possibility of several different methods 116 | * each wanting to affect a certain state without messing up the query by 117 | * duplicate calls to methods. Has the pleasant side effect of describing 118 | * the results of operations on a more intimate level than the plain object 119 | * API can allow for, making for more self-documenting code. 120 | * 121 | * @param string $status 122 | * @param callback(QueryBuilder) $operation 123 | * @return QueryBuilder 124 | */ 125 | protected function ensure($status, $operation) 126 | { 127 | if (empty($this->_statuses[$status])) { 128 | $operation($this); 129 | $this->_statuses[$status] = true; 130 | } 131 | return $this; 132 | } 133 | 134 | /** 135 | * Configures the query to force result objects to be partially loaded 136 | * 137 | * @return QueryBuilder 138 | */ 139 | protected function asPartial() 140 | { 141 | return $this->ensure('partial loading is forced', function (QueryBuilder $builder) { 142 | $builder->_configureQuery(function (Query $query) { 143 | $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); 144 | }); 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Provider/Doctrine/ORM/Locking/TableLock.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 25 | } 26 | 27 | /** 28 | * @return Repository 29 | */ 30 | protected function getRepository() 31 | { 32 | return $this->repository; 33 | } 34 | 35 | /** 36 | * Attempt to acquire a table level lock in MySQL for the duration of the 37 | * given transaction. IS NOT IN ANY WAY GUARANTEED TO WORK. MySQL requires 38 | * that the aliases through which a table is accessed during this 39 | * transaction are enumerated when locking tables, which due to the nature 40 | * of Doctrine is a somewhat difficult task. Nevertheless, in simple cases a 41 | * good guesstimate as to the table aliases can be made; see relevant 42 | * methods below. 43 | * 44 | * @param int $lockMode a TableLockMode constant 45 | * @param callback $transaction 46 | * @return mixed 47 | * @throws LockException 48 | */ 49 | public function transaction($lockMode, $transaction) 50 | { 51 | $lock = $this->getLockString($lockMode); 52 | $unlock = $this->getUnlockString(); 53 | 54 | return $this->getRepository()->transaction(function (EntityManager $em, Repository $repository) use ($lock, $unlock, $transaction) { 55 | $conn = $em->getConnection(); 56 | $conn->executeQuery($lock); 57 | try { 58 | $result = $repository->transaction($transaction); 59 | $conn->executeQuery($unlock); 60 | return $result; 61 | } catch (Exception $e) { 62 | // Transaction rollback does not release table locks 63 | $conn->executeQuery($unlock); 64 | throw $e; 65 | } 66 | }); 67 | } 68 | 69 | /** 70 | * Get the MySQL statement for locking the table underlying this repository 71 | * for simple read and/or write operations given an appropriate lock mode 72 | * 73 | * @param int $lockMode a TableLockMode constant 74 | * @return string 75 | * @throws LockException 76 | */ 77 | private function getLockString($lockMode) 78 | { 79 | $lockModeString = TableLockMode::toString($lockMode); 80 | if (!$lockModeString) { 81 | throw new LockException("Invalid lock mode: $lockMode"); 82 | } 83 | 84 | $tableName = $this->getTableName(); 85 | $aliases = $this->getTableAliasGuesstimates($tableName); 86 | 87 | return $this->constructLockString($tableName, $aliases, $lockModeString); 88 | } 89 | 90 | /** 91 | * @return string 92 | */ 93 | private function getTableName() 94 | { 95 | // Blatant violation of law of demeter 96 | return $this->getRepository()->getClassMetadata()->getTableName(); 97 | } 98 | 99 | /** 100 | * @param string $tableName 101 | * @param array $aliases 102 | * @param string $lockModeString 103 | * @return string 104 | */ 105 | private function constructLockString($tableName, array $aliases, $lockModeString) 106 | { 107 | $lock = "LOCK TABLES $tableName $lockModeString"; 108 | foreach ($aliases as $alias) { 109 | $lock .= ", $tableName as $alias $lockModeString"; 110 | } 111 | return $lock; 112 | } 113 | 114 | /** 115 | * Attempt to guess at the table name aliases used by Doctrine for a given 116 | * table name 117 | * 118 | * @param string $tableName 119 | * @return array 120 | */ 121 | private function getTableAliasGuesstimates($tableName) 122 | { 123 | return array_unique([ 124 | // the default generated alias: the first letter of the table name prepended with a zero 125 | strtolower(substr($tableName, 0, 1)) . '0', 126 | // a generic alias used by Doctrine in many cases 127 | 't0' 128 | ]); 129 | } 130 | 131 | /** 132 | * The MySQL statement required to unlock tables after a transaction 133 | * 134 | * @return string 135 | */ 136 | private function getUnlockString() 137 | { 138 | return 'UNLOCK TABLES'; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/Provider/Doctrine/Fixtures/BasicUsageTest.php: -------------------------------------------------------------------------------- 1 | factory 15 | ->defineEntity('SpaceShip', [ 16 | 'name' => 'My BattleCruiser' 17 | ]) 18 | ->get('SpaceShip'); 19 | 20 | $this->assertSame('My BattleCruiser', $ss->getName()); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function acceptsGeneratorFunctionsInEntityDefinitions() 27 | { 28 | $name = "Star"; 29 | $this->factory->defineEntity('SpaceShip', [ 30 | 'name' => function () use (&$name) { 31 | return "M/S $name"; 32 | } 33 | ]); 34 | 35 | $this->assertSame('M/S Star', $this->factory->get('SpaceShip')->getName()); 36 | $name = "Superstar"; 37 | $this->assertSame('M/S Superstar', $this->factory->get('SpaceShip')->getName()); 38 | } 39 | 40 | /** 41 | * @test 42 | */ 43 | public function valuesCanBeOverriddenAtCreationTime() 44 | { 45 | $ss = $this->factory 46 | ->defineEntity('SpaceShip', [ 47 | 'name' => 'My BattleCruiser' 48 | ]) 49 | ->get('SpaceShip', ['name' => 'My CattleBruiser']); 50 | $this->assertSame('My CattleBruiser', $ss->getName()); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function preservesDefaultValuesOfEntity() 57 | { 58 | $ss = $this->factory 59 | ->defineEntity('SpaceStation') 60 | ->get('SpaceStation'); 61 | $this->assertSame('Babylon5', $ss->getName()); 62 | } 63 | 64 | /** 65 | * @test 66 | */ 67 | public function doesNotCallTheConstructorOfTheEntity() 68 | { 69 | $ss = $this->factory 70 | ->defineEntity('SpaceShip', []) 71 | ->get('SpaceShip'); 72 | $this->assertFalse($ss->constructorWasCalled()); 73 | } 74 | 75 | /** 76 | * @test 77 | */ 78 | public function instantiatesCollectionAssociationsToBeEmptyCollectionsWhenUnspecified() 79 | { 80 | $ss = $this->factory 81 | ->defineEntity('SpaceShip', [ 82 | 'name' => 'Battlestar Galaxy' 83 | ]) 84 | ->get('SpaceShip'); 85 | 86 | $this->assertInstanceOf(ArrayCollection::class, $ss->getCrew()); 87 | $this->assertEmpty($ss->getCrew()); 88 | } 89 | 90 | /** 91 | * @test 92 | */ 93 | public function arrayElementsAreMappedToCollectionAsscociationFields() 94 | { 95 | $this->factory->defineEntity('SpaceShip'); 96 | $this->factory->defineEntity('Person', [ 97 | 'spaceShip' => FieldDef::reference('SpaceShip') 98 | ]); 99 | 100 | $p1 = $this->factory->get('Person'); 101 | $p2 = $this->factory->get('Person'); 102 | 103 | $ship = $this->factory->get('SpaceShip', [ 104 | 'name' => 'Battlestar Galaxy', 105 | 'crew' => [$p1, $p2] 106 | ]); 107 | 108 | $this->assertInstanceOf(ArrayCollection::class, $ship->getCrew()); 109 | $this->assertTrue($ship->getCrew()->contains($p1)); 110 | $this->assertTrue($ship->getCrew()->contains($p2)); 111 | } 112 | 113 | /** 114 | * @test 115 | */ 116 | public function unspecifiedFieldsAreLeftNull() 117 | { 118 | $this->factory->defineEntity('SpaceShip'); 119 | $this->assertNull($this->factory->get('SpaceShip')->getName()); 120 | } 121 | 122 | /** 123 | * @test 124 | */ 125 | public function entityIsDefinedToDefaultNamespace() 126 | { 127 | $this->factory->defineEntity('SpaceShip'); 128 | $this->factory->defineEntity('Person\User'); 129 | 130 | $this->assertInstanceOf( 131 | 'FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestEntity\SpaceShip', 132 | $this->factory->get('SpaceShip') 133 | ); 134 | 135 | $this->assertInstanceOf( 136 | 'FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestEntity\Person\User', 137 | $this->factory->get('Person\User') 138 | ); 139 | } 140 | 141 | /** 142 | * @test 143 | */ 144 | public function entityCanBeDefinedToAnotherNamespace() 145 | { 146 | $this->factory->defineEntity( 147 | '\FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestAnotherEntity\Artist' 148 | ); 149 | 150 | $this->assertInstanceOf( 151 | 'FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestAnotherEntity\Artist', 152 | $this->factory->get( 153 | '\FactoryGirl\Tests\Provider\Doctrine\Fixtures\TestAnotherEntity\Artist' 154 | ) 155 | ); 156 | } 157 | 158 | /** 159 | * @test 160 | */ 161 | public function returnsListOfEntities() 162 | { 163 | $this->factory->defineEntity('SpaceShip'); 164 | 165 | $this->assertCount(1, $this->factory->getList('SpaceShip')); 166 | } 167 | 168 | /** 169 | * @test 170 | */ 171 | public function canSpecifyNumberOfReturnedInstances() 172 | { 173 | $this->factory->defineEntity('SpaceShip'); 174 | 175 | $this->assertCount(5, $this->factory->getList('SpaceShip', [], 5)); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Factory Girl in PHP 2 | =================== 3 | 4 | [![Continuous Integration](https://github.com/unhashable/factory-girl-php/workflows/Continuous%20Integration/badge.svg)](https://github.com/unhashable/factory-girl-php/actions) 5 | 6 | A PHP port of Thoughtbot's Ruby [Factory Girl](https://github.com/thoughtbot/factory_girl). Based on a fork of [xi-doctrine](https://github.com/xi-project/xi-doctrine). 7 | 8 | 9 | FactoryGirl FixtureFactory 10 | -------------- 11 | 12 | `FactoryGirl\Provider\Doctrine\FixtureFactory` provides convenient creation of Doctrine entities in tests. If you're familiar with [FactoryGirl](https://github.com/thoughtbot/factory_girl) for Ruby, then this is essentially the same thing for Doctrine/PHP. 13 | 14 | ### Motivation ### 15 | 16 | Many web applications have non-trivial database structures with lots of dependencies between tables. A component of such an application might deal with entities from only one or two tables, but the entities may depend on a complex entity graph to be useful or pass validation. 17 | 18 | For instance, a `User` may be a member of a `Group`, which is part of an `Organization`, which in turn depends on five different tables describing who-knows-what about the organization. You are writing a component that change's the user's password and are currently uninterested in groups, organizations and their dependencies. How do you set up your test? 19 | 20 | 1. Do you create all dependencies for `Organization` and `Group` to get a valid `User` in your `setUp()`? No, that would be horribly tedious and verbose. 21 | 2. Do you make a shared fixture for all your tests that includes an example organization with satisifed dependencies? No, that would make the fixture extremely fragile. 22 | 3. Do you use mock objects? Sure, where practical. In many cases, however, the code you're testing interacts with the entities in such a complex way that mocking them sufficiently is impractical. 23 | 24 | `FixtureFactory` is a middle ground between *(1)* and *(2)*. You specify how to generate your entities and their dependencies in one central place but explicitly create them in your tests, overriding only the fields you want. 25 | 26 | ### Tutorial ### 27 | 28 | We'll assume you have a base class for your tests that arranges a fresh `EntityManager` connected to a minimally initialized blank test database. A simple factory setup looks like this. 29 | 30 | ```php 31 | entityManager) ... 44 | 45 | $this->factory = new FixtureFactory($this->entityManager); 46 | $this->factory->setEntityNamespace('What\Ever'); // If applicable 47 | 48 | // Define that users have names like user_1, user_2, etc., 49 | // that they are not administrators by default and 50 | // that they point to a Group entity. 51 | $this->factory->defineEntity('User', [ 52 | 'username' => FieldDef::sequence("user_%d"), 53 | 'administrator' => false, 54 | 'group' => FieldDef::reference('Group') 55 | ]); 56 | 57 | // Define a Group to just have a unique name as above. 58 | // The order of the definitions does not matter. 59 | $this->factory->defineEntity('Group', [ 60 | 'name' => FieldDef::sequence("group_%d") 61 | ]); 62 | 63 | 64 | // If you want your created entities to be saved by default 65 | // then do the following. You can selectively re-enable or disable 66 | // this behavior in each test as well. 67 | // It's recommended to only enable this in tests that need it. 68 | // In any case, you'll need to call flush() yourself. 69 | //$this->factory->persistOnGet(); 70 | } 71 | } 72 | ``` 73 | 74 | Now you can easily get entities and override fields relevant to your test case like this. 75 | 76 | ```php 77 | factory->get('User', [ 86 | 'name' => 'John' 87 | ]); 88 | $this->service->changePassword($user, 'xoo'); 89 | $this->assertSame($user, $this->service->authenticateUser('john', 'xoo')); 90 | } 91 | } 92 | ``` 93 | 94 | ### Singletons ### 95 | 96 | Sometimes your entity has a dependency graph with several references to some entity type. For instance, the application may have a concept of a "current organization" with users, groups, products, categories etc. belonging to an organization. By default `FixtureFactory` would create a new `Organization` each time one is needed, which is not always what you want. Sometimes you'd like each new entity to point to one shared `Organization`. 97 | 98 | Your first reaction should be to avoid situations like this and specify the shared entity explicitly when you can't. If that isn't feasible for whatever reason, `FixtureFactory` allows you to make an entity a *singleton*. If a singleton exists for a type of entity then `get()` will return that instead of creating a new instance. 99 | 100 | ```php 101 | org = $this->factory->getAsSingleton('Organization'); 109 | } 110 | 111 | public function testSomething(): void 112 | { 113 | $user1 = $this->factory->get('User'); 114 | $user2 = $this->factory->get('User'); 115 | 116 | // now $user1->getOrganization() === $user2->getOrganization() ... 117 | } 118 | } 119 | ``` 120 | 121 | It's highly recommended to create singletons only in the setups of individual test classes and *NOT* in the base class of your tests. 122 | 123 | ### Advanced ### 124 | 125 | You can give an 'afterCreate' callback to be called after an entity is created and its fields are set. Here you can, for instance, invoke the entity's constructor, since `FixtureFactory` doesn't do that by default. 126 | 127 | ```php 128 | defineEntity( 131 | 'User', 132 | [ 133 | 'username' => FieldDef::sequence("user_%d"), 134 | ], 135 | [ 136 | 'afterCreate' => function(User $user, array $fieldValues) { 137 | $user->__construct($fieldValues['username']); 138 | } 139 | ]); 140 | ``` 141 | 142 | ### API reference ### 143 | 144 | ```php 145 | defineEntity( 149 | 'EntityName', 150 | [ 151 | 'simpleField' => 'constantValue', 152 | 153 | 'generatedField' => function($factory) { return ...; }, 154 | 155 | 'sequenceField1' => FieldDef::sequence('name-%d'), // name-1, name-2, ... 156 | 'sequenceField2' => FieldDef::sequence('name-'), // the same 157 | 'sequenceField3' => FieldDef::sequence(function($n) { return "name-$n"; }), 158 | 159 | 'referenceField' => FieldDef::reference('OtherEntity') 160 | ], 161 | [ 162 | 'afterCreate' => function($entity, $fieldValues) { 163 | // ... 164 | } 165 | ] 166 | ); 167 | 168 | // Getting an entity (new or singleton) 169 | $factory->get('EntityName', ['field' => 'value']); 170 | 171 | // Getting an array of entities 172 | $numberOfEntities = 15; 173 | $factory->getList('EntityName', ['field' => 'value'], $numberOfEntities); 174 | 175 | // Singletons 176 | $factory->getAsSingleton('EntityName', ['field' => 'value']); 177 | $factory->setSingleton('EntityName', $entity); 178 | $factory->unsetSingleton('EntityName'); 179 | 180 | // Configuration 181 | $this->factory->setEntityNamespace('What\Ever'); // Default: empty 182 | $this->factory->persistOnGet(); // Default: don't persist 183 | $this->factory->persistOnGet(false); 184 | ``` 185 | 186 | ### Miscellaneous ### 187 | 188 | - `FixtureFactory` and `FieldDef` are designed to be subclassable. 189 | - With bidirectional one-to-many associations, the collection on the 'one' 190 | side will get updated as long as you've remembered to specify the 191 | `inversedBy` attribute in your mapping. 192 | 193 | ### Development ### 194 | 195 | #### Tests #### 196 | 197 | The composer packages must be installed with 198 | 199 | ``` 200 | composer install --prefer-source 201 | ``` 202 | -------------------------------------------------------------------------------- /src/Provider/Doctrine/ORM/Repository.php: -------------------------------------------------------------------------------- 1 | getQuery()->getResult(); 22 | } 23 | 24 | /** 25 | * @param \Doctrine\ORM\QueryBuilder $builder 26 | * @param callback(Exception) $fallback optional 27 | * @return object | null result or return value from fallback 28 | */ 29 | protected function getSingleQueryResult($builder, $fallback = null) 30 | { 31 | return $this->attemptQuery(function () use ($builder) { 32 | return $builder->getQuery()->getSingleResult(); 33 | }, $fallback); 34 | } 35 | 36 | /** 37 | * @param \Doctrine\ORM\QueryBuilder $builder 38 | * @param callback(Exception) $fallback optional 39 | * @return object | null result or return value from fallback 40 | */ 41 | protected function getSingleScalarQueryResult($builder, $fallback = null) 42 | { 43 | return $this->attemptQuery(function () use ($builder) { 44 | return $builder->getQuery()->getSingleScalarResult(); 45 | }, $fallback); 46 | } 47 | 48 | /** 49 | * Guards against NoResultException and NonUniqueResultException within a 50 | * callback. Uses a fallback callback in case an exception does occur. 51 | * 52 | * @param callback $do 53 | * @param callback $fallback optional 54 | * @return mixed 55 | */ 56 | private function attemptQuery($do, $fallback = null) 57 | { 58 | if (null === $fallback) { 59 | $fallback = function () { 60 | }; 61 | } 62 | try { 63 | return $do(); 64 | } catch (\Doctrine\ORM\NoResultException $e) { 65 | return $fallback($e); 66 | } catch (\Doctrine\ORM\NonUniqueResultException $e) { 67 | return $fallback($e); 68 | } 69 | } 70 | 71 | /** 72 | * Create a query builder, perform the given operation on it and return the 73 | * query builder. The operation callback receives the query builder and its 74 | * associated expression builder as arguments. 75 | * 76 | * @param callback(QueryBuilder, Doctrine\ORM\Query\Expr) $do 77 | * @return QueryBuilder 78 | */ 79 | protected function withQueryBuilder($do) 80 | { 81 | $qb = $this->getBaseQueryBuilder(); 82 | $do($qb, $qb->expr()); 83 | return $qb; 84 | } 85 | 86 | /** 87 | * Create a query builder. Override this in a child class to create a 88 | * builder of the appropriate type. 89 | * 90 | * @return QueryBuilder 91 | */ 92 | protected function getBaseQueryBuilder() 93 | { 94 | return QueryBuilder::create($this->_em); 95 | } 96 | 97 | /** 98 | * Gets a reference to the entity identified by the given type and identifier 99 | * without actually loading it, if the entity is not yet loaded. 100 | * 101 | * @param mixed $identifier The entity identifier. 102 | * @return object The entity reference. 103 | */ 104 | public function getReference($identifier) 105 | { 106 | return $this->getEntityManager() 107 | ->getReference($this->getEntityName(), $identifier); 108 | } 109 | 110 | /** 111 | * Perform a callback function within a transaction. If an exception occurs 112 | * within the function, it's catched, the transaction is rolled back and 113 | * the exception rethrown. 114 | * 115 | * @param callback(Doctrine\ORM\EntityManager, Repository) $transaction 116 | * @return mixed the callback return value 117 | * @throws Exception 118 | */ 119 | public function transaction($transaction) 120 | { 121 | $em = $this->getEntityManager(); 122 | $conn = $em->getConnection(); 123 | 124 | $conn->beginTransaction(); 125 | try { 126 | $result = $transaction($em, $this); 127 | $em->flush(); 128 | $conn->commit(); 129 | return $result; 130 | } catch (Exception $e) { 131 | $em->close(); 132 | $conn->rollback(); 133 | throw $e; 134 | } 135 | } 136 | 137 | /** 138 | * Acquires a lock to an entity, provides the entity to a callback function 139 | * and relinquishes the lock by flushing the entity manager immediately 140 | * after. 141 | * 142 | * @param int $id 143 | * @param int $lockMode 144 | * @param callback($entity, Doctrine\ORM\EntityManager, Repository) $callback 145 | * @return mixed callback return type 146 | * @throws LockException 147 | */ 148 | public function useWithLock($id, $lockMode, $callback) 149 | { 150 | $entityName = $this->getEntityName(); 151 | return $this->transaction(function ($em, $self) use ($id, $lockMode, $callback, $entityName) { 152 | $entity = $self->find($id, $lockMode); 153 | if (empty($entity)) { 154 | $message = \sprintf("Could not lock %s entity by id %d: entity not found", $entityName, $id); 155 | throw new LockException($message); 156 | } 157 | $result = $callback($entity, $em, $self); 158 | return $result; 159 | }); 160 | } 161 | 162 | /** 163 | * Calls useWithLock() with a pessimistic write lock mode 164 | * 165 | * @param int $id 166 | * @param callback($entity, Doctrine\ORM\EntityManager, Repository) $callback 167 | * @return mixed callback return type 168 | */ 169 | public function useWithPessimisticWriteLock($id, $callback) 170 | { 171 | return $this->useWithLock($id, LockMode::PESSIMISTIC_WRITE, $callback); 172 | } 173 | 174 | /** 175 | * Acquires an optimistic lock within a pessimistic lock transaction. For 176 | * use in fail-fast scenarios; guaranteed to throw an exception on 177 | * concurrent modification attempts. The one to first acquire the write lock 178 | * will update the version field, leading subsequent acquisitions of the 179 | * optimistic lock to fail. 180 | * 181 | * FIXME: Only works on entities implementing VersionLockable and does not 182 | * work in conjunction with the Doctrine @Version column. 183 | * 184 | * @param int $id 185 | * @param mixed $lockVersion 186 | * @param callback($entity, Doctrine\ORM\EntityManager, Repository) $callback 187 | * @return mixed callback return type 188 | * @throws OptimisticLockException 189 | */ 190 | public function useWithPessimisticVersionLock($id, $lockVersion, $callback) 191 | { 192 | return $this->useWithPessimisticWriteLock($id, function (VersionLockable $entity, EntityManager $em, $self) use ($lockVersion, $callback) { 193 | if ($entity->getVersion() !== $lockVersion) { 194 | // FIXME: This isn't the appropriate exception type. 195 | throw OptimisticLockException::lockFailedVersionMissmatch($entity, $lockVersion, $entity->getVersion()); 196 | } 197 | $entity->incrementVersion(); 198 | return $callback($entity, $em, $self); 199 | }); 200 | } 201 | 202 | /** 203 | * @return string 204 | */ 205 | public function getEntityName() 206 | { 207 | return parent::getEntityName(); 208 | } 209 | 210 | /** 211 | * Attempt to acquire a table level lock in MySQL for the duration of the 212 | * given transaction. IS NOT IN ANY WAY GUARANTEED TO WORK. 213 | * 214 | * @see TableLock 215 | * @param int $lockMode a TableLockMode constant 216 | * @param callback $transaction 217 | * @return mixed 218 | * @throws LockException 219 | */ 220 | public function transactionWithTableLock($lockMode, $transaction) 221 | { 222 | return $this->getTableLock()->transaction($lockMode, $transaction); 223 | } 224 | 225 | /** 226 | * @return TableLock 227 | */ 228 | private function getTableLock() 229 | { 230 | return new TableLock($this); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/Provider/Doctrine/FixtureFactory.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected $entityDefs; 31 | 32 | /** 33 | * @var array 34 | */ 35 | protected $singletons; 36 | 37 | /** 38 | * @var boolean 39 | */ 40 | protected $persist; 41 | 42 | public function __construct(EntityManager $em) 43 | { 44 | $this->em = $em; 45 | 46 | $this->entityNamespace = ''; 47 | 48 | $this->entityDefs = []; 49 | 50 | $this->singletons = []; 51 | 52 | $this->persist = false; 53 | } 54 | 55 | /** 56 | * Sets the namespace to be prefixed to all entity names passed to this class. 57 | */ 58 | public function setEntityNamespace($namespace) 59 | { 60 | $this->entityNamespace = trim($namespace, '\\'); 61 | } 62 | 63 | public function getEntityNamespace() 64 | { 65 | return $this->entityNamespace; 66 | } 67 | 68 | /** 69 | * Get an entity and its dependencies. 70 | * 71 | * Whether the entity is new or not depends on whether you've created 72 | * a singleton with the entity name. See `getAsSingleton()`. 73 | * 74 | * If you've called `persistOnGet()` then the entity is also persisted. 75 | * 76 | * @throws EntityDefinitionUnavailable 77 | */ 78 | public function get($name, array $fieldOverrides = []) 79 | { 80 | if (isset($this->singletons[$name])) { 81 | return $this->singletons[$name]; 82 | } 83 | 84 | if (!array_key_exists($name, $this->entityDefs)) { 85 | throw EntityDefinitionUnavailable::for($name); 86 | } 87 | 88 | /** @var EntityDef $def */ 89 | $def = $this->entityDefs[$name]; 90 | 91 | $config = $def->getConfig(); 92 | 93 | $this->checkFieldOverrides($def, $fieldOverrides); 94 | 95 | /** @var Mapping\ClassMetadata $entityMetadata */ 96 | $entityMetadata = $def->getEntityMetadata(); 97 | 98 | $ent = $entityMetadata->newInstance(); 99 | $fieldValues = []; 100 | foreach ($def->getFieldDefs() as $fieldName => $fieldDef) { 101 | $fieldValues[$fieldName] = array_key_exists($fieldName, $fieldOverrides) 102 | ? $fieldOverrides[$fieldName] 103 | : $fieldDef($this); 104 | } 105 | 106 | foreach ($fieldValues as $fieldName => $fieldValue) { 107 | $this->setField($ent, $def, $fieldName, $fieldValue); 108 | } 109 | 110 | if (isset($config['afterCreate'])) { 111 | $config['afterCreate']($ent, $fieldValues); 112 | } 113 | 114 | if ($this->persist && false === $entityMetadata->isEmbeddedClass) { 115 | $this->em->persist($ent); 116 | } 117 | 118 | return $ent; 119 | } 120 | 121 | /** 122 | * Get an array of entities and their dependencies. 123 | * 124 | * Whether the entities are new or not depends on whether you've created 125 | * a singleton with the entity name. See `getAsSingleton()`. 126 | * 127 | * If you've called `persistOnGet()` then the entities are also persisted. 128 | */ 129 | public function getList($name, array $fieldOverrides = [], $numberOfInstances = 1) 130 | { 131 | if ($numberOfInstances < 1) { 132 | throw new \InvalidArgumentException('Can only get >= 1 instances'); 133 | } 134 | 135 | if ($numberOfInstances > 1 && array_key_exists($name, $this->singletons)) { 136 | $numberOfInstances = 1; 137 | } 138 | 139 | $instances = []; 140 | for ($i = 0; $i < $numberOfInstances; $i++) { 141 | $instances[] = $this->get($name, $fieldOverrides); 142 | } 143 | 144 | return $instances; 145 | } 146 | 147 | protected function checkFieldOverrides(EntityDef $def, array $fieldOverrides) 148 | { 149 | $extraFields = array_diff(array_keys($fieldOverrides), array_keys($def->getFieldDefs())); 150 | if (!empty($extraFields)) { 151 | throw new Exception("Field(s) not in " . $def->getEntityType() . ": '" . implode("', '", $extraFields) . "'"); 152 | } 153 | } 154 | 155 | protected function setField($ent, EntityDef $def, $fieldName, $fieldValue) 156 | { 157 | $metadata = $def->getEntityMetadata(); 158 | 159 | if ($metadata->isCollectionValuedAssociation($fieldName)) { 160 | $metadata->setFieldValue($ent, $fieldName, $this->createCollectionFrom($fieldValue)); 161 | } else { 162 | $metadata->setFieldValue($ent, $fieldName, $fieldValue); 163 | 164 | if (is_object($fieldValue) && $metadata->isSingleValuedAssociation($fieldName)) { 165 | $this->updateCollectionSideOfAssocation($ent, $metadata, $fieldName, $fieldValue); 166 | } 167 | } 168 | } 169 | 170 | protected function createCollectionFrom($array = []) 171 | { 172 | if (is_array($array)) { 173 | return new ArrayCollection($array); 174 | } 175 | 176 | return new ArrayCollection(); 177 | } 178 | 179 | /** 180 | * Sets whether `get()` should automatically persist the entity it creates. 181 | * By default it does not. In any case, you still need to call 182 | * flush() yourself. 183 | */ 184 | public function persistOnGet($enabled = true) 185 | { 186 | $this->persist = $enabled; 187 | } 188 | 189 | /** 190 | * A shorthand combining `get()` and `setSingleton()`. 191 | * 192 | * It's illegal to call this if `$name` already has a singleton. 193 | */ 194 | public function getAsSingleton($name, array $fieldOverrides = []) 195 | { 196 | if (isset($this->singletons[$name])) { 197 | throw new Exception("Already a singleton: $name"); 198 | } 199 | $this->singletons[$name] = $this->get($name, $fieldOverrides); 200 | return $this->singletons[$name]; 201 | } 202 | 203 | /** 204 | * Sets `$entity` to be the singleton for `$name`. 205 | * 206 | * This causes `get($name)` to return `$entity`. 207 | */ 208 | public function setSingleton($name, $entity) 209 | { 210 | $this->singletons[$name] = $entity; 211 | } 212 | 213 | /** 214 | * Unsets the singleton for `$name`. 215 | * 216 | * This causes `get($name)` to return new entities again. 217 | */ 218 | public function unsetSingleton($name) 219 | { 220 | unset($this->singletons[$name]); 221 | } 222 | 223 | /** 224 | * Defines how to create a default entity of type `$name`. 225 | * 226 | * See the readme for a tutorial. 227 | * 228 | * @return FixtureFactory 229 | */ 230 | public function defineEntity($name, array $fieldDefs = [], array $config = []) 231 | { 232 | if (isset($this->entityDefs[$name])) { 233 | throw new Exception("Entity '$name' already defined in fixture factory"); 234 | } 235 | 236 | $type = $this->addNamespace($name); 237 | if (!class_exists($type, true)) { 238 | throw new Exception("Not a class: $type"); 239 | } 240 | 241 | $metadata = $this->em->getClassMetadata($type); 242 | if (!isset($metadata)) { 243 | throw new Exception("Unknown entity type: $type"); 244 | } 245 | 246 | $this->entityDefs[$name] = new EntityDef($this->em, $name, $type, $fieldDefs, $config); 247 | 248 | return $this; 249 | } 250 | 251 | /** 252 | * @param string $name 253 | * @return string 254 | */ 255 | protected function addNamespace($name) 256 | { 257 | $name = rtrim($name, '\\'); 258 | 259 | if ($name[0] === '\\') { 260 | return $name; 261 | } 262 | 263 | return $this->entityNamespace . '\\' . $name; 264 | } 265 | 266 | protected function updateCollectionSideOfAssocation($entityBeingCreated, $metadata, $fieldName, $value) 267 | { 268 | $assoc = $metadata->getAssociationMapping($fieldName); 269 | $inverse = $assoc['inversedBy']; 270 | if ($inverse) { 271 | $valueMetadata = $this->em->getClassMetadata(get_class($value)); 272 | $collection = $valueMetadata->getFieldValue($value, $inverse); 273 | if ($collection instanceof Collection) { 274 | $collection->add($entityBeingCreated); 275 | } 276 | } 277 | } 278 | } 279 | --------------------------------------------------------------------------------