From 0a1aaac5dee9d66f5c7b4b2f25074192a5586564 Mon Sep 17 00:00:00 2001 From: Robin Wieschendorf Date: Thu, 21 Dec 2023 15:38:24 +0100 Subject: [PATCH 01/17] feat: add method loadInstalledVersionByArchiveName() to LocalModuleLoader --- src/Classes/Loader/LocalModuleLoader.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Classes/Loader/LocalModuleLoader.php b/src/Classes/Loader/LocalModuleLoader.php index 5b748ee7..e5027740 100644 --- a/src/Classes/Loader/LocalModuleLoader.php +++ b/src/Classes/Loader/LocalModuleLoader.php @@ -140,6 +140,19 @@ public function loadAllInstalledVersions(): array return $installedModules; } + /** + * Loads the installed module version by a given archiveName + * + * @return Module|null Returns a array of installed module versions. + */ + public function loadInstalledVersionByArchiveName(string $arhciveName): ?Module + { + $modules = $this->loadAllVersions(); + $installedModules = $this->moduleFilter->filterInstalled($modules); + $installedModules = $this->moduleFilter->filterByArchiveName($installedModules, $arhciveName); + return $installedModules[0] ?? null; + } + public function getVendorDirs() { return FileHelper::scanDir($this->modulesRootPath, FileHelper::DIRS_ONLY); From 388e2d00946cbf7e279dad84d82657346ee87c8d Mon Sep 17 00:00:00 2001 From: Robin Wieschendorf Date: Thu, 21 Dec 2023 15:39:01 +0100 Subject: [PATCH 02/17] refactor: remove unused code --- src/Classes/ModuleChangeManager.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Classes/ModuleChangeManager.php b/src/Classes/ModuleChangeManager.php index 9bd14f92..1876ae3c 100644 --- a/src/Classes/ModuleChangeManager.php +++ b/src/Classes/ModuleChangeManager.php @@ -30,10 +30,6 @@ class ModuleChangeManager */ public static function getChangedFiles(Module $module): ChangedEntryCollection { - if ($module->getArchiveName() === 'robinthehood/modified-std-module') { - $a = 1; - } - $hashFileLoader = new HashFileLoader(); $hashFileLoader->setDefaultScope(ModuleHasher::SCOPE_SHOP_ROOT); $hashFile = $hashFileLoader->load($module->getHashPath()); @@ -50,12 +46,6 @@ public static function getChangedFiles(Module $module): ChangedEntryCollection $srcMmlcChangedEntiresCollection ]); - // if ($module->getArchiveName() === 'robinthehood/modified-std-module') { - // echo '
';
-        //     print_r($hashFile);
-        //     print_r($changedEntiresCollection);
-        //     die();
-        // }
         return $changedEntiresCollection;
     }
 

From ef1e428f5a744a03a9d42c87e7ff6ad873412320 Mon Sep 17 00:00:00 2001
From: Robin Wieschendorf 
Date: Thu, 21 Dec 2023 15:48:31 +0100
Subject: [PATCH 03/17] refactor: fix typo and add more docblocks

---
 .../CombinationSatisfyer.php                  |  16 +-
 .../DependencyManager/DependencyBuilder.php   | 193 ++++++++----------
 2 files changed, 95 insertions(+), 114 deletions(-)

