Completed
Push — master ( e4f005...8391dd )
by ANTHONIUS
03:42
created

AssetsInstaller::getRootDir()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Yawik project.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
11
namespace Yawik\Composer;
12
13
use Core\Application;
14
use Core\Asset\AssetProviderInterface;
15
use Core\Options\ModuleOptions;
16
use Psr\Log\LoggerInterface;
17
use Psr\Log\LogLevel;
18
use Symfony\Component\Console\Input\InputInterface;
19
use Symfony\Component\Filesystem\Filesystem;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Console\Style\SymfonyStyle;
22
use Symfony\Component\Finder\Finder;
23
use Zend\ModuleManager\ModuleManager;
24
25
/**
26
 * Class AssetsInstaller
27
 * @package Yawik\Composer
28
 * @author  Anthonius Munthi <[email protected]>
29
 * @TODO    Create more documentation for methods
30
 */
31
class AssetsInstaller
32
{
33
    const METHOD_COPY               = 'copy';
34
    const METHOD_ABSOLUTE_SYMLINK   = 'absolute symlink';
35
    const METHOD_RELATIVE_SYMLINK   = 'relative symlink';
36
37
    /**
38
     * @var Filesystem
39
     */
40
    private $filesystem;
41
42
    /**
43
     * @var OutputInterface
44
     */
45
    private $output;
46
47
    /**
48
     * @var InputInterface
49
     */
50
    private $input;
51
52
    /**
53
     * @var LoggerInterface
54
     */
55
    private $logger;
56
57
    /**
58
     * The root dir of Yawik application
59
     * @var string
60
     */
61
    private $rootDir;
0 ignored issues
show
introduced by
The private property $rootDir is not used, and could be removed.
Loading history...
62
63
    /**
64
     * @var Application
65
     */
66
    private $application;
67
68 7
    public function __construct()
69
    {
70 7
        umask(0000);
71 7
        $this->filesystem = new Filesystem();
72
        // @codeCoverageIgnoreStart
73
        if (!class_exists('Core\\Application')) {
74
            include_once __DIR__.'/../../../autoload.php';
75
        }
76
        // @codeCoverageIgnoreEnd
77
78 7
        $this->application = Application::init();
79 7
    }
80
81
    /**
82
     * Set a logger to use
83
     * @param LoggerInterface $logger
84
     */
85
    public function setLogger(LoggerInterface $logger)
86
    {
87
        $this->logger = $logger;
88
89
        return;
90
    }
91
92 1
    public function setFilesystem(Filesystem $filesystem)
93
    {
94 1
        $this->filesystem = $filesystem;
95 1
    }
96
97
    /**
98
     * @return OutputInterface
99
     */
100
    public function getOutput()
101
    {
102
        return $this->output;
103
    }
104
105
    /**
106
     * @param OutputInterface $output
107
     * @return AssetsInstaller
108
     */
109 7
    public function setOutput($output)
110
    {
111 7
        $this->output = $output;
112 7
        return $this;
113
    }
114
115
    /**
116
     * @param InputInterface $input
117
     * @return AssetsInstaller
118
     */
119 7
    public function setInput($input)
120
    {
121 7
        $this->input = $input;
122 7
        return $this;
123
    }
124
125
    /**
126
     * Install modules assets with the given $modules.
127
     * $modules should within this format:
128
     *
129
     * [module_name] => module_public_directory
130
     *
131
     * @param array     $modules An array of modules
132
     * @param string    $expectedMethod Expected install method
133
     */
134 5
    public function install($modules, $expectedMethod = self::METHOD_RELATIVE_SYMLINK)
135
    {
136 5
        $publicDir      = $this->getModuleAssetDir();
137 5
        $loadedModules  = $this->scanInstalledModules();
138 5
        $modules        = array_merge($modules, $loadedModules);
139 5
        $rows           = [];
140 5
        $exitCode       = 0;
141 5
        $copyUsed       = false;
142
143 5
        foreach ($modules as $name => $originDir) {
144 5
            $targetDir = $publicDir.DIRECTORY_SEPARATOR.$name;
145 5
            $message = $name;
146
            try {
147 5
                $this->filesystem->remove($targetDir);
148 5
                if (self::METHOD_RELATIVE_SYMLINK == $expectedMethod) {
149 2
                    $method = $this->relativeSymlinkWithFallback($originDir, $targetDir);
150 3
                } elseif (self::METHOD_ABSOLUTE_SYMLINK == $expectedMethod) {
151 2
                    $expectedMethod = self::METHOD_ABSOLUTE_SYMLINK;
152 2
                    $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
153
                } else {
154 1
                    $expectedMethod = self::METHOD_COPY;
155 1
                    $method = $this->hardCopy($originDir, $targetDir);
156
                }
157
158 5
                if (self::METHOD_COPY === $method) {
159 1
                    $copyUsed = true;
160
                }
161
162 5
                if ($method === $expectedMethod) {
163 5
                    $rows[] = array(sprintf('<fg=green;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'OK' : "\xE2\x9C\x94" /* HEAVY CHECK MARK (U+2714) */), $message, $method);
164
                } else {
165 5
                    $rows[] = array(sprintf('<fg=yellow;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'WARNING' : '!'), $message, $method);
166
                }
167
            } catch (\Exception $e) {
168
                $exitCode = 1;
169 5
                $rows[] = array(sprintf('<fg=red;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'ERROR' : "\xE2\x9C\x98" /* HEAVY BALLOT X (U+2718) */), $message, $e->getMessage());
170
            }
171
        }
172
173
        // render this output only on cli environment
174 5
        if ($this->isCli()) {
175 5
            $this->renderInstallOutput($copyUsed, $rows, $exitCode);
176
        }
177 5
    }
178
179 1
    public function uninstall($modules)
180
    {
181 1
        $assetDir = $this->getModuleAssetDir();
182 1
        foreach ($modules as $name) {
183 1
            $publicPath = $assetDir.DIRECTORY_SEPARATOR.$name;
184 1
            if (is_dir($publicPath) || is_link($publicPath)) {
185 1
                $this->filesystem->remove($publicPath);
186 1
                $this->log("Removed module assets: <info>${name}</info>");
187
            }
188
        }
189
    }
190
191
    /**
192
     *
193
     */
194
    public function fixDirPermissions()
195
    {
196
        /* @var ModuleOptions $options */
197 1
        $app        = $this->application;
198 1
        $options    = $app->getServiceManager()->get('Core/Options');
199 1
        $filesystem = $this->filesystem;
200
201 1
        $logDir     = $options->getLogDir();
202 1
        $cacheDir   = $options->getCacheDir();
203 1
        $configDir  = realpath(Application::getConfigDir());
204
205 1
        if (!is_dir($options->getLogDir())) {
206
            $filesystem->mkdir($logDir, 0777);
207
        }
208
209 1
        if (!is_dir($cacheDir)) {
210
            $filesystem->mkdir($cacheDir, 0777);
211
        }
212
213 1
        $filesystem->chmod($cacheDir, 0777);
214 1
        $filesystem->chmod($logDir, 0777);
215 1
        $filesystem->chmod($logDir.'/tracy', 0777);
216 1
        $filesystem->chmod($configDir.'/autoload', 0777);
217 1
    }
218
219
    public function getPublicDir()
220
    {
221 6
        return $this->getRootDir().'/public';
222
    }
223
224
    public function getModuleAssetDir()
225
    {
226 6
        return $this->getPublicDir().'/modules';
227
    }
228
229
    /**
230
     * @return string
231
     */
232
    public function getRootDir()
233
    {
234 6
        return dirname(realpath(Application::getConfigDir()));
235
    }
236
237
    /**
238
     * @param $message
239
     */
240
    public function logDebug($message)
241
    {
242
        $this->doLog(LogLevel::DEBUG, $message);
243
        if ($this->isCli()) {
244
            $this->output->writeln($message, OutputInterface::VERBOSITY_VERY_VERBOSE);
245
        }
246
    }
247
248
    /**
249
     * @param $message
250
     */
251
    public function logError($message)
252
    {
253
        $this->doLog(LogLevel::ERROR, $message);
254
        if ($this->isCli()) {
255
            $this->output->writeln("<error>[error] {$message}</error>");
256
        }
257
    }
258
259
    public function log($message)
260
    {
261 1
        $this->doLog(LogLevel::INFO, $message, ['yawik.assets']);
0 ignored issues
show
Unused Code introduced by
The call to Yawik\Composer\AssetsInstaller::doLog() has too many arguments starting with array('yawik.assets'). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

261
        $this->/** @scrutinizer ignore-call */ 
262
               doLog(LogLevel::INFO, $message, ['yawik.assets']);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
262 1
        if ($this->isCli()) {
263 1
            $this->output->writeln($message);
264
        }
265 1
    }
266
267
    private function renderInstallOutput($copyUsed, $rows, $exitCode)
268
    {
269 5
        $io = new SymfonyStyle($this->input, $this->output);
270 5
        $io->newLine();
271
272 5
        $io->section('Yawik Assets Installed!');
273
274 5
        if ($rows) {
275 5
            $io->table(array('', 'Module', 'Method / Error'), $rows);
276
        }
277
278 5
        if (0 !== $exitCode) {
279
            $io->error('Some errors occurred while installing assets.');
280
        } else {
281 5
            if ($copyUsed) {
282 1
                $io->note('Some assets were installed via copy. If you make changes to these assets you have to run this command again.');
283
            }
284 5
            $io->success($rows ? 'All assets were successfully installed.' : 'No assets were provided by any bundle.');
285
        }
286 5
    }
287
288
    private function isCli()
289
    {
290 5
        return php_sapi_name() === 'cli';
291
    }
292
293
    private function scanInstalledModules()
294
    {
295
        /* @var ModuleManager $manager */
296 5
        $app            = $this->application;
297 5
        $manager        = $app->getServiceManager()->get('ModuleManager');
298 5
        $modules        = $manager->getLoadedModules(true);
299 5
        $moduleAssets   = array();
300
301 5
        foreach ($modules as $module) {
302
            try {
303 5
                $className = get_class($module);
304 5
                $moduleName = substr($className, 0, strpos($className, '\\'));
305 5
                $r = new \ReflectionClass($className);
306 5
                $file = $r->getFileName();
307 5
                $dir = null;
308 5
                if ($module instanceof AssetProviderInterface) {
309
                    $dir = $module->getPublicDir();
310
                } else {
311 5
                    $testDir = substr($file, 0, stripos($file, 'src'.DIRECTORY_SEPARATOR.'Module.php')).'/public';
312 5
                    if (is_dir($testDir)) {
313 5
                        $dir = $testDir;
314
                    }
315
                }
316 5
                if (is_dir($dir)) {
317 5
                    $moduleAssets[$moduleName] = realpath($dir);
318
                }
319
            } catch (\Exception $e) {
320 5
                $this->logError($e->getMessage());
321
            }
322
        }
323 5
        return $moduleAssets;
324
    }
325
326
    private function doLog($level, $message)
327
    {
328 1
        if ($this->logger instanceof LoggerInterface) {
0 ignored issues
show
introduced by
$this->logger is always a sub-type of Psr\Log\LoggerInterface.
Loading history...
329
            $this->logger->log($level, $message, ['yawik.assets']);
330
        }
331 1
    }
332
333
    /**
334
     * Try to create absolute symlink.
335
     *
336
     * Falling back to hard copy.
337
     */
338
    private function absoluteSymlinkWithFallback($originDir, $targetDir)
339
    {
340
        try {
341 2
            $this->symlink($originDir, $targetDir);
342 2
            $method = self::METHOD_ABSOLUTE_SYMLINK;
343
        } catch (\Exception $e) {
344
            // @codeCoverageIgnoreStart
345
            // fall back to copy
346
            $method = $this->hardCopy($originDir, $targetDir);
347
            // @codeCoverageIgnoreEnd
348
        }
349
350 2
        return $method;
351
    }
352
353
    /**
354
     * Try to create relative symlink.
355
     *
356
     * Falling back to absolute symlink and finally hard copy.
357
     */
358
    private function relativeSymlinkWithFallback($originDir, $targetDir)
359
    {
360
        try {
361 2
            $this->symlink($originDir, $targetDir, true);
362 2
            $method = self::METHOD_RELATIVE_SYMLINK;
363
        }
364
        // @codeCoverageIgnoreStart
365
        catch (\Exception $e) {
366
            $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
367
        }
368
        // @codeCoverageIgnoreEnd
369
370 2
        return $method;
371
    }
372
373
    /**
374
     * Creates symbolic link.
375
     *
376
     * @throws \Exception if link can not be created
377
     */
378
    private function symlink($originDir, $targetDir, $relative = false)
379
    {
380 4
        if ($relative) {
381 2
            $this->filesystem->mkdir(dirname($targetDir));
382 2
            $originDir = $this->filesystem->makePathRelative($originDir, realpath(dirname($targetDir)));
383
        }
384 4
        $this->filesystem->symlink($originDir, $targetDir);
385
        // @codeCoverageIgnoreStart
386
        if (!file_exists($targetDir)) {
387
            throw new \Exception(
388
                sprintf('Symbolic link "%s" was created but appears to be broken.', $targetDir),
389
                0,
390
                null
391
            );
392
        }
393
        // @codeCoverageIgnoreEnd
394 4
    }
395
396
    /**
397
     * Copies origin to target.
398
     */
399
    private function hardCopy($originDir, $targetDir)
400
    {
401 1
        $this->filesystem->mkdir($targetDir, 0777);
402
        // We use a custom iterator to ignore VCS files
403 1
        $this->filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir));
404
405 1
        return self::METHOD_COPY;
406
    }
407
}
408