├── .coveralls.yml ├── .gitignore ├── tests ├── ORMTestInfrastructure │ ├── Fixtures │ │ ├── Importer │ │ │ ├── ReturnEntities.php │ │ │ └── LoadEntities.php │ │ ├── EntityNamespace2 │ │ │ ├── TestEntity.php │ │ │ └── TestEntityWithDependency.php │ │ └── EntityNamespace1 │ │ │ ├── InterfaceAssociation │ │ │ ├── EntityInterface.php │ │ │ ├── EntityImplementation.php │ │ │ └── EntityWithAssociationAgainstInterface.php │ │ │ ├── DependencyResolverFixtures │ │ │ ├── SingleEntity │ │ │ │ └── Entity.php │ │ │ ├── TransientBaseClass │ │ │ │ └── Entity.php │ │ │ ├── MappedSuperclassInheritance │ │ │ │ └── Entity.php │ │ │ ├── JoinedTableInheritance │ │ │ │ └── Entity.php │ │ │ ├── SingleTableInheritance │ │ │ │ └── Entity.php │ │ │ ├── TwoEntitiesInheritance │ │ │ │ └── Entity.php │ │ │ ├── JoinedTableInheritanceWithTwoSubclasses │ │ │ │ └── Entity.php │ │ │ └── JoinedTableInheritanceWithTwoLevels │ │ │ │ └── Entity.php │ │ │ ├── TestEntityRepository.php │ │ │ ├── Inheritance │ │ │ ├── DiscriminatorMapChildEntity.php │ │ │ ├── MappedSuperClassChild.php │ │ │ ├── ClassTableChildEntity.php │ │ │ ├── ClassTableChildWithParentReferenceEntity.php │ │ │ ├── ClassTableParentEntity.php │ │ │ ├── DiscriminatorMapEntity.php │ │ │ ├── MappedSuperClassParentWithReference.php │ │ │ └── ClassTableParentWithReferenceEntity.php │ │ │ ├── ReferencedEntity.php │ │ │ ├── Cascade │ │ │ ├── CascadePersistedEntity.php │ │ │ └── CascadePersistingEntity.php │ │ │ ├── TestEntity.php │ │ │ ├── TestEntityWithDependency.php │ │ │ ├── ReferenceCycleEntity.php │ │ │ └── ChainReferenceEntity.php │ ├── ConfigurationFactoryTest.php │ ├── QueryTest.php │ ├── EntityListDriverDecoratorTest.php │ ├── ImporterTest.php │ ├── EntityDependencyResolverTest.php │ └── ORMInfrastructureTest.php └── Config │ ├── InMemoryDatabaseConnectionConfigurationTest.php │ ├── ExistingConnectionConfigurationTest.php │ ├── ConnectionConfigurationTest.php │ └── FileDatabaseConnectionConfigurationTest.php ├── src ├── Config │ ├── InMemoryDatabaseConnectionConfiguration.php │ ├── ExistingConnectionConfiguration.php │ ├── ConnectionConfiguration.php │ └── FileDatabaseConnectionConfiguration.php └── ORMTestInfrastructure │ ├── QueryLogger.php │ ├── Query.php │ ├── EntityListDriverDecorator.php │ ├── ConfigurationFactory.php │ ├── Importer.php │ ├── EntityDependencyResolver.php │ └── ORMInfrastructure.php ├── phpunit.xml.dist ├── LICENSE ├── .github └── workflows │ ├── dependencies.yml │ └── tests.yml ├── composer.json ├── CHANGELOG.md └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: build/logs/clover.xml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | phpunit.xml$ 3 | .phpunit.result.cache 4 | composer.lock 5 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/Importer/ReturnEntities.php: -------------------------------------------------------------------------------- 1 | persist(new \stdClass()); 5 | $objectManager->persist(new \stdClass()); 6 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace2/TestEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1; 11 | 12 | use Doctrine\ORM\EntityRepository; 13 | 14 | /** 15 | * A custom repository for test entities. 16 | */ 17 | class TestEntityRepository extends EntityRepository 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/DependencyResolverFixtures/TransientBaseClass/Entity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | #[ORM\Entity] 15 | class DiscriminatorMapChildEntity extends DiscriminatorMapEntity 16 | { 17 | /** 18 | * @var string 19 | */ 20 | #[ORM\Column(type: 'string', name: 'child_name', nullable: false)] 21 | public $childName = 'child-name'; 22 | } 23 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/Inheritance/MappedSuperClassChild.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | #[ORM\Entity] 15 | class MappedSuperClassChild extends MappedSuperClassParentWithReference 16 | { 17 | /** 18 | * @var string 19 | */ 20 | #[ORM\Column(type: 'string', name: 'child_name', nullable: false)] 21 | public $childName = 'child-name'; 22 | } 23 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/DependencyResolverFixtures/JoinedTableInheritance/Entity.php: -------------------------------------------------------------------------------- 1 | 'BaseEntity', 'sub' => 'Entity'])] 11 | class BaseEntity 12 | { 13 | #[ORM\Column(type: 'integer')] 14 | #[ORM\Id] 15 | private $id; 16 | 17 | #[ORM\Column] 18 | protected $fieldA; 19 | } 20 | 21 | #[ORM\Entity] 22 | class Entity extends BaseEntity 23 | { 24 | #[ORM\Column] 25 | protected $fieldB; 26 | } 27 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/DependencyResolverFixtures/SingleTableInheritance/Entity.php: -------------------------------------------------------------------------------- 1 | 'BaseEntity', 'sub' => 'Entity'])] 11 | class BaseEntity 12 | { 13 | #[ORM\Column(type: 'integer')] 14 | #[ORM\Id] 15 | private $id; 16 | 17 | #[ORM\Column] 18 | protected $fieldA; 19 | } 20 | 21 | #[ORM\Entity] 22 | class Entity extends BaseEntity 23 | { 24 | #[ORM\Column] 25 | protected $fieldB; 26 | } 27 | -------------------------------------------------------------------------------- /src/Config/InMemoryDatabaseConnectionConfiguration.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Config; 11 | 12 | /** 13 | * Specifies a connection to an in-memory SQLite database. 14 | */ 15 | class InMemoryDatabaseConnectionConfiguration extends ConnectionConfiguration 16 | { 17 | /** 18 | * Creates a connection configuration that connects to an in-memory database. 19 | */ 20 | public function __construct() 21 | { 22 | parent::__construct([ 23 | 'driver' => 'pdo_sqlite', 24 | 'memory' => true 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace2/TestEntityWithDependency.php: -------------------------------------------------------------------------------- 1 | dependency = new ReferencedEntity(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/ReferencedEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * An entity that is referenced by another one. 16 | */ 17 | #[ORM\Table(name: 'referenced_entity')] 18 | #[ORM\Entity] 19 | class ReferencedEntity 20 | { 21 | /** 22 | * A unique ID. 23 | * 24 | * @var integer|null 25 | */ 26 | #[ORM\Id] 27 | #[ORM\Column(type: 'integer', name: 'id')] 28 | #[ORM\GeneratedValue] 29 | public $id = null; 30 | } 31 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/DependencyResolverFixtures/TwoEntitiesInheritance/Entity.php: -------------------------------------------------------------------------------- 1 | Superclass::class, 'entity' => Entity::class])] 12 | class Superclass 13 | { 14 | #[ORM\Column(type: 'integer')] 15 | #[ORM\Id] 16 | private $id; 17 | 18 | #[ORM\Column] 19 | protected $fieldA; 20 | } 21 | 22 | #[ORM\Entity] 23 | class Entity extends Superclass 24 | { 25 | #[ORM\Column] 26 | protected $fieldB; 27 | } 28 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/InterfaceAssociation/EntityImplementation.php: -------------------------------------------------------------------------------- 1 | id; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/InterfaceAssociation/EntityWithAssociationAgainstInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * Child class that uses class table inheritance. 16 | * 17 | * @see http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html#class-table-inheritance 18 | */ 19 | #[ORM\Table(name: 'class_inheritance_child')] 20 | #[ORM\Entity] 21 | class ClassTableChildEntity extends ClassTableParentEntity 22 | { 23 | /** 24 | * @var string 25 | */ 26 | #[ORM\Column(type: 'string', name: 'child_name', nullable: false)] 27 | public $childName = 'child-name'; 28 | } 29 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/DependencyResolverFixtures/JoinedTableInheritanceWithTwoSubclasses/Entity.php: -------------------------------------------------------------------------------- 1 | 'BaseEntity', 'first' => 'Entity', 'second' => 'SecondEntity'])] 11 | class BaseEntity 12 | { 13 | #[ORM\Column(type: 'integer')] 14 | #[ORM\Id] 15 | private $id; 16 | 17 | #[ORM\Column] 18 | protected $fieldA; 19 | } 20 | 21 | #[ORM\Entity] 22 | class SecondEntity extends BaseEntity 23 | { 24 | #[ORM\Column] 25 | protected $fieldB; 26 | } 27 | 28 | #[ORM\Entity] 29 | class Entity extends BaseEntity 30 | { 31 | #[ORM\Column] 32 | protected $fieldC; 33 | } 34 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | src 12 | 13 | 14 | 15 | 16 | tests/ 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/DependencyResolverFixtures/JoinedTableInheritanceWithTwoLevels/Entity.php: -------------------------------------------------------------------------------- 1 | 'BaseEntity', 'intermediate' => 'IntermediateEntity', 'child' => 'Entity'])] 11 | class BaseEntity 12 | { 13 | #[ORM\Column(type: 'integer')] 14 | #[ORM\Id] 15 | private $id; 16 | 17 | #[ORM\Column] 18 | protected $fieldA; 19 | } 20 | 21 | #[ORM\Entity] 22 | class IntermediateEntity extends BaseEntity 23 | { 24 | #[ORM\Column] 25 | protected $fieldB; 26 | } 27 | 28 | #[ORM\Entity] 29 | class Entity extends IntermediateEntity 30 | { 31 | #[ORM\Column] 32 | protected $fieldC; 33 | } 34 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/Cascade/CascadePersistedEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Cascade; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * Entity that automatically persists its associated entities. 16 | */ 17 | #[ORM\Table(name: 'cascade_persisted_entity')] 18 | #[ORM\Entity] 19 | class CascadePersistedEntity 20 | { 21 | /** 22 | * A unique ID. 23 | * 24 | * @var integer|null 25 | */ 26 | #[ORM\Id] 27 | #[ORM\Column(type: 'integer', name: 'id')] 28 | #[ORM\GeneratedValue] 29 | public $id = null; 30 | 31 | /** 32 | * @var CascadePersistingEntity 33 | */ 34 | #[ORM\ManyToOne(targetEntity: \CascadePersistingEntity::class)] 35 | public $parent; 36 | } 37 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/TestEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * Doctrine entity that is used for testing. 16 | */ 17 | #[ORM\Table(name: 'test_entity')] 18 | #[ORM\Entity(repositoryClass: \TestEntityRepository::class)] 19 | class TestEntity 20 | { 21 | /** 22 | * A unique ID. 23 | * 24 | * @var integer|null 25 | */ 26 | #[ORM\Id] 27 | #[ORM\Column(type: 'integer', name: 'id')] 28 | #[ORM\GeneratedValue] 29 | public $id = null; 30 | 31 | /** 32 | * A string property. 33 | * 34 | * @var string 35 | */ 36 | #[ORM\Column(type: 'string', name: 'name', nullable: true)] 37 | public $name = null; 38 | } 39 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/Inheritance/ClassTableChildWithParentReferenceEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * Child class that uses class table inheritance with a parent that references another entity. 16 | * 17 | * @see http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html#class-table-inheritance 18 | */ 19 | #[ORM\Table(name: 'class_inheritance_with_reference_child')] 20 | #[ORM\Entity] 21 | class ClassTableChildWithParentReferenceEntity extends ClassTableParentWithReferenceEntity 22 | { 23 | /** 24 | * @var string 25 | */ 26 | #[ORM\Column(type: 'string', name: 'child_name', nullable: false)] 27 | public $childName = 'child-name'; 28 | } 29 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/Inheritance/ClassTableParentEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * Base class for entities with class table strategy. 16 | * 17 | * @see http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html#class-table-inheritance 18 | */ 19 | #[ORM\Table(name: 'class_inheritance_parent')] 20 | #[ORM\Entity] 21 | #[ORM\InheritanceType('JOINED')] 22 | #[ORM\DiscriminatorColumn(name: 'class', type: 'string')] 23 | abstract class ClassTableParentEntity 24 | { 25 | /** 26 | * A unique ID. 27 | * 28 | * @var integer|null 29 | */ 30 | #[ORM\Id] 31 | #[ORM\Column(type: 'integer', name: 'id')] 32 | #[ORM\GeneratedValue] 33 | public $id = null; 34 | } 35 | -------------------------------------------------------------------------------- /src/ORMTestInfrastructure/QueryLogger.php: -------------------------------------------------------------------------------- 1 | enabled) { 16 | return; 17 | } 18 | 19 | if (str_starts_with($message, 'Executing')) { 20 | $this->queries[] = new Query($context['sql'], $context['params'] ?? []); 21 | } else if ('Beginning transaction' === $message) { 22 | $this->queries[] = new Query('"START TRANSACTION"', []); 23 | } else if ('Committing transaction' === $message) { 24 | $this->queries[] = new Query('"COMMIT"', []); 25 | } else if ('Rolling back transaction' === $message) { 26 | $this->queries[] = new Query('"ROLLBACK"', []); 27 | } 28 | } 29 | 30 | public function getQueries() 31 | { 32 | return $this->queries; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Config/InMemoryDatabaseConnectionConfigurationTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\Config; 11 | 12 | use PHPUnit\Framework\TestCase; 13 | use Webfactory\Doctrine\Config\InMemoryDatabaseConnectionConfiguration; 14 | use Webfactory\Doctrine\ORMTestInfrastructure\ORMInfrastructure; 15 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\TestEntity; 16 | 17 | class InMemoryDatabaseConnectionConfigurationTest extends TestCase 18 | { 19 | /** 20 | * Checks if the connection configuration *really* works with the infrastructure. 21 | */ 22 | public function testWorksWithInfrastructure() 23 | { 24 | $configuration = new InMemoryDatabaseConnectionConfiguration(); 25 | 26 | $infrastructure = ORMInfrastructure::createOnlyFor(TestEntity::class, $configuration); 27 | $this->assertNull( 28 | $infrastructure->import(new TestEntity()) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 webfactory GmbH, Bonn (info@webfactory.de) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/Config/ExistingConnectionConfiguration.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 25 | } 26 | 27 | /** 28 | * Provides the existing DBAL connection 29 | * 30 | * This makes use of the fact that the first argument to EntityManager::create() is in fact 31 | * un-typed: You can pass in either a configuration array or an existing DBAL connection. 32 | * 33 | * @return Connection 34 | */ 35 | public function getConnectionParameters() 36 | { 37 | return $this->connection; 38 | } 39 | 40 | public function getConnection(): Connection 41 | { 42 | return $this->connection; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/Inheritance/DiscriminatorMapEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * Base entity with an explicit discriminator map. 16 | * 17 | * The discriminator map contains fully qualified as well as relative entity class names. 18 | * 19 | * @see http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html#class-table-inheritance 20 | */ 21 | #[ORM\Entity] 22 | #[ORM\InheritanceType('JOINED')] 23 | #[ORM\DiscriminatorColumn(name: 'type', type: 'string')] 24 | #[ORM\DiscriminatorMap(['parent' => 'Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance\DiscriminatorMapEntity', 'child' => 'DiscriminatorMapChildEntity'])] 25 | class DiscriminatorMapEntity 26 | { 27 | /** 28 | * A unique ID. 29 | * 30 | * @var integer|null 31 | */ 32 | #[ORM\Id] 33 | #[ORM\Column(type: 'integer', name: 'id')] 34 | #[ORM\GeneratedValue] 35 | public $id = null; 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yml: -------------------------------------------------------------------------------- 1 | name: AllDependenciesDeclared 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | env: 10 | PHP_VERSION: 8.4 11 | 12 | jobs: 13 | composer-require-checker: 14 | name: Check missing composer requirements 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Configure PHP version 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ env.PHP_VERSION }} 21 | coverage: none 22 | tools: composer:v2 23 | - uses: actions/checkout@v4 24 | - name: Cache Composer Dependencies 25 | uses: actions/cache@v4 26 | with: 27 | path: vendor/ 28 | key: composer-${{ env.PHP_VERSION }}-${{ hashFiles('composer.*') }} 29 | restore-keys: | 30 | composer-${{ env.PHP_VERSION }}-${{ github.ref }} 31 | composer-${{ env.PHP_VERSION }}- 32 | - run: | 33 | composer update --no-interaction --no-scripts --no-progress 34 | composer show 35 | - name: ComposerRequireChecker 36 | uses: docker://ghcr.io/webfactory/composer-require-checker:4.12.0 37 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/TestEntityWithDependency.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * Test entity that references another entity and therefore implicitly depends 16 | * on it in test scenarios. 17 | */ 18 | #[ORM\Table(name: 'test_entity_with_dependency')] 19 | #[ORM\Entity] 20 | class TestEntityWithDependency 21 | { 22 | /** 23 | * A unique ID. 24 | * 25 | * @var integer|null 26 | */ 27 | #[ORM\Id] 28 | #[ORM\Column(type: 'integer', name: 'id')] 29 | #[ORM\GeneratedValue] 30 | public $id = null; 31 | 32 | /** 33 | * Required reference to another entity. 34 | * 35 | * @var ReferencedEntity 36 | */ 37 | #[ORM\JoinColumn(nullable: false)] 38 | #[ORM\OneToOne(targetEntity: \ReferencedEntity::class, cascade: ['all'])] 39 | protected $dependency = null; 40 | 41 | /** 42 | * Automatically creates a reference on construction. 43 | */ 44 | public function __construct() 45 | { 46 | $this->dependency = new ReferencedEntity(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webfactory/doctrine-orm-test-infrastructure", 3 | "description": "Provides utils to create a test infrastructure for Doctrine 2 entities.", 4 | "keywords": [ 5 | "Doctrine", 6 | "ORM", 7 | "testing" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "webfactory GmbH", 13 | "email": "info@webfactory.de", 14 | "homepage": "http://www.webfactory.de", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": ">= 8.1", 20 | "doctrine/common": "^3.0", 21 | "doctrine/dbal": "^3.2", 22 | "doctrine/event-manager": "^1.1|^2.0", 23 | "doctrine/orm": "^2.20|^3.0", 24 | "doctrine/persistence": "^2.5|^3.0|^4.0", 25 | "psr/log": "^2.0|^3.0", 26 | "symfony/cache": "^6.4|^7.0" 27 | }, 28 | "require-dev": { 29 | "doctrine/collections": "^1.6.8|^2.2.1", 30 | "phpunit/phpunit": "^10.5.58", 31 | "symfony/var-exporter": "^6.4|^7.3" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Webfactory\\Doctrine\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Webfactory\\Doctrine\\Tests\\": "tests/" 41 | } 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/ReferenceCycleEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * Entity with a minimal reference cycle. 16 | */ 17 | #[ORM\Table(name: 'test_reference_cycle_entity')] 18 | #[ORM\Entity] 19 | class ReferenceCycleEntity 20 | { 21 | /** 22 | * A unique ID. 23 | * 24 | * @var integer|null 25 | */ 26 | #[ORM\Id] 27 | #[ORM\Column(type: 'integer', name: 'id')] 28 | #[ORM\GeneratedValue] 29 | public $id = null; 30 | 31 | /** 32 | * Reference to an entity of the same type (minimal cycle). 33 | * 34 | * @var ReferenceCycleEntity|null 35 | */ 36 | #[ORM\JoinColumn(nullable: true)] 37 | #[ORM\OneToOne(targetEntity: \ReferenceCycleEntity::class, cascade: ['all'])] 38 | protected $referenceCycle = null; 39 | 40 | /** 41 | * Creates an entity that references the provided entity. 42 | * 43 | * @param ReferenceCycleEntity|null $entity 44 | */ 45 | public function __construct(?ReferenceCycleEntity $entity = null) 46 | { 47 | $this->referenceCycle = $entity; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/ConfigurationFactoryTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure; 11 | 12 | use Doctrine\ORM\Configuration; 13 | use PHPUnit\Framework\TestCase; 14 | use Webfactory\Doctrine\ORMTestInfrastructure\ConfigurationFactory; 15 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\TestEntity; 16 | 17 | /** 18 | * Tests the ORM configuration factory. 19 | */ 20 | class ConfigurationFactoryTest extends TestCase 21 | { 22 | /** 23 | * System under test. 24 | * 25 | * @var ConfigurationFactory 26 | */ 27 | protected $factory = null; 28 | 29 | /** 30 | * Initializes the test environment. 31 | */ 32 | protected function setUp(): void 33 | { 34 | parent::setUp(); 35 | $this->factory = new ConfigurationFactory(); 36 | } 37 | 38 | /** 39 | * Ensures that createFor() returns an ORM configuration object. 40 | */ 41 | public function testCreateForReturnsConfiguration() 42 | { 43 | $configuration = $this->factory->createFor(array( 44 | TestEntity::class, 45 | )); 46 | 47 | $this->assertInstanceOf(Configuration::class, $configuration); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/ChainReferenceEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | 14 | /** 15 | * Entity that references an entity indirectly (over another reference). 16 | * 17 | * Reference chain: 18 | * 19 | * ChainReferenceEntity -> TestEntityWithDependency -> ReferencedEntity 20 | */ 21 | #[ORM\Table(name: 'test_chain_reference_entity')] 22 | #[ORM\Entity] 23 | class ChainReferenceEntity 24 | { 25 | /** 26 | * A unique ID. 27 | * 28 | * @var integer|null 29 | */ 30 | #[ORM\Id] 31 | #[ORM\Column(type: 'integer', name: 'id')] 32 | #[ORM\GeneratedValue] 33 | public $id = null; 34 | 35 | /** 36 | * Required reference to another entity. 37 | * 38 | * @var TestEntityWithDependency 39 | */ 40 | #[ORM\JoinColumn(nullable: false)] 41 | #[ORM\OneToOne(targetEntity: \TestEntityWithDependency::class, cascade: ['all'])] 42 | protected $dependency = null; 43 | 44 | /** 45 | * Automatically adds a referenced entity on construction. 46 | */ 47 | public function __construct() 48 | { 49 | $this->dependency = new TestEntityWithDependency(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Config/ConnectionConfiguration.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Config; 11 | 12 | /** 13 | * Represents a Doctrine database connection configuration. 14 | * 15 | * This class has been created to be able to use type hints for connection parameters 16 | * and to be able to provide pre-configured connection configurations (for example as 17 | * subclasses or via factory). 18 | * 19 | * Any connection parameters that are supported by Doctrine DBAL can be used in the configuration. 20 | * 21 | * @see http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html 22 | */ 23 | class ConnectionConfiguration 24 | { 25 | /** 26 | * Connection parameters that are compatible to Doctrine DBAL. 27 | * 28 | * @var array 29 | */ 30 | private $connectionParameters = null; 31 | 32 | /** 33 | * @param array $connectionParameters 34 | */ 35 | public function __construct(array $connectionParameters) 36 | { 37 | $this->connectionParameters = $connectionParameters; 38 | } 39 | 40 | /** 41 | * Returns the connection parameters that are passed to Doctrine. 42 | * 43 | * @return array 44 | */ 45 | public function getConnectionParameters() 46 | { 47 | return $this->connectionParameters; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Config/ExistingConnectionConfigurationTest.php: -------------------------------------------------------------------------------- 1 | connection = DriverManager::getConnection([ 31 | 'driver' => 'pdo_sqlite', 32 | 'memory' => true, 33 | ]); 34 | 35 | $this->connectionConfiguration = new ExistingConnectionConfiguration($this->connection); 36 | 37 | parent::setUp(); 38 | } 39 | 40 | public function testWorksWithInfrastructure() 41 | { 42 | $infrastructure = ORMInfrastructure::createOnlyFor( 43 | array(TestEntity::class), 44 | $this->connectionConfiguration 45 | ); 46 | 47 | $infrastructure->import(new TestEntity()); 48 | 49 | $this->assertEquals(1, $this->connection->fetchOne('SELECT COUNT(*) FROM test_entity')); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/Inheritance/MappedSuperClassParentWithReference.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ReferencedEntity; 14 | 15 | /** 16 | * Mapped super class that references another entity. 17 | * 18 | * @see http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html#mapped-superclasses 19 | */ 20 | #[ORM\MappedSuperclass] 21 | abstract class MappedSuperClassParentWithReference 22 | { 23 | /** 24 | * A unique ID. 25 | * 26 | * @var integer|null 27 | */ 28 | #[ORM\Id] 29 | #[ORM\Column(type: 'integer', name: 'id')] 30 | #[ORM\GeneratedValue] 31 | public $id = null; 32 | 33 | /** 34 | * Required reference to another entity. 35 | * 36 | * @var ReferencedEntity 37 | */ 38 | #[ORM\JoinColumn(nullable: false)] 39 | #[ORM\OneToOne(targetEntity: \Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ReferencedEntity::class, cascade: ['all'])] 40 | protected $dependency = null; 41 | 42 | /** 43 | * Automatically creates a reference on construction. 44 | */ 45 | public function __construct() 46 | { 47 | $this->dependency = new ReferencedEntity(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/Cascade/CascadePersistingEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Cascade; 11 | 12 | use Doctrine\Common\Collections\ArrayCollection; 13 | use Doctrine\Common\Collections\Collection; 14 | use Doctrine\ORM\Mapping as ORM; 15 | 16 | /** 17 | * Entity that automatically persists its associated entities. 18 | */ 19 | #[ORM\Table(name: 'cascade_persist')] 20 | #[ORM\Entity] 21 | class CascadePersistingEntity 22 | { 23 | /** 24 | * A unique ID. 25 | * 26 | * @var integer|null 27 | */ 28 | #[ORM\Id] 29 | #[ORM\Column(type: 'integer', name: 'id')] 30 | #[ORM\GeneratedValue] 31 | public $id = null; 32 | 33 | /** 34 | * @var Collection 35 | */ 36 | #[ORM\OneToMany(targetEntity: \CascadePersistedEntity::class, mappedBy: 'parent', cascade: ['persist'])] 37 | private $associated; 38 | 39 | /** 40 | * Initializes the collection. 41 | */ 42 | public function __construct() 43 | { 44 | $this->associated = new ArrayCollection(); 45 | } 46 | 47 | /** 48 | * Adds the given entity to the persisting association. 49 | * 50 | * @param CascadePersistedEntity $entity 51 | */ 52 | public function add(CascadePersistedEntity $entity) 53 | { 54 | $entity->parent = $this; 55 | $this->associated->add($entity); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for `webfactory/doctrine-orm-test-infrastructure` 2 | 3 | This changelog tracks deprecations and changes breaking backwards compatibility. For more details on particular releases, consult the [GitHub releases page](https://github.com/webfactory/doctrine-orm-test-infrastructure/releases). 4 | 5 | # Version 2.0 6 | 7 | - The `ORMInfrastructure::create*()` methods by default read ORM mapping configuration through PHP attributes; annotations support has been removed in https://github.com/webfactory/doctrine-orm-test-infrastructure/pull/55/. You can, however, still create an instance of the `AnnotationDriver` mapping driver yourself (when using ORM 2.0) and pass it into these methods. 8 | - `\Webfactory\Doctrine\ORMTestInfrastructure\ORMInfrastructure::__construct()` is now a private method. Use the `::create*` methods to instantiate the `ORMInfrastructure`. 9 | - The `\Webfactory\Doctrine\ORMTestInfrastructure\Query::getExecutionTimeInSeconds()` method has been removed. 10 | - The `DetachingObjectManagerDecorator` and `MemorizingObjectManagerDecorator` classes have been removed. 11 | 12 | # Version 1.16 13 | 14 | - The `\Webfactory\Doctrine\ORMTestInfrastructure\Query::getExecutionTimeInSeconds()` method has been deprecated without replacement in https://github.com/webfactory/doctrine-orm-test-infrastructure/pull/52, to prepare for the removal of the `DebugStack` class in Doctrine DBAL 4.0. 15 | - Using annotation-based mapping as the default in `ORMInfrastructure::create*()` methods has been deprecated. Pass a mapping driver or upgrade `doctrine/orm` to >= 3.0 to switch to attributes-based mapping. Attributes-based configuration will be the default in the next major version. 16 | -------------------------------------------------------------------------------- /src/ORMTestInfrastructure/Query.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\ORMTestInfrastructure; 11 | 12 | /** 13 | * Represents a query that has been executed. 14 | */ 15 | class Query 16 | { 17 | /** 18 | * The SQL query. 19 | * 20 | * @var string 21 | */ 22 | protected $sql = null; 23 | 24 | /** 25 | * The assigned parameters. 26 | * 27 | * @var mixed[] 28 | */ 29 | protected $params = null; 30 | 31 | /** 32 | * Currently not used: 33 | * - types 34 | * 35 | * @param string $sql - sql 36 | * @param mixed[] $params - params 37 | */ 38 | public function __construct($sql, array $params) 39 | { 40 | $this->sql = $sql; 41 | $this->params = $params; 42 | } 43 | 44 | /** 45 | * Returns the SQL of the query. 46 | * 47 | * @return string 48 | */ 49 | public function getSql() 50 | { 51 | return $this->sql; 52 | } 53 | 54 | /** 55 | * Returns a list of parameters that have been assigned to the statement. 56 | * 57 | * @return mixed[] 58 | */ 59 | public function getParams() 60 | { 61 | return $this->params; 62 | } 63 | 64 | /** 65 | * Returns a string representation of the query and its params. 66 | * 67 | * @return string 68 | */ 69 | public function __toString() 70 | { 71 | $template = '"%s" with parameters [%s]'; 72 | return sprintf($template, $this->getSql(), implode(', ', $this->getParams())); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/Fixtures/EntityNamespace1/Inheritance/ClassTableParentWithReferenceEntity.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance; 11 | 12 | use Doctrine\ORM\Mapping as ORM; 13 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ReferencedEntity; 14 | 15 | /** 16 | * Base class for entities with class table strategy. 17 | * 18 | * References another entity. This dependency is inherited by all children. 19 | * 20 | * @see http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html#class-table-inheritance 21 | */ 22 | #[ORM\Table(name: 'class_inheritance_with_reference_parent')] 23 | #[ORM\Entity] 24 | #[ORM\InheritanceType('JOINED')] 25 | #[ORM\DiscriminatorColumn(name: 'class', type: 'string')] 26 | abstract class ClassTableParentWithReferenceEntity 27 | { 28 | /** 29 | * A unique ID. 30 | * 31 | * @var integer|null 32 | */ 33 | #[ORM\Id] 34 | #[ORM\Column(type: 'integer', name: 'id')] 35 | #[ORM\GeneratedValue] 36 | public $id = null; 37 | 38 | /** 39 | * Required reference to another entity. 40 | * 41 | * @var ReferencedEntity 42 | */ 43 | #[ORM\JoinColumn(nullable: false)] 44 | #[ORM\OneToOne(targetEntity: \Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ReferencedEntity::class, cascade: ['all'])] 45 | protected $dependency = null; 46 | 47 | /** 48 | * Automatically creates a reference on construction. 49 | */ 50 | public function __construct() 51 | { 52 | $this->dependency = new ReferencedEntity(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: '10 6 * * 1' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - { php-version: 8.1, orm-version: '', dependency-version: prefer-lowest } 19 | - { php-version: 8.3, orm-version: '^2.20', dependency-version: prefer-stable } 20 | - { php-version: 8.3, orm-version: '^3.0', dependency-version: prefer-stable } 21 | - { php-version: 8.4, orm-version: '^2.20', dependency-version: prefer-stable } 22 | - { php-version: 8.4, orm-version: '^3.0', dependency-version: prefer-stable } 23 | name: PHPUnit (PHP ${{matrix.php-version}}, Doctrine ORM version lock ${{ matrix.orm-version || 'none' }}, ${{ matrix.dependency-version }}) 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Install PHP 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | php-version: ${{ matrix.php-version }} 30 | coverage: none 31 | - name: Lock Doctrine ORM version 32 | run: composer require --no-interaction --no-progress --no-suggest --no-scripts --ansi --no-update doctrine/orm '${{ matrix.orm-version }}' 33 | if: matrix.orm-version != '' 34 | - name: Install dependencies 35 | run: composer update --no-interaction --no-progress --no-suggest --no-scripts --ansi --${{ matrix.dependency-version}} 36 | - name: Display installed dependencies 37 | run: composer show 38 | - name: Run test suite 39 | run: vendor/bin/phpunit 40 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/QueryTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure; 11 | 12 | use PHPUnit\Framework\TestCase; 13 | use Webfactory\Doctrine\ORMTestInfrastructure\Query; 14 | 15 | /** 16 | * Tests the value object that holds query data. 17 | */ 18 | class QueryTest extends TestCase 19 | { 20 | /** 21 | * System under test. 22 | * 23 | * @var Query 24 | */ 25 | protected $query = null; 26 | 27 | /** 28 | * Initializes the test environment. 29 | */ 30 | protected function setUp(): void 31 | { 32 | parent::setUp(); 33 | $this->query= new Query( 34 | 'SELECT * FROM user WHERE id = ?', 35 | array(42) 36 | ); 37 | } 38 | 39 | /** 40 | * Cleans up the test environment. 41 | */ 42 | protected function tearDown(): void 43 | { 44 | $this->query = null; 45 | parent::tearDown(); 46 | } 47 | 48 | /** 49 | * Checks if the correct SQL is returned by the query object. 50 | */ 51 | public function testGetSqlReturnsCorrectValue() 52 | { 53 | $this->assertEquals('SELECT * FROM user WHERE id = ?', $this->query->getSql()); 54 | } 55 | 56 | /** 57 | * Checks if the query parameters are returned correctly. 58 | */ 59 | public function testGetParamsReturnsCorrectValue() 60 | { 61 | $this->assertEquals(array(42), $this->query->getParams()); 62 | } 63 | 64 | /** 65 | * Checks if the query object can be used to retrieve a string representation of the query. 66 | */ 67 | public function testQueryObjectProvidesStringRepresentation() 68 | { 69 | $this->assertNotEmpty((string)$this->query); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Config/ConnectionConfigurationTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\Config; 11 | 12 | use PHPUnit\Framework\TestCase; 13 | use Webfactory\Doctrine\Config\ConnectionConfiguration; 14 | 15 | class ConnectionConfigurationTest extends TestCase 16 | { 17 | /** 18 | * System under test. 19 | * 20 | * @var ConnectionConfiguration 21 | */ 22 | private $connectionConfiguration = null; 23 | 24 | /** 25 | * Initializes the test environment. 26 | */ 27 | protected function setUp(): void 28 | { 29 | parent::setUp(); 30 | $this->connectionConfiguration = new ConnectionConfiguration(array( 31 | 'driver' => 'pdo_sqlite', 32 | 'user' => 'root', 33 | 'password' => '', 34 | 'memory' => true 35 | )); 36 | } 37 | 38 | /** 39 | * Cleans up the test environment. 40 | */ 41 | protected function tearDown(): void 42 | { 43 | $this->connectionConfiguration = null; 44 | parent::tearDown(); 45 | } 46 | 47 | public function testGetConnectionParametersReturnsProvidedValues() 48 | { 49 | $params = $this->connectionConfiguration->getConnectionParameters(); 50 | 51 | $this->assertIsArray($params); 52 | $expectedParams = array( 53 | 'driver' => 'pdo_sqlite', 54 | 'user' => 'root', 55 | 'password' => '', 56 | 'memory' => true 57 | ); 58 | foreach ($expectedParams as $param => $value) { 59 | $this->assertArrayHasKey($param, $params, 'Connection parameter missing.'); 60 | $this->assertEquals($value, $params[$param], 'Unexpected value for connection parameter "' . $param . '".'); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Config/FileDatabaseConnectionConfiguration.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Config; 11 | 12 | /** 13 | * Specifies a connection to a file-based SQLite database. 14 | */ 15 | class FileDatabaseConnectionConfiguration extends ConnectionConfiguration 16 | { 17 | /** 18 | * Creates a configuration that uses the given SQLite database file. 19 | * 20 | * Omit the file path to ensure that a temporary database file is created. 21 | * 22 | * @param string|null $databaseFilePath 23 | */ 24 | public function __construct($databaseFilePath = null) 25 | { 26 | parent::__construct(array( 27 | 'driver' => 'pdo_sqlite', 28 | 'user' => 'root', 29 | 'password' => '', 30 | 'path' => $this->toDatabaseFilePath($databaseFilePath) 31 | )); 32 | } 33 | 34 | /** 35 | * Returns the path to the database file. 36 | * 37 | * The database file may not exist. 38 | * 39 | * @return \SplFileInfo 40 | */ 41 | public function getDatabaseFile() 42 | { 43 | $parameters = $this->getConnectionParameters(); 44 | return new \SplFileInfo($parameters['path']); 45 | } 46 | 47 | /** 48 | * Removes the database file if it exists. 49 | * 50 | * @return $this Provides a fluent interface. 51 | */ 52 | public function cleanUp() 53 | { 54 | if ($this->getDatabaseFile()->isFile()) { 55 | unlink($this->getDatabaseFile()->getPathname()); 56 | } 57 | return $this; 58 | } 59 | 60 | /** 61 | * Returns a file path for the database file. 62 | * 63 | * Generates a unique file name if the given $filePath is null. 64 | * 65 | * @param string|null $filePath 66 | * @return string 67 | */ 68 | private function toDatabaseFilePath($filePath) 69 | { 70 | if ($filePath === null) { 71 | $temporaryFile = sys_get_temp_dir() . '/' . uniqid('db-', true) . '.sqlite'; 72 | // Ensure that the temporary file is removed on shutdown, otherwise the filesystem 73 | // might be cluttered with database files. 74 | register_shutdown_function(array($this, 'cleanUp')); 75 | return $temporaryFile; 76 | } 77 | return $filePath; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ORMTestInfrastructure/EntityListDriverDecorator.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\ORMTestInfrastructure; 11 | 12 | use Doctrine\Persistence\Mapping\ClassMetadata; 13 | use Doctrine\Persistence\Mapping\Driver\MappingDriver; 14 | 15 | /** 16 | * Driver decorator that restricts metadata access to a defined list of entities. 17 | * 18 | * @see https://github.com/webfactory/doctrine-orm-test-infrastructure/issues/11 19 | */ 20 | class EntityListDriverDecorator implements MappingDriver 21 | { 22 | /** 23 | * The decorated driver. 24 | * 25 | * @var MappingDriver 26 | */ 27 | protected $innerDriver = null; 28 | 29 | /** 30 | * Class names of all entities that are exposed. 31 | * 32 | * @var string[] 33 | */ 34 | protected $exposedEntityClasses = null; 35 | 36 | /** 37 | * @param MappingDriver $innerDriver 38 | * @param string[] $exposedEntityClasses 39 | */ 40 | public function __construct(MappingDriver $innerDriver, array $exposedEntityClasses) 41 | { 42 | $this->innerDriver = $innerDriver; 43 | $this->exposedEntityClasses = $this->normalizeClassNames($exposedEntityClasses); 44 | } 45 | 46 | /** 47 | * Gets the names of all mapped classes known to this driver. 48 | * 49 | * @return array The names of all mapped classes known to this driver. 50 | */ 51 | public function getAllClassNames(): array 52 | { 53 | return array_intersect( 54 | $this->exposedEntityClasses, 55 | $this->innerDriver->getAllClassNames() 56 | ); 57 | } 58 | 59 | /** 60 | * Loads the metadata for the specified class into the provided container. 61 | * 62 | * @param string $className 63 | * @param ClassMetadata $metadata 64 | */ 65 | public function loadMetadataForClass($className, ClassMetadata $metadata): void 66 | { 67 | $this->innerDriver->loadMetadataForClass($className, $metadata); 68 | } 69 | 70 | /** 71 | * Returns whether the class with the specified name should have its metadata loaded. 72 | * This is only the case if it is either mapped as an Entity or a MappedSuperclass. 73 | * 74 | * @param string $className 75 | * @return boolean 76 | */ 77 | public function isTransient($className): bool 78 | { 79 | return $this->innerDriver->isTransient($className); 80 | } 81 | 82 | /** 83 | * Removes leading slashes from the given class names. 84 | * 85 | * @param string[] $entityClasses 86 | * @return string[] 87 | */ 88 | protected function normalizeClassNames(array $entityClasses) 89 | { 90 | return array_map(function ($class) { 91 | return ltrim($class, '\\'); 92 | }, $entityClasses); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/ORMTestInfrastructure/ConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\ORMTestInfrastructure; 11 | 12 | use Doctrine\ORM\Mapping\Driver\AttributeDriver; 13 | use Doctrine\ORM\ORMSetup; 14 | use Doctrine\Persistence\Mapping\Driver\MappingDriver; 15 | use Psr\Cache\CacheItemPoolInterface; 16 | use Symfony\Component\Cache\Adapter\ArrayAdapter; 17 | 18 | /** 19 | * Creates ORM configurations for a set of entities. 20 | * 21 | * These configurations are meant for testing only. 22 | */ 23 | class ConfigurationFactory 24 | { 25 | /** @var ?CacheItemPoolInterface */ 26 | private static $metadataCache = null; 27 | 28 | /** @var ?MappingDriver */ 29 | private $mappingDriver; 30 | 31 | public function __construct(?MappingDriver $mappingDriver = null) 32 | { 33 | $this->mappingDriver = $mappingDriver; 34 | } 35 | 36 | /** 37 | * Creates the ORM configuration for the given set of entities. 38 | * 39 | * @param string[] $entityClasses 40 | * @return \Doctrine\ORM\Configuration 41 | */ 42 | public function createFor(array $entityClasses) 43 | { 44 | if (self::$metadataCache === null) { 45 | self::$metadataCache = new ArrayAdapter(); 46 | } 47 | 48 | $mappingDriver = $this->mappingDriver ?? $this->createDefaultMappingDriver($entityClasses); 49 | 50 | $config = ORMSetup::createConfiguration(true, null, new ArrayAdapter()); 51 | $config->setMetadataCache(self::$metadataCache); 52 | $config->setMetadataDriverImpl(new EntityListDriverDecorator($mappingDriver, $entityClasses)); 53 | 54 | return $config; 55 | } 56 | 57 | /** 58 | * @param list $entityClasses 59 | * 60 | * @return MappingDriver 61 | */ 62 | private function createDefaultMappingDriver(array $entityClasses) 63 | { 64 | $paths = $this->getDirectoryPathsForClassNames($entityClasses); 65 | 66 | return new AttributeDriver($paths); 67 | } 68 | 69 | /** 70 | * Returns a list of file paths for the provided class names. 71 | * 72 | * @param list $classNames 73 | * @return list 74 | */ 75 | protected function getDirectoryPathsForClassNames(array $classNames) 76 | { 77 | $paths = array(); 78 | foreach ($classNames as $className) { 79 | $paths[] = $this->getDirectoryPathForClassName($className); 80 | } 81 | return array_unique($paths); 82 | } 83 | 84 | /** 85 | * Returns the path to the directory that contains the given class. 86 | * 87 | * @param class-string $className 88 | * @return string 89 | */ 90 | protected function getDirectoryPathForClassName($className) 91 | { 92 | $info = new \ReflectionClass($className); 93 | return dirname($info->getFileName()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/EntityListDriverDecoratorTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure; 11 | 12 | use Doctrine\Persistence\Mapping\Driver\MappingDriver; 13 | use Doctrine\ORM\Mapping\ClassMetadata; 14 | use PHPUnit\Framework\MockObject\MockObject; 15 | use PHPUnit\Framework\TestCase; 16 | use Webfactory\Doctrine\ORMTestInfrastructure\EntityListDriverDecorator; 17 | 18 | class EntityListDriverDecoratorTest extends TestCase 19 | { 20 | /** 21 | * System under test. 22 | * 23 | * @var EntityListDriverDecorator 24 | */ 25 | protected $driver = null; 26 | 27 | /** 28 | * The mocked, decorated driver. 29 | * 30 | * @var MappingDriver|MockObject 31 | */ 32 | protected $innerDriver = null; 33 | 34 | /** 35 | * Initializes the test environment. 36 | */ 37 | protected function setUp(): void 38 | { 39 | parent::setUp(); 40 | $this->innerDriver = $this->createMock(MappingDriver::class); 41 | $this->driver = new EntityListDriverDecorator($this->innerDriver, array( 42 | 'My\Namespace\Person', 43 | 'My\Namespace\Address' 44 | )); 45 | } 46 | 47 | /** 48 | * Cleans up the test environment. 49 | */ 50 | protected function tearDown(): void 51 | { 52 | $this->driver = null; 53 | $this->innerDriver = null; 54 | parent::tearDown(); 55 | } 56 | 57 | public function testImplementsInterface() 58 | { 59 | $this->assertInstanceOf(MappingDriver::class, $this->driver); 60 | } 61 | 62 | public function testGetAllClassNamesReturnsOnlyExposedEntityClasses() 63 | { 64 | $this->innerDriver->expects($this->any()) 65 | ->method('getAllClassNames') 66 | ->will($this->returnValue(array( 67 | 'My\Namespace\Person', 68 | 'My\Namespace\Address', 69 | 'My\Namespace\PhoneNumber' 70 | ))); 71 | 72 | $classes = $this->driver->getAllClassNames(); 73 | 74 | $this->assertIsArray($classes); 75 | $this->assertContains('My\Namespace\Person', $classes); 76 | $this->assertContains('My\Namespace\Address', $classes); 77 | $this->assertNotContains('My\Namespace\PhoneNumber', $classes); 78 | } 79 | 80 | /** 81 | * Ensures that the driver decorator does not expose entity classes, which are listed, but 82 | * not supported by the iner driver. 83 | */ 84 | public function testDriverDoesNotExposeEntitiesThatAreInListButNotSupportedByInnerDriver() 85 | { 86 | $this->innerDriver->expects($this->any()) 87 | ->method('getAllClassNames') 88 | ->will($this->returnValue(array( 89 | // The inner driver supports Person, but not Address. 90 | 'My\Namespace\Person' 91 | ))); 92 | 93 | $classes = $this->driver->getAllClassNames(); 94 | 95 | $this->assertIsArray($classes); 96 | $this->assertContains('My\Namespace\Person', $classes); 97 | $this->assertNotContains('My\Namespace\Address', $classes); 98 | } 99 | 100 | public function testGetAllClassNamesWorksIfEntityClassWasPassedWithLeadingBackslash() 101 | { 102 | $this->driver = new EntityListDriverDecorator($this->innerDriver, array( 103 | // The entity class is passed with a leading slash. 104 | '\My\Namespace\Person' 105 | )); 106 | $this->innerDriver->expects($this->any()) 107 | ->method('getAllClassNames') 108 | ->will($this->returnValue(array( 109 | 'My\Namespace\Person' 110 | ))); 111 | 112 | $classes = $this->driver->getAllClassNames(); 113 | 114 | $this->assertIsArray($classes); 115 | $this->assertContains('My\Namespace\Person', $classes); 116 | } 117 | 118 | public function testDriverDelegatesMetadataCalls() 119 | { 120 | $this->innerDriver->expects($this->once()) 121 | ->method('loadMetadataForClass'); 122 | 123 | $this->driver->loadMetadataForClass('My\Namespace\Person', new ClassMetadata('My\Namespace\Person')); 124 | } 125 | 126 | public function testDriverDelegatesIsTransientCall() 127 | { 128 | $this->innerDriver->expects($this->once()) 129 | ->method('isTransient') 130 | ->willReturn(false); 131 | 132 | $this->driver->isTransient('My\Namespace\Person'); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Config/FileDatabaseConnectionConfigurationTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\Config; 11 | 12 | use PHPUnit\Framework\TestCase; 13 | use Webfactory\Doctrine\Config\ConnectionConfiguration; 14 | use Webfactory\Doctrine\Config\FileDatabaseConnectionConfiguration; 15 | use Webfactory\Doctrine\ORMTestInfrastructure\ORMInfrastructure; 16 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\TestEntity; 17 | 18 | class FileDatabaseConnectionConfigurationTest extends TestCase 19 | { 20 | public function testKeepsProvidedFilePath() 21 | { 22 | $path = __DIR__ . '/_files/my-db.sqlite'; 23 | $configuration = new FileDatabaseConnectionConfiguration($path); 24 | 25 | $file = $configuration->getDatabaseFile(); 26 | $this->assertInstanceOf('SplFileInfo', $file); 27 | $this->assertEquals($path, $file->getPathname()); 28 | } 29 | 30 | public function testGeneratedFileNameIsNotChangedForExistingConfigurationObject() 31 | { 32 | $configuration = new FileDatabaseConnectionConfiguration(); 33 | 34 | $firstCall = $configuration->getDatabaseFile(); 35 | $secondCall = $configuration->getDatabaseFile(); 36 | $this->assertInstanceOf('SplFileInfo', $firstCall); 37 | $this->assertInstanceOf('SplFileInfo', $secondCall); 38 | $this->assertEquals($firstCall->getPathname(), $secondCall->getPathname()); 39 | } 40 | 41 | public function testGeneratesUniqueFileNameIfFilePathIsOmitted() 42 | { 43 | $firstConfiguration = new FileDatabaseConnectionConfiguration(); 44 | $secondConfiguration = new FileDatabaseConnectionConfiguration(); 45 | 46 | $firstFile = $firstConfiguration->getDatabaseFile(); 47 | $secondFile = $secondConfiguration->getDatabaseFile(); 48 | $this->assertInstanceOf('SplFileInfo', $firstFile); 49 | $this->assertInstanceOf('SplFileInfo', $secondFile); 50 | $this->assertNotEquals($firstFile->getPathname(), $secondFile->getPathname()); 51 | } 52 | 53 | public function testCleanUpRemovesTheDatabaseFileIfItExists() 54 | { 55 | $configuration = new FileDatabaseConnectionConfiguration(); 56 | $file = $configuration->getDatabaseFile(); 57 | $this->assertInstanceOf('SplFileInfo', $file); 58 | touch($file->getPathname()); 59 | 60 | $configuration->cleanUp(); 61 | 62 | $this->assertFileDoesNotExist($file->getPathname()); 63 | } 64 | 65 | public function testCleanUpDoesNothingIfTheDatabaseFileDoesNotExistYet() 66 | { 67 | $configuration = new FileDatabaseConnectionConfiguration(); 68 | $file = $configuration->getDatabaseFile(); 69 | $this->assertInstanceOf('SplFileInfo', $file); 70 | 71 | $this->assertFileDoesNotExist($file->getPathname()); 72 | 73 | $configuration->cleanUp(); 74 | } 75 | 76 | public function testCleanUpProvidesFluentInterface() 77 | { 78 | $configuration = new FileDatabaseConnectionConfiguration(); 79 | 80 | $this->assertSame($configuration, $configuration->cleanUp()); 81 | } 82 | 83 | /** 84 | * Checks if the connection configuration *really* works with the infrastructure. 85 | */ 86 | public function testWorksWithInfrastructure() 87 | { 88 | $configuration = new FileDatabaseConnectionConfiguration(); 89 | $infrastructure = $this->createInfrastructure($configuration); 90 | 91 | $this->assertNull( 92 | $infrastructure->import(new TestEntity()) 93 | ); 94 | } 95 | 96 | public function testDatabaseFileIsCreated() 97 | { 98 | $configuration = new FileDatabaseConnectionConfiguration(); 99 | 100 | $infrastructure = $this->createInfrastructure($configuration); 101 | $infrastructure->import(new TestEntity()); 102 | 103 | $file = $configuration->getDatabaseFile(); 104 | $this->assertInstanceOf('SplFileInfo', $file); 105 | $this->assertFileExists($file->getPathname()); 106 | } 107 | 108 | /** 109 | * Creates a new infrastructure with the given connection configuration. 110 | * 111 | * @param ConnectionConfiguration $configuration 112 | * @return ORMInfrastructure 113 | */ 114 | private function createInfrastructure(ConnectionConfiguration $configuration) 115 | { 116 | $infrastructure = ORMInfrastructure::createOnlyFor( 117 | array( 118 | TestEntity::class 119 | ), 120 | $configuration 121 | ); 122 | 123 | return $infrastructure; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/ORMTestInfrastructure/Importer.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\ORMTestInfrastructure; 11 | 12 | use Doctrine\Persistence\ObjectManager; 13 | use Doctrine\ORM\EntityManagerInterface; 14 | 15 | /** 16 | * Helper class that is used to import entities via entity manager. 17 | */ 18 | class Importer 19 | { 20 | 21 | /** 22 | * The entity manager that is used to add the imported entities. 23 | * 24 | * @var \Doctrine\ORM\EntityManagerInterface 25 | */ 26 | protected $entityManager = null; 27 | 28 | /** 29 | * Creates an importer that uses the provided entity manager. 30 | * 31 | * @param EntityManagerInterface $entityManager 32 | */ 33 | public function __construct(EntityManagerInterface $entityManager) 34 | { 35 | $this->entityManager = $entityManager; 36 | } 37 | 38 | /** 39 | * Imports entities from the provided data source. 40 | * 41 | * The importer supports several ways to add entities to the database. 42 | * In any case the importer handles necessary flush() calls, therefore, 43 | * manual flushing is not necessary. 44 | * 45 | * # Callbacks # 46 | * 47 | * Callbacks are executed and receive an object manager as argument: 48 | * 49 | * $loader = function (\Doctrine\Common\Persistence\ObjectManager $objectManager) { 50 | * $objectManager->persist(new MyEntity()); 51 | * $objectManager->persist(new MyEntity()); 52 | * } 53 | * $importer->import($loader); 54 | * 55 | * Please note, that an object manager and not the original entity manager is passed. 56 | * 57 | * # Single entities and lists of entities # 58 | * 59 | * Single entities and lists of entities are automatically persisted: 60 | * 61 | * $importer->import(new MyEntity()); 62 | * $importer->import(array(new MyEntity(), new MyEntity())); 63 | * 64 | * # Files # 65 | * 66 | * To create re-usable data sets entities can be imported from PHP files: 67 | * 68 | * $importer->import('/path/to/data/set.php'); 69 | * 70 | * The imported file has access to the global variable $objectManager, which 71 | * can be used to persist the entities: 72 | * 73 | * persist(new MyEntity()); 76 | * $objectManager->persist(new MyEntity()); 77 | * 78 | * Alternatively, the file can return an array of entities that must be persisted. 79 | * This avoids the dependency on the global $objectManager variable: 80 | * 81 | * importFromCallback($dataSource); 95 | return; 96 | } 97 | if (is_object($dataSource)) { 98 | $this->importEntity($dataSource); 99 | return; 100 | } 101 | if (is_array($dataSource)) { 102 | $this->importEntityList($dataSource); 103 | return; 104 | } 105 | if (is_file($dataSource)) { 106 | $this->importFromFile($dataSource); 107 | return; 108 | } 109 | $message = 'Cannot handle data source of type "' . gettype($dataSource) . '".'; 110 | throw new \InvalidArgumentException($message); 111 | } 112 | 113 | /** 114 | * Imports a single entity. 115 | * 116 | * @param object $entity 117 | */ 118 | protected function importEntity($entity) 119 | { 120 | $this->importEntityList(array($entity)); 121 | } 122 | 123 | /** 124 | * Imports a list of entities. 125 | * 126 | * @param object[] $entities 127 | */ 128 | protected function importEntityList(array $entities) 129 | { 130 | $this->importFromCallback(function (ObjectManager $objectManager) use ($entities) { 131 | foreach ($entities as $entity) { 132 | /* @var $entity object */ 133 | $objectManager->persist($entity); 134 | } 135 | }); 136 | } 137 | 138 | /** 139 | * Imports entities from a PHP file. 140 | * 141 | * @param string $path 142 | */ 143 | protected function importFromFile($path) 144 | { 145 | $entities = null; 146 | /* @noinspection PhpUnusedParameterInspection $objectManager should be in the scope of the included file. */ 147 | $this->importFromCallback(function (ObjectManager $objectManager) use ($path, &$entities) { 148 | $entities = include $path; 149 | }); 150 | if (is_array($entities)) { 151 | // Persist entities that were returned by the file. 152 | $this->importEntityList($entities); 153 | } 154 | } 155 | 156 | /** 157 | * Uses the provided callback to import entities. 158 | * 159 | * @param callable $callback 160 | */ 161 | protected function importFromCallback($callback) 162 | { 163 | $this->entityManager->wrapInTransaction(function (ObjectManager $objectManager) use ($callback) { 164 | call_user_func($callback, $objectManager); 165 | }); 166 | 167 | // Clear the entity manager to ensure that there are no leftovers in the identity map. 168 | $this->entityManager->clear(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | doctrine-orm-test-infrastructure 2 | ================================ 3 | 4 | ⚠️ This library is not receiving active maintenance anymore and is likely to be abandoned in the near future. 5 | 6 | There have been major changes in the current Doctrine DBAL 3.x releases that make it increasingly hard to maintain 7 | helper classes that create a working ORM configuration "out of nothing". At the same time, we have been using 8 | the functionality provided here only to limited extent in our own projects. Most of the time, Symfony functional 9 | tests together with Zenstruck Foundry and testing against the real MySQL schema instead of an in-memory SQLite schema 10 | created ad hoc turned out to be good enough. 11 | 12 | --- 13 | 14 | ![Tests](https://github.com/webfactory/doctrine-orm-test-infrastructure/workflows/Tests/badge.svg) 15 | 16 | This library provides some infrastructure for tests of Doctrine ORM entities, featuring: 17 | 18 | - configuration of a SQLite in memory database, compromising well between speed and a database environment being both 19 | realistic and isolated 20 | - a mechanism for importing fixtures into your database that circumvents Doctrine's caching. This results in a more 21 | realistic test environment when loading entities from a repository. 22 | 23 | [We](https://www.webfactory.de/) use it to test Doctrine repositories and entities in Symfony applications. It's a 24 | lightweight alternative to the 25 | heavyweight [functional tests suggested in the Symfony documentation](http://symfony.com/doc/current/cookbook/testing/doctrine.html) 26 | (we don't suggest you should skip those - we just want to open another path). 27 | 28 | In non-application bundles, where functional tests are not possible, 29 | it is our only way to test repositories and entities. 30 | 31 | Installation 32 | ------------ 33 | 34 | Install via composer (see http://getcomposer.org/): 35 | 36 | composer require --dev webfactory/doctrine-orm-test-infrastructure 37 | 38 | Usage 39 | ----- 40 | 41 | ```php 42 | infrastructure = ORMInfrastructure::createWithDependenciesFor(MyEntity::class); 63 | $this->repository = $this->infrastructure->getRepository(MyEntity::class); 64 | } 65 | 66 | /** 67 | * Example test: Asserts imported fixtures are retrieved with findAll(). 68 | */ 69 | public function testFindAllRetrievesFixtures(): void 70 | { 71 | $myEntityFixture = new MyEntity(); 72 | 73 | $this->infrastructure->import($myEntityFixture); 74 | $entitiesLoadedFromDatabase = $this->repository->findAll(); 75 | 76 | /* 77 | import() will use a dedicated entity manager, so imported entities do not 78 | end up in the identity map. But this also means loading entities from the 79 | database will create _different object instances_. 80 | 81 | So, this does not hold: 82 | */ 83 | // self::assertContains($myEntityFixture, $entitiesLoadedFromDatabase); 84 | 85 | // But you can do things like this (you probably want to extract that in a convenient assertion method): 86 | self::assertCount(1, $entitiesLoadedFromDatabase); 87 | $entityLoadedFromDatabase = $entitiesLoadedFromDatabase[0]; 88 | self::assertSame($myEntityFixture->getId(), $entityLoadedFromDatabase->getId()); 89 | } 90 | 91 | /** 92 | * Example test for retrieving Doctrine's entity manager. 93 | */ 94 | public function testSomeFancyThingWithEntityManager(): void 95 | { 96 | $entityManager = $this->infrastructure->getEntityManager(); 97 | // ... 98 | } 99 | } 100 | ``` 101 | 102 | Migrating to attribute-based mapping configuration (with version 1.x) 103 | --------------------------------------------------------------------- 104 | 105 | In versions 1.x of this library, the `ORMInfrastructure::createWithDependenciesFor()` and `ORMInfrastructure::createOnlyFor()` methods 106 | by default assume that the Doctrine ORM mapping is provided through annotations. Annotations-based configuration is no supported anymore in ORM 3.0. 107 | 108 | To allow for a seamless transition towards attribute-based or other types of mapping, a mapping driver can be passed 109 | when creating instances of the `ORMInfrastructure`. 110 | 111 | If you wish to switch to attribute-based mappings, pass a `new \Doctrine\ORM\Mapping\Driver\AttributeDriver($paths)`, 112 | where `$paths` is an array of directory paths where your entity classes are stored. 113 | 114 | For hybrid (annotations and attributes) mapping configurations, you can use `\Doctrine\Persistence\Mapping\Driver\MappingDriverChain`. 115 | Multiple mapping drivers can be registered on the driver chain by providing namespace prefixes. For every namespace prefix, 116 | only one mapping driver can be used. 117 | 118 | Starting in version 2.0.0, attributes-based mapping will be the default. 119 | 120 | Testing the library itself 121 | -------------------------- 122 | 123 | After installing the dependencies managed via composer, just run 124 | 125 | vendor/bin/phpunit 126 | 127 | from the library's root folder. This uses the shipped phpunit.xml.dist - feel free to create your own phpunit.xml if you 128 | need local changes. 129 | 130 | Happy testing! 131 | 132 | Known Issues 133 | ------------ 134 | 135 | Please note that apart from any [open issues in this library](https://github.com/webfactory/doctrine-orm-test-infrastructure/issues), you 136 | may stumble upon any Doctrine issues. Especially take care of 137 | it's [known sqlite issues](http://doctrine-dbal.readthedocs.org/en/latest/reference/known-vendor-issues.html#sqlite). 138 | 139 | Credits, Copyright and License 140 | ------------------------------ 141 | 142 | This package was first written by webfactory GmbH (Bonn, Germany) and received [contributions 143 | from other people](https://github.com/webfactory/doctrine-orm-test-infrastructure/graphs/contributors) since then. 144 | 145 | webfactory is a software development agency with a focus on PHP (mostly [Symfony](http://github.com/symfony/symfony)). 146 | If you're a developer looking for new challenges, we'd like to hear from you! 147 | 148 | - 149 | 150 | Copyright 2012 – 2024 webfactory GmbH, Bonn. Code released under [the MIT license](LICENSE). 151 | -------------------------------------------------------------------------------- /src/ORMTestInfrastructure/EntityDependencyResolver.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\ORMTestInfrastructure; 11 | 12 | use Doctrine\ORM\Configuration; 13 | use Doctrine\ORM\Mapping\ClassMetadata; 14 | use Doctrine\Persistence\Mapping\Driver\MappingDriver; 15 | use Doctrine\Persistence\Mapping\ReflectionService; 16 | use Doctrine\Persistence\Mapping\RuntimeReflectionService; 17 | 18 | /** 19 | * Takes a set of entity classes and resolves to a set that contains all entities 20 | * that are referenced by the provided entity classes (via associations). 21 | * 22 | * The resolved set also includes the original entity classes. 23 | */ 24 | class EntityDependencyResolver implements \IteratorAggregate 25 | { 26 | /** 27 | * Contains the names of the entity classes that were initially provided. 28 | * 29 | * @var string[] 30 | */ 31 | protected $initialEntitySet = null; 32 | 33 | /** 34 | * Service that is used to inspect entity classes. 35 | * 36 | * @var ReflectionService 37 | */ 38 | protected $reflectionService = null; 39 | 40 | /** 41 | * Factory that is used to create ORM configurations. 42 | * 43 | * @var ConfigurationFactory 44 | */ 45 | protected $configFactory = null; 46 | 47 | /** 48 | * Creates a resolver for the given entity classes. 49 | * 50 | * @param string[] $entityClasses 51 | */ 52 | public function __construct(array $entityClasses, ?MappingDriver $mappingDriver = null) 53 | { 54 | $this->initialEntitySet = $this->normalizeClassNames($entityClasses); 55 | $this->reflectionService = new RuntimeReflectionService(); 56 | $this->configFactory = new ConfigurationFactory($mappingDriver); 57 | } 58 | 59 | /** 60 | * Allows iterating over the set of resolved entities. 61 | * 62 | * @link http://php.net/manual/en/iteratoraggregate.getiterator.php 63 | */ 64 | public function getIterator(): \Traversable 65 | { 66 | return new \ArrayIterator($this->resolve($this->initialEntitySet)); 67 | } 68 | 69 | /** 70 | * Resolves the dependencies for the given entities. 71 | * 72 | * @param string[] $entityClasses 73 | * @return string[] 74 | */ 75 | protected function resolve(array $entityClasses) 76 | { 77 | $entitiesToCheck = $entityClasses; 78 | $config = $this->configFactory->createFor($entitiesToCheck); 79 | while (count($associatedEntities = $this->getDirectlyAssociatedEntities($config, $entitiesToCheck)) > 0) { 80 | $associatedEntities = $this->removeInterfaces($associatedEntities); 81 | $newAssociations = array_diff($associatedEntities, $entityClasses); 82 | $entityClasses = array_merge($entityClasses, $newAssociations); 83 | $config = $this->configFactory->createFor($entityClasses); 84 | $entitiesToCheck = $newAssociations; 85 | } 86 | return $entityClasses; 87 | } 88 | 89 | /** 90 | * Returns the class names of additional entities that are directly associated with 91 | * one of the entities that is explicitly mentioned in the given configuration. 92 | * 93 | * @param Configuration $config 94 | * @param string[] $entityClasses Classes whose associations are checked. 95 | * @return string[] Associated entity classes. 96 | */ 97 | protected function getDirectlyAssociatedEntities(Configuration $config, $entityClasses) 98 | { 99 | if (count($entityClasses) === 0) { 100 | return array(); 101 | } 102 | $associatedEntities = []; 103 | $mappingDriver = $config->getMetadataDriverImpl(); 104 | 105 | foreach ($entityClasses as $entityClass) { 106 | /* @var $entityClass string */ 107 | $metadata = new ClassMetadata($entityClass); 108 | $metadata->initializeReflection($this->reflectionService); 109 | $mappingDriver->loadMetadataForClass($entityClass, $metadata); 110 | 111 | foreach ($metadata->getAssociationNames() as $name) { 112 | /* @var $name string */ 113 | $associatedEntity = $metadata->getAssociationTargetClass($name); 114 | $associatedEntities[$metadata->fullyQualifiedClassName($associatedEntity)] = true; 115 | } 116 | 117 | if ($metadata->isInheritanceTypeJoined()) { 118 | foreach ($metadata->discriminatorMap as $childClass) { 119 | $associatedEntities[$childClass] = true; 120 | } 121 | } 122 | 123 | // Add parent classes that are involved in some kind of entity inheritance. 124 | $parentClassesTowardsInheritanceBaseTable = []; 125 | foreach ($this->reflectionService->getParentClasses($entityClass) as $parentClass) { 126 | if ($mappingDriver->isTransient($parentClass)) { 127 | continue; 128 | } 129 | 130 | $parentClassesTowardsInheritanceBaseTable[] = $parentClass; 131 | $metadata = new ClassMetadata($parentClass); 132 | $metadata->initializeReflection($this->reflectionService); 133 | $mappingDriver->loadMetadataForClass($parentClass, $metadata); 134 | if ($metadata->isInheritanceTypeNone()) { 135 | continue; 136 | } 137 | 138 | foreach ($parentClassesTowardsInheritanceBaseTable as $class) { 139 | $associatedEntities[$class] = true; 140 | } 141 | $parentClassesTowardsInheritanceBaseTable = []; 142 | } 143 | } 144 | return array_keys($associatedEntities); 145 | } 146 | 147 | /** 148 | * Removes leading slashes from the given class names. 149 | * 150 | * @param string[] $entityClasses 151 | * @return string[] 152 | */ 153 | protected function normalizeClassNames(array $entityClasses) 154 | { 155 | return array_map(function ($class) { 156 | return ltrim($class, '\\'); 157 | }, $entityClasses); 158 | } 159 | 160 | /** 161 | * Returns all interfaces from the given list of entity types. 162 | * 163 | * Interfaces can be defined as association targets, but this simple resolver cannot handle them properly. 164 | * Interfaces need additional configuration to be resolved to real entity classes. 165 | * 166 | * @param string[] $entityTypes 167 | * @return string[] 168 | */ 169 | private function removeInterfaces($entityTypes) 170 | { 171 | return array_filter( 172 | $entityTypes, 173 | function ($entity) { 174 | return !interface_exists($entity); 175 | } 176 | ); 177 | } 178 | 179 | private function fetchMetadata(string $entityClass, MappingDriver $mappingDriver): ClassMetadata 180 | { 181 | $metadata = new ClassMetadata($entityClass); 182 | $metadata->initializeReflection($this->reflectionService); 183 | $mappingDriver->loadMetadataForClass($entityClass, $metadata); 184 | 185 | return $metadata; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/ImporterTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure; 11 | 12 | use Doctrine\ORM\EntityManager; 13 | use Doctrine\Persistence\ObjectManager; 14 | use PHPUnit\Framework\MockObject\MockObject; 15 | use PHPUnit\Framework\TestCase; 16 | use Webfactory\Doctrine\ORMTestInfrastructure\Importer; 17 | 18 | /** 19 | * Tests the importer. 20 | */ 21 | class ImporterTest extends TestCase 22 | { 23 | /** 24 | * System under test. 25 | * 26 | * @var Importer 27 | */ 28 | protected $importer = null; 29 | 30 | /** 31 | * The (mocked) entity manager. 32 | * 33 | * @var \Doctrine\ORM\EntityManager|MockObject 34 | */ 35 | protected $entityManager = null; 36 | 37 | /** 38 | * Initializes the test environment. 39 | */ 40 | protected function setUp(): void 41 | { 42 | parent::setUp(); 43 | $this->entityManager = $this->createEntityManager(); 44 | $this->importer = new Importer($this->entityManager); 45 | } 46 | 47 | /** 48 | * Cleans up the test environment. 49 | */ 50 | protected function tearDown(): void 51 | { 52 | $this->importer = null; 53 | $this->entityManager = null; 54 | parent::tearDown(); 55 | } 56 | 57 | /** 58 | * Checks if import() passes an object manager to a provided callback. 59 | * 60 | * Only an object manager instance, not the original entity manager is 61 | * expected as the importer may decide to uses a decorator. 62 | */ 63 | public function testImportPassesObjectManagerToCallback() 64 | { 65 | $callable = new class { 66 | public $invoked = false; 67 | public function __invoke(ObjectManager $om) { 68 | $this->invoked = true; 69 | } 70 | }; 71 | 72 | $this->importer->import($callable); 73 | $this->assertTrue($callable->invoked); 74 | } 75 | 76 | /** 77 | * Checks if persist() calls from a callable are delegated to the entity manager. 78 | */ 79 | public function testEntitiesFromCallableArePersisted() 80 | { 81 | $callable = function (ObjectManager $objectManager) { 82 | $objectManager->persist(new \stdClass()); 83 | $objectManager->persist(new \stdClass()); 84 | }; 85 | 86 | $this->entityManager->expects($this->exactly(2)) 87 | ->method('persist') 88 | ->with($this->isInstanceOf(\stdClass::class)); 89 | 90 | $this->assertNull( 91 | $this->importer->import($callable) 92 | ); 93 | } 94 | 95 | /** 96 | * Checks if the importer accepts a file to persist entities. 97 | */ 98 | public function testImportAddsEntitiesFromFile() 99 | { 100 | $this->entityManager->expects($this->exactly(2)) 101 | ->method('persist') 102 | ->with($this->isInstanceOf(\stdClass::class)); 103 | 104 | $path = __DIR__ . '/Fixtures/Importer/LoadEntities.php'; 105 | $this->assertNull( 106 | $this->importer->import($path) 107 | ); 108 | } 109 | 110 | /** 111 | * Ensures that entities, which are returned by a file, are persisted by the importer. 112 | */ 113 | public function testImportAddsEntitiesThatAreReturnedFromFile() 114 | { 115 | $this->entityManager->expects($this->exactly(2)) 116 | ->method('persist') 117 | ->with($this->isInstanceOf(\stdClass::class)); 118 | 119 | $path = __DIR__ . '/Fixtures/Importer/ReturnEntities.php'; 120 | $this->assertNull( 121 | $this->importer->import($path) 122 | ); 123 | } 124 | 125 | /** 126 | * Checks if import() persists a single entity. 127 | */ 128 | public function testImportPersistsSingleEntity() 129 | { 130 | $this->entityManager->expects($this->once()) 131 | ->method('persist') 132 | ->with($this->isInstanceOf(\stdClass::class)); 133 | 134 | $this->assertNull( 135 | $this->importer->import(new \stdClass()) 136 | ); 137 | } 138 | 139 | /** 140 | * Ensures that import persists an array of entities. 141 | */ 142 | public function testImportPersistsArrayOfEntities() 143 | { 144 | $this->entityManager->expects($this->exactly(2)) 145 | ->method('persist') 146 | ->with($this->isInstanceOf(\stdClass::class)); 147 | 148 | $entities = array( 149 | new \stdClass(), 150 | new \stdClass() 151 | ); 152 | $this->assertNull( 153 | $this->importer->import($entities) 154 | ); 155 | } 156 | 157 | public function testImportCanHandleEmptyEntityList() 158 | { 159 | $this->assertNull( 160 | $this->importer->import([]) 161 | ); 162 | } 163 | 164 | /** 165 | * Ensures that import() throws an exception if the given data source 166 | * is not supported. 167 | */ 168 | public function testImportThrowsExceptionIfDataSourceIsNotSupported() 169 | { 170 | $this->expectException(\InvalidArgumentException::class); 171 | $this->importer->import(42); 172 | } 173 | 174 | /** 175 | * Checks if the importer clears the manager after import. 176 | */ 177 | public function testImporterClearsEntityManager() 178 | { 179 | $this->entityManager->expects($this->once()) 180 | ->method('clear'); 181 | 182 | $entities = array( 183 | new \stdClass(), 184 | new \stdClass() 185 | ); 186 | $this->assertNull( 187 | $this->importer->import($entities) 188 | ); 189 | } 190 | 191 | public function testEntityManagerIsFlushedOnlyOnce() 192 | { 193 | $this->entityManager->expects($this->once()) 194 | ->method('flush'); 195 | 196 | $entities = array( 197 | new \stdClass() 198 | ); 199 | $this->assertNull( 200 | $this->importer->import($entities) 201 | ); 202 | } 203 | 204 | public function testEntityManagerIsClearedAfterFlush() 205 | { 206 | $cleared = 0.0; 207 | $callCounter = 0; 208 | $this->entityManager->expects($this->once()) 209 | ->method('clear') 210 | ->will($this->returnCallback(function () use (&$cleared, &$callCounter) { 211 | $cleared = $callCounter++; 212 | })); 213 | $flushed = 0.0; 214 | $this->entityManager->expects($this->once()) 215 | ->method('flush') 216 | ->will($this->returnCallback(function () use (&$flushed, &$callCounter) { 217 | $flushed = $callCounter++; 218 | })); 219 | 220 | $entities = array( 221 | new \stdClass() 222 | ); 223 | $this->importer->import($entities); 224 | 225 | $this->assertGreaterThan($flushed, $cleared, 'clear() was called before flush().'); 226 | } 227 | 228 | /** 229 | * Creates a mocked entity manager. 230 | * 231 | * @return \Doctrine\ORM\EntityManagerInterface|MockObject 232 | */ 233 | protected function createEntityManager() 234 | { 235 | $mock = $this->createMock(EntityManager::class); 236 | // Simulates the transactional() call on the entity manager. 237 | $transactional = function ($callback) use ($mock) { 238 | /* @var $mock \Doctrine\ORM\EntityManagerInterface */ 239 | $result = call_user_func($callback, $mock); 240 | $mock->flush(); 241 | return $result ?: true; 242 | }; 243 | $mock->expects($this->any()) 244 | ->method('wrapInTransaction') 245 | ->will($this->returnCallback($transactional)); 246 | return $mock; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/EntityDependencyResolverTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure; 11 | 12 | use PHPUnit\Framework\TestCase; 13 | use Webfactory\Doctrine\ORMTestInfrastructure\EntityDependencyResolver; 14 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ChainReferenceEntity; 15 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance\ClassTableChildEntity; 16 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance\ClassTableChildWithParentReferenceEntity; 17 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance\ClassTableParentEntity; 18 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance\DiscriminatorMapChildEntity; 19 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance\DiscriminatorMapEntity; 20 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Inheritance\MappedSuperClassChild; 21 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\InterfaceAssociation\EntityInterface; 22 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\InterfaceAssociation\EntityWithAssociationAgainstInterface; 23 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ReferenceCycleEntity; 24 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ReferencedEntity; 25 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\TestEntity; 26 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\TestEntityWithDependency; 27 | 28 | /** 29 | * Tests the entity resolver. 30 | */ 31 | class EntityDependencyResolverTest extends TestCase 32 | { 33 | /** 34 | * Ensures that the resolver is traversable. 35 | */ 36 | public function testResolverIsTraversable() 37 | { 38 | $resolver = new EntityDependencyResolver(array( 39 | TestEntity::class 40 | )); 41 | 42 | $this->assertInstanceOf(\Traversable::class, $resolver); 43 | } 44 | 45 | /** 46 | * Checks if the resolved set contains the initially provided entity classes. 47 | */ 48 | public function testSetContainsProvidedEntityClasses() 49 | { 50 | $resolver = new EntityDependencyResolver(array( 51 | TestEntity::class 52 | )); 53 | 54 | $this->assertContainsEntity( 55 | TestEntity::class, 56 | $resolver 57 | ); 58 | } 59 | 60 | /** 61 | * Checks if entities that are directly associated to the initially provided entities 62 | * are contained in the resolved set. 63 | */ 64 | public function testSetContainsEntityClassesThatAreDirectlyConnectedToInitialSet() 65 | { 66 | $resolver = new EntityDependencyResolver(array( 67 | TestEntityWithDependency::class 68 | )); 69 | 70 | $this->assertContainsEntity( 71 | ReferencedEntity::class, 72 | $resolver 73 | ); 74 | } 75 | 76 | /** 77 | * Ensures that entities, which are connected via other associated entities, 78 | * are contained in the generated set. 79 | * 80 | * Example: 81 | * 82 | * A -> B -> C 83 | */ 84 | public function testSetContainsIndirectlyConnectedEntityClasses() 85 | { 86 | $resolver = new EntityDependencyResolver(array( 87 | ChainReferenceEntity::class 88 | )); 89 | 90 | $this->assertContainsEntity( 91 | ReferencedEntity::class, 92 | $resolver 93 | ); 94 | } 95 | 96 | /** 97 | * Ensures that the resolver can handle dependency cycles. 98 | */ 99 | public function testResolverCanHandleDependencyCycles() 100 | { 101 | $resolver = new EntityDependencyResolver(array( 102 | ReferenceCycleEntity::class 103 | )); 104 | 105 | $this->assertContainsEntity( 106 | ReferenceCycleEntity::class, 107 | $resolver 108 | ); 109 | } 110 | 111 | /** 112 | * Ensures that the resolved entity list contains each entity class only once. 113 | */ 114 | public function testSetContainsEntitiesOnlyOnce() 115 | { 116 | $resolver = new EntityDependencyResolver(array( 117 | ReferenceCycleEntity::class 118 | )); 119 | 120 | $resolvedSet = $this->getResolvedSet($resolver); 121 | 122 | $normalized = array_unique($resolvedSet); 123 | sort($resolvedSet); 124 | sort($normalized); 125 | $this->assertEquals($normalized, $resolvedSet); 126 | } 127 | 128 | /** 129 | * Ensures that the resolver returns the entity class names without leading slash. 130 | */ 131 | public function testResolvedSetContainsEntityClassesWithoutLeadingSlash() 132 | { 133 | $resolver = new EntityDependencyResolver(array( 134 | ChainReferenceEntity::class 135 | )); 136 | 137 | $resolvedSet = $this->getResolvedSet($resolver); 138 | 139 | foreach ($resolvedSet as $entityClass) { 140 | /* @var $entityClass string */ 141 | $message = 'Entity class name must be normalized and must not start with \\.'; 142 | $this->assertStringStartsNotWith('\\', $entityClass, $message); 143 | } 144 | } 145 | 146 | /** 147 | * Ensures that a parent that uses class table inheritance is listed in the resolved set. 148 | */ 149 | public function testResolvedSetContainsNameOfClassTableInheritanceParent() 150 | { 151 | $resolver = new EntityDependencyResolver(array( 152 | ClassTableChildEntity::class 153 | )); 154 | 155 | $this->assertContainsEntity( 156 | ClassTableParentEntity::class, 157 | $resolver 158 | ); 159 | } 160 | 161 | /** 162 | * Ensures that the resolved set contains an entity class that is referenced by a parent 163 | * entity (with class table inheritance strategy). 164 | */ 165 | public function testResolvedSetContainsNameOfClassThatIsReferencedByParentWithClassTableStrategy() 166 | { 167 | $resolver = new EntityDependencyResolver(array( 168 | ClassTableChildWithParentReferenceEntity::class 169 | )); 170 | 171 | $this->assertContainsEntity( 172 | ReferencedEntity::class, 173 | $resolver 174 | ); 175 | } 176 | 177 | /** 178 | * Ensures that an entity, that is referenced by a mapped super class, is listed in the resolved set. 179 | */ 180 | public function testResolvedSetContainsNameOfEntityThatIsReferencedByMappedSuperClass() 181 | { 182 | $resolver = new EntityDependencyResolver(array( 183 | MappedSuperClassChild::class 184 | )); 185 | 186 | $this->assertContainsEntity( 187 | ReferencedEntity::class, 188 | $resolver 189 | ); 190 | } 191 | 192 | /** 193 | * Ensures that the resolved set contains the entities that are explicitly mentioned in 194 | * a discriminator map. 195 | * 196 | * Doctrine uses the information from the discriminator map to generate its queries. 197 | * Therefore, the tables on the mentioned entities must be generated in the tests. 198 | */ 199 | public function testResolvedSetContainsNamesOfEntitiesThatAreMentionedInDiscriminatorMap() 200 | { 201 | $resolver = new EntityDependencyResolver(array( 202 | DiscriminatorMapEntity::class 203 | )); 204 | 205 | $this->assertContainsEntity( 206 | DiscriminatorMapChildEntity::class, 207 | $resolver 208 | ); 209 | } 210 | 211 | /** 212 | * Interfaces can be used as association targets, but this simple resolver cannot handle them. 213 | * Nevertheless, the resolver should not fail and the interfaces should not show up in the dependency list. 214 | */ 215 | public function testResolvedSetDoesNotContainInterfaces() 216 | { 217 | $resolver = new EntityDependencyResolver([ 218 | EntityWithAssociationAgainstInterface::class 219 | ]); 220 | 221 | $this->assertNotContains(EntityInterface::class, $this->getResolvedSet($resolver)); 222 | } 223 | 224 | /** 225 | * Returns the resolved set of entity classes as array. 226 | * 227 | * @param EntityDependencyResolver $resolver 228 | * @return string[] 229 | */ 230 | protected function getResolvedSet(EntityDependencyResolver $resolver) 231 | { 232 | $this->assertInstanceOf(\Traversable::class, $resolver); 233 | $entities = iterator_to_array($resolver); 234 | $this->assertContainsOnly('string', $entities); 235 | return $entities; 236 | } 237 | 238 | /** 239 | * Asserts that the resolved entity list contains the given entity. 240 | * 241 | * @param string $entity Name of the entity class. 242 | * @param EntityDependencyResolver|mixed $resolver 243 | */ 244 | protected function assertContainsEntity($entity, $resolver) 245 | { 246 | $normalizedEntity = ltrim($entity, '\\'); 247 | $this->assertContains( 248 | $normalizedEntity, 249 | $this->getResolvedSet($resolver) 250 | ); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/ORMTestInfrastructure/ORMInfrastructure.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\ORMTestInfrastructure; 11 | 12 | use Doctrine\Common\EventManager; 13 | use Doctrine\DBAL\DriverManager; 14 | use Doctrine\DBAL\Logging\Middleware as LoggingMiddleware; 15 | use Doctrine\Persistence\Mapping\Driver\MappingDriver; 16 | use Doctrine\Persistence\ObjectRepository; 17 | use Doctrine\ORM\EntityManager; 18 | use Doctrine\ORM\Events; 19 | use Doctrine\ORM\Mapping\ClassMetadataFactory; 20 | use Doctrine\ORM\Mapping\DefaultNamingStrategy; 21 | use Doctrine\ORM\Mapping\NamingStrategy; 22 | use Doctrine\ORM\Tools\ResolveTargetEntityListener; 23 | use Doctrine\ORM\Tools\SchemaTool; 24 | use Doctrine\Common\EventSubscriber; 25 | use Webfactory\Doctrine\Config\ConnectionConfiguration; 26 | use Webfactory\Doctrine\Config\ExistingConnectionConfiguration; 27 | 28 | /** 29 | * Helper class that creates the database infrastructure for a defined set of entity classes. 30 | * 31 | * The required database is created in memory (via SQLite). This provides full isolation 32 | * and allows testing repositories and entities against a real database. 33 | * 34 | * # Example # 35 | * 36 | * ## Setup ## 37 | * 38 | * Create the infrastructure for a set of entities: 39 | * 40 | * $infrastructure = ORMInfrastructure::createOnlyFor(array( 41 | * 'My\Entity\ClassName' 42 | * )); 43 | * 44 | * Use the infrastructure to retrieve the entity manager: 45 | * 46 | * $entityManager = $infrastructure->getEntityManager(); 47 | * 48 | * The entity manager can be used as usual. It operates on an in-memory database that contains 49 | * the schema for all entities that have been mentioned in the infrastructure constructor. 50 | * 51 | * ### Advanced Setup ### 52 | * 53 | * Use the ``createWithDependenciesFor()`` factory method to create an infrastructure for 54 | * the given entity, including all entities that are associated with it: 55 | * 56 | * $infrastructure = ORMInfrastructure::createWithDependenciesFor( 57 | * 'My\Entity\ClassName' 58 | * ); 59 | * 60 | * This is convenient as it avoids touching tests when associations are added, but it 61 | * might also hide the existence of entity dependencies that you are not really aware 62 | * of. 63 | * 64 | * ## Import Test Data ## 65 | * 66 | * Additionally, the infrastructure provides means to import entities: 67 | * 68 | * $myEntity = new \My\Entity\ClassName(); 69 | * $infrastructure->import($myEntity); 70 | * 71 | * The import ensures that the imported entities are loaded from the database when requested via repository. This 72 | * circumvents Doctrine's caching via identity map and thereby leads to a more realistic test environment. 73 | */ 74 | class ORMInfrastructure 75 | { 76 | /** 77 | * List of entity classes that are managed by this infrastructure. 78 | * 79 | * @var string[] 80 | */ 81 | protected $entityClasses; 82 | 83 | /** 84 | * The entity manager that is used to perform entity operations. 85 | * 86 | * Contains null if the entity manager has not been created yet. 87 | * 88 | * @var \Doctrine\ORM\EntityManager|null 89 | */ 90 | protected $entityManager = null; 91 | 92 | /** 93 | * The query logger that is used. 94 | * 95 | * @var DebugStack 96 | */ 97 | protected $queryLogger = null; 98 | 99 | /** 100 | * The naming strategy that is used. 101 | * 102 | * @var NamingStrategy 103 | */ 104 | protected $namingStrategy = null; 105 | 106 | private readonly ?MappingDriver $mappingDriver; 107 | 108 | /** 109 | * Listener that is used to resolve entity mappings. 110 | * 111 | * Null if the listener is not registered yet. 112 | * 113 | * @var ResolveTargetEntityListener|null 114 | */ 115 | private $resolveTargetListener; 116 | 117 | /** 118 | * The configuration that is used to connect to the test database. 119 | * 120 | * @var ConnectionConfiguration 121 | */ 122 | private $connectionConfiguration = null; 123 | 124 | /** 125 | * @var bool 126 | */ 127 | private $createSchema = true; 128 | 129 | /** 130 | * @var EventSubscriber[] 131 | */ 132 | private $eventSubscribers; 133 | 134 | /** 135 | * Creates an infrastructure for the given entity or entities, including all 136 | * referenced entities. 137 | * 138 | * @param string[]|string $entityClassOrClasses 139 | * @param ConnectionConfiguration|null $connectionConfiguration Optional, specific database connection information. 140 | * @return ORMInfrastructure 141 | */ 142 | public static function createWithDependenciesFor($entityClassOrClasses, ?ConnectionConfiguration $connectionConfiguration = null, ?MappingDriver $mappingDriver = null) { 143 | $entityClasses = static::normalizeEntityList($entityClassOrClasses); 144 | return new static(new EntityDependencyResolver($entityClasses, $mappingDriver), $connectionConfiguration, $mappingDriver); 145 | } 146 | 147 | /** 148 | * Creates an infrastructure for the given entity or entities. 149 | * 150 | * The infrastructure that is required for entities that are associated with the given 151 | * entities is *not* created automatically. 152 | * 153 | * @param string[]|string $entityClassOrClasses 154 | * @param ConnectionConfiguration|null $connectionConfiguration Optional, specific database connection information. 155 | * @return ORMInfrastructure 156 | */ 157 | public static function createOnlyFor($entityClassOrClasses, ?ConnectionConfiguration $connectionConfiguration = null, ?MappingDriver $mappingDriver = null) 158 | { 159 | return new static(static::normalizeEntityList($entityClassOrClasses), $connectionConfiguration, $mappingDriver); 160 | } 161 | 162 | /** 163 | * Accepts a single entity class or a list of entity classes and always returns a 164 | * list of entity classes. 165 | * 166 | * @param string[]|string $entityClassOrClasses 167 | * @return string[] 168 | */ 169 | protected static function normalizeEntityList($entityClassOrClasses) 170 | { 171 | $entityClasses = (is_string($entityClassOrClasses)) ? array($entityClassOrClasses) : $entityClassOrClasses; 172 | static::assertClassNames($entityClasses); 173 | return $entityClasses; 174 | } 175 | 176 | /** 177 | * Creates an entity helper that provides a database infrastructure 178 | * for the provided entities. 179 | * 180 | * Foreach entity the fully qualified class name must be provided. 181 | * 182 | * @param string[]|\Traversable $entityClasses 183 | * @param ConnectionConfiguration|null $connectionConfiguration Optional, specific database connection information. 184 | */ 185 | private function __construct($entityClasses, ?ConnectionConfiguration $connectionConfiguration = null, ?MappingDriver $mappingDriver = null) 186 | { 187 | if ($entityClasses instanceof \Traversable) { 188 | $entityClasses = iterator_to_array($entityClasses); 189 | } 190 | if ($connectionConfiguration === null) { 191 | $connectionConfiguration = new ConnectionConfiguration([ 192 | 'driver' => 'pdo_sqlite', 193 | 'user' => 'root', 194 | 'password' => '', 195 | 'memory' => true, 196 | ]); 197 | } 198 | $this->entityClasses = $entityClasses; 199 | $this->connectionConfiguration = $connectionConfiguration; 200 | $this->queryLogger = new QueryLogger(); 201 | $this->namingStrategy = new DefaultNamingStrategy(); 202 | $this->mappingDriver = $mappingDriver; 203 | $this->resolveTargetListener = new ResolveTargetEntityListener(); 204 | 205 | $this->eventSubscribers = [$this->resolveTargetListener]; 206 | } 207 | 208 | public function addEventSubscriber(EventSubscriber $subscriber): void 209 | { 210 | $this->eventSubscribers[] = $subscriber; 211 | } 212 | 213 | public function disableSchemaCreation() 214 | { 215 | $this->createSchema = false; 216 | } 217 | 218 | /** 219 | * @param NamingStrategy $namingStrategy 220 | */ 221 | public function setNamingStrategy(NamingStrategy $namingStrategy): void 222 | { 223 | $this->namingStrategy = $namingStrategy; 224 | } 225 | 226 | /** 227 | * Returns the repository for the provided entity. 228 | * 229 | * @param string|object $classNameOrEntity Class name of an entity or entity instance. 230 | * @return ObjectRepository 231 | */ 232 | public function getRepository($classNameOrEntity) 233 | { 234 | $className = is_object($classNameOrEntity) ? get_class($classNameOrEntity) : $classNameOrEntity; 235 | return $this->getEntityManager()->getRepository($className); 236 | } 237 | 238 | /** 239 | * Returns the queries that have been executed so far. 240 | * 241 | * @return Query[] 242 | */ 243 | public function getQueries() 244 | { 245 | return $this->queryLogger->getQueries(); 246 | } 247 | 248 | /** 249 | * Imports entities from the provided data source. 250 | * 251 | * The supported data sources are documented at \Webfactory\Doctrine\ORMTestInfrastructure\Importer::import(). 252 | * 253 | * @param mixed $dataSource Callback, single entity, array of entities or file path. 254 | * @see \Webfactory\Doctrine\ORMTestInfrastructure\Importer::import() 255 | */ 256 | public function import($dataSource) 257 | { 258 | $loggerWasEnabled = $this->queryLogger->enabled; 259 | $this->queryLogger->enabled = false; 260 | $importer = new Importer($this->copyEntityManager()); 261 | $importer->import($dataSource); 262 | $this->queryLogger->enabled = $loggerWasEnabled; 263 | } 264 | 265 | /** 266 | * Returns the entity manager. 267 | * 268 | * @return \Doctrine\ORM\EntityManager 269 | */ 270 | public function getEntityManager() 271 | { 272 | if ($this->entityManager === null) { 273 | $loggerWasEnabled = $this->queryLogger->enabled; 274 | $this->queryLogger->enabled = false; 275 | $this->entityManager = $this->createEntityManager(); 276 | $this->setupEventSubscribers(); 277 | if ($this->createSchema) { 278 | $this->createSchemaForSupportedEntities(); 279 | } 280 | $this->queryLogger->enabled = $loggerWasEnabled; 281 | } 282 | return $this->entityManager; 283 | } 284 | 285 | /** 286 | * Returns the event manager that will be used by the entity manager. 287 | * 288 | * Can be used to register type mappings for interfaces. 289 | * 290 | * @return EventManager 291 | * @internal Do not rely on this method if you don't have to. Might be removed in future versions. 292 | */ 293 | public function getEventManager() 294 | { 295 | return $this->getEntityManager()->getEventManager(); 296 | } 297 | 298 | /** 299 | * Registers a type mapping. 300 | * 301 | * Might be required if you define an association mapping against an interface. 302 | * 303 | * @param string $originalEntity 304 | * @param string $targetEntity 305 | * @throws \LogicException If you call this method after using the infrastructure. 306 | * @internal Might be replaced in the future by a more advanced config system. 307 | * Do not rely on this feature if you don't have to. 308 | * @see http://symfony.com/doc/current/doctrine/resolve_target_entity.html#set-up 309 | */ 310 | public function registerEntityMapping($originalEntity, $targetEntity) 311 | { 312 | if ($this->entityManager !== null) { 313 | $message = 'Call %s() before using the entity manager or importing data. ' 314 | . 'Otherwise your entity mapping might not take effect.'; 315 | throw new \LogicException(sprintf($message, __FUNCTION__)); 316 | } 317 | $this->resolveTargetListener->addResolveTargetEntity($originalEntity, $targetEntity, array()); 318 | } 319 | 320 | /** 321 | * Creates a new entity manager. 322 | * 323 | * @return \Doctrine\ORM\EntityManager 324 | */ 325 | protected function createEntityManager() 326 | { 327 | $configFactory = new ConfigurationFactory($this->mappingDriver); 328 | $config = $configFactory->createFor($this->entityClasses); 329 | $middlewares = $config->getMiddlewares(); 330 | $middlewares[] = new LoggingMiddleware($this->queryLogger); 331 | $config->setMiddlewares($middlewares); 332 | $config->setNamingStrategy($this->namingStrategy); 333 | 334 | if ($this->connectionConfiguration instanceof ExistingConnectionConfiguration) { 335 | $connection = $this->connectionConfiguration->getConnection(); 336 | } else { 337 | $connection = DriverManager::getConnection($this->connectionConfiguration->getConnectionParameters(), $config); 338 | } 339 | 340 | return new EntityManager($connection, $config); 341 | } 342 | 343 | /** 344 | * Creates the schema for the managed entities. 345 | */ 346 | protected function createSchemaForSupportedEntities() 347 | { 348 | $metadata = $this->getMetadataForSupportedEntities(); 349 | $schemaTool = new SchemaTool($this->entityManager); 350 | $schemaTool->createSchema($metadata); 351 | } 352 | 353 | /** 354 | * Returns the metadata for each managed entity. 355 | * 356 | * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata[] 357 | */ 358 | public function getMetadataForSupportedEntities() 359 | { 360 | $metadataFactory = $this->getEntityManager()->getMetadataFactory(); 361 | $metadata = array(); 362 | foreach ($this->entityClasses as $class) { 363 | $metadata[] = $metadataFactory->getMetadataFor($class); 364 | } 365 | return $metadata; 366 | } 367 | 368 | /** 369 | * Creates a copy of the current entity manager. 370 | * 371 | * @return EntityManager 372 | */ 373 | private function copyEntityManager() 374 | { 375 | $entityManager = $this->getEntityManager(); 376 | 377 | return new EntityManager( 378 | $entityManager->getConnection(), 379 | $entityManager->getConfiguration(), 380 | $this->getEventManager() 381 | ); 382 | } 383 | 384 | private function setupEventSubscribers() 385 | { 386 | $eventManager = $this->getEventManager(); 387 | 388 | foreach ($this->eventSubscribers as $subscriber) { 389 | $eventManager->addEventSubscriber($subscriber); 390 | } 391 | } 392 | 393 | /** 394 | * Checks if all entries in the given list are names of existing classes. 395 | * 396 | * @param string[] $classes 397 | * @throws \InvalidArgumentException If an entry is not a valid class name. 398 | */ 399 | private static function assertClassNames(array $classes) 400 | { 401 | foreach ($classes as $class) { 402 | if (class_exists($class, true)) { 403 | continue; 404 | } 405 | $message = sprintf('"%s" is no existing class. Did you configure your autoloader correctly?', $class); 406 | throw new \InvalidArgumentException($message); 407 | } 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /tests/ORMTestInfrastructure/ORMInfrastructureTest.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webfactory\Doctrine\Tests\ORMTestInfrastructure; 11 | 12 | use Doctrine\Common\EventManager; 13 | use Doctrine\DBAL\Schema\Schema; 14 | use Doctrine\ORM\EntityManager; 15 | use Doctrine\ORM\Mapping\ClassMetadata; 16 | use Doctrine\ORM\Mapping\Driver\AttributeDriver; 17 | use Doctrine\ORM\Tools\SchemaTool; 18 | use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; 19 | use PHPUnit\Framework\TestCase; 20 | use Webfactory\Doctrine\Config\ConnectionConfiguration; 21 | use Webfactory\Doctrine\ORMTestInfrastructure\ORMInfrastructure; 22 | use Webfactory\Doctrine\ORMTestInfrastructure\Query; 23 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace2\TestEntity as TestEntity_Namespace2; 24 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace2\TestEntityWithDependency as TestEntityWithDependency_Attributes; 25 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Cascade\CascadePersistedEntity; 26 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\Cascade\CascadePersistingEntity; 27 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ChainReferenceEntity; 28 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\DependencyResolverFixtures; 29 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\InterfaceAssociation\EntityImplementation; 30 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\InterfaceAssociation\EntityInterface; 31 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\InterfaceAssociation\EntityWithAssociationAgainstInterface; 32 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ReferenceCycleEntity; 33 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\ReferencedEntity; 34 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\TestEntity; 35 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\TestEntityRepository; 36 | use Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1\TestEntityWithDependency; 37 | 38 | /** 39 | * Tests the infrastructure. 40 | */ 41 | class ORMInfrastructureTest extends TestCase 42 | { 43 | /** 44 | * System under test. 45 | * 46 | * @var \Webfactory\Doctrine\ORMTestInfrastructure\ORMInfrastructure 47 | */ 48 | protected $infrastructure = null; 49 | 50 | /** 51 | * Initializes the test environment. 52 | */ 53 | protected function setUp(): void 54 | { 55 | parent::setUp(); 56 | $this->infrastructure = ORMInfrastructure::createOnlyFor([ 57 | TestEntity::class 58 | ]); 59 | } 60 | 61 | /** 62 | * Checks if getEntityManager() returns the Doctrine entity manager, 63 | */ 64 | public function testGetEntityManagerReturnsDoctrineEntityManager() 65 | { 66 | $entityManager = $this->infrastructure->getEntityManager(); 67 | 68 | $this->assertInstanceOf(EntityManager::class, $entityManager); 69 | } 70 | 71 | /** 72 | * Ensures that getRepository() returns the Doctrine repository that belongs 73 | * to the given entity class. 74 | */ 75 | public function testGetRepositoryReturnsRepositoryThatBelongsToEntityClass() 76 | { 77 | $repository = $this->infrastructure->getRepository( 78 | TestEntity::class 79 | ); 80 | 81 | $this->assertInstanceOf( 82 | TestEntityRepository::class, 83 | $repository 84 | ); 85 | } 86 | 87 | /** 88 | * Ensures that getRepository() returns the Doctrine repository that belongs 89 | * to the given entity object. 90 | */ 91 | public function testGetRepositoryReturnsRepositoryThatBelongsToEntityObject() 92 | { 93 | $entity = new TestEntity(); 94 | $repository = $this->infrastructure->getRepository($entity); 95 | 96 | $this->assertInstanceOf( 97 | TestEntityRepository::class, 98 | $repository 99 | ); 100 | } 101 | 102 | /** 103 | * Ensure that the infrastructure fails fast if obviously invalid data is passed. 104 | */ 105 | public function testInfrastructureRejectsNonClassNames() 106 | { 107 | $this->expectException(\InvalidArgumentException::class); 108 | ORMInfrastructure::createOnlyFor(array('NotAClass')); 109 | } 110 | 111 | /** 112 | * Checks if import() adds entities to the database. 113 | * 114 | * There are different options to import entities, but these are handled in detail 115 | * in the importer tests. 116 | * 117 | * @see \Webfactory\Doctrine\ORMTestInfrastructure\ImporterTest 118 | */ 119 | public function testImportAddsEntities() 120 | { 121 | $entity = new TestEntity(); 122 | $repository = $this->infrastructure->getRepository($entity); 123 | 124 | $entities = $repository->findAll(); 125 | $this->assertCount(0, $entities); 126 | 127 | $this->infrastructure->import($entity); 128 | 129 | $entities = $repository->findAll(); 130 | $this->assertCount(1, $entities); 131 | } 132 | 133 | public function testImportEntityWithAttributeMapping() 134 | { 135 | if (PHP_VERSION_ID < 80000) { 136 | self::markTestSkipped('This test requires PHP 8.0 or greater'); 137 | } 138 | 139 | $this->infrastructure = ORMInfrastructure::createOnlyFor([TestEntity_Namespace2::class], null, new AttributeDriver([__DIR__.'/Fixtures/EntityNamespace2'])); 140 | 141 | $entity = new TestEntity_Namespace2(); 142 | $repository = $this->infrastructure->getRepository($entity); 143 | 144 | $entities = $repository->findAll(); 145 | $this->assertCount(0, $entities); 146 | 147 | $this->infrastructure->import($entity); 148 | 149 | $entities = $repository->findAll(); 150 | $this->assertCount(1, $entities); 151 | } 152 | 153 | /** 154 | * Checks if an imported entity receives a generated ID. 155 | */ 156 | public function testEntityIdIsAvailableAfterImport() 157 | { 158 | $entity = new TestEntity(); 159 | 160 | $this->infrastructure->import($entity); 161 | 162 | $this->assertNotNull($entity->id); 163 | } 164 | 165 | /** 166 | * Ensures that imported entities are really loaded from the database and 167 | * not provided from identity map. 168 | */ 169 | public function testImportedEntitiesAreReloadedFromDatabase() 170 | { 171 | $entity = new TestEntity(); 172 | $repository = $this->infrastructure->getRepository($entity); 173 | 174 | $this->infrastructure->import($entity); 175 | 176 | $loadedEntity = $repository->find($entity->id); 177 | $this->assertInstanceOf( 178 | TestEntity::class, 179 | $loadedEntity 180 | ); 181 | $this->assertNotSame($entity, $loadedEntity); 182 | } 183 | 184 | /** 185 | * Ensures that different infrastructure instances provide database isolation. 186 | */ 187 | public function testDifferentInfrastructureInstancesUseSeparatedDatabases() 188 | { 189 | $entity = new TestEntity(); 190 | $anotherInfrastructure = ORMInfrastructure::createOnlyFor([ 191 | TestEntity::class 192 | ]); 193 | $repository = $anotherInfrastructure->getRepository($entity); 194 | 195 | $this->infrastructure->import($entity); 196 | 197 | // Entity must not be visible in the scope of another infrastructure. 198 | $entities = $repository->findAll(); 199 | $this->assertCount(0, $entities); 200 | } 201 | 202 | /** 203 | * Ensures that the query list that is provided by getQueries() is initially empty. 204 | */ 205 | public function testGetQueriesReturnsInitiallyEmptyList() 206 | { 207 | $queries = $this->infrastructure->getQueries(); 208 | 209 | $this->assertIsArray($queries); 210 | $this->assertCount(0, $queries); 211 | } 212 | 213 | /** 214 | * Ensures that getQueries() returns the logged SQL queries as objects. 215 | */ 216 | public function testGetQueriesReturnsQueryObjects() 217 | { 218 | $entity = new TestEntity(); 219 | $repository = $this->infrastructure->getRepository($entity); 220 | $repository->find(42); 221 | 222 | $queries = $this->infrastructure->getQueries(); 223 | 224 | $this->assertIsArray($queries); 225 | $this->assertContainsOnly(Query::class, $queries); 226 | } 227 | 228 | /** 229 | * Checks if the queries that are executed with the entity manager are logged. 230 | */ 231 | public function testInfrastructureLogsExecutedQueries() 232 | { 233 | $entity = new TestEntity(); 234 | $repository = $this->infrastructure->getRepository($entity); 235 | $repository->find(42); 236 | 237 | $queries = $this->infrastructure->getQueries(); 238 | 239 | $this->assertIsArray($queries); 240 | $this->assertCount(1, $queries); 241 | } 242 | 243 | /** 244 | * Ensures that the queries that are issued during data import are not logged. 245 | */ 246 | public function testInfrastructureDoesNotLogImportQueries() 247 | { 248 | $entity = new TestEntity(); 249 | $this->infrastructure->import($entity); 250 | 251 | $queries = $this->infrastructure->getQueries(); 252 | 253 | $this->assertIsArray($queries); 254 | $this->assertCount(0, $queries); 255 | } 256 | 257 | /** 258 | * Ensures that the infrastructure logs queries, which are executed after an import. 259 | */ 260 | public function testInfrastructureLogsQueriesThatAreExecutedAfterImport() 261 | { 262 | $entity = new TestEntity(); 263 | $this->infrastructure->import($entity); 264 | $repository = $this->infrastructure->getRepository($entity); 265 | $repository->find(42); 266 | 267 | $queries = $this->infrastructure->getQueries(); 268 | 269 | $this->assertIsArray($queries); 270 | $this->assertCount(1, $queries); 271 | } 272 | 273 | /** 274 | * Ensures that createWithDependenciesFor() returns an infrastructure object if a set of 275 | * entities classes is provided. 276 | */ 277 | public function testCreateWithDependenciesForCreatesInfrastructureForSetOfEntities() 278 | { 279 | $infrastructure = ORMInfrastructure::createWithDependenciesFor(array( 280 | TestEntity::class, 281 | ReferencedEntity::class 282 | )); 283 | 284 | $this->assertInstanceOf(ORMInfrastructure::class, $infrastructure); 285 | } 286 | 287 | /** 288 | * Ensures that createWithDependenciesFor() returns an infrastructure object if a single 289 | * entity class is provided. 290 | */ 291 | public function testCreateWithDependenciesForCreatesInfrastructureForSingleEntity() 292 | { 293 | $infrastructure = ORMInfrastructure::createWithDependenciesFor( 294 | TestEntity::class 295 | ); 296 | 297 | $this->assertInstanceOf(ORMInfrastructure::class, $infrastructure); 298 | } 299 | 300 | /** 301 | * Ensures that createOnlyFor() returns an infrastructure object if a set of 302 | * entities classes is provided. 303 | */ 304 | public function testCreateOnlyForCreatesInfrastructureForSetOfEntities() 305 | { 306 | $infrastructure = ORMInfrastructure::createOnlyFor(array( 307 | TestEntity::class, 308 | ReferencedEntity::class 309 | )); 310 | 311 | $this->assertInstanceOf(ORMInfrastructure::class, $infrastructure); 312 | } 313 | 314 | /** 315 | * Ensures that createOnlyFor() returns an infrastructure object if a single 316 | * entity class is provided. 317 | */ 318 | public function testCreateOnlyForCreatesInfrastructureForSingleEntity() 319 | { 320 | $infrastructure = ORMInfrastructure::createOnlyFor( 321 | TestEntity::class 322 | ); 323 | 324 | $this->assertInstanceOf(ORMInfrastructure::class, $infrastructure); 325 | } 326 | 327 | /** 328 | * Ensures that referenced sub-entities are automatically prepared if the infrastructure is 329 | * requested to handle such cases. 330 | */ 331 | public function testInfrastructureAutomaticallyPerformsDependencySetupIfRequested() 332 | { 333 | $infrastructure = ORMInfrastructure::createWithDependenciesFor(array( 334 | TestEntityWithDependency::class 335 | )); 336 | 337 | $entityWithDependency = new TestEntityWithDependency(); 338 | 339 | // Saving without prepared sub-entity would fail. 340 | $infrastructure->getEntityManager()->persist($entityWithDependency); 341 | $this->assertNull( 342 | $infrastructure->getEntityManager()->flush() 343 | ); 344 | } 345 | 346 | /** 347 | * Ensures that referenced sub-entities are automatically prepared if the infrastructure is 348 | * requested to handle such cases. 349 | */ 350 | public function testInfrastructureAutomaticallyPerformsDependencySetupAcrossMappingDrivers() 351 | { 352 | $mappingDriver = new MappingDriverChain(); 353 | $mappingDriver->addDriver(new AttributeDriver([__DIR__.'/Fixtures/EntityNamespace1']), 'Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace1'); 354 | $mappingDriver->addDriver(new AttributeDriver([__DIR__.'/Fixtures/EntityNamespace2']), 'Webfactory\Doctrine\Tests\ORMTestInfrastructure\Fixtures\EntityNamespace2'); 355 | 356 | $this->infrastructure = ORMInfrastructure::createWithDependenciesFor([TestEntityWithDependency_Attributes::class], null, $mappingDriver); 357 | 358 | $entityWithDependency = new TestEntityWithDependency_Attributes(); 359 | 360 | self::expectNotToPerformAssertions(); 361 | 362 | $this->infrastructure->getEntityManager()->persist($entityWithDependency); 363 | $this->infrastructure->getEntityManager()->flush(); 364 | } 365 | 366 | /** 367 | * Checks if the automatic dependency setup can cope with reference cycles, 368 | * for example if an entity references itself. 369 | */ 370 | public function testAutomaticDependencyDetectionCanHandleCycles() 371 | { 372 | $infrastructure = ORMInfrastructure::createWithDependenciesFor(array( 373 | ReferenceCycleEntity::class 374 | )); 375 | 376 | $entityWithCycle = new ReferenceCycleEntity(); 377 | 378 | // Saving will most probably work as no additional table is needed, but the reference 379 | // detection, which is performed before, might lead to an endless loop. 380 | $infrastructure->getEntityManager()->persist($entityWithCycle); 381 | $this->assertNull( 382 | $infrastructure->getEntityManager()->flush() 383 | ); 384 | } 385 | 386 | /** 387 | * Checks if the automatic dependency setup can cope with chained references. 388 | * 389 | * Example: 390 | * 391 | * A -> B -> C 392 | * 393 | * A references B, B references C. A is not directly related to C. 394 | */ 395 | public function testAutomaticDependencyDetectionCanHandleChainedRelations() 396 | { 397 | $infrastructure = ORMInfrastructure::createWithDependenciesFor(array( 398 | ChainReferenceEntity::class 399 | )); 400 | 401 | $entityWithReferenceChain = new ChainReferenceEntity(); 402 | 403 | // All tables must be created properly, otherwise it is not possible to store the entity. 404 | $infrastructure->getEntityManager()->persist($entityWithReferenceChain); 405 | $this->assertNull( 406 | $infrastructure->getEntityManager()->flush() 407 | ); 408 | } 409 | 410 | /** 411 | * Ensures that it is not possible to retrieve the class names of entities, 412 | * which are not simulated by the infrastructure. 413 | * 414 | * If not handled properly, the metadata provides access to several entity classes. 415 | */ 416 | public function testNotSimulatedEntitiesAreNotExposed() 417 | { 418 | $infrastructure = ORMInfrastructure::createOnlyFor(array( 419 | TestEntity::class 420 | )); 421 | 422 | $metadata = $infrastructure->getEntityManager()->getMetadataFactory()->getAllMetadata(); 423 | $entities = array_map(function (ClassMetadata $info) { 424 | return ltrim($info->name, '\\'); 425 | }, $metadata); 426 | $this->assertEquals( 427 | array(TestEntity::class), 428 | $entities 429 | ); 430 | } 431 | 432 | public function testGetEventManagerReturnsEventManager() 433 | { 434 | $this->assertInstanceOf(EventManager::class, $this->infrastructure->getEventManager()); 435 | } 436 | 437 | public function testGetEventManagerReturnsSameEventManagerThatIsUsedByEntityManager() 438 | { 439 | $this->assertSame( 440 | $this->infrastructure->getEventManager(), 441 | $this->infrastructure->getEntityManager()->getEventManager() 442 | ); 443 | } 444 | 445 | public function testCanHandleInterfaceAssociationsIfMappingIsProvided() 446 | { 447 | $infrastructure = ORMInfrastructure::createWithDependenciesFor(EntityWithAssociationAgainstInterface::class); 448 | 449 | $infrastructure->registerEntityMapping(EntityInterface::class, EntityImplementation::class); 450 | 451 | $this->assertInstanceOf( 452 | EntityManager::class, 453 | $infrastructure->getEntityManager() 454 | ); 455 | } 456 | 457 | public function testCannotRegisterEntityMappingAfterEntityManagerCreation() 458 | { 459 | $this->infrastructure->getEntityManager(); 460 | 461 | $this->expectException(\LogicException::class); 462 | $this->assertNull( 463 | $this->infrastructure->registerEntityMapping(EntityInterface::class, EntityImplementation::class) 464 | ); 465 | } 466 | 467 | /** 468 | * Checks if it is possible to pass a more specific connection configuration. 469 | */ 470 | public function testUsesMoreSpecificConnectionConfiguration() 471 | { 472 | $this->infrastructure = ORMInfrastructure::createOnlyFor([ 473 | TestEntity::class 474 | ], new ConnectionConfiguration([ 475 | 'invalid' => 'configuration' 476 | ])); 477 | 478 | // The passed configuration is simply invalid, therefore, we expect an exception. 479 | $this->expectException('Exception'); 480 | $this->infrastructure->getEntityManager(); 481 | } 482 | 483 | /** 484 | * @see https://github.com/webfactory/doctrine-orm-test-infrastructure/issues/23 485 | */ 486 | public function testWorksWithCascadePersist() 487 | { 488 | $infrastructure = ORMInfrastructure::createWithDependenciesFor(CascadePersistingEntity::class); 489 | $cascadingPersistingEntity = new CascadePersistingEntity(); 490 | $cascadingPersistingEntity->add(new CascadePersistedEntity()); 491 | $infrastructure->import($cascadingPersistingEntity); 492 | 493 | // If this call fails, then there are leftovers in the identity map. 494 | $this->assertNull( 495 | $infrastructure->getEntityManager()->flush() 496 | ); 497 | } 498 | 499 | /** 500 | * @dataProvider resolverFixtures 501 | */ 502 | public function testSchemaResults(array $classes, callable $validator): void 503 | { 504 | $infrastructure = ORMInfrastructure::createWithDependenciesFor($classes); 505 | $entityManager = $infrastructure->getEntityManager(); 506 | $schemaTool = new SchemaTool($entityManager); 507 | 508 | $validator($schemaTool->getSchemaFromMetadata($infrastructure->getMetadataForSupportedEntities())); 509 | } 510 | 511 | public static function resolverFixtures() 512 | { 513 | yield 'single entity' => [ 514 | [DependencyResolverFixtures\SingleEntity\Entity::class], 515 | function (Schema $schema) { 516 | self::assertCount(1, $schema->getTableNames()); 517 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldA')); 518 | }, 519 | ]; 520 | 521 | yield 'simple entity hierarchy' => [ 522 | [DependencyResolverFixtures\TwoEntitiesInheritance\Entity::class], 523 | function (Schema $schema) { 524 | self::assertCount(1, $schema->getTableNames()); 525 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldA')); 526 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldB')); 527 | }, 528 | ]; 529 | 530 | yield 'entity with mapped superclass as base class' => [ 531 | [DependencyResolverFixtures\MappedSuperclassInheritance\Entity::class], 532 | function (Schema $schema) { 533 | self::assertCount(1, $schema->getTableNames()); 534 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldA')); 535 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldB')); 536 | }, 537 | ]; 538 | 539 | yield 'fields from transient base class are present, but class is otherwise ignored' => [ 540 | [DependencyResolverFixtures\TransientBaseClass\Entity::class], 541 | function (Schema $schema) { 542 | self::assertCount(1, $schema->getTableNames()); 543 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldA')); 544 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldB')); 545 | }, 546 | ]; 547 | 548 | yield 'with single table inheritance, the table for the base class is present with all fields' => [ 549 | [DependencyResolverFixtures\SingleTableInheritance\Entity::class], 550 | function (Schema $schema) { 551 | self::assertCount(1, $schema->getTableNames()); 552 | self::assertTrue($schema->getTable('BaseEntity')->hasColumn('fieldA')); 553 | self::assertTrue($schema->getTable('BaseEntity')->hasColumn('fieldB')); 554 | }, 555 | ]; 556 | 557 | yield 'with joined table inheritance, tables for the base and subclass are present with all fields' => [ 558 | [DependencyResolverFixtures\JoinedTableInheritance\Entity::class], 559 | function (Schema $schema) { 560 | self::assertCount(2, $schema->getTableNames()); 561 | 562 | self::assertCount(3, $schema->getTable('BaseEntity')->getColumns()); // id, class, fieldA 563 | self::assertTrue($schema->getTable('BaseEntity')->hasColumn('fieldA')); 564 | 565 | self::assertCount(2, $schema->getTable('Entity')->getColumns()); // id-baseref, fieldB 566 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldB')); 567 | }, 568 | ]; 569 | 570 | yield 'with joined table inheritance, three tables are present along the class hierarchy' => [ 571 | [DependencyResolverFixtures\JoinedTableInheritanceWithTwoLevels\Entity::class], 572 | function (Schema $schema) { 573 | self::assertCount(3, $schema->getTableNames()); 574 | 575 | self::assertCount(3, $schema->getTable('BaseEntity')->getColumns()); // id, class, fieldA 576 | self::assertTrue($schema->getTable('BaseEntity')->hasColumn('fieldA')); 577 | 578 | self::assertCount(2, $schema->getTable('IntermediateEntity')->getColumns()); // id-baseref, fieldB 579 | self::assertTrue($schema->getTable('IntermediateEntity')->hasColumn('fieldB')); 580 | 581 | self::assertCount(2, $schema->getTable('Entity')->getColumns()); // id-baseref, fieldC 582 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldC')); 583 | }, 584 | ]; 585 | 586 | yield 'with joined table inheritance, all tables for the complete inheritance tree are present' => [ 587 | [DependencyResolverFixtures\JoinedTableInheritanceWithTwoSubclasses\Entity::class], 588 | function (Schema $schema) { 589 | self::assertCount(3, $schema->getTableNames()); 590 | 591 | self::assertCount(3, $schema->getTable('BaseEntity')->getColumns()); // id, class, fieldA 592 | self::assertTrue($schema->getTable('BaseEntity')->hasColumn('fieldA')); 593 | 594 | self::assertCount(2, $schema->getTable('SecondEntity')->getColumns()); // id-baseref, fieldB 595 | self::assertTrue($schema->getTable('SecondEntity')->hasColumn('fieldB')); 596 | 597 | self::assertCount(2, $schema->getTable('Entity')->getColumns()); // id-baseref, fieldC 598 | self::assertTrue($schema->getTable('Entity')->hasColumn('fieldC')); 599 | }, 600 | ]; 601 | } 602 | } 603 | --------------------------------------------------------------------------------