diff --git a/src/Classes/DependencyManager/CombinationSatisfyer.php b/src/Classes/DependencyManager/CombinationSatisfyer.php
index e7fb471f..54a06dc5 100644
--- a/src/Classes/DependencyManager/CombinationSatisfyer.php
+++ b/src/Classes/DependencyManager/CombinationSatisfyer.php
@@ -42,7 +42,7 @@ public function satisfiesCominationsFromModuleTrees(
 
         foreach ($combinations as $testCombination) {
             $foundCombination = new Combination();
-            $result = $this->satisfiesCominationFromModuleTrees(
+            $result = $this->satisfiesCombinationFromModuleTrees(
                 $moduleTrees,
                 $testCombination,
                 $foundCombination,
@@ -85,7 +85,7 @@ public function satisfiesCominationsFromModuleTree(
         $failLog = new FailLog();
 
         foreach ($combinations as $testCombination) {
-            $result = $this->satisfiesCominationFromModuleTree(
+            $result = $this->satisfiesCombinationFromModuleTree(
                 $moduleTree,
                 $testCombination,
                 $foundCombination,
@@ -113,7 +113,7 @@ public function satisfiesCominationsFromModuleTree(
         return $combinationSatisfyerResult;
     }
 
-    public function satisfiesCominationsFromModuleWithIterator(
+    public function satisfiesCombinationsFromModuleWithIterator(
         ModuleTree $moduleTree,
         CombinationIterator $combinationIterator
     ): CombinationSatisfyerResult {
@@ -121,7 +121,7 @@ public function satisfiesCominationsFromModuleWithIterator(
             $foundCombination = new Combination();
             $failLog = new FailLog();
             $testCombination = $combinationIterator->current();
-            $result = $this->satisfiesCominationFromModuleTree(
+            $result = $this->satisfiesCombinationFromModuleTree(
                 $moduleTree,
                 $testCombination,
                 $foundCombination,
@@ -160,7 +160,7 @@ public function satisfiesCominationsFromModuleWithIterator(
      *
      * @return bool
      */
-    public function satisfiesCominationFromModuleTree(
+    public function satisfiesCombinationFromModuleTree(
         ModuleTree $moduleTree,
         Combination $combination,
         Combination &$foundCombination,
@@ -198,7 +198,7 @@ public function satisfiesCominationFromModuleTree(
                     $moduleTree->versionConstraint
                 );
 
-                return $this->satisfiesCominationFromModuleTrees(
+                return $this->satisfiesCombinationFromModuleTrees(
                     $moduleVersion->require,
                     $combination,
                     $foundCombination,
@@ -222,7 +222,7 @@ public function satisfiesCominationFromModuleTree(
      *
      * @return bool
      */
-    public function satisfiesCominationFromModuleTrees(
+    public function satisfiesCombinationFromModuleTrees(
         array $moduleTrees,
         Combination $combination,
         Combination &$foundCombination,
@@ -232,7 +232,7 @@ public function satisfiesCominationFromModuleTrees(
         // Context: Expanded
         $moduleResult = true;
         foreach ($moduleTrees as $moduleTree) {
-            $result = $this->satisfiesCominationFromModuleTree(
+            $result = $this->satisfiesCombinationFromModuleTree(
                 $moduleTree,
                 $combination,
                 $foundCombination,
diff --git a/src/Classes/DependencyManager/DependencyBuilder.php b/src/Classes/DependencyManager/DependencyBuilder.php
index bdd2f01a..c408db37 100644
--- a/src/Classes/DependencyManager/DependencyBuilder.php
+++ b/src/Classes/DependencyManager/DependencyBuilder.php
@@ -15,7 +15,6 @@
 use RobinTheHood\ModifiedModuleLoaderClient\Config;
 use RobinTheHood\ModifiedModuleLoaderClient\Loader\ModuleLoader;
 use RobinTheHood\ModifiedModuleLoaderClient\Module;
-use RobinTheHood\ModifiedModuleLoaderClient\Semver\Comparator;
 use RobinTheHood\ModifiedModuleLoaderClient\Semver\Constraint;
 
 class DependencyBuilder
@@ -40,60 +39,104 @@ public function __construct(ModuleTreeBuilder $moduleTreeBuilder, ModuleLoader $
         $this->moduleLoader = $moduleLoader;
     }
 
-    private function logFile($value, $file)
+    /**
+     * Testet, ob alle Bedingungen für ein Modul erfüllt sind. Als Ergbnis gibt die Methode ein
+     * CombinationSatisfyerResult zurück. Als $constraint kann eine konkrete Version oder ein Constraint wie z. B.
+     * ^1.0.0 angegeben werden. Die Methode versucht eine Kombination mit den neusten Versionen zu finden. Wenn eine
+     * Kombination gefunden wurde, befindet sich diese in CombinationSatisfyerResult::foundCombination. Das Modul auf
+     * das mit $archiveName getestet wurde befindet sich ebenfalls in CombinationSatisfyerResult::foundCombination
+     *
+     * @param string $archiveName
+     * @param string $constraint
+     * @param SystemSet $systemSet Mit $systemSet kann festgelegt werden, welche Module bereits installiert sind,
+     *      welche PHP, MMLC und modified Version vorhanden ist.
+     *
+     * @return CombinationSatisfyerResult
+     */
+    public function satisfies(string $archiveName, string $constraint, SystemSet $systemSet): CombinationSatisfyerResult
     {
-        if (!Config::getLogging()) {
-            return;
+        $systemSet->remove($archiveName);
+        $constraint = $this->createConstraint($archiveName, $constraint, $systemSet);
+
+        $moduleTree = $this->moduleTreeBuilder->buildByConstraints($archiveName, $constraint);
+        $this->logFile($moduleTree, '3-moduleTrees.json');
+
+        $flatEntryBuilder = new FlatEntryBuilder();
+        $flatEntries = $flatEntryBuilder->buildListFromModuleTree($moduleTree);
+        $this->logFile($flatEntries, '3-flatEntries.json');
+
+        $flatEntries = $flatEntryBuilder->fitSystemSet($flatEntries, $systemSet);
+        $this->logFile($flatEntries, '3-flatEntries-fit.json');
+
+        $combinationIterator = new CombinationIterator($flatEntries);
+        $combinationSatisfyer = new CombinationSatisfyer();
+        $combinationSatisfyerResult = $combinationSatisfyer->satisfiesCombinationsFromModuleWithIterator(
+            $moduleTree,
+            $combinationIterator
+        );
+
+        return $combinationSatisfyerResult;
+    }
+
+    /**
+     * Gehe alle Module durch, die in $systemSet sind und gleichzeitig das Modul $archiveName benötigen.
+     * Gibt ein $constraint zurück, sodass die Anforderungenden der Module in $systemSet erhaltenbleiben.
+     */
+    private function createConstraint(string $archiveName, string $constraint, SystemSet $systemSet): string
+    {
+        /** @var string[] */
+        $requiredConstraints = [$constraint];
+
+        $archives = $systemSet->getArchives();
+        foreach ($archives as $archiveNameB => $version) {
+            $installedModule = $this->getModuleByArchiveNameAndVersion($archiveNameB, $version);
+            if (!$installedModule) {
+                continue;
+            }
+
+            $requiredConstraint = $this->getRequiredConstraint($installedModule, $archiveName);
+            if (!$requiredConstraint) {
+                continue;
+            }
+
+            $requiredConstraints[] = $requiredConstraint;
         }
 
-        $logsRootPath = App::getLogsRoot();
+        $constraint = Constraint::createConstraintFromConstraints($requiredConstraints);
 
-        @mkdir($logsRootPath);
-        @mkdir($logsRootPath . '/debug');
-        @mkdir($logsRootPath . '/debug/DependencyMananger/');
-        $path = $logsRootPath . '/debug/DependencyMananger/' . $file;
-        file_put_contents($path, json_encode($value, JSON_PRETTY_PRINT));
+        return $constraint;
     }
 
-    public function log($var)
+    private function getModuleByArchiveNameAndVersion(string $archiveName, string $version): ?Module
     {
-        print_r($var);
+        return $this->moduleLoader->loadByArchiveNameAndVersion($archiveName, $version);
     }
 
-    public function test()
+    private function getRequiredConstraint(Module $installedModule, string $archiveName): string
     {
-        $moduleLoader = ModuleLoader::create(Comparator::CARET_MODE_STRICT);
-        $module = $moduleLoader->loadLatestVersionByArchiveName('firstweb/multi-order');
+        $required = $installedModule->getRequire();
+        return $required[$archiveName] ?? '';
+    }
 
-        if (!$module) {
-            die('Can not find base module');
+    private function logFile($value, $file)
+    {
+        if (!Config::getLogging()) {
+            return;
         }
 
-        $systemSet = new SystemSet();
-        $systemSet->set([
-            "modified" => '2.0.4.2',
-            "php" => '7.4.0',
-            "mmlc" => '1.19.0',
-            "composer/autoload" => '1.3.0',
-            "robinthehood/modified-std-module" => '0.9.0',
-            "robinthehood/modified-orm" => '1.8.1',
-            "robinthehood/pdf-bill" => '0.17.0'
-        ]);
-
-        $this->log('TEST: satisfiesContraints1');
-        $combinationSatisfyerResult = $this->satisfiesContraints1($module, $systemSet);
-        $this->log($combinationSatisfyerResult);
-
-        $this->log('TEST: satisfiesContraints2');
-        $combinationSatisfyerResult = $this->satisfiesContraints2('firstweb/multi-order', '^1.0.0', $systemSet);
-        $this->log($combinationSatisfyerResult);
-
-        // var_dump('TEST: satisfiesContraints3');
-        $combinationSatisfyerResult = $this->satisfies('firstweb/multi-order', '^1.0.0', $systemSet);
-        $this->log($combinationSatisfyerResult);
+        $logsRootPath = App::getLogsRoot();
+
+        @mkdir($logsRootPath);
+        @mkdir($logsRootPath . '/debug');
+        @mkdir($logsRootPath . '/debug/DependencyMananger/');
+        $path = $logsRootPath . '/debug/DependencyMananger/' . $file;
+        file_put_contents($path, json_encode($value, JSON_PRETTY_PRINT));
     }
 
-    public function satisfiesContraints1(Module $module, SystemSet $systemSet): CombinationSatisfyerResult
+    /**
+     * Diese Methode wird zurzeit nicht verwednet und wurde deswegen auf private gestellt.
+     */
+    private function satisfiesContraints1(Module $module, SystemSet $systemSet): CombinationSatisfyerResult
     {
         $moduleTrees = $this->moduleTreeBuilder->buildListByConstraints($module);
         $this->logFile($moduleTrees, '1-moduleTrees.json');
@@ -118,8 +161,10 @@ public function satisfiesContraints1(Module $module, SystemSet $systemSet): Comb
         return $combinationSatisfyerResult;
     }
 
-
-    public function satisfiesContraints2(
+    /**
+     * Diese Methode wird zurzeit nicht verwednet und wurde deswegen auf private gestellt.
+     */
+    private function satisfiesContraints2(
         string $archiveName,
         string $constraint,
         SystemSet $systemSet
@@ -146,68 +191,4 @@ public function satisfiesContraints2(
 
         return $combinationSatisfyerResult;
     }
-
-    public function satisfies(string $archiveName, string $constraint, SystemSet $systemSet): CombinationSatisfyerResult
-    {
-        $systemSet->remove($archiveName);
-        $constraint = $this->createConstraint($archiveName, $constraint, $systemSet);
-
-        $moduleTree = $this->moduleTreeBuilder->buildByConstraints($archiveName, $constraint);
-        $this->logFile($moduleTree, '3-moduleTrees.json');
-
-        $flatEntryBuilder = new FlatEntryBuilder();
-        $flatEntries = $flatEntryBuilder->buildListFromModuleTree($moduleTree);
-        $this->logFile($flatEntries, '3-flatEntries.json');
-
-        $flatEntries = $flatEntryBuilder->fitSystemSet($flatEntries, $systemSet);
-        $this->logFile($flatEntries, '3-flatEntries-fit.json');
-
-        $combinationIterator = new CombinationIterator($flatEntries);
-        $combinationSatisfyer = new CombinationSatisfyer();
-        $combinationSatisfyerResult = $combinationSatisfyer->satisfiesCominationsFromModuleWithIterator(
-            $moduleTree,
-            $combinationIterator
-        );
-        return $combinationSatisfyerResult;
-    }
-
-    /**
-     * Gehe alle Module durch, die in $systemSet sind und das gleichzeitig Modul $archiveName benötigen.
-     * Gibt ein $constraint zurück, sodass die Anforderungenden der Module in $systemSet erhaltenbleiben.
-     */
-    private function createConstraint(string $archiveName, string $constraint, SystemSet $systemSet): string
-    {
-        /** @var string[] */
-        $requiredConstraints = [$constraint];
-
-        $archives = $systemSet->getArchives();
-        foreach ($archives as $archiveNameB => $version) {
-            $installedModule = $this->getModuleByArchiveNameAndVersion($archiveNameB, $version);
-            if (!$installedModule) {
-                continue;
-            }
-
-            $requiredConstraint = $this->getRequiredConstraint($installedModule, $archiveName);
-            if (!$requiredConstraint) {
-                continue;
-            }
-
-            $requiredConstraints[] = $requiredConstraint;
-        }
-
-        $constraint = Constraint::createConstraintFromConstraints($requiredConstraints);
-
-        return $constraint;
-    }
-
-    private function getModuleByArchiveNameAndVersion(string $archiveName, string $version): ?Module
-    {
-        return $this->moduleLoader->loadByArchiveNameAndVersion($archiveName, $version);
-    }
-
-    private function getRequiredConstraint(Module $installedModule, string $archiveName): string
-    {
-        $required = $installedModule->getRequire();
-        return $required[$archiveName] ?? '';
-    }
 }

From bf0ff1841ec4f486ce316d5a908e41f186e4f4fa Mon Sep 17 00:00:00 2001
From: Robin Wieschendorf 
Date: Thu, 21 Dec 2023 15:51:29 +0100
Subject: [PATCH 04/17] docs: improve docblocks

---
 .../CombinationSatisfyerResult.php                | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/src/Classes/DependencyManager/CombinationSatisfyerResult.php b/src/Classes/DependencyManager/CombinationSatisfyerResult.php
index df6cad59..c7d2985a 100644
--- a/src/Classes/DependencyManager/CombinationSatisfyerResult.php
+++ b/src/Classes/DependencyManager/CombinationSatisfyerResult.php
@@ -13,6 +13,9 @@
 
 namespace RobinTheHood\ModifiedModuleLoaderClient\DependencyManager;
 
+/**
+ * Das CombinationSatisfyerResult Objekt liefert Information zum Ergebnis des CombinationStisfyer.
+ */
 class CombinationSatisfyerResult
 {
     public const RESULT_COMBINATION_NOT_FOUND = 0;
@@ -21,10 +24,18 @@ class CombinationSatisfyerResult
     /** @var int */
     public $result = -1;
 
-    /** @var ?Combination */
+    /**
+     * @var ?Combination $testCombination Beinhaltet eine Kombination an allen Modulen und PHP, MMLC und modified
+     *      Versionen die durch ein SystemSet als Auswahl standen. Welche Kombination verwendet wird hängt vom
+     *      CombinationStisfyer ab. Oft ist es die letzte Kombination die probiert wurde.
+     */
     public $testCombination = null;
 
-    /** @var ?Combination */
+    /**
+     * @var ?Combination $foundCombination Enthält nur die Elemente (Module, PHP, MMLC und modified Version) aus
+     *      $testCombination die nötig sind, um die Voraussetung für ein Modul zu erfüllen. Ist das Ergbnis
+     *      RESULT_COMBINATION_NOT_FOUND, fehlen in $foundCombination Elemente.
+     */
     public $foundCombination = null;
 
     /** @var ?FailLog */

From f4acd6b67c821aa37ee7a50295f9dd2fdee2f634 Mon Sep 17 00:00:00 2001
From: Robin Wieschendorf 
Date: Thu, 21 Dec 2023 15:53:02 +0100
Subject: [PATCH 05/17] feat: add ModuleManager

---
 .../ModuleManager/AutoloadFileCreator.php     |  84 +++
 .../ModuleManager/ModuleFileInstaller.php     | 187 +++++++
 src/Classes/ModuleManager/ModuleInstaller.php | 495 +++++++++++++++++
 src/Classes/ModuleManager/ModuleManager.php   | 511 ++++++++++++++++++
 .../ModuleManager/ModuleManagerLog.php        |  47 ++
 .../ModuleManagerLoggerInterface.php          |  23 +
 .../ModuleManager/ModuleManagerMessage.php    | 232 ++++++++
 .../ModuleManager/ModuleManagerNullLogger.php |  37 ++
 .../ModuleManager/ModuleManagerResult.php     |  76 +++
 9 files changed, 1692 insertions(+)
 create mode 100644 src/Classes/ModuleManager/AutoloadFileCreator.php
 create mode 100644 src/Classes/ModuleManager/ModuleFileInstaller.php
 create mode 100644 src/Classes/ModuleManager/ModuleInstaller.php
 create mode 100644 src/Classes/ModuleManager/ModuleManager.php
 create mode 100644 src/Classes/ModuleManager/ModuleManagerLog.php
 create mode 100644 src/Classes/ModuleManager/ModuleManagerLoggerInterface.php
 create mode 100644 src/Classes/ModuleManager/ModuleManagerMessage.php
 create mode 100644 src/Classes/ModuleManager/ModuleManagerNullLogger.php
 create mode 100644 src/Classes/ModuleManager/ModuleManagerResult.php

diff --git a/src/Classes/ModuleManager/AutoloadFileCreator.php b/src/Classes/ModuleManager/AutoloadFileCreator.php
new file mode 100644
index 00000000..c0ab1952
--- /dev/null
+++ b/src/Classes/ModuleManager/AutoloadFileCreator.php
@@ -0,0 +1,84 @@
+
+ *
+ * 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\App;
+use RobinTheHood\ModifiedModuleLoaderClient\Loader\LocalModuleLoader;
+
+class AutoloadFileCreator
+{
+    // TODO: In createAutoloadFile() Exceptions werfen im Fehlerfall
+    public function createAutoloadFile(): void
+    {
+        $installedLocalModules = $this->getInstalledModules();
+        $autoloadFileContent = $this->buildAutoloadFile($installedLocalModules);
+        $this->writeAutoloadFile($autoloadFileContent);
+    }
+
+    private function getInstalledModules()
+    {
+        $localModuleLoader = LocalModuleLoader::createFromConfig();
+        $localModuleLoader->resetCache();
+        $installedModules = $localModuleLoader->loadAllInstalledVersions();
+        return $installedModules;
+    }
+
+    private function buildAutoloadFile(array $installedModules): string
+    {
+        $namespaceEntrys = [];
+        foreach ($installedModules as $installedModule) {
+            $autoload = $installedModule->getAutoload();
+
+            if (!$autoload) {
+                continue;
+            }
+
+            if (!$autoload['psr-4']) {
+                continue;
+            }
+
+            foreach ($autoload['psr-4'] as $namespace => $path) {
+                $path = str_replace(
+                    $installedModule->getSourceMmlcDir(),
+                    'vendor-mmlc/' . $installedModule->getArchiveName(),
+                    $path
+                );
+
+                $namespaceEntrys[] =
+                    '$loader->setPsr4(\'' . $namespace . '\\\', DIR_FS_DOCUMENT_ROOT . \'' . $path . '\');';
+            }
+        }
+
+        $namespaceEntrys = array_unique($namespaceEntrys);
+        $namespaceMapping = implode("\n", $namespaceEntrys);
+
+        $template = file_get_contents(App::getTemplatesRoot() . '/autoload.php.tmpl');
+        $autoloadFileContent = str_replace('{VENDOR_PSR4_NAMESPACE_MAPPINGS}', $namespaceMapping, $template);
+
+        return $autoloadFileContent;
+    }
+
+    private function writeAutoloadFile(string $autoloadFileContent): void
+    {
+        if (!file_exists(App::getShopRoot() . '/vendor-no-composer')) {
+            mkdir(App::getShopRoot() . '/vendor-no-composer');
+        }
+        file_put_contents(App::getShopRoot() . '/vendor-no-composer/autoload.php', $autoloadFileContent);
+
+        if (!file_exists(App::getShopRoot() . '/vendor-mmlc')) {
+            mkdir(App::getShopRoot() . '/vendor-mmlc');
+        }
+        file_put_contents(App::getShopRoot() . '/vendor-mmlc/autoload.php', $autoloadFileContent);
+    }
+}
diff --git a/src/Classes/ModuleManager/ModuleFileInstaller.php b/src/Classes/ModuleManager/ModuleFileInstaller.php
new file mode 100644
index 00000000..b19a2ec5
--- /dev/null
+++ b/src/Classes/ModuleManager/ModuleFileInstaller.php
@@ -0,0 +1,187 @@
+
+ *
+ * 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\App;
+use RobinTheHood\ModifiedModuleLoaderClient\Config;
+use RobinTheHood\ModifiedModuleLoaderClient\FileInfo;
+use RobinTheHood\ModifiedModuleLoaderClient\Helpers\FileHelper;
+use RobinTheHood\ModifiedModuleLoaderClient\Module;
+use RobinTheHood\ModifiedModuleLoaderClient\ModuleHasher\ModuleHashFileCreator;
+use RobinTheHood\ModifiedModuleLoaderClient\ModulePathMapper;
+use RuntimeException;
+
+class ModuleFileInstaller
+{
+    /**
+     * (Re-) Installiert / überschreibt ein Modul (archiveName, Version) ohne dabei auf Abhängigkeiten und den
+     * Modulstatus zu achten. Es wird nur auf Dateiebene kontrolliert, ob alle Dateien geschrieben werden konnten.
+     * Erzeugt die modulehash.json. Die Autoload Datei wird NICHT erzeugt / erneuert.
+     */
+    public function install(Module $module, bool $overwriteTemplateFiles = false): void
+    {
+        $this->installFiles($module, $overwriteTemplateFiles);
+        $this->createHashFile($module);
+    }
+
+    /**
+     * Deinstalliert / entfernt ein Modul (archiveName, Version) ohne dabei auf Abhängigkeiten und den Modulstatus zu
+     * achten. Es wird nur auf Dateiebene kontrolliert, ob alles Datien entfernt werden konnten. Die Autoload Datei
+     * wird NICHT aktualisiert.
+     */
+    public function uninstall(Module $module): void
+    {
+        $this->uninstallFiles($module);
+        $this->removeHashFile($module);
+    }
+
+    /**
+     * (Re-) Installiert / Überschreibt nur die Datei zu einem Modul (archiveName, Version). Es wird nur auf Datei-Ebene
+     * kontrolliert, ob alle Dateien geschrieben werden konnten. Die `modulehash.json` Datei wird NICHT erzeugt /
+     * erneuert.
+     */
+    private function installFiles(Module $module, bool $overwriteTemplateFiles): void
+    {
+        // Install Source Files to Shop Root
+        $files = $module->getSrcFilePaths();
+
+        foreach ($files as $file) {
+            $src = $module->getLocalRootPath() . $module->getSrcRootPath() . '/' . $file;
+
+            $expandedFiles = $module->getTemplateFiles($file);
+            foreach ($expandedFiles as $expandedFile) {
+                $overwrite = false;
+
+                if ($overwriteTemplateFiles === true) {
+                    $overwrite = true;
+                }
+
+                if (!FileInfo::isTemplateFile($expandedFile)) {
+                    $overwrite = true;
+                }
+
+                $expandedFile = ModulePathMapper::moduleSrcToShopRoot($expandedFile);
+
+                $dest = App::getShopRoot() . $expandedFile;
+                $this->installFile($src, $dest, $overwrite);
+            }
+        }
+
+        // Install Source Mmlc Files to shop vendor-mmlc
+        $files = $module->getSrcMmlcFilePaths();
+        foreach ($files as $file) {
+            $src = $module->getLocalRootPath() . $module->getSrcMmlcRootPath() . '/' . $file;
+            $file = ModulePathMapper::moduleSrcMmlcToShopVendorMmlc($file, $module->getArchiveName());
+            $dest = App::getShopRoot() . '/' . $file;
+            $this->installFile($src, $dest, true);
+        }
+    }
+
+    private function installFile(string $src, string $dest, bool $overwrite = false): bool
+    {
+        if (!file_exists($src)) {
+            throw new RuntimeException("Can not install file $src - File not exists.");
+        }
+
+        if ($this->fileOrLinkExists($dest) && $overwrite === false) {
+            // Die Datei existiert bereits und soll NICHT überschrieben werden.
+            return false;
+        } elseif ($this->fileOrLinkExists($dest) && $overwrite === true) {
+            // Die Datei existiert bereits soll überschrieben.
+            // Wir löschen die Datei zuerst, bevor wir sie überschreiben.
+            $this->removeFile($dest);
+        }
+
+        FileHelper::makeDirIfNotExists($dest);
+
+        // TODO: Kontrollieren ob hier eine Exception geworfen werden muss, wenn die Datei existiert.
+        if ($this->fileOrLinkExists($dest)) {
+            return false;
+        }
+
+        $this->copyFile($src, $dest);
+
+        return true;
+    }
+
+    private function copyFile(string $srcPath, string $destPath): void
+    {
+        if (Config::getInstallMode() === 'link') {
+            $result = symlink($srcPath, $destPath);
+        } else {
+            $result = copy($srcPath, $destPath);
+        }
+
+        if (!$result) {
+            throw new RuntimeException("Can not copy file $srcPath to $destPath");
+        }
+    }
+
+    /**
+     * Erzeugt / Überschreibt die `modulehash.json zu einem Modul (archive, Version)` Es wird nur auf Datei-Ebene
+     * kontrolliert, ob alle Dateien geschrieben werden konnten.
+     */
+    private function createHashFile(Module $module): void
+    {
+        $moduleHashFileCreator = new ModuleHashFileCreator();
+        $moduleHashFile = $moduleHashFileCreator->createHashFile($module);
+        $moduleHashFile->writeTo($module->getHashPath());
+    }
+
+    private function uninstallFiles(Module $module): void
+    {
+        // Uninstall from shop-root
+        $files = $module->getSrcFilePaths();
+        foreach ($files as $file) {
+            $file = ModulePathMapper::moduleSrcToShopRoot($file);
+            $dest = App::getShopRoot() . $file;
+            $this->removeIfFileExists($dest);
+        }
+
+        // Uninstall from shop-vendor-mmlc
+        $files = $module->getSrcMmlcFilePaths();
+        foreach ($files as $file) {
+            $file = ModulePathMapper::moduleSrcMmlcToShopVendorMmlc($file, $module->getArchiveName());
+            $dest = App::getShopRoot() . $file;
+            $this->removeIfFileExists($dest);
+            FileHelper::deletePathIsEmpty($dest);
+        }
+    }
+
+    private function fileOrLinkExists(string $path): bool
+    {
+        return file_exists($path) || is_link($path);
+    }
+
+    private function removeIfFileExists(string $path): void
+    {
+        if ($this->fileOrLinkExists($path)) {
+            $this->removeFile($path);
+        }
+    }
+
+    private function removeFile(string $path): void
+    {
+        $result = unlink($path);
+        if (!$result) {
+            throw new RuntimeException("Can not remove file $path");
+        }
+    }
+
+    private function removeHashFile(Module $module): void
+    {
+        $path = $module->getHashPath();
+        $this->removeIfFileExists($path);
+    }
+}
diff --git a/src/Classes/ModuleManager/ModuleInstaller.php b/src/Classes/ModuleManager/ModuleInstaller.php
new file mode 100644
index 00000000..3346d0aa
--- /dev/null
+++ b/src/Classes/ModuleManager/ModuleInstaller.php
@@ -0,0 +1,495 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace RobinTheHood\ModifiedModuleLoaderClient\ModuleManager;
+
+use RuntimeException;
+use RobinTheHood\ModifiedModuleLoaderClient\Config;
+use RobinTheHood\ModifiedModuleLoaderClient\Module;
+use RobinTheHood\ModifiedModuleLoaderClient\Api\V1\ApiRequest;
+use RobinTheHood\ModifiedModuleLoaderClient\Helpers\FileHelper;
+use RobinTheHood\ModifiedModuleLoaderClient\Loader\LocalModuleLoader;
+use RobinTheHood\ModifiedModuleLoaderClient\Archive\ArchivePuller;
+use RobinTheHood\ModifiedModuleLoaderClient\Archive\ArchiveHandler;
+use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\Combination;
+use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\DependencyManager;
+
+class ModuleInstaller
+{
+    /** @var DependencyManager */
+    private $dependencyManager;
+
+    /** @var LocalModuleLoader */
+    private $localModuleLoader;
+
+    /** @var ArchivePuller */
+    private $archivePuller;
+
+    /** @var ArchiveHandler */
+    private $archiveHandler;
+
+    public static function create(int $mode): ModuleInstaller
+    {
+        $dependencyManager = DependencyManager::create($mode);
+        $localModuleLoader = LocalModuleLoader::create($mode);
+        $archivePuller = ArchivePuller::create();
+        $archiveHandler = ArchiveHandler::create($mode);
+        $moduleInstaller = new ModuleInstaller(
+            $dependencyManager,
+            $localModuleLoader,
+            $archivePuller,
+            $archiveHandler
+        );
+        return $moduleInstaller;
+    }
+
+    public static function createFromConfig(): ModuleInstaller
+    {
+        return self::create(Config::getDependenyMode());
+    }
+
+    public function __construct(
+        DependencyManager $dependencyManager,
+        LocalModuleLoader $localModuleLoader,
+        ArchivePuller $archivePuller,
+        ArchiveHandler $archiveHandler
+    ) {
+        $this->dependencyManager = $dependencyManager;
+        $this->localModuleLoader = $localModuleLoader;
+        $this->archivePuller = $archivePuller;
+        $this->archiveHandler = $archiveHandler;
+    }
+
+    /**
+     * Downloads and prepares a module for installation.
+     *
+     * This method is responsible for downloading a module's archive, extracting its contents, and preparing it for
+     * installation. It performs several checks to ensure the module is not already loaded and retrieves the archive URL
+     * using the specified Module object. Upon successful execution, the method returns the Module instance representing
+     * the downloaded and prepared module.
+     *
+     * @param Module $module The Module to be pulled and prepared for installation.
+     *
+     * @return Module The Module instance representing the downloaded and prepared module.
+     *
+     * @throws RuntimeException
+     *      If the module is already loaded or if any errors occur during the download, extraction, or preparation
+     *      process, a RuntimeException is thrown with a detailed error message.
+     */
+    public function pull(Module $module): Module
+    {
+        $moduleText = "module {$module->getArchiveName()} {$module->getVersion()}";
+
+        if ($module->isLoaded()) {
+            $this->error("Can not pull $moduleText. Modul is already loaded.");
+        }
+
+        // Retrieve the module's archive URL
+        $archiveUrl = $this->getArchiveUrl($module);
+
+        // Download and extract the module's archive
+        $archive = $this->archivePuller->pull($module->getArchiveName(), $module->getVersion(), $archiveUrl);
+        $this->archiveHandler->extract($archive);
+
+        // Reload and return the Module instance
+        $pulledModule = $this->reload($module);
+        return $pulledModule;
+    }
+
+    /**
+     * Deletes a loaded module's files.
+     *
+     * This method is responsible for deleting the files associated with a loaded module. It checks whether the module
+     * is loaded and, if required, verifies whether it's uninstalled before proceeding with the deletion. If the 'force'
+     * parameter is set to true, the method will skip the installation check and forcefully delete the module. It then
+     * calls the 'deleteModuleFiles' method to remove the module's files from the system.
+     *
+     * @param Module $module The loaded module to be deleted.
+     * @param bool $force Set to true to force the deletion even if the module is installed.
+     *
+     * @throws RuntimeException
+     *      If the module is not loaded or is installed (and 'force' is false), a RuntimeException is thrown with a
+     *      detailed error message. Any errors encountered during the deletion process are also reported via exceptions.
+     */
+    public function delete(Module $module, bool $force = false): void
+    {
+        $moduleText = "module {$module->getArchiveName()} {$module->getVersion()}";
+
+        if (!$module->isLoaded()) {
+            $this->error("Can not delete $moduleText. Module is not loaded.");
+        }
+
+        if (!$force && $module->isInstalled()) {
+            $this->error("Can not delete $moduleText. Module is installed.");
+        }
+
+        $this->deleteModuleFiles($module);
+    }
+
+    /**
+     * Installs a module and its dependencies.
+     *
+     * This method is responsible for installing a module along with its dependencies into the shop system. It checks
+     * whether the module is already installed (unless 'force' is set to true) and ensures that a valid combination of
+     * versions for the module's dependencies can be found. If the installation is successful, it proceeds to install
+     * its dependencies. The method offers the flexibility to force the installation of the module even if it's already
+     * installed and skip the dependency check. Any errors encountered during the installation process are reported via
+     * exceptions.
+     *
+     * @param Module $module The module to be installed.
+     * @param bool $force Set to true to force the installation even if the module is already installed.
+     *
+     * @throws RuntimeException
+     *      If the module is already installed (and 'force' is false) or if no valid combination of versions for the
+     *      dependencies can be found, a RuntimeException is thrown with detailed error messages. Any other errors
+     *      during the installation process are also reported via exceptions.
+     */
+    public function install(Module $module, bool $force = false): void
+    {
+        $moduleText = "module {$module->getArchiveName()} {$module->getVersion()}";
+
+        $installedModule = $module->getInstalledVersion();
+        if (!$force && $installedModule) {
+            $installedModuleText = "module {$installedModule->getArchiveName()} {$installedModule->getVersion()}";
+            $this->error("Can not install $moduleText, because $installedModuleText is already installed.");
+        }
+
+        $combinationSatisfyerResult = $this->dependencyManager->canBeInstalled($module);
+
+        if (!$combinationSatisfyerResult->foundCombination) {
+            $this->error("Can not install $moduleText with dependencies. No possible combination of versions found");
+        }
+
+        $this->installWithoutDependencies($module, true, true);
+        $this->installDependencies($module, $combinationSatisfyerResult->foundCombination);
+    }
+
+    /**
+     * Install a module without installing its dependencies.
+     *
+     * This method is responsible for installing a module into the shop system without installing its dependencies. It
+     * checks whether the module is already installed (unless 'force' is set to true) and optionally performs a
+     * dependency check to ensure that a valid combination of versions for the module can be found (unless
+     * 'skipDependencyCheck' is set to true). Any errors during the installation process are reported via exceptions.
+     *
+     * @param Module $module
+     *      The module to be installed.
+     * @param bool $skipDependencyCheck
+     *      Set to true to skip the dependency check (default is false).
+     * @param bool $force
+     *      Set to true to force the installation even if the module is already installed (default is false).
+     *
+     * @throws RuntimeException
+     *      If the module is already installed (and 'force' is false) or if no valid combination of versions for the
+     *      module's dependencies can be found (and 'skipDependencyCheck' is false), a RuntimeException is thrown with
+     *      detailed error messages. Any other errors during the installation process are also reported via exceptions.
+     */
+    public function installWithoutDependencies(
+        Module $module,
+        bool $skipDependencyCheck = false,
+        bool $force = false
+    ): void {
+        $moduleText = "module {$module->getArchiveName()} {$module->getVersion()}";
+
+        if (!$force && $module->isInstalled()) {
+            $this->error("Can not install $moduleText. Module is already installed.");
+        }
+
+        if (!$skipDependencyCheck) {
+            $combinationSatisfyerResult = $this->dependencyManager->canBeInstalled($module);
+
+            if (!$combinationSatisfyerResult->foundCombination) {
+                $this->error("Can not update $moduleText. No possible combination of versions found");
+            }
+        }
+
+        $moduleFileInstaller = new ModuleFileInstaller();
+        $moduleFileInstaller->install($module);
+        $this->reload($module);
+    }
+
+    /**
+     * Update a module to its newest version while potentially updating its dependencies.
+     *
+     * This method is responsible for updating a module to its newest version. It verifies whether the module is
+     * installed (unless 'force' is set to true) and ensures that a valid combination of versions for the module's
+     * dependencies can be found. The newest version is pulled and installed, and its dependencies are also installed as
+     * necessary. Any errors during the update process are reported via exceptions, and the loaded new module is
+     * returned.
+     *
+     * @param Module $module The module to be updated.
+     * @param bool $force Set to true to force the update even if the module is not installed (default is false).
+     *
+     * @throws RuntimeException
+     *      If the module is not installed (and 'force' is false) or if no valid combination of versions for the
+     *      module's dependencies can be found, a RuntimeException is thrown with detailed error messages. Any other
+     *      errors during the update process are also reported via exceptions.
+     */
+    public function update(Module $installedModule, Module $newModule, bool $force = false): void
+    {
+        $moduleText = "module {$installedModule->getArchiveName()} {$installedModule->getVersion()}";
+
+        if (!$force && !$installedModule->isInstalled()) {
+            $this->error("Can not update $moduleText. Module is not installed.");
+        }
+
+        $moduleText = "module {$installedModule->getArchiveName()} {$installedModule->getVersion()}";
+        $newModuleText = "module {$newModule->getArchiveName()} {$newModule->getVersion()}";
+
+        if ($installedModule->getVersion() === $newModule->getVersion()) {
+            $this->error("Can not update $moduleText to $newModuleText.");
+        }
+
+        $combinationSatisfyerResult = $this->dependencyManager->canBeInstalled($newModule);
+        $foundCombination = $combinationSatisfyerResult->foundCombination;
+
+        if (!$foundCombination) {
+            $this->error(
+                "Can not update $moduleText to $newModuleText."
+                . " No possible combination of versions found"
+            );
+        }
+
+        if ($newModule->isLoaded()) {
+            $loadedNewModule = $newModule;
+        } else {
+            $loadedNewModule = $this->pull($newModule);
+        }
+
+        $this->uninstall($installedModule);
+        // Modul installieren
+        $this->installWithoutDependencies($loadedNewModule);
+        // Modul Abhängigkeiten installieren
+        $this->installDependencies($loadedNewModule, $foundCombination);
+    }
+
+    /**
+     * //TODO: Nicht zur Neusten sondern zu höchst möglichsten Version aktualisieren.
+     */
+    public function updateWithoutMissingDependencies(
+        Module $instaledModule,
+        Module $newModule,
+        bool $skipDependencyCheck = false,
+        bool $force = false
+    ): Module {
+        $moduleText = "module {$instaledModule->getArchiveName()} {$instaledModule->getVersion()}";
+
+        if (!$force && !$instaledModule->isInstalled()) {
+            $this->error("Can not update $moduleText. Module is not installed.");
+        }
+
+        $moduleText = "module {$instaledModule->getArchiveName()} {$instaledModule->getVersion()}";
+        $newModuleText = "module {$newModule->getArchiveName()} {$newModule->getVersion()}";
+
+        if (!$skipDependencyCheck) {
+            $combinationSatisfyerResult = $this->dependencyManager->canBeInstalled($newModule);
+
+            if (!$combinationSatisfyerResult->foundCombination) {
+                $this->error(
+                    "Can not update $moduleText to $newModuleText."
+                    . " No possible combination of versions found"
+                );
+            }
+        }
+
+        if ($newModule->isLoaded()) {
+            $loadedNewModule = $newModule;
+        } else {
+            $loadedNewModule = $this->pull($newModule);
+        }
+
+        $this->uninstall($instaledModule);
+        $this->installWithoutDependencies($loadedNewModule);
+
+        return $loadedNewModule;
+    }
+
+    public function discard(Module $module, bool $withTemplate, bool $force): void
+    {
+        $moduleText = "module {$module->getArchiveName()} {$module->getVersion()}";
+
+        if (!$force && !$module->isInstalled()) {
+            $this->error("Can not revert changes because $moduleText is not installed.");
+        }
+
+        $moduleFileInstaller = new ModuleFileInstaller();
+        $moduleFileInstaller->install($module, $withTemplate);
+    }
+
+    public function uninstall(Module $module, bool $force = false): void
+    {
+        $moduleText = "module {$module->getArchiveName()}";
+
+        if (!$module->isInstalled()) {
+            $this->error("Can not uninstall $moduleText because module is not installed.");
+        }
+
+        $moduleText = "module {$module->getArchiveName()} {$module->getVersion()}";
+
+
+        if ($module->isChanged() && $force === false) {
+            $this->error("Can not uninstall $moduleText because the module has changes.");
+        }
+
+        $moduleFileInstaller = new ModuleFileInstaller();
+        $moduleFileInstaller->uninstall($module);
+
+        $this->reload($module);
+    }
+
+    /**
+     * Installs the dependencies specified in the given Combination for a parent Module.
+     *
+     * This method is used internally to install the dependencies specified in a provided Combination for a parent
+     * Module. It retrieves the required modules from the Combination and iterates through them, checking for
+     * compatibility and installing each one. The parent Module is excluded from the installation process.
+     *
+     * @param Module $parentModule The parent Module for which dependencies need to be installed.
+     * @param Combination $combination The Combination specifying the dependencies to be installed.
+     *
+     * @throws \RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\DependencyException
+     *      If any of the dependencies cannot be installed due to conflicting versions or other issues,
+     *      a DependencyException may be thrown with details.
+     */
+    private function installDependencies(Module $parentModule, Combination $combination): void
+    {
+        $modules = $this->dependencyManager->getAllModulesFromCombination($combination);
+
+        foreach ($modules as $module) {
+            if ($parentModule->getArchiveName() === $module->getArchiveName()) {
+                continue;
+            }
+            $this->pullAndInstallWithoutDependencies($module);
+        }
+    }
+
+    /**
+     * Loads and installs a module.
+     *
+     * This method is responsible for loading and installing a module specified by the provided Module object. It checks
+     * whether the module is already installed, and if not, it attempts to load and install it. The method also handles
+     * potential errors during the loading and installation process.
+     *
+     * @param Module $module The Module to be loaded and installed.
+     *
+     * @throws RuntimeException
+     *      If the module cannot be loaded or installed successfully, a RuntimeException is thrown with a detailed
+     *      error message.
+     */
+    private function pullAndInstallWithoutDependencies(Module $module): void
+    {
+        if ($module->isInstalled()) {
+            return;
+        }
+
+        if ($module->isLoaded()) {
+            $pulledModule = $module;
+        } else {
+            $pulledModule = $this->pull($module);
+        }
+
+        $this->installWithoutDependencies($pulledModule);
+    }
+
+    /**
+     * Retrieves the archive URL for a given module.
+     *
+     * This method is responsible for obtaining the archive URL for a specific module by making an API request. It
+     * constructs the URL to download the module's archive, handles potential errors in the API response, and returns
+     * the archive URL.
+     *
+     * @param Module $module The Module for which the archive URL should be retrieved.
+     *
+     * @return string The URL to download the module's archive.
+     *
+     * @throws \Exception
+     *      If the API response is empty or lacks the necessary information to construct the archive URL, an Exception
+     *      is thrown with a detailed error message.
+     */
+    private function getArchiveUrl(Module $module): string
+    {
+        $moduleText = "module {$module->getArchiveName()} {$module->getVersion()}";
+
+        $apiRequest = new ApiRequest();
+        $result = $apiRequest->getArchive($module->getArchiveName(), $module->getVersion());
+
+        $content = $result['content'] ?? [];
+        if (!$content) {
+            $this->error("Can not pull $moduleText. ApiRespond is empty.");
+        }
+
+        $archiveUrl = $content['archiveUrl'] ?? '';
+        if (!$archiveUrl) {
+            $this->error("Can not pull $moduleText. archiveUrl is empty.");
+        }
+
+        return $archiveUrl;
+    }
+
+    /**
+     * Löscht alle Module Dateien aus dem Verzeichnis Modules. Es wird nicht kontrolliert ob das Modul geladen oder
+     * installiert ist.
+     */
+    private function deleteModuleFiles(Module $module): void
+    {
+        $path = $module->getLocalRootPath() . $module->getModulePath();
+
+        $filePaths = FileHelper::scanDirRecursive($path, FileHelper::FILES_ONLY);
+
+        $dirPaths = FileHelper::scanDirRecursive($path, FileHelper::DIRS_ONLY);
+        $dirPaths = array_reverse($dirPaths);
+        $dirPaths[] = $path;
+        $dirPaths[] = dirname($path);
+
+        // Delete Files
+        foreach ($filePaths as $path) {
+            if (file_exists($path)) {
+                unlink($path);
+            }
+        }
+
+        // Delete Folders
+        foreach ($dirPaths as $path) {
+            if (file_exists($path)) {
+                @rmdir($path);
+            }
+        }
+    }
+
+    private function reload(Module $module): Module
+    {
+        $moduleText = "module {$module->getArchiveName()} {$module->getVersion()}";
+
+        $this->localModuleLoader->resetCache();
+        $reloadedModule = $this->localModuleLoader->loadByArchiveNameAndVersion(
+            $module->getArchiveName(),
+            $module->getVersion()
+        );
+
+        if (!$reloadedModule) {
+            $this->error("Can not reload $moduleText.");
+        }
+
+        return $reloadedModule;
+    }
+
+    /**
+     * // NOTE: Vielleicht neue class ModuleInstallerException hinzufügen
+     * @return never
+     */
+    private function error(string $message): void
+    {
+        // StaticLogger::log(LogLevel::WARNING, $message);
+        throw new RuntimeException($message);
+    }
+}
diff --git a/src/Classes/ModuleManager/ModuleManager.php b/src/Classes/ModuleManager/ModuleManager.php
new file mode 100644
index 00000000..79371757
--- /dev/null
+++ b/src/Classes/ModuleManager/ModuleManager.php
@@ -0,0 +1,511 @@
+
+ *
+ * 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\Config;
+use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\CombinationSatisfyerResult;
+use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\DependencyBuilder;
+use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\SystemSetFactory;
+use RobinTheHood\ModifiedModuleLoaderClient\Loader\LocalModuleLoader;
+use RobinTheHood\ModifiedModuleLoaderClient\Loader\ModuleLoader;
+
+class ModuleManager
+{
+    /** @var ModuleInstaller */
+    private $moduleInstaller;
+
+    /** @var ModuleLoader */
+    private $moduleLoader;
+
+    /** @var LocalModuleLoader */
+    private $localModuleLoader;
+
+    /** @var DependencyBuilder */
+    private $dependencyBuilder;
+
+    /** @var SystemSetFactory */
+    private $systemSetFactory;
+
+    /** @var ModuleManagerLoggerInterface */
+    private $logger;
+
+    public static function create(int $mode): ModuleManager
+    {
+        $moduleInstaller = ModuleInstaller::create($mode);
+        $moduleLoader = ModuleLoader::create($mode);
+        $localModuleLoader = LocalModuleLoader::create($mode);
+        $dependencyBuilder = DependencyBuilder::create($mode);
+        $systemSetFactory = SystemSetFactory::create($mode);
+
+        $moduleInstaller = new ModuleManager(
+            $moduleInstaller,
+            $moduleLoader,
+            $localModuleLoader,
+            $dependencyBuilder,
+            $systemSetFactory
+        );
+
+        return $moduleInstaller;
+    }
+
+    public static function createFromConfig(): ModuleManager
+    {
+        return self::create(Config::getDependenyMode());
+    }
+
+    public function __construct(
+        ModuleInstaller $moduleInstaller,
+        ModuleLoader $moduleLoader,
+        LocalModuleLoader $localModuleLoader,
+        DependencyBuilder $dependencyBuilder,
+        SystemSetFactory $systemSetFactory
+    ) {
+        $this->moduleInstaller = $moduleInstaller;
+        $this->moduleLoader = $moduleLoader;
+        $this->localModuleLoader = $localModuleLoader;
+        $this->dependencyBuilder = $dependencyBuilder;
+        $this->systemSetFactory = $systemSetFactory;
+
+        $this->logger = new ModuleManagerNullLogger();
+    }
+
+    public function setLogger(ModuleManagerLoggerInterface $logger): void
+    {
+        $this->logger = $logger;
+    }
+
+    private function error(ModuleManagerMessage $message): ModuleManagerResult
+    {
+        $this->logger->error($message);
+        return ModuleManagerResult::error($message);
+    }
+
+    private function info(ModuleManagerMessage $message): void
+    {
+        $this->logger->info($message);
+    }
+
+    /**
+     * Lädt ein Modul vom Server herunter.
+     */
+    public function pull(string $archiveName, string $versionConstraint): ModuleManagerResult
+    {
+        if ($versionConstraint) {
+            $module = $this->moduleLoader->loadLatestByArchiveNameAndConstraint($archiveName, $versionConstraint);
+        } else {
+            $module = $this->moduleLoader->loadLatestVersionByArchiveName($archiveName);
+        }
+
+        if (!$module) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::PULL_ERROR_MODULE_NOT_FOUND)
+                ->setArchiveName($archiveName)
+                ->setVersionConstraint($versionConstraint)
+            );
+        }
+
+        if ($module->isLoaded()) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::PULL_ERROR_MODULE_ALLREADY_LOADED)
+                ->setArchiveName($archiveName)
+                ->setVersionConstraint($versionConstraint)
+            );
+        }
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::PULL_INFO_START)
+            ->setModule($module)
+        );
+
+        $module = $this->moduleInstaller->pull($module);
+        return ModuleManagerResult::success()
+            ->setModule($module);
+    }
+
+    /**
+     * Löscht ein Modul das bereits heruntergeladen wurde.
+     */
+    public function delete(string $archiveName, string $version): ModuleManagerResult
+    {
+        $module = $this->localModuleLoader->loadByArchiveNameAndVersion($archiveName, $version);
+
+        if (!$module) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::DELETE_ERROR_MODULE_NOT_FOUND)
+                ->setArchiveName($archiveName)
+                ->setVersion($version)
+            );
+        }
+
+        if ($module->isInstalled()) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::DELETE_ERROR_MODULE_IS_INSTALED)
+                ->setModule($module)
+            );
+        }
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::DELETE_INFO_START)
+            ->setModule($module)
+        );
+
+        $this->moduleInstaller->delete($module, false);
+        return ModuleManagerResult::success();
+    }
+
+    /**
+     * Lädt und installiert ein Modul in das Shop System UND lädt und installiert alle Abhängigkeiten bzw.
+     * abhängige Module nach.
+     */
+    public function install(string $archiveName, $versionConstraint): ModuleManagerResult
+    {
+        $moduleLoader = ModuleLoader::createFromConfig();
+        $module = $moduleLoader->loadLatestByArchiveNameAndConstraint($archiveName, $versionConstraint);
+
+        if (!$module) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_ERROR_MODULE_NOT_FOUND)
+                ->setArchiveName($archiveName)
+                ->setVersionConstraint($versionConstraint)
+            );
+        }
+
+        $systemSet = $this->systemSetFactory->getSystemSet();
+
+        $combinationSatisfyerResult = $this->dependencyBuilder->satisfies($archiveName, $versionConstraint, $systemSet);
+        if (
+            $combinationSatisfyerResult->result === CombinationSatisfyerResult::RESULT_COMBINATION_NOT_FOUND
+            || !$combinationSatisfyerResult->foundCombination
+        ) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_ERROR_MODULE_MISSING_REQUIREMENTS)
+                ->setArchiveName($archiveName)
+                ->setVersionConstraint($versionConstraint)
+                ->setCombinationSatisfyerResult($combinationSatisfyerResult)
+            );
+        }
+
+        $version = $combinationSatisfyerResult->foundCombination->getVersion($archiveName);
+
+        $module = $this->moduleLoader->loadByArchiveNameAndVersion($archiveName, $version);
+
+        if (!$module) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_ERROR_MODULE_NOT_FOUND)
+                ->setArchiveName($archiveName)
+                ->setVersionConstraint($versionConstraint)
+            );
+        }
+
+        if ($module->isInstalled()) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_ERROR_MODULE_ALLREADY_INSTALED)
+                ->setModule($module)
+            );
+        }
+
+        if (!$module->isLoaded()) {
+            $this->info(
+                ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_INFO_PULL_MODULE_START)
+                ->setModule($module)
+            );
+            $module = $this->moduleInstaller->pull($module);
+        }
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_INFO_START)
+            ->setModule($module)
+        );
+        $this->moduleInstaller->install($module);
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_INFO_UPDATE_AUTOLOAD_START)
+        );
+        $autoloadFileCreator = new AutoloadFileCreator();
+        $autoloadFileCreator->createAutoloadFile();
+
+        return ModuleManagerResult::success()
+            ->setModule($module);
+    }
+
+    /**
+     * Installiert ein Modul in das Shop System ABER lädt und installiert KEINE Abhängigkeiten / abhängige Module nach.
+     *
+     * @param bool $skipDependencyCheck skip dependency check.
+     */
+    public function installWithoutDependencies(
+        string $archiveName,
+        string $versionConstraint,
+        bool $skipDependencyCheck = false
+    ): ModuleManagerResult {
+        $moduleLoader = ModuleLoader::createFromConfig();
+        $module = $moduleLoader->loadLatestByArchiveNameAndConstraint($archiveName, $versionConstraint);
+
+        if (!$module) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_ERROR_MODULE_NOT_FOUND)
+                ->setArchiveName($archiveName)
+                ->setVersionConstraint($versionConstraint)
+            );
+        }
+
+        if ($skipDependencyCheck === false) {
+            $systemSet = $this->systemSetFactory->getSystemSet();
+
+            $combinationSatisfyerResult
+                = $this->dependencyBuilder->satisfies($archiveName, $versionConstraint, $systemSet);
+            if (
+                $combinationSatisfyerResult->result === CombinationSatisfyerResult::RESULT_COMBINATION_NOT_FOUND
+                || !$combinationSatisfyerResult->foundCombination
+            ) {
+                return $this->error(
+                    ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_ERROR_MODULE_MISSING_REQUIREMENTS)
+                    ->setArchiveName($archiveName)
+                    ->setVersionConstraint($versionConstraint)
+                    ->setCombinationSatisfyerResult($combinationSatisfyerResult)
+                );
+            }
+
+            $version = $combinationSatisfyerResult->foundCombination->getVersion($archiveName);
+
+            $module = $this->moduleLoader->loadByArchiveNameAndVersion($archiveName, $version);
+
+            if (!$module) {
+                return $this->error(
+                    ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_ERROR_MODULE_NOT_FOUND)
+                    ->setArchiveName($archiveName)
+                    ->setVersionConstraint($versionConstraint)
+                );
+            }
+        }
+
+        if ($module->isInstalled()) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_ERROR_MODULE_ALLREADY_INSTALED)
+                ->setModule($module)
+            );
+        }
+
+        if (!$module->isLoaded()) {
+            $this->info(
+                ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_INFO_PULL_MODULE_START)
+                ->setModule($module)
+            );
+            $module = $this->moduleInstaller->pull($module);
+        }
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_INFO_START)
+            ->setModule($module)
+        );
+        $this->moduleInstaller->installWithoutDependencies($module, true, true);
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::INSTALL_INFO_UPDATE_AUTOLOAD_START)
+        );
+        $autoloadFileCreator = new AutoloadFileCreator();
+        $autoloadFileCreator->createAutoloadFile();
+
+        return ModuleManagerResult::success()
+            ->setModule($module);
+    }
+
+    /**
+     * Aktuallisiert das Modul auf die neuste Version. Dabei werden keine Abhänggigkeiten
+     * aktualisiert. Kommen durch das Update jedoch neue Abhängigkeiten hinzu, werden diese installt. Können nicht alle
+     * Abhängigkeiten erfüllt werten, wird nicht aktualisiert und eine Exception geworfen.
+     */
+    public function update(string $archiveName): ModuleManagerResult
+    {
+        $moduleLoader = LocalModuleLoader::createFromConfig();
+        $module = $moduleLoader->loadInstalledVersionByArchiveName($archiveName);
+
+        if (!$module) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::UPDATE_ERROR_MODULE_NOT_FOUND)
+                ->setArchiveName($archiveName)
+            );
+        }
+
+        if (!$module->isInstalled()) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::UPDATE_ERROR_MODULE_NOT_INSTALLED)
+                ->setModule($module)
+            );
+        }
+
+        if ($module->isChanged()) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::UPDATE_ERROR_MODULE_IS_CHANGED)
+                ->setModule($module)
+            );
+        }
+
+        $systemSet = $this->systemSetFactory->getSystemSet();
+        $versionConstraint = '>' . $module->getVersion();
+        $combinationSatisfyerResult = $this->dependencyBuilder->satisfies($archiveName, $versionConstraint, $systemSet);
+        if (
+            $combinationSatisfyerResult->result === CombinationSatisfyerResult::RESULT_COMBINATION_NOT_FOUND
+            || !$combinationSatisfyerResult->foundCombination
+        ) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::UPDATE_ERROR_MODULE_MISSING_REQUIREMENTS)
+                ->setArchiveName($archiveName)
+                ->setVersionConstraint($versionConstraint)
+                ->setCombinationSatisfyerResult($combinationSatisfyerResult)
+            );
+        }
+
+        $version = $combinationSatisfyerResult->foundCombination->getVersion($archiveName);
+        $newModule = $this->moduleLoader->loadByArchiveNameAndVersion($archiveName, $version);
+
+        if (!$newModule->isLoaded()) {
+            $this->info(
+                ModuleManagerMessage::create(ModuleManagerMessage::UDPATE_INFO_PULL_MODULE_START)
+                ->setModule($newModule)
+            );
+            $newModule = $this->moduleInstaller->pull($newModule);
+        }
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::UPDATE_INFO_START)
+            ->setModule($module)
+        );
+        $this->moduleInstaller->update($module, $newModule, false);
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::UPDATE_INFO_TO)
+            ->setModule($newModule)
+        );
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::UPDATE_INFO_UPDATE_AUTOLOAD_START)
+        );
+
+        $autoloadFileCreator = new AutoloadFileCreator();
+        $autoloadFileCreator->createAutoloadFile();
+
+        return ModuleManagerResult::success()
+            ->setModule($newModule);
+    }
+
+    /**
+     * Aktualiseirt NUR das Modul auf die neuste Version. Es werden keine fehlenden Abhänggigkeiten
+     * installiert. Es werden keine Abhänggigkeiten aktualisiert. Können nicht alle Abhängigkeiten erfüllt werten,
+     * wird nicht aktualisiert und eine Exception geworfen.
+     */
+    // public function updateWithoutMissingDependencies(string $archvieName, bool $skipDependencyCheck = false): Module
+    // {
+    //     $loadedNewModul = $this->moduleInstaller->updateWithoutMissingDependencies(
+    //         $module,
+    //         $skipDependencyCheck,
+    //         false
+    //     );
+
+    //     $autoloadFileCreator = new AutoloadFileCreator();
+    //     $autoloadFileCreator->createAutoloadFile();
+
+    //     return $loadedNewModul;
+    // }
+
+    /**
+     * Entfernt alle Änderungen die an den Modul-Dateien im Shop gemacht wurden. Änderungen an Template Dateien werden
+     * nicht rückgängig gemacht.
+     */
+    public function discard(string $archiveName, bool $withTemplate = false): ModuleManagerResult
+    {
+        $moduleLoader = LocalModuleLoader::createFromConfig();
+        $module = $moduleLoader->loadInstalledVersionByArchiveName($archiveName);
+
+        if (!$module) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::DISCARD_ERROR_MODULE_NOT_FOUND)
+                ->setArchiveName($archiveName)
+            );
+        }
+
+        if (!$module->isChanged()) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::DISCARD_ERROR_MODULE_NOT_CHANGED)
+                ->setModule($module)
+            );
+        }
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::DISCARD_INFO_START)
+            ->setModule($module)
+        );
+
+        $this->moduleInstaller->discard($module, $withTemplate, false);
+        return ModuleManagerResult::success()
+            ->setModule($module);
+    }
+
+    /**
+     * Deinstalliert nur das Modul, wenn es installiert und nicht mehr als abhänigkeit von einem anderen Modul benötigt
+     * wird. Es werden keine Abhängigkeiten deinstalliert.
+     *
+     * Mit der force Option, kann der Abhängigkeits check übersprungen werden und das Modul wird trozdem deinstalliert.
+     * Das kann aber zur folge haben, dass andere Module nicht mehr funktionieren.
+     */
+    public function uninstall(string $archiveName, bool $force = false): ModuleManagerResult
+    {
+        $moduleLoader = LocalModuleLoader::createFromConfig();
+        $module = $moduleLoader->loadInstalledVersionByArchiveName($archiveName);
+
+        if (!$module) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::UNINSTALL_ERROR_MODULE_NOT_FOUND)
+                ->setArchiveName($archiveName)
+            );
+        }
+
+        if (!$module->isInstalled()) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::UNINSTALL_ERROR_MODULE_NOT_INSTALLED)
+                ->setModule($module)
+            );
+        }
+
+        if ($module->isChanged() && $force === false) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::UNINSTALL_ERROR_MODULE_IS_CHANGED)
+                ->setModule($module)
+            );
+        }
+
+        if ($module->getUsedBy() && $force === false) {
+            return $this->error(
+                ModuleManagerMessage::create(ModuleManagerMessage::UNINSTALL_ERROR_MODULE_IS_USED_BY)
+                ->setModule($module)
+            );
+        }
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::UNINSTALL_INFO_START)
+            ->setModule($module)
+        );
+
+        $this->moduleInstaller->uninstall($module, $force);
+
+        $this->info(
+            ModuleManagerMessage::create(ModuleManagerMessage::UNINSTALL_INFO_UPDATE_AUTOLOAD_START)
+        );
+        $autoloadFileCreator = new AutoloadFileCreator();
+        $autoloadFileCreator->createAutoloadFile();
+
+        return ModuleManagerResult::success()
+            ->setModule($module);
+    }
+}
diff --git a/src/Classes/ModuleManager/ModuleManagerLog.php b/src/Classes/ModuleManager/ModuleManagerLog.php
new file mode 100644
index 00000000..af5124cb
--- /dev/null
+++ b/src/Classes/ModuleManager/ModuleManagerLog.php
@@ -0,0 +1,47 @@
+
+ *
+ * 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 ModuleManagerLog
+{
+    /** @var callable */
+    private $writeFunction;
+
+    /** @var callable */
+    private $errorFunction;
+
+    public function setWriteFunction(callable $writeFunction)
+    {
+        $this->writeFunction = $writeFunction;
+    }
+
+    public function setErrorFunction(callable $errorFunction)
+    {
+        $this->errorFunction = $errorFunction;
+    }
+
+    public function write(string $message, $data1 = null, $data2 = null): void
+    {
+        $function = $this->writeFunction;
+        $function($message, $data1, $data2);
+    }
+
+    public function error(int $errorNo, string $message, $data1 = null, $data2 = null): void
+    {
+        $function = $this->errorFunction;
+        $function($errorNo, $message, $data1, $data2);
+    }
+}
diff --git a/src/Classes/ModuleManager/ModuleManagerLoggerInterface.php b/src/Classes/ModuleManager/ModuleManagerLoggerInterface.php
new file mode 100644
index 00000000..e34bc356
--- /dev/null
+++ b/src/Classes/ModuleManager/ModuleManagerLoggerInterface.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace RobinTheHood\ModifiedModuleLoaderClient\ModuleManager;
+
+interface ModuleManagerLoggerInterface
+{
+    public function debug(ModuleManagerMessage $message): void;
+    public function info(ModuleManagerMessage $message): void;
+    public function notice(ModuleManagerMessage $message): void;
+    public function warning(ModuleManagerMessage $message): void;
+    public function error(ModuleManagerMessage $message): void;
+}
diff --git a/src/Classes/ModuleManager/ModuleManagerMessage.php b/src/Classes/ModuleManager/ModuleManagerMessage.php
new file mode 100644
index 00000000..fd718a21
--- /dev/null
+++ b/src/Classes/ModuleManager/ModuleManagerMessage.php
@@ -0,0 +1,232 @@
+
+ *
+ * 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\DependencyManager\CombinationSatisfyerResult;
+use RobinTheHood\ModifiedModuleLoaderClient\Module;
+
+class ModuleManagerMessage
+{
+    public const PULL_INFO_START = 100;
+    public const PULL_ERROR_MODULE_NOT_FOUND = 151;
+    public const PULL_ERROR_MODULE_ALLREADY_LOADED = 152;
+
+    public const DELETE_INFO_START = 200;
+    public const DELETE_ERROR_MODULE_NOT_FOUND = 251;
+    public const DELETE_ERROR_MODULE_IS_INSTALED = 252;
+
+    public const INSTALL_INFO_START = 300;
+    public const INSTALL_INFO_PULL_MODULE_START = 301;
+    public const INSTALL_INFO_UPDATE_AUTOLOAD_START = 302;
+    public const INSTALL_ERROR_MODULE_NOT_FOUND = 351;
+    public const INSTALL_ERROR_MODULE_MISSING_REQUIREMENTS = 352;
+    public const INSTALL_ERROR_MODULE_ALLREADY_INSTALED = 353;
+
+    public const UPDATE_INFO_START = 400;
+    public const UDPATE_INFO_PULL_MODULE_START = 401;
+    public const UPDATE_INFO_UPDATE_AUTOLOAD_START = 402;
+    public const UPDATE_INFO_TO = 403;
+    public const UPDATE_ERROR_MODULE_NOT_FOUND = 451;
+    public const UPDATE_ERROR_MODULE_NOT_INSTALLED = 452;
+    public const UPDATE_ERROR_MODULE_MISSING_REQUIREMENTS = 453;
+    public const UPDATE_ERROR_MODULE_IS_CHANGED = 454;
+
+    public const DISCARD_INFO_START = 500;
+    public const DISCARD_ERROR_MODULE_NOT_FOUND = 551;
+    public const DISCARD_ERROR_MODULE_NOT_CHANGED = 552;
+
+    public const UNINSTALL_INFO_START = 600;
+    public const UNINSTALL_INFO_UPDATE_AUTOLOAD_START = 601;
+    public const UNINSTALL_ERROR_MODULE_NOT_FOUND = 651;
+    public const UNINSTALL_ERROR_MODULE_NOT_INSTALLED = 652;
+    public const UNINSTALL_ERROR_MODULE_IS_CHANGED = 653;
+    public const UNINSTALL_ERROR_MODULE_IS_USED_BY = 654;
+
+    /** @var int */
+    private $code = 0;
+
+    /** @var string */
+    private $message = '';
+
+    /** @var string */
+    private $archiveName = '';
+
+    /** @var string */
+    private $version = '';
+
+    /** @var string */
+    private $versionContraint = '';
+
+    /** @var Module */
+    private $module;
+
+    /** @var CombinationSatisfyerResult */
+    private $combinationSatisfyerResult;
+
+    public static function create(int $code): ModuleManagerMessage
+    {
+        return new ModuleManagerMessage($code);
+    }
+
+    public function __construct(int $code)
+    {
+        $this->code = $code;
+    }
+
+    public function setMessage(string $message): ModuleManagerMessage
+    {
+        $this->message = $message;
+        return $this;
+    }
+
+    public function setArchiveName(string $archiveName): ModuleManagerMessage
+    {
+        $this->archiveName = $archiveName;
+        return $this;
+    }
+
+    public function setVersion(string $version): ModuleManagerMessage
+    {
+        $this->version = $version;
+        return $this;
+    }
+
+    public function setVersionConstraint(string $versionContraint): ModuleManagerMessage
+    {
+        $this->versionContraint = $versionContraint;
+        return $this;
+    }
+
+    public function setModule(Module $module): ModuleManagerMessage
+    {
+        $this->module = $module;
+        return $this;
+    }
+
+    public function setCombinationSatisfyerResult(
+        CombinationSatisfyerResult $combinationSatisfyerResult
+    ): ModuleManagerMessage {
+        $this->combinationSatisfyerResult = $combinationSatisfyerResult;
+        return $this;
+    }
+
+    public function getCode(): int
+    {
+        return $this->code;
+    }
+
+    private function getModulName(): string
+    {
+        if ($this->module) {
+            $name = "{$this->module->getArchiveName()} version {$this->module->getVersion()}";
+        } elseif ($this->archiveName && $this->version) {
+            $name = "{$this->archiveName} version {$this->version}";
+        } elseif ($this->archiveName && $this->versionContraint) {
+            $name = "{$this->archiveName} version {$this->versionContraint}";
+        } elseif ($this->archiveName) {
+            $name = "{$this->archiveName}";
+        } else {
+            $name = "unknown";
+        }
+
+        return "module $name";
+    }
+
+    private function getUsedBy(): string
+    {
+        $subModulesArchiveNames = [];
+        foreach ($this->module->getUsedBy() as $subModule) {
+            $subModulesArchiveNames[] .= $subModule->getArchiveName();
+        }
+        $usedBy = implode("\n", $subModulesArchiveNames);
+        return $usedBy;
+    }
+
+    public function __toString()
+    {
+        if ($this->code === self::PULL_INFO_START) {
+            return sprintf("Pulling %s ...", $this->getModulName());
+        } elseif ($this->code === self::PULL_ERROR_MODULE_NOT_FOUND) {
+            return sprintf("Can not pull %s, because module not found.", $this->getModulName());
+        } elseif ($this->code === self::PULL_ERROR_MODULE_ALLREADY_LOADED) {
+            return sprintf("Can not pull %s, because module is already loaded.", $this->getModulName());
+        } elseif ($this->code === self::DELETE_INFO_START) {
+            return sprintf("Deleting %s ...", $this->getModulName());
+        } elseif ($this->code === self::DELETE_ERROR_MODULE_NOT_FOUND) {
+            return sprintf("Can not delete %s, because module not found.", $this->getModulName());
+        } elseif ($this->code === self::DELETE_ERROR_MODULE_IS_INSTALED) {
+            return sprintf("Can not delete %s, because it is installed.", $this->getModulName());
+        } elseif ($this->code === self::INSTALL_INFO_START) {
+            return sprintf("Installing %s ...", $this->getModulName());
+        } elseif ($this->code === self::INSTALL_INFO_PULL_MODULE_START) {
+            return sprintf("Pulling %s ...", $this->getModulName());
+        } elseif ($this->code === self::INSTALL_INFO_UPDATE_AUTOLOAD_START) {
+            return sprintf("Updating autotoload file ...");
+        } elseif ($this->code === self::INSTALL_ERROR_MODULE_NOT_FOUND) {
+            return sprintf("Can not install %s, because module not found.", $this->getModulName());
+        } elseif ($this->code === self::INSTALL_ERROR_MODULE_MISSING_REQUIREMENTS) {
+            return sprintf(
+                "Can not install %s, because not all requirements are met.\n%s",
+                $this->getModulName(),
+                '' . $this->combinationSatisfyerResult->failLog
+            );
+        } elseif ($this->code === self::INSTALL_ERROR_MODULE_ALLREADY_INSTALED) {
+            return sprintf("Can not install %s, because it is already installed.", $this->getModulName());
+        } elseif ($this->code === self::UPDATE_INFO_START) {
+            return sprintf("Updating %s ...", $this->getModulName());
+        } elseif ($this->code === self::UDPATE_INFO_PULL_MODULE_START) {
+            return sprintf("Pulling %s ...", $this->getModulName());
+        } elseif ($this->code === self::UPDATE_INFO_TO) {
+            return sprintf("Updated to %s.", $this->getModulName());
+        } elseif ($this->code === self::UPDATE_INFO_UPDATE_AUTOLOAD_START) {
+            return sprintf("Updating autotoload file ...", $this->getModulName());
+        } elseif ($this->code === self::UPDATE_ERROR_MODULE_NOT_FOUND) {
+            return sprintf("Can not update %s, because module not found.", $this->getModulName());
+        } elseif ($this->code === self::UPDATE_ERROR_MODULE_NOT_INSTALLED) {
+            return sprintf("Can not update %s, because module is not installed.", $this->getModulName());
+        } elseif ($this->code === self::UPDATE_ERROR_MODULE_MISSING_REQUIREMENTS) {
+            return sprintf(
+                "Can not update %s, because not all requirements are met.\n%s",
+                $this->getModulName(),
+                '' . $this->combinationSatisfyerResult->failLog
+            );
+        } elseif ($this->code === self::UPDATE_ERROR_MODULE_IS_CHANGED) {
+            return sprintf("Can not update %s, because module has changes.", $this->getModulName());
+        } elseif ($this->code === self::DISCARD_INFO_START) {
+            return sprintf("Discarding %s ...", $this->getModulName());
+        } elseif ($this->code === self::DISCARD_ERROR_MODULE_NOT_FOUND) {
+            return sprintf("Can not discard %s, because module not found.", $this->getModulName());
+        } elseif ($this->code === self::DISCARD_ERROR_MODULE_NOT_CHANGED) {
+            return sprintf("Can not discard %s, because module has no changes.", $this->getModulName());
+        } elseif ($this->code === self::UNINSTALL_INFO_START) {
+            return sprintf("Uninstalling %s ...", $this->getModulName());
+        } elseif ($this->code === self::UNINSTALL_INFO_UPDATE_AUTOLOAD_START) {
+            return sprintf("Updating autotoload file ...", $this->getModulName());
+        } elseif ($this->code === self::UNINSTALL_ERROR_MODULE_NOT_FOUND) {
+            return sprintf("Can not uninstall %s, because module not found.", $this->getModulName());
+        } elseif ($this->code === self::UNINSTALL_ERROR_MODULE_NOT_INSTALLED) {
+            return sprintf("Can not uninstall %s, because module is not installed.", $this->getModulName());
+        } elseif ($this->code === self::UNINSTALL_ERROR_MODULE_IS_CHANGED) {
+            return sprintf("Can not uninstall %s, because module has changes.", $this->getModulName());
+        } elseif ($this->code === self::UNINSTALL_ERROR_MODULE_IS_USED_BY) {
+            return sprintf(
+                "Can not uninstall %s, because module is used by other modules.\n%s",
+                $this->getModulName(),
+                $this->getUsedBy()
+            );
+        }
+
+        return "Unknown message";
+    }
+}
diff --git a/src/Classes/ModuleManager/ModuleManagerNullLogger.php b/src/Classes/ModuleManager/ModuleManagerNullLogger.php
new file mode 100644
index 00000000..9177f2bf
--- /dev/null
+++ b/src/Classes/ModuleManager/ModuleManagerNullLogger.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace RobinTheHood\ModifiedModuleLoaderClient\ModuleManager;
+
+class ModuleManagerNullLogger implements ModuleManagerLoggerInterface
+{
+    public function debug(ModuleManagerMessage $message): void
+    {
+    }
+
+    public function info(ModuleManagerMessage $message): void
+    {
+    }
+
+    public function notice(ModuleManagerMessage $message): void
+    {
+    }
+
+    public function warning(ModuleManagerMessage $message): void
+    {
+    }
+
+    public function error(ModuleManagerMessage $message): void
+    {
+    }
+}
diff --git a/src/Classes/ModuleManager/ModuleManagerResult.php b/src/Classes/ModuleManager/ModuleManagerResult.php
new file mode 100644
index 00000000..2d563b0c
--- /dev/null
+++ b/src/Classes/ModuleManager/ModuleManagerResult.php
@@ -0,0 +1,76 @@
+
+ *
+ * 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 ModuleManagerResult
+{
+    public const TYPE_ERROR = 1;
+    public const TYPE_SUCCESS = 2;
+
+    /** @var int */
+    private $type;
+
+    /** @var ModuleManagerMessage */
+    private $message;
+
+    /** @var Module */
+    private $module;
+
+    public function __construct(int $type)
+    {
+        $this->type = $type;
+    }
+
+    public static function success(): ModuleManagerResult
+    {
+        $moduleManagerResult = new ModuleManagerResult(self::TYPE_SUCCESS);
+        return $moduleManagerResult;
+    }
+
+    public static function error(ModuleManagerMessage $message): ModuleManagerResult
+    {
+        $moduleManagerResult = new ModuleManagerResult(self::TYPE_ERROR);
+        $moduleManagerResult->setMessage($message);
+        return $moduleManagerResult;
+    }
+
+    public function setModule(Module $module): ModuleManagerResult
+    {
+        $this->module = $module;
+        return $this;
+    }
+
+    public function setMessage(ModuleManagerMessage $message): ModuleManagerResult
+    {
+        $this->message = $message;
+        return $this;
+    }
+
+    public function getType(): int
+    {
+        return $this->type;
+    }
+
+    public function getModule(): ?Module
+    {
+        return $this->module;
+    }
+
+    public function getMessage(): ?ModuleManagerMessage
+    {
+        return $this->message;
+    }
+}

