Skip to content

Commit

Permalink
Allow passing the mapping driver to ORMInfrastructure::create*() me…
Browse files Browse the repository at this point in the history
…thods (#48)

This PR makes it possible to transition to attribute-based or other
types of Doctrine ORM mapping configuration by (optionally) passing the
mapping driver into `ORMInfrastructure::create*()` methods.

Also, hybrid configurations (different types of mapping) can be used by
leveraging the `MappingDriverChain` from doctrine/persistence.
  • Loading branch information
mpdude authored Mar 20, 2024
1 parent 6d1115b commit 8808c1b
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 113 deletions.
162 changes: 95 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,79 +6,105 @@ doctrine-orm-test-infrastructure
This library provides some infrastructure for tests of Doctrine ORM entities, featuring:

- configuration of a SQLite in memory database, compromising well between speed and a database environment being both
realistic and isolated
realistic and isolated
- a mechanism for importing fixtures into your database that circumvents Doctrine's caching. This results in a more
realistic test environment when loading entities from a repository.

[We](https://www.webfactory.de/) use it to test Doctrine repositories and entities in Symfony applications. It's a
lightweight alternative to the heavyweight [functional tests suggested in the Symfony documentation](http://symfony.com/doc/current/cookbook/testing/doctrine.html)
(we don't suggest you should skip those - we just want to open another path).
lightweight alternative to the
heavyweight [functional tests suggested in the Symfony documentation](http://symfony.com/doc/current/cookbook/testing/doctrine.html)
(we don't suggest you should skip those - we just want to open another path).

In non-application bundles, where functional tests are not possible,
it is our only way to test repositories and entities.


Installation
------------

Install via composer (see http://getcomposer.org/):

composer require --dev webfactory/doctrine-orm-test-infrastructure


Usage
-----

<?php

use Entity\MyEntity;
use Entity\MyEntityRepository;
use Webfactory\Doctrine\ORMTestInfrastructure\ORMInfrastructure;

class MyEntityRepositoryTest extends \PHPUnit_Framework_TestCase
```php
<?php

use Doctrine\ORM\EntityManagerInterface;
use Entity\MyEntity;
use Entity\MyEntityRepository;
use PHPUnit\Framework\TestCase;
use Webfactory\Doctrine\ORMTestInfrastructure\ORMInfrastructure;

class MyEntityRepositoryTest extends TestCase
{
private ORMInfrastructure $infrastructure;
private MyEntityRepository $repository;

protected function setUp(): void
{
/*
This will create an in-memory SQLite database with the necessary schema
for the MyEntity entity class and and everything reachable from it through
associations.
*/
$this->infrastructure = ORMInfrastructure::createWithDependenciesFor(MyEntity::class);
$this->repository = $this->infrastructure->getRepository(MyEntity::class);
}

/**
* Example test: Asserts imported fixtures are retrieved with findAll().
*/
public function testFindAllRetrievesFixtures(): void
{
$myEntityFixture = new MyEntity();

$this->infrastructure->import($myEntityFixture);
$entitiesLoadedFromDatabase = $this->repository->findAll();

/*
import() will use a dedicated entity manager, so imported entities do not
end up in the identity map. But this also means loading entities from the
database will create _different object instances_.

So, this does not hold:
*/
// self::assertContains($myEntityFixture, $entitiesLoadedFromDatabase);

// But you can do things like this (you probably want to extract that in a convenient assertion method):
self::assertCount(1, $entitiesLoadedFromDatabase);
$entityLoadedFromDatabase = $entitiesLoadedFromDatabase[0];
self::assertSame($myEntityFixture->getId(), $entityLoadedFromDatabase->getId());
}

/**
* Example test for retrieving Doctrine's entity manager.
*/
public function testSomeFancyThingWithEntityManager(): void
{
/** @var ORMInfrastructure */
private $infrastructure;
/** @var MyEntityRepository */
private $repository;
/** @see \PHPUnit_Framework_TestCase::setUp() */
protected function setUp()
{
$this->infrastructure = ORMInfrastructure::createWithDependenciesFor(MyEntity::class);
$this->repository = $this->infrastructure->getRepository(MyEntity::class);
}
/**
* Example test: Asserts imported fixtures are retrieved with findAll().
*/
public function testFindAllRetrievesFixtures()
{
$myEntityFixture = new MyEntity();
$this->infrastructure->import($myEntityFixture);
$entitiesLoadedFromDatabase = $this->repository->findAll();

// Please note that you cannot do the following:
// $this->assertContains($myEntityFixture, $entitiesLoadedFromDatabase);

// But you can do things like this (you probably want to extract that in a convenient assertion method):
$this->assertCount(1, $entitiesLoadedFromDatabase);
$entityLoadedFromDatabase = $entitiesLoadedFromDatabase[0];
$this->assertEquals($myEntityFixture->getId(), $entityLoadedFromDatabase->getId());
}
/**
* Example test for retrieving Doctrine's entity manager.
*/
public function testSomeFancyThingWithEntityManager()
{
$entityManager = $this->infrastructure->getEntityManager();
// ...
}
$entityManager = $this->infrastructure->getEntityManager();
// ...
}

}
```

Migrating to attribute-based mapping configuration
--------------------------------------------------

The `ORMInfrastructure::createWithDependenciesFor()` and ``ORMInfrastructure::createOnlyFor()` methods by default
assume that the Doctrine ORM mapping is provided through annotations. This has been deprecated in Doctrine ORM 2.x
and is no longer be supported in ORM 3.0.

To allow for a seamless transition towards attribute-based or other types of mapping, a mapping driver can be passed
when creating instances of the `ORMInfrastructure`.

If you wish to switch to attribute-based mappings, pass a `new \Doctrine\ORM\Mapping\Driver\AttributeDriver($paths)`,
where `$paths` is an array of directory paths where your entity classes are stored.

For hybrid (annotations and attributes) mapping configurations, you can use `\Doctrine\Persistence\Mapping\Driver\MappingDriverChain`.
Multiple mapping drivers can be registered on the driver chain by providing namespace prefixes. For every namespace prefix,
only one mapping driver can be used.

Testing the library itself
--------------------------
Expand All @@ -92,20 +118,23 @@ need local changes.

Happy testing!


## Changelog ##

### 1.5.0 -> 1.5.1 ###

- Clear entity manager after import to avoid problems with entities detected by cascade operations [(#23)](https://github.com/webfactory/doctrine-orm-test-infrastructure/issues/23)
- Use separate entity managers for imports to avoid interference between import and test phase [(#2)](https://github.com/webfactory/doctrine-orm-test-infrastructure/issues/2)
- Deprecated internal class ``\Webfactory\Doctrine\ORMTestInfrastructure\MemorizingObjectManagerDecorator`` as it is not needed anymore: there are no more selective ``detach()`` calls`after imports
- Clear entity manager after import to avoid problems with entities detected by cascade
operations [(#23)](https://github.com/webfactory/doctrine-orm-test-infrastructure/issues/23)
- Use separate entity managers for imports to avoid interference between import and test
phase [(#2)](https://github.com/webfactory/doctrine-orm-test-infrastructure/issues/2)
- Deprecated internal class ``\Webfactory\Doctrine\ORMTestInfrastructure\MemorizingObjectManagerDecorator`` as it is not needed anymore: there are no
more selective ``detach()`` calls`after imports

### 1.4.6 -> 1.5.0 ###

- Introduced ``ConnectionConfiguration`` to explicitly define the type of database connection [(#15)](https://github.com/webfactory/doctrine-orm-test-infrastructure/pull/15)
- Added support for simple SQLite file databases via ``FileDatabaseConnectionConfiguration``; useful when data must persist for some time, but the connection is reset, e.g. in Symfony's [Functional Tests](http://symfony.com/doc/current/testing.html#functional-tests)

- Introduced ``ConnectionConfiguration`` to explicitly define the type of database
connection [(#15)](https://github.com/webfactory/doctrine-orm-test-infrastructure/pull/15)
- Added support for simple SQLite file databases via ``FileDatabaseConnectionConfiguration``; useful when data must persist for some time, but the
connection is reset, e.g. in Symfony's [Functional Tests](http://symfony.com/doc/current/testing.html#functional-tests)

Create file-backed database:

Expand All @@ -122,7 +151,6 @@ Create file-backed database:
- Ignore associations against interfaces when detecting dependencies via ``ORMInfrastructure::createWithDependenciesFor`` to avoid errors
- Exposed event manager and created helper method to be able to register entity mappings


Register entity type mapping:

$infrastructure->registerEntityMapping(EntityInterface::class, EntityImplementation::class);
Expand All @@ -131,7 +159,8 @@ Do not rely on this "feature" if you don't have to. Might be restructured in fut

### 1.4.4 -> 1.4.5 ###

- Fixed bug [#20](https://github.com/webfactory/doctrine-orm-test-infrastructure/issues/20): Entities might have been imported twice in case of bidirectional cascade
- Fixed bug [#20](https://github.com/webfactory/doctrine-orm-test-infrastructure/issues/20): Entities might have been imported twice in case of
bidirectional cascade
- Deprecated class ``Webfactory\Doctrine\ORMTestInfrastructure\DetachingObjectManagerDecorator`` (will be removed in next major release)

### 1.4.3 -> 1.4.4 ###
Expand All @@ -140,12 +169,12 @@ Do not rely on this "feature" if you don't have to. Might be restructured in fut
- Dropped support for PHP < 5.5
- Officially support PHP 7


Known Issues
------------

Please note that apart from any [open issues in this library](https://github.com/webfactory/doctrine-orm-test-infrastructure/issues), you
may stumble upon any Doctrine issues. Especially take care of it's [known sqlite issues](http://doctrine-dbal.readthedocs.org/en/latest/reference/known-vendor-issues.html#sqlite).
may stumble upon any Doctrine issues. Especially take care of
it's [known sqlite issues](http://doctrine-dbal.readthedocs.org/en/latest/reference/known-vendor-issues.html#sqlite).

Credits, Copyright and License
------------------------------
Expand All @@ -154,9 +183,8 @@ This package was first written by webfactory GmbH (Bonn, Germany) and received [
from other people](https://github.com/webfactory/doctrine-orm-test-infrastructure/graphs/contributors) since then.

webfactory is a software development agency with a focus on PHP (mostly [Symfony](http://github.com/symfony/symfony)).
If you're a developer looking for new challenges, we'd like to hear from you!
If you're a developer looking for new challenges, we'd like to hear from you!

- <https://www.webfactory.de>
- <https://twitter.com/webfactory>

Copyright 2012 – 2020 webfactory GmbH, Bonn. Code released under [the MIT license](LICENSE).
Copyright 2012 – 2024 webfactory GmbH, Bonn. Code released under [the MIT license](LICENSE).
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
],
"require": {
"php": "^7.2|^8.0",
"cache/array-adapter": "^1.0",
"doctrine/annotations": "^1.8|^2.0",
"doctrine/common": "^2.11|^3.0",
"doctrine/dbal": "^2.12|^3.0",
"doctrine/event-manager": "^1.1|^2.0",
"doctrine/orm": "^2.12",
"doctrine/persistence": "^1.3|^2.0|^3.0"
"doctrine/persistence": "^1.3|^2.0|^3.0",
"symfony/cache": "^4.0|^5.0|^6.0|^7.0"
},
"require-dev": {
"phpunit/phpunit": "^8.5.36|^9.6.17"
Expand Down
53 changes: 39 additions & 14 deletions src/ORMTestInfrastructure/ConfigurationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@

namespace Webfactory\Doctrine\ORMTestInfrastructure;

use Cache\Adapter\PHPArray\ArrayCachePool;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\PsrCachedReader;
use Doctrine\Common\Annotations\Reader;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\ORMSetup;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

/**
* Creates ORM configurations for a set of entities.
Expand All @@ -32,6 +35,17 @@ class ConfigurationFactory
*/
protected static $defaultAnnotationReader = null;

/** @var ?CacheItemPoolInterface */
private static $metadataCache = null;

/** @var ?MappingDriver */
private $mappingDriver;

public function __construct(MappingDriver $mappingDriver = null)
{
$this->mappingDriver = $mappingDriver;
}

/**
* Creates the ORM configuration for the given set of entities.
*
Expand All @@ -40,23 +54,36 @@ class ConfigurationFactory
*/
public function createFor(array $entityClasses)
{
$config = ORMSetup::createConfiguration(true, null, new ArrayCachePool());
if (self::$metadataCache === null) {
self::$metadataCache = new ArrayAdapter();
}

$mappingDriver = $this->mappingDriver ?? $this->createDefaultAnnotationsDriver($entityClasses);

$driver = new AnnotationDriver(
$this->getAnnotationReader(),
$this->getDirectoryPathsForClassNames($entityClasses)
);
$driver = new EntityListDriverDecorator($driver, $entityClasses);
$config->setMetadataDriverImpl($driver);
$config = ORMSetup::createConfiguration(true, null, new ArrayAdapter());
$config->setMetadataCache(self::$metadataCache);
$config->setMetadataDriverImpl(new EntityListDriverDecorator($mappingDriver, $entityClasses));

return $config;
}

/**
* @param list<class-string> $entityClasses
*
* @return MappingDriver
*/
private function createDefaultAnnotationsDriver(array $entityClasses)
{
$paths = $this->getDirectoryPathsForClassNames($entityClasses);

return new AnnotationDriver($this->getAnnotationReader(), $paths);
}

/**
* Returns a list of file paths for the provided class names.
*
* @param string[] $classNames
* @return string[]
* @param list<class-string> $classNames
* @return list<string>
*/
protected function getDirectoryPathsForClassNames(array $classNames)
{
Expand All @@ -70,7 +97,7 @@ protected function getDirectoryPathsForClassNames(array $classNames)
/**
* Returns the path to the directory that contains the given class.
*
* @param string $className
* @param class-string $className
* @return string
*/
protected function getDirectoryPathForClassName($className)
Expand All @@ -87,9 +114,7 @@ protected function getDirectoryPathForClassName($className)
protected function getAnnotationReader()
{
if (static::$defaultAnnotationReader === null) {
// Use just the reader as the driver depends on the configured
// paths and, therefore, should not be shared.
static::$defaultAnnotationReader = ORMSetup::createDefaultAnnotationDriver()->getReader();
static::$defaultAnnotationReader = new PsrCachedReader(new AnnotationReader(), new ArrayAdapter());
}

return static::$defaultAnnotationReader;
Expand Down
4 changes: 2 additions & 2 deletions src/ORMTestInfrastructure/EntityDependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ class EntityDependencyResolver implements \IteratorAggregate
*
* @param string[] $entityClasses
*/
public function __construct(array $entityClasses)
public function __construct(array $entityClasses, MappingDriver $mappingDriver = null)
{
$this->initialEntitySet = $this->normalizeClassNames($entityClasses);
$this->reflectionService = new RuntimeReflectionService();
$this->configFactory = new ConfigurationFactory();
$this->configFactory = new ConfigurationFactory($mappingDriver);
}

/**
Expand Down
Loading

0 comments on commit 8808c1b

Please sign in to comment.