Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.06% covered (success)
97.06%
66 / 68
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApplicationManager
97.06% covered (success)
97.06%
66 / 68
80.00% covered (warning)
80.00%
8 / 10
35
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadInfo
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 install
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 uninstall
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 reInit
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getProvidingForModule
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getInstalledApplications
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
8
 installFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 uninstallFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replacePlaceholder
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * Jingga
5 *
6 * PHP Version 8.1
7 *
8 * @package   phpOMS\Application
9 * @copyright Dennis Eichhorn
10 * @license   OMS License 2.0
11 * @version   1.0.0
12 * @link      https://jingga.app
13 */
14declare(strict_types=1);
15
16namespace phpOMS\Application;
17
18use phpOMS\System\File\Local\Directory;
19use phpOMS\System\File\PathException;
20
21/**
22 * Application manager class.
23 *
24 * General application managing functionality.
25 *
26 * @package phpOMS\Application
27 * @license OMS License 2.0
28 * @link    https://jingga.app
29 * @since   1.0.0
30 */
31final class ApplicationManager
32{
33    /**
34     * Application instance.
35     *
36     * @var ApplicationAbstract
37     * @since 1.0.0
38     */
39    private ApplicationAbstract $app;
40
41    /**
42     * Installed modules.
43     *
44     * @var array<string, ApplicationInfo>
45     * @since 1.0.0
46     */
47    private array $installed = [];
48
49    /**
50     * Constructor.
51     *
52     * @param ApplicationAbstract $app Application
53     *
54     * @since 1.0.0
55     */
56    public function __construct(ApplicationAbstract $app)
57    {
58        $this->app = $app;
59    }
60
61    /**
62     * Load info of application.
63     *
64     * @param string $appPath Application path
65     *
66     * @return ApplicationInfo
67     *
68     * @throws PathException
69     *
70     * @since 1.0.0
71     */
72    private function loadInfo(string $appPath) : ApplicationInfo
73    {
74        $path = \realpath($appPath);
75        if ($path === false) {
76            throw new PathException($appPath);
77        }
78
79        $info = new ApplicationInfo($path);
80        $info->load();
81
82        return $info;
83    }
84
85    /**
86     * Install the application
87     *
88     * @param string $source      Source of the application
89     * @param string $destination Destination of the application
90     * @param string $theme       Theme
91     *
92     * @return bool
93     *
94     * @since 1.0.0
95     */
96    public function install(string $source, string $destination, string $theme = 'Default') : bool
97    {
98        $destination = \rtrim($destination, '\\/');
99        $source      = \rtrim($source, '/\\');
100
101        if (!\is_dir($source) || \is_dir($destination)
102            || !\is_file($source . '/Admin/Installer.php')
103        ) {
104            return false;
105        }
106
107        try {
108            $info                                      = $this->loadInfo($source . '/info.json');
109            $this->installed[$info->getInternalName()] = $info;
110
111            $this->installFiles($source, $destination);
112            $this->replacePlaceholder($destination);
113
114            if (($path = \realpath($destination)) === false) {
115                return false; // @codeCoverageIgnore
116            }
117
118            $classPath = \substr($path . '/Admin/Installer', (int) \strlen((string) \realpath(__DIR__ . '/../../')));
119
120            // @var class-string<InstallerAbstract> $class
121            $class = \strtr($classPath, '/', '\\');
122            $class::install($this->app, $info, $this->app->appSettings);
123
124            return true;
125        } catch (\Throwable $_) {
126            return false; // @codeCoverageIgnore
127        }
128    }
129
130    /**
131     * Uninstall the application
132     *
133     * @param string $source Source of the application
134     *
135     * @return bool
136     *
137     * @since 1.0.0
138     */
139    public function uninstall(string $source) : bool
140    {
141        $source = \rtrim($source, '/\\');
142        if (($path = \realpath($source)) === false || !\is_file($source . '/Admin/Uninstaller.php')) {
143            return false;
144        }
145
146        try {
147            $info                                      = $this->loadInfo($source . '/info.json');
148            $this->installed[$info->getInternalName()] = $info;
149
150            $classPath = \substr($path . '/Admin/Uninstaller', (int) \strlen((string) \realpath(__DIR__ . '/../../')));
151
152            // @var class-string<UninstallerAbstract> $class
153            $class = \strtr($classPath, '/', '\\');
154            $class::uninstall($this->app->dbPool, $info, $this->app->appSettings);
155
156            $this->uninstallFiles($source);
157
158            return true;
159        } catch (\Throwable $_) {
160            return false; // @codeCoverageIgnore
161        }
162    }
163
164    /**
165     * Re-init application.
166     *
167     * @param string $appPath App path
168     *
169     * @return void
170     *
171     * @since 1.0.0
172     */
173    public function reInit(string $appPath) : void
174    {
175        $info = $this->loadInfo($appPath . '/info.json');
176        if ($info === null) {
177            return;
178        }
179
180        if (($path = \realpath($appPath)) === false) {
181            return; // @codeCoverageIgnore
182        }
183
184        // @var class-string<InstallerAbstract> $class
185        $classPath = \substr($path . '/Admin/Installer', (int) \strlen((string) \realpath(__DIR__ . '/../../')));
186        $class     = \strtr($classPath, '/', '\\');
187
188        /** @var $class InstallerAbstract */
189        $class::reInit($info);
190    }
191
192    /**
193     * Get all applications who are providing for a specific module.
194     *
195     * @param string $module Module to check providings for
196     *
197     * @return array<string, string[]>
198     *
199     * @since 1.0.0
200     */
201    public function getProvidingForModule(string $module) : array
202    {
203        $providing = [];
204        $installed = $this->getInstalledApplications();
205
206        foreach ($installed as $app => $info) {
207            if (!isset($providing[$app])) {
208                $providing[$app] = [];
209            }
210
211            $appProviding = $info->getProviding();
212            foreach ($appProviding as $for => $version) {
213                if ($for !== $module) {
214                    continue;
215                }
216
217                $providing[$app][] = $for;
218            }
219        }
220
221        return $providing;
222    }
223
224    /**
225     * Get all installed modules.
226     *
227     * @param bool   $useCache Use Cache
228     * @param string $basePath Base path for the applications
229     *
230     * @return array<string, ApplicationInfo>
231     *
232     * @since 1.0.0
233     */
234    public function getInstalledApplications(bool $useCache = true, string $basePath = __DIR__ . '/../../Web') : array
235    {
236        if (empty($this->installed) || !$useCache) {
237            $apps = \scandir($basePath);
238
239            if ($apps === false) {
240                return $this->installed; // @codeCoverageIgnore
241            }
242
243            foreach ($apps as $app) {
244                if ($app === '.' || $app === '..' || !\is_file($basePath . '/' . $app . '/info.json')) {
245                    continue;
246                }
247
248                $this->installed[$app] = $this->loadInfo($basePath . '/' . $app . '/info.json');
249            }
250        }
251
252        return $this->installed;
253    }
254
255    /**
256     * Install the files to the destination
257     *
258     * @param string $source      Source path
259     * @param string $destination Destination of the application
260     *
261     * @return void
262     *
263     * @since 1.0.0
264     */
265    private function installFiles(string $source, string $destination) : void
266    {
267        Directory::copy($source, $destination);
268    }
269
270    /**
271     * Uninstall files
272     *
273     * @param string $source Source path
274     *
275     * @return void
276     *
277     * @since 1.0.0
278     */
279    private function uninstallFiles(string $source) : void
280    {
281        Directory::delete($source);
282    }
283
284    /**
285     * Replace placeholder string (application placeholder name)
286     *
287     * @param string $destination Destination of the application
288     *
289     * @return void
290     *
291     * @since 1.0.0
292     */
293    private function replacePlaceholder(string $destination) : void
294    {
295        $files = Directory::list($destination, '*', true);
296        foreach ($files as $file) {
297            if (!\is_file($destination . '/' . $file)) {
298                continue;
299            }
300
301            $content = \file_get_contents($destination . '/' . $file);
302            if ($content === false) {
303                continue; // @codeCoverageIgnore
304            }
305
306            \file_put_contents($destination . '/' . $file, \str_replace('{APPNAME}', \basename($destination), $content));
307        }
308    }
309}