From ae1df04ff3b4a55bcdeb9f97bccbc4da4678d8ef Mon Sep 17 00:00:00 2001
From: Robin Wieschendorf 
Date: Thu, 21 Dec 2023 16:29:45 +0100
Subject: [PATCH 06/17] feat: improve DependencyManager

---
 .../DependencyManager/DependencyManager.php   | 69 ++++++++++++++-----
 1 file changed, 52 insertions(+), 17 deletions(-)

diff --git a/src/Classes/DependencyManager/DependencyManager.php b/src/Classes/DependencyManager/DependencyManager.php
index a6990760..044230a4 100644
--- a/src/Classes/DependencyManager/DependencyManager.php
+++ b/src/Classes/DependencyManager/DependencyManager.php
@@ -110,6 +110,9 @@ public function getAllModulesFromCombination(Combination $combination): array
     }
 
     /**
+     * Kontrolliert, ob ein Modul installiert werden kann. Es wird getestet, ob genau die Version installiert werden
+     * kann.
+     *
      * @param Module $module
      * @param string[] $doNotCheck
      *
@@ -137,35 +140,45 @@ public function canBeInstalled(Module $module, array $doNotCheck = []): Combinat
             . " The following combination is required: {$combinationSatisfyerResult->failLog}";
 
             StaticLogger::log(LogLevel::WARNING, $message);
-            throw new DependencyException($message);
         }
 
-        // $modules = $this->getAllModulesFromCombination($combinationSatisfyerResult->foundCombination);
-        // $this->canBeInstalledTestChanged($module, $modules);
-
         return $combinationSatisfyerResult;
     }
 
     /**
-     * Testet, ob das Modul in $module installiert werden kann, ob das Modul $module
-     * selbst oder eine Abhängigkeit in $modules im Status 'changed' ist.
+     * Kontrolliert, ob ein Modul aktuallisert werden kann, Es wird getestet, ob es eine neuerer Version gibt, die alle
+     * Bedingungen erfüllt. Wenn ja, steckt eine passende Kombination in CombinationSatisfyerResult::foundCombination.
      *
-     * @param Module[] $modules
+     * @param Module $module
+     * @param string[] $doNotCheck
+     *
+     * @return CombinationSatisfyerResult
      */
