diff --git a/app/etc/di.xml b/app/etc/di.xml old mode 100755 new mode 100644 index 00eb93bd1cc5f..4bdf8648c430d --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -44,6 +44,7 @@ + @@ -1075,6 +1076,11 @@ lessFileOverriddenBase + + + Magento\Framework\Filesystem\Driver\File + + diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/circular.csv b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/circular.csv index 6d00c9f481554..34e69204e8e9f 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/circular.csv +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/circular.csv @@ -2,8 +2,8 @@ "","2" "Circular dependencies for each module:","" -"magento/module-a","1" -"magento/module-a->magento/module-b->magento/module-a" - "magento/module-b","1" "magento/module-b->magento/module-a->magento/module-b" + +"magento/module-a","1" +"magento/module-a->magento/module-b->magento/module-a" diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/framework.csv b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/framework.csv index b6abb23cd683f..5f3acfe70bd27 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/framework.csv +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/framework.csv @@ -2,8 +2,8 @@ "","2" "Dependencies for each module:","" -"Magento\A","1" +"Magento\B","1" " -- Magento\Framework","1" -"Magento\B","1" +"Magento\A","1" " -- Magento\Framework","1" diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/modules.csv b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/modules.csv index 41deca9466eca..0bc98654f4f5f 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/modules.csv +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/expected/modules.csv @@ -2,8 +2,8 @@ "Total number of dependencies","2","2","0" "Dependencies for each module:","All","Hard","Soft" -"magento/module-a","1","1","0" -" -- magento/module-b","","1","0" - "magento/module-b","1","1","0" " -- magento/module-a","","1","0" + +"magento/module-a","1","1","0" +" -- magento/module-b","","1","0" diff --git a/lib/internal/Magento/Framework/Module/Dir.php b/lib/internal/Magento/Framework/Module/Dir.php index 749e443fe7c10..4d3b811ea8e60 100644 --- a/lib/internal/Magento/Framework/Module/Dir.php +++ b/lib/internal/Magento/Framework/Module/Dir.php @@ -10,6 +10,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Stdlib\String as StringHelper; +use Magento\Framework\Module\ModuleRegistryInterface; class Dir { @@ -25,14 +27,26 @@ class Dir */ protected $_string; + /** + * Module registry + * + * @var ModuleRegistryInterface + */ + private $moduleRegistry; + /** * @param Filesystem $filesystem - * @param \Magento\Framework\Stdlib\String $string + * @param StringHelper $string + * @param ModuleRegistryInterface $moduleRegistry */ - public function __construct(Filesystem $filesystem, \Magento\Framework\Stdlib\String $string) - { + public function __construct( + Filesystem $filesystem, + StringHelper $string, + ModuleRegistryInterface $moduleRegistry + ) { $this->_modulesDirectory = $filesystem->getDirectoryRead(DirectoryList::MODULES); $this->_string = $string; + $this->moduleRegistry = $moduleRegistry; } /** @@ -45,7 +59,11 @@ public function __construct(Filesystem $filesystem, \Magento\Framework\Stdlib\St */ public function getDir($moduleName, $type = '') { - $path = $this->_string->upperCaseWords($moduleName, '_', '/'); + if (null === $path = $this->moduleRegistry->getModulePath($moduleName)) { + $relativePath = $this->_string->upperCaseWords($moduleName, '_', '/'); + $path = $this->_modulesDirectory->getAbsolutePath($relativePath); + } + if ($type) { if (!in_array($type, ['etc', 'i18n', 'view', 'Controller'])) { throw new \InvalidArgumentException("Directory type '{$type}' is not recognized."); @@ -53,8 +71,6 @@ public function getDir($moduleName, $type = '') $path .= '/' . $type; } - $result = $this->_modulesDirectory->getAbsolutePath($path); - - return $result; + return $path; } } diff --git a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php index 08660d98bb6eb..8708d86ba3006 100644 --- a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php +++ b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php @@ -10,6 +10,8 @@ use Magento\Framework\Filesystem; use Magento\Framework\Module\Declaration\Converter\Dom; use Magento\Framework\Xml\Parser; +use Magento\Framework\Module\ModuleRegistryInterface; +use Magento\Framework\Filesystem\DriverInterface; /** * Loader of module list information from the filesystem @@ -37,19 +39,42 @@ class Loader */ private $parser; + /** + * Module registry + * + * @var ModuleRegistryInterface + */ + private $moduleRegistry; + + /** + * Filesystem driver to allow reading of module.xml files which live outside of app/code + * + * @var DriverInterface + */ + private $filesystemDriver; + /** * Constructor * * @param Filesystem $filesystem * @param Dom $converter * @param Parser $parser + * @param ModuleRegistryInterface $moduleRegistry + * @param DriverInterface $filesystemDriver */ - public function __construct(Filesystem $filesystem, Dom $converter, Parser $parser) - { + public function __construct( + Filesystem $filesystem, + Dom $converter, + Parser $parser, + ModuleRegistryInterface $moduleRegistry, + DriverInterface $filesystemDriver + ) { $this->filesystem = $filesystem; $this->converter = $converter; $this->parser = $parser; $this->parser->initErrorHandler(); + $this->moduleRegistry = $moduleRegistry; + $this->filesystemDriver = $filesystemDriver; } /** @@ -61,10 +86,7 @@ public function __construct(Filesystem $filesystem, Dom $converter, Parser $pars public function load() { $result = []; - $dir = $this->filesystem->getDirectoryRead(DirectoryList::MODULES); - foreach ($dir->search('*/*/etc/module.xml') as $file) { - $contents = $dir->readFile($file); - + foreach ($this->getModuleConfigs() as $contents) { try { $this->parser->loadXML($contents); } catch (\Magento\Framework\Exception\LocalizedException $e) { @@ -84,6 +106,26 @@ public function load() return $this->sortBySequence($result); } + /** + * Returns a traversable yielding content of all module.xml files + * + * @return \Traversable + * + * @author Josh Di Fabio + */ + private function getModuleConfigs() + { + $modulesDir = $this->filesystem->getDirectoryRead(DirectoryList::MODULES); + foreach ($modulesDir->search('*/*/etc/module.xml') as $filePath) { + yield $modulesDir->readFile($filePath); + } + + foreach ($this->moduleRegistry->getModulePaths() as $modulePath) { + $filePath = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, "$modulePath/etc/module.xml"); + yield $this->filesystemDriver->fileGetContents($filePath); + } + } + /** * Sort the list of modules using "sequence" key in meta-information * diff --git a/lib/internal/Magento/Framework/Module/ModuleRegistryInterface.php b/lib/internal/Magento/Framework/Module/ModuleRegistryInterface.php new file mode 100644 index 0000000000000..e589057bf0a12 --- /dev/null +++ b/lib/internal/Magento/Framework/Module/ModuleRegistryInterface.php @@ -0,0 +1,27 @@ + + */ +interface ModuleRegistryInterface +{ + /** + * Get list of Magento module paths + * + * Returns an array where key is fully-qualified module name and value is absolute path to module + * + * @return array + */ + public function getModulePaths(); + + /** + * @param string $moduleName + * @return null|string + */ + public function getModulePath($moduleName); +} diff --git a/lib/internal/Magento/Framework/Module/Registrar.php b/lib/internal/Magento/Framework/Module/Registrar.php new file mode 100644 index 0000000000000..4494f3fc030d0 --- /dev/null +++ b/lib/internal/Magento/Framework/Module/Registrar.php @@ -0,0 +1,48 @@ + + */ +class Registrar implements ModuleRegistryInterface +{ + /** + * Paths to modules + * + * @var string[] + */ + private static $modulePaths = []; + + /** + * Sets the location of a module. Necessary for modules which do not reside in modules directory + * + * @param string $moduleName Fully-qualified module name + * @param string $path Absolute file path to the module + */ + public static function registerModule($moduleName, $path) + { + self::$modulePaths[$moduleName] = $path; + } + + /** + * {@inheritdoc} + */ + public function getModulePaths() + { + return self::$modulePaths; + } + + /** + * {@inheritdoc} + */ + public function getModulePath($moduleName) + { + return isset(self::$modulePaths[$moduleName]) ? self::$modulePaths[$moduleName] : null; + } +} diff --git a/lib/internal/Magento/Framework/Module/Test/Unit/DirTest.php b/lib/internal/Magento/Framework/Module/Test/Unit/DirTest.php index 8b25ec2523c07..616aae5a11007 100644 --- a/lib/internal/Magento/Framework/Module/Test/Unit/DirTest.php +++ b/lib/internal/Magento/Framework/Module/Test/Unit/DirTest.php @@ -27,6 +27,11 @@ class DirTest extends \PHPUnit_Framework_TestCase */ protected $directoryMock; + /** + * @var \Magento\Framework\Module\ModuleRegistryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $moduleRegistryMock; + protected function setUp() { $this->filesystemMock = $this->getMock('Magento\Framework\Filesystem', [], [], '', false, false); @@ -39,8 +44,14 @@ protected function setUp() false ); $this->_stringMock = $this->getMock('Magento\Framework\Stdlib\String', [], [], '', false, false); - - $this->_stringMock->expects($this->once())->method('upperCaseWords')->will($this->returnValue('Test/Module')); + $this->moduleRegistryMock = $this->getMock( + 'Magento\Framework\Module\ModuleRegistryInterface', + [], + [], + '', + false, + false + ); $this->filesystemMock->expects( $this->once() @@ -50,11 +61,27 @@ protected function setUp() $this->returnValue($this->directoryMock) ); - $this->_model = new \Magento\Framework\Module\Dir($this->filesystemMock, $this->_stringMock); + $this->_model = new \Magento\Framework\Module\Dir( + $this->filesystemMock, + $this->_stringMock, + $this->moduleRegistryMock + ); } public function testGetDirModuleRoot() { + $this->moduleRegistryMock->expects( + $this->once() + )->method( + 'getModulePath' + )->with( + 'Test_Module' + )->will( + $this->returnValue(null) + ); + + $this->_stringMock->expects($this->once())->method('upperCaseWords')->will($this->returnValue('Test/Module')); + $this->directoryMock->expects( $this->once() )->method( @@ -64,20 +91,39 @@ public function testGetDirModuleRoot() )->will( $this->returnValue('/Test/Module') ); + $this->assertEquals('/Test/Module', $this->_model->getDir('Test_Module')); } + public function testGetDirModuleRootFromResolver() + { + $this->moduleRegistryMock->expects( + $this->once() + )->method( + 'getModulePath' + )->with( + 'Test_Module2' + )->will( + $this->returnValue('/path/to/module') + ); + + $this->assertEquals('/path/to/module', $this->_model->getDir('Test_Module2')); + } + public function testGetDirModuleSubDir() { + $this->_stringMock->expects($this->once())->method('upperCaseWords')->will($this->returnValue('Test/Module')); + $this->directoryMock->expects( $this->once() )->method( 'getAbsolutePath' )->with( - 'Test/Module/etc' + 'Test/Module' )->will( - $this->returnValue('/Test/Module/etc') + $this->returnValue('/Test/Module') ); + $this->assertEquals('/Test/Module/etc', $this->_model->getDir('Test_Module', 'etc')); } @@ -87,6 +133,8 @@ public function testGetDirModuleSubDir() */ public function testGetDirModuleSubDirUnknown() { + $this->_stringMock->expects($this->once())->method('upperCaseWords')->will($this->returnValue('Test/Module')); + $this->_model->getDir('Test_Module', 'unknown'); } } diff --git a/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php b/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php index 5185851d78bb5..3e232f7b97cfe 100644 --- a/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php +++ b/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php @@ -40,6 +40,21 @@ class LoaderTest extends \PHPUnit_Framework_TestCase */ private $parser; + /* + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $registry; + + /* + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $driver; + + /** + * @var \Magento\Framework\Module\ModuleList\Loader + */ + private $loader; + protected function setUp() { $this->filesystem = $this->getMock('Magento\Framework\Filesystem', [], [], '', false); @@ -50,34 +65,44 @@ protected function setUp() ->willReturn($this->dir); $this->converter = $this->getMock('Magento\Framework\Module\Declaration\Converter\Dom', [], [], '', false); $this->parser = $this->getMock('Magento\Framework\Xml\Parser', [], [], '', false); + $this->parser->expects($this->once())->method('initErrorHandler'); + $this->registry = $this->getMock('Magento\Framework\Module\ModuleRegistryInterface', [], [], '', false, false); + $this->driver = $this->getMock('Magento\Framework\Filesystem\DriverInterface', [], [], '', false, false); + $this->loader = new Loader($this->filesystem, $this->converter, $this->parser, $this->registry, $this->driver); } public function testLoad() { - $fixture = [ + $fixtures = [ 'a' => ['name' => 'a', 'sequence' => []], // a is on its own - 'b' => ['name' => 'b', 'sequence' => ['c']], // b is after c - 'c' => ['name' => 'c', 'sequence' => ['a']], // c is after a - // so expected sequence is a -> c -> b + 'b' => ['name' => 'b', 'sequence' => ['d']], // b is after c + 'c' => ['name' => 'c', 'sequence' => ['e']], // c is after e + 'd' => ['name' => 'd', 'sequence' => ['c']], // d is after c + 'e' => ['name' => 'e', 'sequence' => ['a']], // e is after a + // so expected sequence is a -> e -> c -> d -> b ]; $this->dir->expects($this->once())->method('search')->willReturn(['a', 'b', 'c']); + $this->registry->expects($this->once())->method('getModulePaths')->willReturn(['/path/to/d', '/path/to/e']); $this->dir->expects($this->exactly(3))->method('readFile')->will($this->returnValueMap([ ['a', null, null, self::$sampleXml], ['b', null, null, self::$sampleXml], ['c', null, null, self::$sampleXml], ])); - $this->converter->expects($this->at(0))->method('convert')->willReturn(['a' => $fixture['a']]); - $this->converter->expects($this->at(1))->method('convert')->willReturn(['b' => $fixture['b']]); - $this->converter->expects($this->at(2))->method('convert')->willReturn(['c' => $fixture['c']]); - $this->parser->expects($this->once())->method('initErrorHandler'); + $this->driver->expects($this->exactly(2))->method('fileGetContents')->will($this->returnValueMap([ + ['/path/to/d', null, null, self::$sampleXml], + ['/path/to/e', null, null, self::$sampleXml], + ])); + $index = 0; + foreach ($fixtures as $name => $fixture) { + $this->converter->expects($this->at($index++))->method('convert')->willReturn([$name => $fixture]); + } $this->parser->expects($this->atLeastOnce())->method('loadXML'); $this->parser->expects($this->atLeastOnce())->method('getDom'); - $object = new Loader($this->filesystem, $this->converter, $this->parser); - $result = $object->load(); - $this->assertSame(['a', 'c', 'b'], array_keys($result)); - $this->assertSame($fixture['a'], $result['a']); - $this->assertSame($fixture['b'], $result['b']); - $this->assertSame($fixture['c'], $result['c']); + $result = $this->loader->load(); + $this->assertSame(['a', 'e', 'c', 'd', 'b'], array_keys($result)); + foreach ($fixtures as $name => $fixture) { + $this->assertSame($fixture, $result[$name]); + } } /** @@ -97,7 +122,7 @@ public function testLoadCircular() ])); $this->converter->expects($this->at(0))->method('convert')->willReturn(['a' => $fixture['a']]); $this->converter->expects($this->at(1))->method('convert')->willReturn(['b' => $fixture['b']]); - $object = new Loader($this->filesystem, $this->converter, $this->parser); - $object->load(); + $this->registry->expects($this->once())->method('getModulePaths')->willReturn([]); + $this->loader->load(); } } diff --git a/setup/config/di.config.php b/setup/config/di.config.php index 8d0f258ba8287..30e6af237e912 100644 --- a/setup/config/di.config.php +++ b/setup/config/di.config.php @@ -28,6 +28,8 @@ 'Zend\ServiceManager\ServiceLocatorInterface' => 'ServiceManager', 'Magento\Framework\DB\LoggerInterface' => 'Magento\Framework\DB\Logger\Null', 'Magento\Framework\Locale\ConfigInterface' => 'Magento\Framework\Locale\Config', + 'Magento\Framework\Module\ModuleRegistryInterface' => 'Magento\Framework\Module\Registrar', + 'Magento\Framework\Filesystem\DriverInterface' => 'Magento\Framework\Filesystem\Driver\File', ], ], ],