diff --git a/.travis.yml b/.travis.yml index 0d3bc4f..3ae44ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: php php: - - 5.5 - 5.6 - 7.0 - 7.1 diff --git a/composer.json b/composer.json index bccf08b..10081f5 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,12 @@ } ], "require": { - "php": ">=5.5.0", + "php": ">=5.6.0", "doctrine/orm": "2.5.*", "doctrine/inflector": "^1.1" }, "require-dev": { - "phpunit/phpunit": "~4.0", + "phpunit/phpunit": "~5.7", "mockery/mockery": "~0.9", "beberlei/DoctrineExtensions": "~1.0", "zf1/zend-date": "~1.12", diff --git a/src/FluentDriver.php b/src/FluentDriver.php index 452f931..91ecc30 100644 --- a/src/FluentDriver.php +++ b/src/FluentDriver.php @@ -6,9 +6,15 @@ use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder; use Doctrine\ORM\Mapping\MappingException; +use FilesystemIterator; use InvalidArgumentException; use LaravelDoctrine\Fluent\Builders\Builder; use LaravelDoctrine\Fluent\Mappers\MapperSet; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use RecursiveRegexIterator; +use ReflectionClass; +use RegexIterator; class FluentDriver implements MappingDriver { @@ -22,20 +28,37 @@ class FluentDriver implements MappingDriver */ protected $fluentFactory; + /** + * The file extension of mapping documents. + * + * @var string + */ + protected $fileExtension = '.php'; + /** * Initializes a new FileDriver that looks in the given path(s) for mapping * documents and operates in the specified operating mode. * * @param string[] $mappings + * @param array $paths + * + * @throws MappingException */ - public function __construct(array $mappings = []) + public function __construct(array $mappings = [], array $paths = []) { $this->fluentFactory = function (ClassMetadata $metadata) { return new Builder(new ClassMetadataBuilder($metadata)); }; $this->mappers = new MapperSet(); - $this->addMappings($mappings); + + if (!empty($paths)) { + $this->loadPaths($paths); + } + + if (!empty($mappings)) { + $this->addMappings($mappings); + } } /** @@ -78,8 +101,74 @@ public function isTransient($className) $this->mappers->getMapperFor($className)->isTransient(); } + /** + * @param array $paths + * + * @throws MappingException + */ + public function loadPaths(array $paths) + { + $includedFiles = []; + + foreach ($paths as $path) { + if (!is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new RegexIterator( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY + ), + '/^.+'.preg_quote($this->fileExtension).'$/i', + RecursiveRegexIterator::GET_MATCH + ); + + foreach ($iterator as $file) { + $sourceFile = $file[0]; + + if (!preg_match('(^phar:)i', $sourceFile)) { + $sourceFile = realpath($sourceFile); + } + + require_once $sourceFile; + + $includedFiles[] = $sourceFile; + } + + $declared = get_declared_classes(); + + foreach ($declared as $className) { + $rc = new ReflectionClass($className); + $sourceFile = $rc->getFileName(); + + if (!in_array($sourceFile, $includedFiles)) { + continue; + } + + if ($rc->isAbstract() || $rc->isInterface()) { + continue; + } + + if (!$rc->implementsInterface(Mapping::class)) { + continue; + } + + if ($this->isTransient($className)) { + + /** @var Mapping $mapping */ + $mapping = $rc->newInstanceWithoutConstructor(); + + $this->addMapping($mapping); + } + } + } + } + /** * @param string[] $mappings + * + * @throws InvalidArgumentException */ public function addMappings(array $mappings = []) { diff --git a/tests/FluentDriverTest.php b/tests/FluentDriverTest.php index 8f8dc62..a84ef68 100644 --- a/tests/FluentDriverTest.php +++ b/tests/FluentDriverTest.php @@ -82,6 +82,7 @@ public function test_it_should_load_metadata_for_mappings_passed_as_constructor_ StubEntity::class, new ClassMetadataInfo(StubEntity::class) ); + $this->assertInstanceOf( EntityMapper::class, $driver->getMappers()->getMapperFor(StubEntity::class) @@ -91,6 +92,7 @@ public function test_it_should_load_metadata_for_mappings_passed_as_constructor_ StubEmbeddable::class, new ClassMetadataInfo(StubEmbeddable::class) ); + $this->assertInstanceOf( EmbeddableMapper::class, $driver->getMappers()->getMapperFor(StubEmbeddable::class) @@ -100,6 +102,7 @@ public function test_it_should_load_metadata_for_mappings_passed_as_constructor_ StubMappedSuperClass::class, new ClassMetadataInfo(StubMappedSuperClass::class) ); + $this->assertInstanceOf( MappedSuperClassMapper::class, $driver->getMappers()->getMapperFor(StubMappedSuperClass::class) @@ -126,9 +129,96 @@ public function test_can_add_array_of_new_mappings() ); } + public function test_can_load_mappings_through_file_path() + { + $driver = new FluentDriver([], [__DIR__ . '/' . 'Stubs/Mappings']); + + $this->assertContains( + StubEntity::class, + $driver->getAllClassNames() + ); + + $this->assertContains( + StubEmbeddable::class, + $driver->getAllClassNames() + ); + + $this->assertContains( + StubMappedSuperClass::class, + $driver->getAllClassNames() + ); + + $driver->loadMetadataForClass( + StubEntity::class, + new ClassMetadataInfo(StubEntity::class) + ); + + $this->assertInstanceOf( + EntityMapper::class, + $driver->getMappers()->getMapperFor(StubEntity::class) + ); + + $driver->loadMetadataForClass( + StubEmbeddable::class, + new ClassMetadataInfo(StubEmbeddable::class) + ); + + $this->assertInstanceOf( + EmbeddableMapper::class, + $driver->getMappers()->getMapperFor(StubEmbeddable::class) + ); + + $driver->loadMetadataForClass( + StubMappedSuperClass::class, + new ClassMetadataInfo(StubMappedSuperClass::class) + ); + + $this->assertInstanceOf( + MappedSuperClassMapper::class, + $driver->getMappers()->getMapperFor(StubMappedSuperClass::class) + ); + } + + public function test_can_load_paths() + { + $driver = new FluentDriver(); + $driver->loadPaths([__DIR__ . '/' . 'Stubs/Mappings']); + + $this->assertContains( + StubEntity::class, + $driver->getAllClassNames() + ); + + $this->assertContains( + StubEmbeddable::class, + $driver->getAllClassNames() + ); + + $this->assertContains( + StubMappedSuperClass::class, + $driver->getAllClassNames() + ); + } + + public function test_loading_by_paths_throws_exception_if_dir_not_exists() + { + $folder = __DIR__ . '/' . 'Stubs/non-existing-folder'; + + $this->setExpectedException( + MappingException::class, + 'File mapping drivers must have a valid directory path, however the given path [' . $folder . '] seems to be incorrect!' + ); + + $driver = new FluentDriver(); + $driver->loadPaths([$folder]); + + $driver->getAllClassNames(); + } + public function test_the_given_mapping_class_should_exist() { - $this->setExpectedException(\InvalidArgumentException::class, 'Mapping class [Tests\DoesnExist] does not exist'); + $this->setExpectedException(\InvalidArgumentException::class, + 'Mapping class [Tests\DoesnExist] does not exist'); $driver = new FluentDriver; @@ -139,7 +229,8 @@ public function test_the_given_mapping_class_should_exist() public function test_the_given_mapping_class_should_implement_mapping() { - $this->setExpectedException(\InvalidArgumentException::class, 'Mapping class [Tests\Stubs\Entities\StubEntity] should implement LaravelDoctrine\Fluent\Mapping'); + $this->setExpectedException(\InvalidArgumentException::class, + 'Mapping class [Tests\Stubs\Entities\StubEntity] should implement LaravelDoctrine\Fluent\Mapping'); $driver = new FluentDriver; @@ -223,7 +314,7 @@ public function test_allow_other_fluent_implementations() return new CustomBuilder(new ClassMetadataBuilder($metadata)); }); - $mapping = $this->getMock(EntityMapping::class); + $mapping = $this->createMock(EntityMapping::class); $mapping->expects($this->once())->method('map')->with($this->isInstanceOf(CustomBuilder::class)); $driver->getMappers()->addMapper('fake', new EntityMapper($mapping));