-    private function canBeInstalledTestChanged(Module $module, array $modules): void
+    public function canBeUpdated(Module $module, array $doNotCheck = []): CombinationSatisfyerResult
     {
-        $module = $module->getInstalledVersion();
-        if ($module && $module->isInstalled() && $module->isChanged()) {
-            $a = $module->getArchiveName();
-            throw new DependencyException("Module $a can not be installed because the Module has changes");
+        $systemSet = $this->systemSetFactory->getSystemSet();
+
+        foreach ($doNotCheck as $name) {
+            $systemSet->remove($name);
         }
 
-        foreach ($modules as $module) {
-            if ($module && $module->isInstalled() && $module->isChanged()) {
-                $a = $module->getArchiveName();
-                throw new DependencyException("Required Module $a can not be installed because the Module has changes");
-            }
+        $combinationSatisfyerResult = $this->dependencyBuilder->satisfies(
+            $module->getArchiveName(),
+            '>' . $module->getVersion(),
+            $systemSet
+        );
+
+        if ($combinationSatisfyerResult->result === CombinationSatisfyerResult::RESULT_COMBINATION_NOT_FOUND) {
+            $message = "Can not update module {$module->getArchiveName()} in version {$module->getVersion()} "
+            . "because there are conflicting version contraints. "
+            . "Perhaps you have installed a module that requires a different version, "
+            . "or there is no compatible combination of dependencies. "
+            . " The following combination is required: {$combinationSatisfyerResult->failLog}";
+
+            StaticLogger::log(LogLevel::WARNING, $message);
         }
+
+        return $combinationSatisfyerResult;
     }
 
     /**
@@ -202,4 +215,26 @@ public function getMissingDependencies(Module $module): array
 
         return $missing;
     }
+
+    /**
+     * Testet, ob das Modul in $module installiert werden kann, ob das Modul $module
+     * selbst oder eine Abhängigkeit in $modules im Status 'changed' ist.
+     *
+     * @param Module[] $modules
+     */
+    private function canBeInstalledTestChanged(Module $module, array $modules): void
+    {
+        $module = $module->getInstalledVersion();
+        if ($module && $module->isInstalled() && $module->isChanged()) {
+            $a = $module->getArchiveName();
+            throw new DependencyException("Module $a can not be installed because the Module has changes");
+        }
+
+        foreach ($modules as $module) {
+            if ($module && $module->isInstalled() && $module->isChanged()) {
+                $a = $module->getArchiveName();
+                throw new DependencyException("Required Module $a can not be installed because the Module has changes");
+            }
+        }
+    }
 }

From 6b22d03efaf2bbe93f5a34f1557d9748ef6acf53 Mon Sep 17 00:00:00 2001
From: Robin Wieschendorf 
Date: Thu, 21 Dec 2023 16:30:37 +0100
Subject: [PATCH 07/17] docs: fix typo

---
 src/Templates/Settings.tmpl.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Templates/Settings.tmpl.php b/src/Templates/Settings.tmpl.php
index aaa47ee6..60493ff0 100644
--- a/src/Templates/Settings.tmpl.php
+++ b/src/Templates/Settings.tmpl.php
@@ -138,7 +138,7 @@ function viewIsSelected(bool $value): string
                                         
                                     
 
-                                    

Du kannst zwischen strict und lax wählen. Mit strict werden die Abhänigkeiten von Modulen mit einer Version kleiner als 1.0.0 genauer kontrolliert. Wenn sich einige Module nicht installieren lassen, kannst du es mit lax versuchen. Beachte, dass im Lex-Modus die Wahrscheinlichkeit größer ist, dass verschiedene Module nicht miteinander harmonieren.

+

Du kannst zwischen strict und lax wählen. Mit strict werden die Abhängigkeiten von Modulen mit einer Version kleiner als 1.0.0 genauer kontrolliert. Wenn sich einige Module nicht installieren lassen, kannst du es mit lax versuchen. Beachte, dass im Lex-Modus die Wahrscheinlichkeit größer ist, dass verschiedene Module nicht miteinander harmonieren.

From f7dfa03e2482ece9d4976c1818d65a364bb9943b Mon Sep 17 00:00:00 2001 From: Robin Wieschendorf Date: Thu, 21 Dec 2023 16:31:31 +0100 Subject: [PATCH 08/17] feat: add new methods getForceInstallUrl() and getRevertChangesWithTemplateUrl() --- src/Classes/ViewModels/ModuleViewModel.php | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Classes/ViewModels/ModuleViewModel.php b/src/Classes/ViewModels/ModuleViewModel.php index fae5a649..7b291f6a 100644 --- a/src/Classes/ViewModels/ModuleViewModel.php +++ b/src/Classes/ViewModels/ModuleViewModel.php @@ -39,11 +39,21 @@ public function getInstallUrl(string $ref = ''): string return $this->getUrl('install', $ref); } + public function getForceInstallUrl(string $ref = ''): string + { + return $this->getUrl('install', $ref, null, ['force' => 'true']); + } + public function getRevertChangesUrl(string $ref = ''): string { return $this->getUrl('revertChanges', $ref); } + public function getRevertChangesWithTemplateUrl(string $ref = ''): string + { + return $this->getUrl('revertChanges', $ref, null, ['withTemplate' => 'true']); + } + public function getLoadAndInstallUrl(string $ref = ''): string { return $this->getUrl('loadAndInstall', $ref); @@ -194,17 +204,31 @@ public function isChanged(): bool return $this->module->isChanged(); } - private function getUrl(string $action, string $ref, ?Module $module = null): string + /** + * @param string $action + * @param string $ref + * @param ?Module $module + * @param string[] $queryParams + * + * @return string + */ + private function getUrl(string $action, string $ref, ?Module $module = null, array $queryParams = []): string { if (!$module) { $module = $this->module; } + $queryString = http_build_query($queryParams); + if ($queryString) { + $queryString = '&' . $queryString; + } + return '?action=' . $action . '&archiveName=' . $module->getArchiveName() . '&version=' . $module->getVersion() . - '&ref=' . $ref; + '&ref=' . $ref + . $queryString; } /** From b341a13e4a7cc96da156dbf0e02006683d1b4c50 Mon Sep 17 00:00:00 2001 From: Robin Wieschendorf Date: Thu, 21 Dec 2023 16:32:32 +0100 Subject: [PATCH 09/17] docs: remove "noch" from "Dieses Module wurde noch nicht mit ..." --- src/Classes/ViewModels/ModuleViewModel.php | 2 +- src/Templates/ModuleListingModule.tmpl.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Classes/ViewModels/ModuleViewModel.php b/src/Classes/ViewModels/ModuleViewModel.php index 7b291f6a..aadadc58 100644 --- a/src/Classes/ViewModels/ModuleViewModel.php +++ b/src/Classes/ViewModels/ModuleViewModel.php @@ -241,7 +241,7 @@ public function getCompatibleStrings(): array if (!$this->module->isCompatibleWithModified()) { $version = ShopInfo::getModifiedVersion(); $array[] = [ - 'text' => "Dieses Modul wurde noch nicht mit deiner Version von modified getestet. Du hast modifed Version $version installiert.", + 'text' => "Dieses Modul wurde nicht mit deiner Version von modified getestet. Du hast modifed Version $version installiert.", 'type' => 'warning' ]; } diff --git a/src/Templates/ModuleListingModule.tmpl.php b/src/Templates/ModuleListingModule.tmpl.php index a68c377e..02beaeb3 100644 --- a/src/Templates/ModuleListingModule.tmpl.php +++ b/src/Templates/ModuleListingModule.tmpl.php @@ -13,7 +13,7 @@ $tooltip = 'Dieses Modul wurde getestet und funktioniert mit deiner Version von modified.'; } else { $compatibility = 'inkompatibel'; - $tooltip = 'Dieses Modul wurde noch nicht mit deiner Version von modified getestet.'; + $tooltip = 'Dieses Modul wurde nicht mit deiner Version von modified getestet.'; } $modulePrice = $module->isInstalled() ? 'installiert' : $moduleView->getPriceFormated(); From 121b02858535cf859207ef97b0f801f6030922a4 Mon Sep 17 00:00:00 2001 From: Robin Wieschendorf Date: Thu, 21 Dec 2023 16:34:00 +0100 Subject: [PATCH 10/17] docs: fix typo --- src/Classes/Controllers/IndexController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Classes/Controllers/IndexController.php b/src/Classes/Controllers/IndexController.php index 15b8f284..328f5990 100644 --- a/src/Classes/Controllers/IndexController.php +++ b/src/Classes/Controllers/IndexController.php @@ -294,7 +294,7 @@ public function invokeModuleInfo() 'type' => 'warning', 'text' => 'Einige Abhängigkeiten sind nicht installiert. Das Fehlen von Abhängigkeiten kann zu Fehlern - bei der Ausführung des Moduls führen. Installiere die folgenden fehlenden Abhänigkeiten:
' + bei der Ausführung des Moduls führen. Installiere die folgenden fehlenden Abhängigkeiten:
' . nl2br($string) ]); } From 1307e2eb2bf40ba606d427a2f5034985217a1865 Mon Sep 17 00:00:00 2001 From: Robin Wieschendorf Date: Thu, 21 Dec 2023 16:34:34 +0100 Subject: [PATCH 11/17] feat: use new ModuleManager --- src/Classes/Controllers/IndexController.php | 263 +++++++------------- src/Templates/ModuleInfo.tmpl.php | 15 +- 2 files changed, 108 insertions(+), 170 deletions(-) diff --git a/src/Classes/Controllers/IndexController.php b/src/Classes/Controllers/IndexController.php index 328f5990..f59e1a9e 100644 --- a/src/Classes/Controllers/IndexController.php +++ b/src/Classes/Controllers/IndexController.php @@ -28,6 +28,8 @@ use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\DependencyManager; use RobinTheHood\ModifiedModuleLoaderClient\MmlcVersionInfoLoader; use RobinTheHood\ModifiedModuleLoaderClient\ModuleInstaller; +use RobinTheHood\ModifiedModuleLoaderClient\ModuleManager\ModuleManager; +use RobinTheHood\ModifiedModuleLoaderClient\ModuleManager\ModuleManagerResult; use RobinTheHood\ModifiedModuleLoaderClient\ModuleStatus; use RobinTheHood\ModifiedModuleLoaderClient\Notification; use RobinTheHood\ModifiedModuleLoaderClient\SelfUpdater; @@ -66,20 +68,20 @@ public function invoke() return $this->invokeLazyModuleChangeCount(); case 'lazySystemUpdateCount': return $this->invokeLazySystemUpdateCount(); + case 'loadRemoteModule': + return $this->invokePull(); case 'install': return $this->invokeInstall(); + case 'loadAndInstall': + return $this->invokeInstall(); case 'update': return $this->invokeUpdate(); + case 'revertChanges': + return $this->invokeDiscard(); case 'uninstall': return $this->invokeUninstall(); - case 'loadRemoteModule': - return $this->invokeLoadRemoteModule(); - case 'loadAndInstall': - return $this->invokeLoadAndInstall(); case 'unloadLocalModule': - return $this->invokeUnloadLocalModule(); - case 'revertChanges': - return $this->invokeRevertChanges(); + return $this->invokeDelete(); case 'signIn': return $this->invokeSignIn(); case 'signOut': @@ -374,7 +376,7 @@ public function invokeLazySystemUpdateCount() } } - public function invokeInstall() + public function invokePull() { if ($accessRedirect = $this->checkAccessRight()) { return $accessRedirect; @@ -384,49 +386,30 @@ public function invokeInstall() $archiveName = $queryParams['archiveName'] ?? ''; $version = $queryParams['version'] ?? ''; - $moduleLoader = LocalModuleLoader::createFromConfig(); - $module = $moduleLoader->loadByArchiveNameAndVersion($archiveName, $version); - - if (!$module) { - $this->addModuleNotFoundNotification($archiveName, $version); - return $this->redirect('/'); - } - - $force = $queryParams['force'] ?? ''; - $force = $force === 'true' ? true : false; - - - try { - if ($force) { - $this->moduleInstaller->install($module, true); - } else { - $this->moduleInstaller->installWithDependencies($module); - } - } catch (DependencyException $e) { - $foreInstallUrl = "?action=install" - . "&archiveName={$module->getArchiveName()}" - . "&version={$module->getVersion()}" - . "&ref=moduleInfo" - . "&force=true"; + $moduleManager = ModuleManager::createFromConfig(); + $moduleManagerResult = $moduleManager->pull($archiveName, $version); + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { Notification::pushFlashMessage([ - 'text' => $e->getMessage() - . '

Click here to ' - . 'force install ' . $module->getArchiveName() . ':' . $module->getVersion() - . ' without dependencies.', + 'text' => (string) $moduleManagerResult->getMessage(), 'type' => 'error' ]); - } catch (RuntimeException $e) { + } else { Notification::pushFlashMessage([ - 'text' => $e->getMessage(), - 'type' => 'error' + 'text' => 'Module pulled successfully', + 'type' => 'success' ]); } - return $this->redirectRef($archiveName, $module->getVersion()); + $module = $moduleManagerResult->getModule(); + if ($module) { + return $this->redirectRef($module->getArchiveName(), $module->getVersion()); + } + + return $this->redirectRef($archiveName, $version); } - private function invokeRevertChanges() + public function invokeInstall() { if ($accessRedirect = $this->checkAccessRight()) { return $accessRedirect; @@ -435,33 +418,37 @@ private function invokeRevertChanges() $queryParams = $this->serverRequest->getQueryParams(); $archiveName = $queryParams['archiveName'] ?? ''; $version = $queryParams['version'] ?? ''; + $force = $queryParams['force'] ?? ''; + $force = $force === 'true' ? true : false; - $moduleLoader = LocalModuleLoader::createFromConfig(); - $module = $moduleLoader->loadByArchiveNameAndVersion($archiveName, $version); - - if (!$module) { - $this->addModuleNotFoundNotification($archiveName, $version); - return $this->redirect('/'); + $moduleManager = ModuleManager::createFromConfig(); + if ($force === false) { + $moduleManagerResult = $moduleManager->install($archiveName, $version); + } else { + $moduleManagerResult = $moduleManager->installWithoutDependencies($archiveName, $version, true); } - try { - $this->moduleInstaller->revertChanges($module); - } catch (DependencyException $e) { + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { Notification::pushFlashMessage([ - 'text' => $e->getMessage(), + 'text' => (string) $moduleManagerResult->getMessage(), 'type' => 'error' ]); - } catch (RuntimeException $e) { + } else { Notification::pushFlashMessage([ - 'text' => $e->getMessage(), - 'type' => 'error' + 'text' => 'Module installed successfully', + 'type' => 'success' ]); } - return $this->redirectRef($archiveName, $module->getVersion()); + $module = $moduleManagerResult->getModule(); + if ($module) { + return $this->redirectRef($module->getArchiveName(), $module->getVersion()); + } + + return $this->redirectRef($archiveName, $version); } - public function invokeUninstall() + public function invokeUpdate() { if ($accessRedirect = $this->checkAccessRight()) { return $accessRedirect; @@ -471,32 +458,30 @@ public function invokeUninstall() $archiveName = $queryParams['archiveName'] ?? ''; $version = $queryParams['version'] ?? ''; - $moduleLoader = LocalModuleLoader::createFromConfig(); - $module = $moduleLoader->loadByArchiveNameAndVersion($archiveName, $version); + $moduleManager = ModuleManager::createFromConfig(); + $moduleManagerResult = $moduleManager->update($archiveName); - if (!$module) { - $this->addModuleNotFoundNotification($archiveName, $version); - return $this->redirect('/'); - } - - try { - $this->moduleInstaller->uninstall($module); - } catch (DependencyException $e) { + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { Notification::pushFlashMessage([ - 'text' => $e->getMessage(), + 'text' => (string) $moduleManagerResult->getMessage(), 'type' => 'error' ]); - } catch (RuntimeException $e) { + } else { Notification::pushFlashMessage([ - 'text' => $e->getMessage(), - 'type' => 'error' + 'text' => 'Module updated successfully', + 'type' => 'success' ]); } - return $this->redirectRef($archiveName, $module->getVersion()); + $module = $moduleManagerResult->getModule(); + if ($module) { + return $this->redirectRef($module->getArchiveName(), $module->getVersion()); + } + + return $this->redirectRef($archiveName, $version); } - public function invokeUpdate() + public function invokeDiscard() { if ($accessRedirect = $this->checkAccessRight()) { return $accessRedirect; @@ -505,41 +490,33 @@ public function invokeUpdate() $queryParams = $this->serverRequest->getQueryParams(); $archiveName = $queryParams['archiveName'] ?? ''; $version = $queryParams['version'] ?? ''; + $withTemplate = $queryParams['withTemplate'] ?? ''; + $withTemplate = $withTemplate === 'true' ? true : false; - $moduleLoader = LocalModuleLoader::createFromConfig(); - $module = $moduleLoader->loadByArchiveNameAndVersion($archiveName, $version); - - if (!$module) { - $this->addModuleNotFoundNotification($archiveName, $version); - return $this->redirect('/'); - } - - $newModule = $module; + $moduleManager = ModuleManager::createFromConfig(); + $moduleManagerResult = $moduleManager->discard($archiveName, $withTemplate); - try { - $newModule = $this->moduleInstaller->updateWithDependencies($module); - } catch (DependencyException $e) { + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { Notification::pushFlashMessage([ - 'text' => $e->getMessage(), + 'text' => (string) $moduleManagerResult->getMessage(), 'type' => 'error' ]); - } catch (RuntimeException $e) { + } else { Notification::pushFlashMessage([ - 'text' => $e->getMessage(), - 'type' => 'error' + 'text' => 'Module discard successfully', + 'type' => 'success' ]); } - if (!$newModule) { - $newestModule = $module->getNewestVersion(); - $this->addModuleNotFoundNotification($archiveName, $newestModule->getVersion()); - return $this->redirect('/'); + $module = $moduleManagerResult->getModule(); + if ($module) { + return $this->redirectRef($module->getArchiveName(), $module->getVersion()); } - return $this->redirectRef($archiveName, $newModule->getVersion()); + return $this->redirectRef($archiveName, $version); } - public function invokeLoadRemoteModule() + public function invokeUninstall() { if ($accessRedirect = $this->checkAccessRight()) { return $accessRedirect; @@ -549,76 +526,30 @@ public function invokeLoadRemoteModule() $archiveName = $queryParams['archiveName'] ?? ''; $version = $queryParams['version'] ?? ''; - $moduleLoader = RemoteModuleLoader::create(); - $module = $moduleLoader->loadByArchiveNameAndVersion($archiveName, $version); - - if (!$module) { - $this->addModuleNotFoundNotification($archiveName, $version); - return $this->redirect('/'); - } + $moduleManager = ModuleManager::createFromConfig(); + $moduleManagerResult = $moduleManager->uninstall($archiveName); - if (!$this->moduleInstaller->pull($module)) { + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { Notification::pushFlashMessage([ - 'text' => "Fehler: Das Module $archiveName - $version konnte nicht geladen werden.", + 'text' => (string) $moduleManagerResult->getMessage(), 'type' => 'error' ]); - } - - return $this->redirectRef($archiveName, $module->getVersion()); - } - - public function invokeLoadAndInstall() - { - if ($accessRedirect = $this->checkAccessRight()) { - return $accessRedirect; - } - - $queryParams = $this->serverRequest->getQueryParams(); - $archiveName = $queryParams['archiveName'] ?? ''; - $version = $queryParams['version'] ?? ''; - - $moduleLoader = RemoteModuleLoader::create(); - $module = $moduleLoader->loadByArchiveNameAndVersion($archiveName, $version); - - if (!$module) { - $this->addModuleNotFoundNotification($archiveName, $version); - return $this->redirect('/'); - } - - if (!$this->moduleInstaller->pull($module)) { + } else { Notification::pushFlashMessage([ - 'text' => "Fehler: Das Module $archiveName - $version konnte nicht geladen werden.", - 'type' => 'error' + 'text' => 'Module uninstalled successfully', + 'type' => 'success' ]); - return $this->redirect('/'); } - $moduleLoader = LocalModuleLoader::createFromConfig(); - $module = $moduleLoader->loadByArchiveNameAndVersion($archiveName, $version); - - if (!$module) { - $this->addModuleNotFoundNotification($archiveName, $version); - return $this->redirect('/'); + $module = $moduleManagerResult->getModule(); + if ($module) { + return $this->redirectRef($module->getArchiveName(), $module->getVersion()); } - try { - $this->moduleInstaller->installWithDependencies($module); - } catch (DependencyException $e) { - Notification::pushFlashMessage([ - 'text' => $e->getMessage(), - 'type' => 'error' - ]); - } catch (RuntimeException $e) { - Notification::pushFlashMessage([ - 'text' => $e->getMessage(), - 'type' => 'error' - ]); - } - - return $this->redirectRef($archiveName, $module->getVersion()); + return $this->redirectRef($archiveName, $version); } - public function invokeUnloadLocalModule() + public function invokeDelete() { if ($accessRedirect = $this->checkAccessRight()) { return $accessRedirect; @@ -628,29 +559,27 @@ public function invokeUnloadLocalModule() $archiveName = $queryParams['archiveName'] ?? ''; $version = $queryParams['version'] ?? ''; - $moduleLoader = LocalModuleLoader::createFromConfig(); - $module = $moduleLoader->loadByArchiveNameAndVersion($archiveName, $version); + $moduleManager = ModuleManager::createFromConfig(); + $moduleManagerResult = $moduleManager->delete($archiveName, $version); - if (!$module) { - $this->addModuleNotFoundNotification($archiveName, $version); - return $this->redirect('/'); - } - - try { - $this->moduleInstaller->delete($module); - } catch (DependencyException $e) { + if ($moduleManagerResult->getType() === ModuleManagerResult::TYPE_ERROR) { Notification::pushFlashMessage([ - 'text' => $e->getMessage(), + 'text' => (string) $moduleManagerResult->getMessage(), 'type' => 'error' ]); - } catch (RuntimeException $e) { + } else { Notification::pushFlashMessage([ - 'text' => $e->getMessage(), - 'type' => 'error' + 'text' => 'Module deleded successfully', + 'type' => 'success' ]); } - return $this->redirect('/'); + $module = $moduleManagerResult->getModule(); + if ($module) { + return $this->redirectRef($module->getArchiveName(), $module->getVersion()); + } + + return $this->redirectRef($archiveName, $version); } public function invokeReportIssue() diff --git a/src/Templates/ModuleInfo.tmpl.php b/src/Templates/ModuleInfo.tmpl.php index 13152c49..2eea5f9c 100644 --- a/src/Templates/ModuleInfo.tmpl.php +++ b/src/Templates/ModuleInfo.tmpl.php @@ -97,8 +97,7 @@ isRepairable()) { ?> - + Änderungen verwerfen @@ -112,6 +111,16 @@ + + + + + Änderungen verwerfen inkl. Templates + + + Änderungen übernehmen inkl. Templates (Link-Mode) + + isCompatibleLoadableAndInstallable()) { ?> @@ -127,7 +136,7 @@ Installieren isIncompatibleInstallable()) { ?> - Installieren (inkompatible Version) + Installieren (inkompatible Version) hasInstalledVersion()) { ?> Zur installierten Version From 20f47bd5506a5001404d5a6f08a790839b9bdb43 Mon Sep 17 00:00:00 2001 From: Robin Wieschendorf Date: Thu, 21 Dec 2023 16:35:15 +0100 Subject: [PATCH 12/17] chore: add old DependencyBuilder --- .../DependencyBuilderOld.php | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 src/Classes/DependencyManager/DependencyBuilderOld.php diff --git a/src/Classes/DependencyManager/DependencyBuilderOld.php b/src/Classes/DependencyManager/DependencyBuilderOld.php new file mode 100644 index 00000000..634bca67 --- /dev/null +++ b/src/Classes/DependencyManager/DependencyBuilderOld.php @@ -0,0 +1,213 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace RobinTheHood\ModifiedModuleLoaderClient\DependencyManager; + +use RobinTheHood\ModifiedModuleLoaderClient\App; +use RobinTheHood\ModifiedModuleLoaderClient\Config; +use RobinTheHood\ModifiedModuleLoaderClient\Loader\ModuleLoader; +use RobinTheHood\ModifiedModuleLoaderClient\Module; +use RobinTheHood\ModifiedModuleLoaderClient\Semver\Comparator; +use RobinTheHood\ModifiedModuleLoaderClient\Semver\Constraint; + +class DependencyBuilder +{ + /** @var ModuleTreeBuilder*/ + private $moduleTreeBuilder; + + /** @var ModuleLoader*/ + private $moduleLoader; + + public static function create(int $mode): DependencyBuilder + { + $moduleTreeBuilder = ModuleTreeBuilder::create($mode); + $moduleLoader = ModuleLoader::create($mode); + $dependencyBuilder = new DependencyBuilder($moduleTreeBuilder, $moduleLoader); + return $dependencyBuilder; + } + + public function __construct(ModuleTreeBuilder $moduleTreeBuilder, ModuleLoader $moduleLoader) + { + $this->moduleTreeBuilder = $moduleTreeBuilder; + $this->moduleLoader = $moduleLoader; + } + + private function logFile($value, $file) + { + if (!Config::getLogging()) { + return; + } + + $logsRootPath = App::getLogsRoot(); + + @mkdir($logsRootPath); + @mkdir($logsRootPath . '/debug'); + @mkdir($logsRootPath . '/debug/DependencyMananger/'); + $path = $logsRootPath . '/debug/DependencyMananger/' . $file; + file_put_contents($path, json_encode($value, JSON_PRETTY_PRINT)); + } + + public function log($var) + { + print_r($var); + } + + public function test() + { + $moduleLoader = ModuleLoader::create(Comparator::CARET_MODE_STRICT); + $module = $moduleLoader->loadLatestVersionByArchiveName('firstweb/multi-order'); + + if (!$module) { + die('Can not find base module'); + } + + $systemSet = new SystemSet(); + $systemSet->set([ + "modified" => '2.0.4.2', + "php" => '7.4.0', + "mmlc" => '1.19.0', + "composer/autoload" => '1.3.0', + "robinthehood/modified-std-module" => '0.9.0', + "robinthehood/modified-orm" => '1.8.1', + "robinthehood/pdf-bill" => '0.17.0' + ]); + + $this->log('TEST: satisfiesContraints1'); + $combinationSatisfyerResult = $this->satisfiesContraints1($module, $systemSet); + $this->log($combinationSatisfyerResult); + + $this->log('TEST: satisfiesContraints2'); + $combinationSatisfyerResult = $this->satisfiesContraints2('firstweb/multi-order', '^1.0.0', $systemSet); + $this->log($combinationSatisfyerResult); + + // var_dump('TEST: satisfiesContraints3'); + $combinationSatisfyerResult = $this->satisfies('firstweb/multi-order', '^1.0.0', $systemSet); + $this->log($combinationSatisfyerResult); + } + + public function satisfiesContraints1(Module $module, SystemSet $systemSet): CombinationSatisfyerResult + { + $moduleTrees = $this->moduleTreeBuilder->buildListByConstraints($module); + $this->logFile($moduleTrees, '1-moduleTrees.json'); + + $flatEntryBuilder = new FlatEntryBuilder(); + $flatEntries = $flatEntryBuilder->buildListFromModuleTrees($moduleTrees); + $this->logFile($flatEntries, '1-flatEntries.json'); + + $flatEntries = $flatEntryBuilder->fitSystemSet($flatEntries, $systemSet); + $this->logFile($flatEntries, '1-flatEntries-fit.json'); + + $combinationBuilder = new CombinationBuilder(); + $combinations = $combinationBuilder->buildAllFromModuleFlatEntries($flatEntries); + $this->logFile($combinations, '1-combinations.json'); + + $combinationSatisfyer = new CombinationSatisfyer(); + $combinationSatisfyerResult = $combinationSatisfyer->satisfiesCominationsFromModuleTrees( + $moduleTrees, + $combinations + ); + + return $combinationSatisfyerResult; + } + + + public function satisfiesContraints2( + string $archiveName, + string $constraint, + SystemSet $systemSet + ): CombinationSatisfyerResult { + $moduleTree = $this->moduleTreeBuilder->buildByConstraints($archiveName, $constraint); + $this->logFile($moduleTree, '2-moduleTrees.json'); + + $flatEntryBuilder = new FlatEntryBuilder(); + $flatEntries = $flatEntryBuilder->buildListFromModuleTree($moduleTree); + $this->logFile($flatEntries, '2-flatEntries.json'); + + $flatEntries = $flatEntryBuilder->fitSystemSet($flatEntries, $systemSet); + $this->logFile($flatEntries, '2-flatEntries-fit.json'); + + $combinationBuilder = new CombinationBuilder(); + $combinations = $combinationBuilder->buildAllFromModuleFlatEntries($flatEntries); + $this->logFile($combinations, '2-combinations.json'); + + $combinationSatisfyer = new CombinationSatisfyer(); + $combinationSatisfyerResult = $combinationSatisfyer->satisfiesCominationsFromModuleTree( + $moduleTree, + $combinations + ); + + return $combinationSatisfyerResult; + } + + public function satisfies(string $archiveName, string $constraint, SystemSet $systemSet): CombinationSatisfyerResult + { + $systemSet->remove($archiveName); + $constraint = $this->createConstraint($archiveName, $constraint, $systemSet); + + $moduleTree = $this->moduleTreeBuilder->buildByConstraints($archiveName, $constraint); + $this->logFile($moduleTree, '3-moduleTrees.json'); + + $flatEntryBuilder = new FlatEntryBuilder(); + $flatEntries = $flatEntryBuilder->buildListFromModuleTree($moduleTree); + $this->logFile($flatEntries, '3-flatEntries.json'); + + $flatEntries = $flatEntryBuilder->fitSystemSet($flatEntries, $systemSet); + $this->logFile($flatEntries, '3-flatEntries-fit.json'); + + $combinationIterator = new CombinationIterator($flatEntries); + $combinationSatisfyer = new CombinationSatisfyer(); + $combinationSatisfyerResult = $combinationSatisfyer->satisfiesCombinationsFromModuleWithIterator( + $moduleTree, + $combinationIterator + ); + return $combinationSatisfyerResult; + } + + /** + * Gehe alle Module durch, die in $systemSet sind und das gleichzeitig Modul $archiveName benötigen. + * Gibt ein $constraint zurück, sodass die Anforderungenden der Module in $systemSet erhaltenbleiben. + */ + private function createConstraint(string $archiveName, string $constraint, SystemSet $systemSet): string + { + /** @var string[] */ + $requiredConstraints = [$constraint]; + + $archives = $systemSet->getArchives(); + foreach ($archives as $archiveNameB => $version) { + $installedModule = $this->getModuleByArchiveNameAndVersion($archiveNameB, $version); + if (!$installedModule) { + continue; + } + + $requiredConstraint = $this->getRequiredConstraint($installedModule, $archiveName); + if (!$requiredConstraint) { + continue; + } + + $requiredConstraints[] = $requiredConstraint; + } + + $constraint = Constraint::createConstraintFromConstraints($requiredConstraints); + + return $constraint; + } + + private function getModuleByArchiveNameAndVersion(string $archiveName, string $version): ?Module + { + return $this->moduleLoader->loadByArchiveNameAndVersion($archiveName, $version); + } + + private function getRequiredConstraint(Module $installedModule, string $archiveName): string + { + $required = $installedModule->getRequire(); + return $required[$archiveName] ?? ''; + } +} From da62b117ff36ef78abc6102acbec7367759ee5aa Mon Sep 17 00:00:00 2001 From: Robin Wieschendorf Date: Thu, 21 Dec 2023 21:01:56 +0100 Subject: [PATCH 13/17] refactor: remove unused code --- src/Classes/Controllers/IndexController.php | 3 - src/Classes/Module.php | 2 - src/Classes/ModuleInstaller.php | 465 ------------------ src/Classes/ViewModels/ModuleViewModel.php | 1 - .../DependencyBuilderTest.php | 8 - 5 files changed, 479 deletions(-) delete mode 100644 src/Classes/ModuleInstaller.php diff --git a/src/Classes/Controllers/IndexController.php b/src/Classes/Controllers/IndexController.php index f59e1a9e..56ba3011 100644 --- a/src/Classes/Controllers/IndexController.php +++ b/src/Classes/Controllers/IndexController.php @@ -18,13 +18,11 @@ use RobinTheHood\ModifiedModuleLoaderClient\App; use RobinTheHood\ModifiedModuleLoaderClient\Loader\ModuleLoader; use RobinTheHood\ModifiedModuleLoaderClient\Loader\LocalModuleLoader; -use RobinTheHood\ModifiedModuleLoaderClient\Loader\RemoteModuleLoader; use RobinTheHood\ModifiedModuleLoaderClient\ModuleFilter; use RobinTheHood\ModifiedModuleLoaderClient\ModuleSorter; use RobinTheHood\ModifiedModuleLoaderClient\Category; use RobinTheHood\ModifiedModuleLoaderClient\SendMail; use RobinTheHood\ModifiedModuleLoaderClient\Config; -use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\DependencyException; use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\DependencyManager; use RobinTheHood\ModifiedModuleLoaderClient\MmlcVersionInfoLoader; use RobinTheHood\ModifiedModuleLoaderClient\ModuleInstaller; @@ -33,7 +31,6 @@ use RobinTheHood\ModifiedModuleLoaderClient\ModuleStatus; use RobinTheHood\ModifiedModuleLoaderClient\Notification; use RobinTheHood\ModifiedModuleLoaderClient\SelfUpdater; -use RuntimeException; class IndexController extends Controller { diff --git a/src/Classes/Module.php b/src/Classes/Module.php index 871aad9d..b1b99a2c 100644 --- a/src/Classes/Module.php +++ b/src/Classes/Module.php @@ -23,8 +23,6 @@ use RobinTheHood\ModifiedModuleLoaderClient\Loader\ModuleLoader; use RobinTheHood\ModifiedModuleLoaderClient\Loader\LocalModuleLoader; use RobinTheHood\ModifiedModuleLoaderClient\Helpers\FileHelper; -use RobinTheHood\ModifiedModuleLoaderClient\Semver\Comparator; -use RobinTheHood\ModifiedModuleLoaderClient\Semver\Parser; class Module extends ModuleInfo { diff --git a/src/Classes/ModuleInstaller.php b/src/Classes/ModuleInstaller.php deleted file mode 100644 index 5116780a..00000000 --- a/src/Classes/ModuleInstaller.php +++ /dev/null @@ -1,465 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace RobinTheHood\ModifiedModuleLoaderClient; - -use RobinTheHood\ModifiedModuleLoaderClient\App; -use RobinTheHood\ModifiedModuleLoaderClient\Archive; -use RobinTheHood\ModifiedModuleLoaderClient\Config; -use RobinTheHood\ModifiedModuleLoaderClient\FileInfo; -use RobinTheHood\ModifiedModuleLoaderClient\Api\V1\ApiRequest; -use RobinTheHood\ModifiedModuleLoaderClient\Archive\Archive as ArchiveArchive; -use RobinTheHood\ModifiedModuleLoaderClient\Archive\ArchiveHandler; -use RobinTheHood\ModifiedModuleLoaderClient\Archive\ArchiveName; -use RobinTheHood\ModifiedModuleLoaderClient\Archive\ArchivePuller; -use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\Combination; -use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\DependencyManager; -use RobinTheHood\ModifiedModuleLoaderClient\Loader\LocalModuleLoader; -use RobinTheHood\ModifiedModuleLoaderClient\Helpers\FileHelper; -use RobinTheHood\ModifiedModuleLoaderClient\Logger\LogLevel; -use RobinTheHood\ModifiedModuleLoaderClient\Logger\StaticLogger; -use RobinTheHood\ModifiedModuleLoaderClient\ModuleHasher\ModuleHashFileCreator; -use RuntimeException; - -class ModuleInstaller -{ - /** @var DependencyManager */ - private $dependencyManager; - - /** @var ModuleFilter */ - private $moduleFilter; - - /** @var LocalModuleLoader */ - private $localModuleLoader; - - /** @var ArchivePuller */ - private $archivePuller; - - /** @var ArchiveHandler */ - private $archiveHandler; - - - - // new ArchiveHandler($this->localModuleLoader, App::getModulesRoot()); - public static function create(int $mode): ModuleInstaller - { - $dependencyManager = DependencyManager::create($mode); - $moduleFilter = ModuleFilter::create($mode); - $localModuleLoader = LocalModuleLoader::create($mode); - $archivePuller = ArchivePuller::create(); - $archiveHandler = ArchiveHandler::create($mode); - $moduleInstaller = new ModuleInstaller( - $dependencyManager, - $moduleFilter, - $localModuleLoader, - $archivePuller, - $archiveHandler - ); - return $moduleInstaller; - } - - public static function createFromConfig(): ModuleInstaller - { - return self::create(Config::getDependenyMode()); - } - - public function __construct( - DependencyManager $dependencyManager, - ModuleFilter $moduleFilter, - LocalModuleLoader $localModuleLoader, - ArchivePuller $archivePuller, - ArchiveHandler $archiveHandler - ) { - $this->dependencyManager = $dependencyManager; - $this->moduleFilter = $moduleFilter; - $this->localModuleLoader = $localModuleLoader; - $this->archivePuller = $archivePuller; - $this->archiveHandler = $archiveHandler; - } - - public function pull(Module $module): bool - { - if ($module->isLoaded()) { - return true; - } - - $apiRequest = new ApiRequest(); - $result = $apiRequest->getArchive($module->getArchiveName(), $module->getVersion()); - - $content = $result['content'] ?? []; - if (!$content) { - return false; - } - - $archiveUrl = $content['archiveUrl'] ?? ''; - - if (!$archiveUrl) { - return false; - } - - try { - // New - $archive = $this->archivePuller->pull($module->getArchiveName(), $module->getVersion(), $archiveUrl); - $this->archiveHandler->extract($archive); - return true; - - // Old - $archive = Archive::pullArchive($archiveUrl, $module->getArchiveName(), $module->getVersion()); - $archive->untarArchive(); - return true; - } catch (RuntimeException $e) { - //Can not pull Archive - return false; - } - } - - public function delete(Module $module) - { - $path = $module->getLocalRootPath() . $module->getModulePath(); - - $filePaths = FileHelper::scanDirRecursive($path, FileHelper::FILES_ONLY); - - $dirPaths = FileHelper::scanDirRecursive($path, FileHelper::DIRS_ONLY); - $dirPaths = array_reverse($dirPaths); - $dirPaths[] = $path; - $dirPaths[] = dirname($path); - - // Delete Files - foreach ($filePaths as $path) { - if (file_exists($path)) { - unlink($path); - } - } - - // Delete Folders - foreach ($dirPaths as $path) { - if (file_exists($path)) { - @rmdir($path); - } - } - } - - public function install(Module $module, bool $force = false): void - { - if (!$force) { - $this->dependencyManager->canBeInstalled($module); - } - - $this->internalInstall($module); - $this->createAutoloadFile(); - } - - public function installWithDependencies(Module $module): void - { - $combinationSatisfyerResult = $this->dependencyManager->canBeInstalled($module, ['']); - - if (!$combinationSatisfyerResult->foundCombination) { - $message = - "Can not install module {$module->getArchiveName()} {$module->getVersion()} with dependencies. " - . "No possible combination of versions found"; - StaticLogger::log(LogLevel::WARNING, $message); - // NOTE: Vielleicht neue class ModuleException hinzufügen - throw new RuntimeException($message); - } - - $this->uninstall($module); - $this->internalInstall($module); - $this->internalInstallDependencies($module, $combinationSatisfyerResult->foundCombination); - $this->createAutoloadFile(); - } - - private function internalInstall(Module $module): void - { - // Install Source Files to Shop Root - $files = $module->getSrcFilePaths(); - - foreach ($files as $file) { - $src = $module->getLocalRootPath() . $module->getSrcRootPath() . '/' . $file; - - $files = $module->getTemplateFiles($file); - foreach ($files as $file) { - $overwrite = false; - if (!FileInfo::isTemplateFile($file)) { - $overwrite = true; - } - - $file = ModulePathMapper::moduleSrcToShopRoot($file); - - $dest = App::getShopRoot() . $file; - $this->installFile($src, $dest, $overwrite); - } - } - - // Install Source Mmlc Files to shop vendor-mmlc - $files = $module->getSrcMmlcFilePaths(); - foreach ($files as $file) { - $src = $module->getLocalRootPath() . $module->getSrcMmlcRootPath() . '/' . $file; - $file = ModulePathMapper::moduleSrcMmlcToShopVendorMmlc($file, $module->getArchiveName()); - $dest = App::getShopRoot() . '/' . $file; - $this->installFile($src, $dest, true); - } - - $moduleHashFileCreator = new ModuleHashFileCreator(); - $moduleHashFile = $moduleHashFileCreator->createHashFile($module); - $moduleHashFile->writeTo($module->getHashPath()); - } - - private function internalPullAndInstall(Module $module): void - { - if (!$module->isLoaded()) { - $this->pull($module); - } - - $reloaded = $this->reload($module); - - if (!$reloaded->isLoaded()) { - $message = - "Can not pull and install module {$module->getArchiveName()} {$module->getVersion()}. " - . "Module is not loaded."; - StaticLogger::log(LogLevel::WARNING, $message); - // NOTE: Vielleicht neue class ModuleOperationException hinzufügen - throw new RuntimeException($message); - } - - if ($reloaded->isInstalled()) { - return; - } - - $this->uninstall($module); - $this->internalInstall($module); - } - - private function internalInstallDependencies(Module $parentModule, Combination $combination): void - { - $modules = $this->dependencyManager->getAllModulesFromCombination($combination); - - foreach ($modules as $module) { - if ($parentModule->getArchiveName() === $module->getArchiveName()) { - continue; - } - $this->internalPullAndInstall($module); - } - } - - public function update(Module $module): ?Module - { - $installedModule = $module->getInstalledVersion(); - $newModule = $module->getNewestVersion(); - - $combinationSatisfyerResult = $this->dependencyManager->canBeInstalled($module); - - if (!$combinationSatisfyerResult->foundCombination) { - $message = - "Can not update module {$module->getArchiveName()} {$module->getVersion()}. " - . "No possible combination of versions found"; - StaticLogger::log(LogLevel::WARNING, $message); - // NOTE: Vielleicht neue class ModuleException hinzufügen - throw new RuntimeException($message); - } - - if ($installedModule) { - $this->uninstall($installedModule); - } - - $this->pull($newModule); - $newModule = $this->reload($newModule); - - $this->internalInstall($newModule); - $this->createAutoloadFile(); - - return $newModule; - } - - public function updateWithDependencies(Module $module): ?Module - { - $installedModule = $module->getInstalledVersion(); - $newModule = $module->getNewestVersion(); - - $combinationSatisfyerResult = $this->dependencyManager->canBeInstalled($module); - - if (!$combinationSatisfyerResult->foundCombination) { - $message = - "Can not update module {$module->getArchiveName()} {$module->getVersion()} with dependencies. " - . "No possible combination of versions found"; - StaticLogger::log(LogLevel::WARNING, $message); - // NOTE: Vielleicht neue class ModuleException hinzufügen - throw new RuntimeException($message); - } - - if ($installedModule) { - $this->uninstall($installedModule); - } - - $this->pull($newModule); - $newModule = $this->reload($newModule); - - $this->internalInstall($newModule); - $this->internalInstallDependencies($newModule, $combinationSatisfyerResult->foundCombination); - $this->createAutoloadFile(); - - return $newModule; - } - - private function reload(Module $module): Module - { - $this->localModuleLoader->resetCache(); - $reloadedModule = $this->localModuleLoader->loadByArchiveNameAndVersion( - $module->getArchiveName(), - $module->getVersion() - ); - - if (!$reloadedModule) { - $message = "Can not reload module {$module->getArchiveName()} {$module->getVersion()}"; - StaticLogger::log(LogLevel::WARNING, $message); - // NOTE: Vielleicht neue class ModuleException hinzufügen - throw new RuntimeException($message); - } - - return $reloadedModule; - } - - public function revertChanges(Module $module): void - { - if (!$module->isInstalled()) { - $message = - "Can not revert changes because {$module->getArchiveName()} {$module->getVersion()} is not installed."; - StaticLogger::log(LogLevel::WARNING, $message); - // NOTE: Vielleicht neue class ModuleException hinzufügen - throw new RuntimeException($message); - } - - $this->internalInstall($module); - } - - private function createAutoloadFile(): void - { - $this->localModuleLoader->resetCache(); - $localModules = $this->localModuleLoader->loadAllVersions(); - $installedLocalModules = $this->moduleFilter->filterInstalled($localModules); - - $namespaceEntrys = []; - foreach ($installedLocalModules as $module) { - $autoload = $module->getAutoload(); - - if (!$autoload) { - continue; - } - - if (!$autoload['psr-4']) { - continue; - } - - foreach ($autoload['psr-4'] as $namespace => $path) { - $path = str_replace($module->getSourceMmlcDir(), 'vendor-mmlc/' . $module->getArchiveName(), $path); - $namespaceEntrys[] = - '$loader->setPsr4(\'' . $namespace . '\\\', DIR_FS_DOCUMENT_ROOT . \'' . $path . '\');'; - } - } - - $namespaceEntrys = array_unique($namespaceEntrys); - $namespaceMapping = implode("\n", $namespaceEntrys); - - $template = \file_get_contents(App::getTemplatesRoot() . '/autoload.php.tmpl'); - $template = \str_replace('{VENDOR_PSR4_NAMESPACE_MAPPINGS}', $namespaceMapping, $template); - - if (!file_exists(App::getShopRoot() . '/vendor-no-composer')) { - mkdir(App::getShopRoot() . '/vendor-no-composer'); - } - \file_put_contents(App::getShopRoot() . '/vendor-no-composer/autoload.php', $template); - - if (!file_exists(App::getShopRoot() . '/vendor-mmlc')) { - mkdir(App::getShopRoot() . '/vendor-mmlc'); - } - \file_put_contents(App::getShopRoot() . '/vendor-mmlc/autoload.php', $template); - } - - public function uninstall(Module $module): bool - { - $installedModule = $module->getInstalledVersion(); - if (!$installedModule) { - return false; - } - - if ($installedModule->isChanged()) { - $message = - "Can not uninstall module {$installedModule->getArchiveName()} {$installedModule->getVersion()} " - . "because the module has changes."; - StaticLogger::log(LogLevel::WARNING, $message); - // NOTE: Vielleicht neue class ModuleException hinzufügen - throw new RuntimeException($message); - } - - $this->internalUninstall($installedModule); - $this->createAutoloadFile(); - - return true; - } - - private function internalUninstall(Module $module): void - { - // Uninstall from shop-root - $files = $module->getSrcFilePaths(); - foreach ($files as $file) { - $file = ModulePathMapper::moduleSrcToShopRoot($file); - $dest = App::getShopRoot() . $file; - $this->uninstallFile($dest); - } - - // Uninstall from shop-vendor-mmlc - $files = $module->getSrcMmlcFilePaths(); - foreach ($files as $file) { - $file = ModulePathMapper::moduleSrcMmlcToShopVendorMmlc($file, $module->getArchiveName()); - $dest = App::getShopRoot() . $file; - $this->uninstallFile($dest); - FileHelper::deletePathIsEmpty($dest); - } - - if (file_exists($module->getHashPath())) { - unlink($module->getHashPath()); - } - } - - private function installFile(string $src, string $dest, bool $overwrite = false): bool - { - if (!file_exists($src)) { - return false; - } - - if ($overwrite == false && (file_exists($dest) || is_link($dest))) { - return false; - } elseif ($overwrite == true && (file_exists($dest) || is_link($dest))) { - unlink($dest); - } - - FileHelper::makeDirIfNotExists($dest); - - if (file_exists($dest) || is_link($dest)) { - return false; - } - - if (Config::getInstallMode() == 'link') { - symlink($src, $dest); - } else { - copy($src, $dest); - } - - return true; - } - - private function uninstallFile(string $dest): void - { - if (file_exists($dest)) { - unlink($dest); - } - } -} diff --git a/src/Classes/ViewModels/ModuleViewModel.php b/src/Classes/ViewModels/ModuleViewModel.php index aadadc58..460aeebb 100644 --- a/src/Classes/ViewModels/ModuleViewModel.php +++ b/src/Classes/ViewModels/ModuleViewModel.php @@ -16,7 +16,6 @@ use RobinTheHood\ModifiedModuleLoaderClient\App; use RobinTheHood\ModifiedModuleLoaderClient\Module; use RobinTheHood\ModifiedModuleLoaderClient\ModuleStatus; -use RobinTheHood\ModifiedModuleLoaderClient\Notification; use RobinTheHood\ModifiedModuleLoaderClient\Semver\ParseErrorException; use RobinTheHood\ModifiedModuleLoaderClient\ShopInfo; diff --git a/tests/unit/DependencyManager/DependencyBuilderTest.php b/tests/unit/DependencyManager/DependencyBuilderTest.php index 8138ac38..0bc81a6d 100644 --- a/tests/unit/DependencyManager/DependencyBuilderTest.php +++ b/tests/unit/DependencyManager/DependencyBuilderTest.php @@ -17,7 +17,6 @@ use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\CombinationSatisfyerResult; use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\DependencyBuilder; use RobinTheHood\ModifiedModuleLoaderClient\DependencyManager\SystemSet; -use RobinTheHood\ModifiedModuleLoaderClient\ModuleFilter; use RobinTheHood\ModifiedModuleLoaderClient\Semver\Comparator; class DependencyBuilderTest extends TestCase @@ -122,11 +121,4 @@ public function testSatisfies2() $combinationSatisfyerResult->result ); } - - public function atestInvokeDependency() - { - $dpb = DependencyBuilder::create(Comparator::CARET_MODE_STRICT); - $dpb->test(); - die('TEST DONE'); - } } From f3bf65d65474936fe8a8e90d707b9487a934bff9 Mon Sep 17 00:00:00 2001 From: Robin Wieschendorf Date: Thu, 21 Dec 2023 21:02:48 +0100 Subject: [PATCH 14/17] style: improve coding style --- src/Classes/ModuleHasher/ModuleHasher.php | 3 ++- src/Classes/ViewModels/ModuleViewModel.php | 9 ++++++--- src/Templates/ModuleListingModule.tmpl.php | 4 ++++ src/Templates/Navi.tmpl.php | 11 +++++++++-- src/Templates/ReportIssue.tmpl.php | 4 ++++ src/Templates/SignIn.tmpl.php | 9 ++++++++- src/Templates/Support.tmpl.php | 4 ++++ 7 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/Classes/ModuleHasher/ModuleHasher.php b/src/Classes/ModuleHasher/ModuleHasher.php index d67d96df..04dedade 100644 --- a/src/Classes/ModuleHasher/ModuleHasher.php +++ b/src/Classes/ModuleHasher/ModuleHasher.php @@ -75,7 +75,8 @@ public function createShopRootHashes(Module $module): HashEntryCollection public function createShopVendorMmlcHashes(Module $module): HashEntryCollection { $files = $module->getSrcMmlcFilePaths(); - $root = App::getShopRoot() . '/' . ModulePathMapper::moduleSrcMmlcToShopVendorMmlc('/', $module->getArchiveName()); + $root = + App::getShopRoot() . '/' . ModulePathMapper::moduleSrcMmlcToShopVendorMmlc('/', $module->getArchiveName()); return $this->fileHasher->createHashes($files, $root, self::SCOPE_SHOP_VENDOR_MMLC); } } diff --git a/src/Classes/ViewModels/ModuleViewModel.php b/src/Classes/ViewModels/ModuleViewModel.php index 460aeebb..6762e508 100644 --- a/src/Classes/ViewModels/ModuleViewModel.php +++ b/src/Classes/ViewModels/ModuleViewModel.php @@ -240,7 +240,8 @@ public function getCompatibleStrings(): array if (!$this->module->isCompatibleWithModified()) { $version = ShopInfo::getModifiedVersion(); $array[] = [ - 'text' => "Dieses Modul wurde nicht mit deiner Version von modified getestet. Du hast modifed Version $version installiert.", + 'text' => "Dieses Modul wurde nicht mit deiner Version von modified getestet. Du hast modifed Version + $version installiert.", 'type' => 'warning' ]; } @@ -249,7 +250,8 @@ public function getCompatibleStrings(): array if (!$this->module->isCompatibleWithPhp()) { $version = phpversion(); $array[] = [ - 'text' => "Dieses Modul wurde noch nicht mit deiner PHP Version getestet. Du verwendest die PHP Version $version.", + 'text' => "Dieses Modul wurde noch nicht mit deiner PHP Version getestet. Du verwendest die PHP + Version $version.", 'type' => 'warning' ]; } @@ -264,7 +266,8 @@ public function getCompatibleStrings(): array if (!$this->module->isCompatibleWithMmlc()) { $version = App::getMmlcVersion(); $array[] = [ - 'text' => "Dieses Modul wurde noch nicht mit deiner MMLC Version getestet. Du verwendest die MMLC Version $version.", + 'text' => "Dieses Modul wurde noch nicht mit deiner MMLC Version getestet. Du verwendest die MMLC + Version $version.", 'type' => 'warning' ]; } diff --git a/src/Templates/ModuleListingModule.tmpl.php b/src/Templates/ModuleListingModule.tmpl.php index 02beaeb3..d3910bdf 100644 --- a/src/Templates/ModuleListingModule.tmpl.php +++ b/src/Templates/ModuleListingModule.tmpl.php @@ -1,5 +1,9 @@ + +/** + * @phpcs:disable Generic.Files.LineLength.TooLong + */ + +defined('LOADED_FROM_INDEX') && LOADED_FROM_INDEX ?? die('Access denied.'); + +use RobinTheHood\ModifiedModuleLoaderClient\ShopInfo; +?>