diff --git a/src/Classes/ModuleManager/AutoloadEntry.php b/src/Classes/ModuleManager/AutoloadEntry.php new file mode 100644 index 0000000..0e2cc5c --- /dev/null +++ b/src/Classes/ModuleManager/AutoloadEntry.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace RobinTheHood\ModifiedModuleLoaderClient\ModuleManager; + +use RobinTheHood\ModifiedModuleLoaderClient\Module; + +class AutoloadEntry +{ + public const TYPE_PSR_4 = 1; + + /** + * @var int $type + */ + public $type; + + /** + * @var ?Module $module + */ + public $module; + + /** + * @var string $namespace + */ + public $namespace; + + /** + * @var string $path + */ + public $path; + + /** + * @var string $realPath + */ + public $realPath; + + /** + * AutoloadEntry constructor. + * + * @param int $type + * @param ?Module $module + * @param string $namespace + * @param string $path + * @param string $realPath + */ + public function __construct(int $type, ?Module $module, string $namespace, string $path, string $realPath) + { + //TODO: Kontrolloeren ob es sich bei $namespace, path und $realPath um valide Werte handelt. + $this->type = $type; + $this->module = $module; + $this->namespace = $namespace; + $this->path = $path; + $this->realPath = $realPath; + } + + public static function createFromModule( + Module $module, + string $namespace, + string $path, + int $type = self::TYPE_PSR_4 + ): AutoloadEntry { + $realPath = str_replace( + $module->getSourceMmlcDir(), + 'vendor-mmlc/' . $module->getArchiveName(), + $path + ); + return new AutoloadEntry($type, $module, $namespace, $path, $realPath); + } +} diff --git a/src/Classes/ModuleManager/AutoloadEntryCollection.php b/src/Classes/ModuleManager/AutoloadEntryCollection.php new file mode 100644 index 0000000..e10e7d0 --- /dev/null +++ b/src/Classes/ModuleManager/AutoloadEntryCollection.php @@ -0,0 +1,202 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace RobinTheHood\ModifiedModuleLoaderClient\ModuleManager; + +use ArrayIterator; +use Exception; +use IteratorAggregate; +use RobinTheHood\ModifiedModuleLoaderClient\Module; + +class AutoloadEntryCollection implements IteratorAggregate +{ + /** + * @var AutoloadEntry[] $autoloadEntries + */ + private $autoloadEntries = []; + + public static function createFromModule(Module $module): AutoloadEntryCollection + { + $collection = new AutoloadEntryCollection(); + + $autoload = $module->getAutoload(); + + if (!$autoload) { + return $collection; + } + + $psr4Autoload = $autoload['psr-4'] ?? []; + if (!$psr4Autoload) { + return $collection; + } + + foreach ($psr4Autoload as $namespace => $path) { + $autoloadEntry = AutoloadEntry::createFromModule($module, $namespace, $path); + $collection->add($autoloadEntry); + } + + return $collection; + } + + /** + * @param Module[] $modules + */ + public static function createFromModules(array $modules): AutoloadEntryCollection + { + $resultCollection = new AutoloadEntryCollection(); + foreach ($modules as $module) { + $collection = AutoloadEntryCollection::createFromModule($module); + $resultCollection = $resultCollection->mergeWith($collection); + } + return $resultCollection; + } + + /** + * Get all AutoloadEntry objects in the collection. + * + * @return AutoloadEntry[] + */ + public function getAutoloadEntries(): array + { + return $this->autoloadEntries; + } + + public function add(AutoloadEntry $autoloadEntry): void + { + $this->validateNamespace($autoloadEntry); + $this->validateRealPath($autoloadEntry); + + $this->autoloadEntries[] = $autoloadEntry; + } + + public function mergeWith(AutoloadEntryCollection $otherCollection): AutoloadEntryCollection + { + $mergedCollection = new AutoloadEntryCollection(); + $mergedCollection->autoloadEntries = array_merge($this->autoloadEntries, $otherCollection->autoloadEntries); + return $mergedCollection; + } + + /** + * Get an AutoloadEntry by namespace. + * + * @param string $namespace + * @return AutoloadEntry|null + */ + public function getEntryByNamespace(string $namespace): ?AutoloadEntry + { + foreach ($this->autoloadEntries as $autoloadEntry) { + if ($autoloadEntry->namespace === $namespace) { + return $autoloadEntry; + } + } + + return null; + } + + /** + * Get an AutoloadEntry by path. + * + * @param string $path + * @return AutoloadEntry|null + */ + public function getEntryByRealPath(string $realPath): ?AutoloadEntry + { + foreach ($this->autoloadEntries as $autoloadEntry) { + if ($autoloadEntry->realPath === $realPath) { + return $autoloadEntry; + } + } + + return null; + } + + /** + * Get a new AutoloadEntryCollection with unique namespaces. + * + * @return AutoloadEntryCollection + */ + public function unique(): AutoloadEntryCollection + { + $uniqueCollection = new AutoloadEntryCollection(); + + foreach ($this->autoloadEntries as $autoloadEntry) { + $existingEntry = $uniqueCollection->getEntryByNamespace($autoloadEntry->namespace); + + if ($existingEntry) { + continue; + } + + $uniqueCollection->add($autoloadEntry); + } + + return $uniqueCollection; + } + + /** + * Get the iterator for iterating over AutoloadEntry objects. + * + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->autoloadEntries); + } + + private function validateNamespace(AutoloadEntry $autoloadEntry): void + { + $foundAutoloadEntry = $this->getEntryByNamespace($autoloadEntry->namespace); + + if (!$foundAutoloadEntry) { + return; + } + + if ($foundAutoloadEntry->realPath === $autoloadEntry->realPath) { + return; + } + + $archiveName = 'unknown module'; + if ($autoloadEntry->module) { + $archiveName = $autoloadEntry->module->getArchiveName() . ':' . $autoloadEntry->module->getVersion(); + } + + throw new Exception( + "Can not add $archiveName because" + . " Namespace {$autoloadEntry->namespace} with path {$autoloadEntry->realPath} already exists with" + . " different path {$foundAutoloadEntry->realPath}" + ); + } + + private function validateRealPath(AutoloadEntry $autoloadEntry): void + { + $foundAutoloadEntry = $this->getEntryByRealPath($autoloadEntry->realPath); + + if (!$foundAutoloadEntry) { + return; + } + + if ($foundAutoloadEntry->namespace === $autoloadEntry->namespace) { + return; + } + + $archiveName = 'unknown module'; + if ($autoloadEntry->module) { + $archiveName = $autoloadEntry->module->getArchiveName() . ':' . $autoloadEntry->module->getVersion() ; + } + + throw new Exception( + "Can not add $archiveName because" + . " path {$autoloadEntry->realPath} with namespace {$autoloadEntry->namespace} already exists with" + . " different namespace {$foundAutoloadEntry->namespace}" + ); + } +} diff --git a/src/Classes/ModuleManager/AutoloadFileCreator.php b/src/Classes/ModuleManager/AutoloadFileCreator.php index c0ab195..a29844d 100644 --- a/src/Classes/ModuleManager/AutoloadFileCreator.php +++ b/src/Classes/ModuleManager/AutoloadFileCreator.php @@ -15,10 +15,10 @@ use RobinTheHood\ModifiedModuleLoaderClient\App; use RobinTheHood\ModifiedModuleLoaderClient\Loader\LocalModuleLoader; +use RobinTheHood\ModifiedModuleLoaderClient\Module; class AutoloadFileCreator { - // TODO: In createAutoloadFile() Exceptions werfen im Fehlerfall public function createAutoloadFile(): void { $installedLocalModules = $this->getInstalledModules(); @@ -26,7 +26,10 @@ public function createAutoloadFile(): void $this->writeAutoloadFile($autoloadFileContent); } - private function getInstalledModules() + /** + * @return Module[] + */ + private function getInstalledModules(): array { $localModuleLoader = LocalModuleLoader::createFromConfig(); $localModuleLoader->resetCache(); @@ -34,37 +37,30 @@ private function getInstalledModules() return $installedModules; } - private function buildAutoloadFile(array $installedModules): string + private function convertAutoloadEntryToPsr4AutoloadPhp(AutoloadEntry $autoloadEntry): string { - $namespaceEntrys = []; - foreach ($installedModules as $installedModule) { - $autoload = $installedModule->getAutoload(); - - if (!$autoload) { - continue; - } - - if (!$autoload['psr-4']) { - continue; - } + $namespace = $autoloadEntry->namespace; + $realPath = $autoloadEntry->realPath; + $php = '$loader->setPsr4(\'' . $namespace . '\\\', DIR_FS_DOCUMENT_ROOT . \'' . $realPath . '\');'; + return $php; + } - foreach ($autoload['psr-4'] as $namespace => $path) { - $path = str_replace( - $installedModule->getSourceMmlcDir(), - 'vendor-mmlc/' . $installedModule->getArchiveName(), - $path - ); + /** + * @param Module[] $installedModules + */ + private function buildAutoloadFile(array $installedModules): string + { + $autoloadEntryCollection = AutoloadEntryCollection::createFromModules($installedModules); + $autoloadEntryCollection = $autoloadEntryCollection->unique(); - $namespaceEntrys[] = - '$loader->setPsr4(\'' . $namespace . '\\\', DIR_FS_DOCUMENT_ROOT . \'' . $path . '\');'; - } + $phpEntries = []; + foreach ($autoloadEntryCollection as $autoloadEntry) { + $phpEntries[] = $this->convertAutoloadEntryToPsr4AutoloadPhp($autoloadEntry); } - - $namespaceEntrys = array_unique($namespaceEntrys); - $namespaceMapping = implode("\n", $namespaceEntrys); + $phpCodeNamespaceMappings = implode("\n", $phpEntries); $template = file_get_contents(App::getTemplatesRoot() . '/autoload.php.tmpl'); - $autoloadFileContent = str_replace('{VENDOR_PSR4_NAMESPACE_MAPPINGS}', $namespaceMapping, $template); + $autoloadFileContent = str_replace('{VENDOR_PSR4_NAMESPACE_MAPPINGS}', $phpCodeNamespaceMappings, $template); return $autoloadFileContent; } diff --git a/src/Classes/ModuleManager/ModuleManager.php b/src/Classes/ModuleManager/ModuleManager.php index 44a24f0..4bcd842 100644 --- a/src/Classes/ModuleManager/ModuleManager.php +++ b/src/Classes/ModuleManager/ModuleManager.php @@ -13,6 +13,7 @@ namespace RobinTheHood\ModifiedModuleLoaderClient\ModuleManager; +use Exception; use RobinTheHood\ModifiedModuleLoaderClient\Config; use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\CombinationSatisfyerResult; use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\DependencyBuilder; @@ -232,8 +233,11 @@ public function install(string $archiveName, $versionConstraint): ModuleManagerR $this->info( ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_INFO_UPDATE_AUTOLOAD_START) ); - $autoloadFileCreator = new AutoloadFileCreator(); - $autoloadFileCreator->createAutoloadFile(); + + $moduleManagerResult = $this->createAutoloadFile(); + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { + return $moduleManagerResult; + } return ModuleManagerResult::success() ->setModule($module); @@ -314,8 +318,11 @@ public function installWithoutDependencies( $this->info( ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_INFO_UPDATE_AUTOLOAD_START) ); - $autoloadFileCreator = new AutoloadFileCreator(); - $autoloadFileCreator->createAutoloadFile(); + + $moduleManagerResult = $this->createAutoloadFile(); + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { + return $moduleManagerResult; + } return ModuleManagerResult::success() ->setModule($module); @@ -401,8 +408,10 @@ public function update(string $archiveName): ModuleManagerResult ModuleManagerMessage::create(ModuleManagerMessage::UPDATE_INFO_UPDATE_AUTOLOAD_START) ); - $autoloadFileCreator = new AutoloadFileCreator(); - $autoloadFileCreator->createAutoloadFile(); + $moduleManagerResult = $this->createAutoloadFile(); + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { + return $moduleManagerResult; + } return ModuleManagerResult::success() ->setModule($newModule); @@ -444,7 +453,8 @@ public function updateWithoutDependencies( if ($skipDependencyCheck === false) { $systemSet = $this->systemSetFactory->getSystemSet(); $versionConstraint = '>' . $module->getVersion(); - $combinationSatisfyerResult = $this->dependencyBuilder->satisfies($archiveName, $versionConstraint, $systemSet); + $combinationSatisfyerResult = + $this->dependencyBuilder->satisfies($archiveName, $versionConstraint, $systemSet); if ( $combinationSatisfyerResult->result === CombinationSatisfyerResult::RESULT_COMBINATION_NOT_FOUND || !$combinationSatisfyerResult->foundCombination @@ -494,8 +504,10 @@ public function updateWithoutDependencies( ModuleManagerMessage::create(ModuleManagerMessage::UPDATE_INFO_UPDATE_AUTOLOAD_START) ); - $autoloadFileCreator = new AutoloadFileCreator(); - $autoloadFileCreator->createAutoloadFile(); + $moduleManagerResult = $this->createAutoloadFile(); + if ($moduleManagerResult->getType() == ModuleManagerResult::TYPE_ERROR) { + return $moduleManagerResult; + } return ModuleManagerResult::success() ->setModule($newModule); @@ -584,10 +596,28 @@ public function uninstall(string $archiveName, bool $force = false): ModuleManag $this->info( ModuleManagerMessage::create(ModuleManagerMessage::UNINSTALL_INFO_UPDATE_AUTOLOAD_START) ); - $autoloadFileCreator = new AutoloadFileCreator(); - $autoloadFileCreator->createAutoloadFile(); + + $moduleManagerResult = $this->createAutoloadFile(); + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { + return $moduleManagerResult; + } return ModuleManagerResult::success() ->setModule($module); } + + public function createAutoloadFile(): ModuleManagerResult + { + try { + $autoloadFileCreator = new AutoloadFileCreator(); + $autoloadFileCreator->createAutoloadFile(); + } catch (Exception $e) { + return $this->error( + ModuleManagerMessage::create(ModuleManagerMessage::AUTOLOAD_ERROR_CAN_NOT_CREATE_AUTOLOAD_FILE) + ->setMessage($e->getMessage()) + ); + } + + return ModuleManagerResult::success(); + } } diff --git a/src/Classes/ModuleManager/ModuleManagerMessage.php b/src/Classes/ModuleManager/ModuleManagerMessage.php index fd718a2..0547865 100644 --- a/src/Classes/ModuleManager/ModuleManagerMessage.php +++ b/src/Classes/ModuleManager/ModuleManagerMessage.php @@ -53,6 +53,8 @@ class ModuleManagerMessage public const UNINSTALL_ERROR_MODULE_IS_CHANGED = 653; public const UNINSTALL_ERROR_MODULE_IS_USED_BY = 654; + public const AUTOLOAD_ERROR_CAN_NOT_CREATE_AUTOLOAD_FILE = 701; + /** @var int */ private $code = 0; @@ -126,6 +128,11 @@ public function getCode(): int return $this->code; } + public function getMessage(): string + { + return $this->message; + } + private function getModulName(): string { if ($this->module) { @@ -225,6 +232,12 @@ public function __toString() $this->getModulName(), $this->getUsedBy() ); + } elseif ($this->code === self::AUTOLOAD_ERROR_CAN_NOT_CREATE_AUTOLOAD_FILE) { + return sprintf( + "Can not create autoload file. %s." + . "You can create the autoload file by installing or uninstalling any module.", + $this->getMessage() + ); } return "Unknown message"; diff --git a/tests/unit/ModuleManagerTests/AutoloadEntryCollectionTest.php b/tests/unit/ModuleManagerTests/AutoloadEntryCollectionTest.php new file mode 100644 index 0000000..c3cec92 --- /dev/null +++ b/tests/unit/ModuleManagerTests/AutoloadEntryCollectionTest.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace RobinTheHood\ModifiedModuleLoaderClient\Tests\Unit\ModuleManagerTests; + +use Exception; +use PHPUnit\Framework\TestCase; +use RobinTheHood\ModifiedModuleLoaderClient\ModuleManager\AutoloadEntry; +use RobinTheHood\ModifiedModuleLoaderClient\ModuleManager\AutoloadEntryCollection; + +class AutoloadEntryCollectionTest extends TestCase +{ + public function testAddAutoloadEntry(): void + { + $collection = new AutoloadEntryCollection(); + $autoloadEntry = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace', 'src-mmlc', '/path/to/f1'); + + $collection->add($autoloadEntry); + + $this->assertEquals([$autoloadEntry], $collection->getAutoloadEntries()); + } + + public function testAddDuplicateNamespace(): void + { + $this->expectException(Exception::class); + + $collection = new AutoloadEntryCollection(); + $autoloadEntry1 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace', 'src-mmlc', '/path/to/f1'); + $autoloadEntry2 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace', 'src-mmlc', '/path/to/f2'); + + $collection->add($autoloadEntry1); + $collection->add($autoloadEntry2); + } + + public function testAddDuplicatePath(): void + { + $this->expectException(Exception::class); + + $collection = new AutoloadEntryCollection(); + $autoloadEntry1 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace1', 'src-mmlc', '/path/to/f1'); + $autoloadEntry2 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace2', 'src-mmlc', '/path/to/f1'); + + $collection->add($autoloadEntry1); + $collection->add($autoloadEntry2); + } + + public function testUniqueCollection(): void + { + $collection = new AutoloadEntryCollection(); + $autoloadEntry1 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace1', 'src-mmlc', '/path/to/f1'); + $autoloadEntry2 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace2', 'src-mmlc', '/path/to/f2'); + $autoloadEntry3 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace2', 'src-mmlc', '/path/to/f2'); + + $collection->add($autoloadEntry1); + $collection->add($autoloadEntry2); + $collection->add($autoloadEntry3); + + $uniqueCollection = $collection->unique(); + + $this->assertEquals([$autoloadEntry1, $autoloadEntry2], $uniqueCollection->getAutoloadEntries()); + } + + public function testGetEntryByNamespace(): void + { + $collection = new AutoloadEntryCollection(); + $autoloadEntry1 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace1', 'src-mmlc', '/path/to/f1'); + $autoloadEntry2 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace2', 'src-mmlc', '/path/to/f2'); + + $collection->add($autoloadEntry1); + $collection->add($autoloadEntry2); + + $resultEntry = $collection->getEntryByNamespace('Namespace1'); + + $this->assertEquals($autoloadEntry1, $resultEntry); + } + + public function testGetEntryByNamespaceNotFound(): void + { + $collection = new AutoloadEntryCollection(); + $autoloadEntry = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace1', 'src-mmlc', '/path/to/f1'); + + $collection->add($autoloadEntry); + + $resultEntry = $collection->getEntryByNamespace('NonExistentNamespace'); + + $this->assertNull($resultEntry); + } + + public function testGetEntryByRealPath(): void + { + $collection = new AutoloadEntryCollection(); + $autoloadEntry1 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace1', 'src-mmlc', '/path/to/f1'); + $autoloadEntry2 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace2', 'src-mmlc', '/path/to/f2'); + + $collection->add($autoloadEntry1); + $collection->add($autoloadEntry2); + + $resultEntry = $collection->getEntryByRealPath('/path/to/f1'); + + $this->assertEquals($autoloadEntry1, $resultEntry); + } + + public function testGetEntryByRealPathNotFound(): void + { + $collection = new AutoloadEntryCollection(); + $autoloadEntry = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace1', 'src-mmlc', '/path/to/f1'); + + $collection->add($autoloadEntry); + + $resultEntry = $collection->getEntryByRealPath('/nonexistent/path'); + + $this->assertNull($resultEntry); + } + + public function testMergeWith(): void + { + $collection1 = new AutoloadEntryCollection(); + $autoloadEntry1 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace1', 'src-mmlc', '/path/to/f1'); + $autoloadEntry2 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace2', 'src-mmlc', '/path/to/f2'); + $collection1->add($autoloadEntry1); + $collection1->add($autoloadEntry2); + + $collection2 = new AutoloadEntryCollection(); + $autoloadEntry3 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace3', 'src-mmlc', '/path/to/f3'); + $autoloadEntry4 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace4', 'src-mmlc', '/path/to/f4'); + $collection2->add($autoloadEntry3); + $collection2->add($autoloadEntry4); + + $mergedCollection = $collection1->mergeWith($collection2); + + $expectedEntries = [$autoloadEntry1, $autoloadEntry2, $autoloadEntry3, $autoloadEntry4]; + $this->assertEquals($expectedEntries, $mergedCollection->getAutoloadEntries()); + } + + public function testMergeWithEmptyCollection(): void + { + $collection1 = new AutoloadEntryCollection(); + $autoloadEntry1 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace1', 'src-mmlc', '/path/to/f1'); + $autoloadEntry2 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace2', 'src-mmlc', '/path/to/f2'); + $collection1->add($autoloadEntry1); + $collection1->add($autoloadEntry2); + + $collection2 = new AutoloadEntryCollection(); + + $mergedCollection = $collection1->mergeWith($collection2); + + $this->assertEquals($collection1->getAutoloadEntries(), $mergedCollection->getAutoloadEntries()); + } + + public function testIterator(): void + { + $collection = new AutoloadEntryCollection(); + $autoloadEntry1 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace1', 'src-mmlc', '/path/to/f1'); + $autoloadEntry2 = new AutoloadEntry(AutoloadEntry::TYPE_PSR_4, null, 'Namespace2', 'src-mmlc', '/path/to/f2'); + $collection->add($autoloadEntry1); + $collection->add($autoloadEntry2); + + $resultEntries = []; + + foreach ($collection as $autoloadEntry) { + $resultEntries[] = $autoloadEntry; + } + + $this->assertEquals([$autoloadEntry1, $autoloadEntry2], $resultEntries); + } +}