diff --git a/CHANGELOG.md b/CHANGELOG.md index d547e97..0966ea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. -## 1.1.0 - TBD +## 1.1.0 - 2017-11-06 ### Added @@ -20,25 +20,11 @@ All notable changes to this project will be documented in this file, in reverse ### Fixed -- Nothing. - -## 1.0.1 - TBD - -### Added - -- Nothing. - -### Deprecated - -- Nothing. - -### Removed - -- Nothing. - -### Fixed - -- Nothing. +- [#40](https://github.com/zendframework/zend-component-installer/pull/40) and + [#44](https://github.com/zendframework/zend-component-installer/pull/44) fix + an issue whereby packages that define an array of paths for a PSR-0 or PSR-4 + autoloader would cause the installer to error. The installer now properly + handles these situations. ## 1.0.0 - 2017-04-25 diff --git a/src/ComponentInstaller.php b/src/ComponentInstaller.php index 9fbbc0e..f4b2674 100644 --- a/src/ComponentInstaller.php +++ b/src/ComponentInstaller.php @@ -7,6 +7,7 @@ namespace Zend\ComponentInstaller; +use ArrayObject; use Composer\Composer; use Composer\EventDispatcher\EventSubscriberInterface; use Composer\Installer\PackageEvent; @@ -203,7 +204,7 @@ public function onPostPackageInstall(PackageEvent $event) /** * Find all Module classes in the package and their dependencies - * - method `getModuleDependencies` of Module class. + * via method `getModuleDependencies` of Module class. * * These dependencies are used later * @see \Zend\ComponentInstaller\Injector\AbstractInjector::injectAfterDependencies @@ -216,87 +217,13 @@ public function onPostPackageInstall(PackageEvent $event) */ private function loadModuleClassesDependencies(PackageInterface $package) { - $dependencies = []; + $dependencies = new ArrayObject([]); $installer = $this->composer->getInstallationManager(); $packagePath = $installer->getInstallPath($package); - $autoload = $package->getAutoload(); - foreach ($autoload as $type => $map) { - foreach ($map as $namespace => $path) { - switch ($type) { - case 'classmap': - $fullPath = sprintf('%s/%s', $packagePath, $path); - if (is_dir(rtrim($fullPath, '/'))) { - $modulePath = sprintf('%s%s', $fullPath, 'Module.php'); - } elseif (substr($path, -10) === 'Module.php') { - $modulePath = $fullPath; - } else { - continue 2; - } - break; - case 'files': - if (substr($path, -10) !== 'Module.php') { - continue 2; - } - $modulePath = sprintf('%s/%s', $packagePath, $path); - break; - case 'psr-0': - $modulePath = sprintf( - '%s/%s%s%s', - $packagePath, - $path, - str_replace('\\', '/', $namespace), - 'Module.php' - ); - break; - case 'psr-4': - $modulePath = sprintf( - '%s/%s%s', - $packagePath, - $path, - 'Module.php' - ); - break; - default: - continue 2; - } - - if (file_exists($modulePath)) { - if ($result = $this->getModuleDependencies($modulePath)) { - $dependencies += $result; - } - } - } - } - - return $dependencies; - } - - /** - * @param string $file - * @return array - */ - private function getModuleDependencies($file) - { - $content = file_get_contents($file); - if (preg_match('/namespace\s+([^\s]+)\s*;/', $content, $m)) { - $moduleName = $m[1]; - - // @codingStandardsIgnoreStart - $regExp = '/public\s+function\s+getModuleDependencies\s*\(\s*\)\s*{[^}]*return\s*(?:array\(|\[)([^})\]]*)(\)|\])/'; - // @codingStandardsIgnoreEnd - if (preg_match($regExp, $content, $m)) { - $dependencies = array_filter( - explode(',', stripslashes(rtrim(preg_replace('/[\s"\']/', '', $m[1]), ','))) - ); - - if ($dependencies) { - return [$moduleName => $dependencies]; - } - } - } + $this->mapAutoloaders($package->getAutoload(), $dependencies, $packagePath); - return []; + return $dependencies->getArrayCopy(); } /** @@ -687,4 +614,143 @@ private function cacheInjector(Injector\InjectorInterface $injector, $packageTyp { $this->cachedInjectors[$packageType] = $injector; } + + /** + * Iterate through each autoloader type to find dependencies. + * + * @param array $autoload List of autoloader types and associated autoloader definitions. + * @param ArrayObject $dependencies Module dependencies defined by the module. + * @param string $packagePath Path to the package on the filesystem. + * @return void + */ + private function mapAutoloaders(array $autoload, ArrayObject $dependencies, $packagePath) + { + foreach ($autoload as $type => $map) { + $this->mapType($map, $type, $dependencies, $packagePath); + } + } + + /** + * Iterate through a single autolaoder type to find dependencies. + * + * @param array $map Map of namespace => path(s) pairs. + * @param string $type Type of autoloader being iterated. + * @param ArrayObject $dependencies Module dependencies defined by the module. + * @param string $packagePath Path to the package on the filesystem. + * @return void + */ + private function mapType(array $map, $type, ArrayObject $dependencies, $packagePath) + { + foreach ($map as $namespace => $paths) { + $paths = (array) $paths; + $this->mapNamespacePaths($paths, $namespace, $type, $dependencies, $packagePath); + } + } + + /** + * Iterate through the paths defined for a given namespace. + * + * @param array $paths Paths defined for the given namespace. + * @param string $namespace PHP namespace to which the paths map. + * @param string $type Type of autoloader being iterated. + * @param ArrayObject $dependencies Module dependencies defined by the module. + * @param string $packagePath Path to the package on the filesystem. + * @return void + */ + private function mapNamespacePaths(array $paths, $namespace, $type, ArrayObject $dependencies, $packagePath) + { + foreach ($paths as $path) { + $this->mapPath($path, $namespace, $type, $dependencies, $packagePath); + } + } + + /** + * Find module dependencies for a given namespace for a given path. + * + * @param string $path Path to inspect. + * @param string $namespace PHP namespace to which the paths map. + * @param string $type Type of autoloader being iterated. + * @param ArrayObject $dependencies Module dependencies defined by the module. + * @param string $packagePath Path to the package on the filesystem. + * @return void + */ + private function mapPath($path, $namespace, $type, ArrayObject $dependencies, $packagePath) + { + switch ($type) { + case 'classmap': + $fullPath = sprintf('%s/%s', $packagePath, $path); + if (substr($path, -10) === 'Module.php') { + $modulePath = $fullPath; + break; + } + + $modulePath = sprintf('%s/Module.php', rtrim($fullPath, '/')); + break; + case 'files': + if (substr($path, -10) !== 'Module.php') { + return; + } + $modulePath = sprintf('%s/%s', $packagePath, $path); + break; + case 'psr-0': + $modulePath = sprintf( + '%s/%s%s%s', + $packagePath, + $path, + str_replace('\\', '/', $namespace), + 'Module.php' + ); + break; + case 'psr-4': + $modulePath = sprintf( + '%s/%s%s', + $packagePath, + $path, + 'Module.php' + ); + break; + default: + return; + } + + if (! file_exists($modulePath)) { + return; + } + + $result = $this->getModuleDependencies($modulePath); + + if (empty($result)) { + return; + } + + // Mimic array + array operation in ArrayObject + $dependencies->exchangeArray($dependencies->getArrayCopy() + $result); + } + + /** + * @param string $file + * @return array + */ + private function getModuleDependencies($file) + { + $content = file_get_contents($file); + if (preg_match('/namespace\s+([^\s]+)\s*;/', $content, $m)) { + $moduleName = $m[1]; + + // @codingStandardsIgnoreStart + $regExp = '/public\s+function\s+getModuleDependencies\s*\(\s*\)\s*{[^}]*return\s*(?:array\(|\[)([^})\]]*)(\)|\])/'; + // @codingStandardsIgnoreEnd + if (preg_match($regExp, $content, $m)) { + $dependencies = array_filter( + explode(',', stripslashes(rtrim(preg_replace('/[\s"\']/', '', $m[1]), ','))) + ); + + if ($dependencies) { + return [$moduleName => $dependencies]; + } + } + } + + return []; + } } diff --git a/test/ComponentInstallerTest.php b/test/ComponentInstallerTest.php index 35d82e8..7e26e6d 100644 --- a/test/ComponentInstallerTest.php +++ b/test/ComponentInstallerTest.php @@ -15,9 +15,11 @@ use Composer\Package\PackageInterface; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamDirectory; +use org\bovigo\vfs\vfsStreamWrapper; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; +use ReflectionObject; use Zend\ComponentInstaller\ComponentInstaller; class ComponentInstallerTest extends TestCase @@ -1503,7 +1505,7 @@ public function moduleClass() */ public function testGetModuleDependenciesFromModuleClass($file, $result) { - $r = new \ReflectionObject($this->installer); + $r = new ReflectionObject($this->installer); $rm = $r->getMethod('getModuleDependencies'); $rm->setAccessible(true); @@ -1511,4 +1513,120 @@ public function testGetModuleDependenciesFromModuleClass($file, $result) $this->assertEquals($result, $dependencies); } + + public function testGetModuleClassesDependenciesHandlesAutoloadersWithMultiplePathsMappedToSameNamespace() + { + $installPath = 'install/path'; + $this->setUpModuleDependencies($installPath); + + $autoloaders = [ + 'psr-0' => [ + 'DoesNotExist\\' => [ + 'src/Psr0/', + 'src/Psr0Too/', + ], + ], + 'psr-4' => [ + 'DoesNotExistEither\\' => [ + 'src/Psr4/', + 'src/Psr4Too/', + ], + ], + 'classmap' => [ + 'src/Classmapped/', + 'src/ClassmappedToo/', + ], + 'files' => [ + 'src/File/Module.php', + 'src/FileToo/Module.php', + ], + ]; + + $package = $this->prophesize(PackageInterface::class); + $package->getAutoload()->willReturn($autoloaders); + + $this->installationManager + ->getInstallPath(Argument::that([$package, 'reveal'])) + ->willReturn(vfsStream::url('project/' . $installPath)); + + $r = new ReflectionObject($this->installer); + $rm = $r->getMethod('loadModuleClassesDependencies'); + $rm->setAccessible(true); + + $dependencies = $rm->invoke($this->installer, $package->reveal()); + $this->assertEquals([ + 'DoesNotExist' => ['DoesNotExistDependency'], + 'DoesNotExistEither' => ['DoesNotExistEitherDependency'], + 'ClassmappedToo' => ['ClassmappedTooDependency'], + 'File' => ['FileDependency'], + ], $dependencies); + } + + public function setUpModuleDependencies($path) + { + $this->createModuleClass( + $path . '/src/Psr0Too/DoesNotExist/Module.php', + <<createModuleClass( + $path . '/src/Psr4/Module.php', + <<createModuleClass( + $path . '/src/ClassmappedToo/Module.php', + <<createModuleClass( + $path . '/src/File/Module.php', + <<