Completed
Push — master ( 6ad13d...fa9e0f )
by ANTHONIUS
03:07
created

AssetsInstaller::renderInstallOutput()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.0729

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 6
nop 3
dl 0
loc 18
ccs 6
cts 7
cp 0.8571
crap 5.0729
rs 9.6111
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
use Zend\View\Helper\Asset;
25
26
/**
27
 * Class AssetsInstaller
28
 * @package Yawik\Composer
29
 * @author  Anthonius Munthi <[email protected]>
30
 * @TODO    Create more documentation for methods
31
 */
32
class AssetsInstaller
33
{
34
    const METHOD_COPY               = 'copy';
35
    const METHOD_ABSOLUTE_SYMLINK   = 'absolute symlink';
36
    const METHOD_RELATIVE_SYMLINK   = 'relative symlink';
37
38
    /**
39
     * @var Filesystem
40
     */
41
    private $filesystem;
42
43
    /**
44
     * @var OutputInterface
45
     */
46
    private $output;
47
48
    /**
49
     * @var InputInterface
50
     */
51
    private $input;
52
53
    /**
54
     * @var LoggerInterface
55
     */
56
    private $logger;
57
58
    /**
59
     * The root dir of Yawik application
60
     * @var string
61
     */
62
    private $rootDir;
0 ignored issues
show
introduced by
The private property $rootDir is not used, and could be removed.
Loading history...
63
64
    /**
65
     * @var Application
66
     */
67
    private $application;
68 7
69
    public function __construct()
70 7
    {
71 7
        umask(0000);
72
        $this->filesystem = new Filesystem();
73
        // @codeCoverageIgnoreStart
74
        if (!class_exists('Core\\Application')) {
75
            include_once __DIR__.'/../../../autoload.php';
76
        }
77
        // @codeCoverageIgnoreEnd
78 7
79 7
        $this->application = Application::init();
80
    }
81
82
    /**
83
     * Set a logger to use
84
     * @param LoggerInterface $logger
85
     * @return AssetsInstaller
86
     */
87
    public function setLogger(LoggerInterface $logger)
88
    {
89
        $this->logger = $logger;
90
91
        return $this;
92 1
    }
93
94 1
    public function setFilesystem(Filesystem $filesystem)
95 1
    {
96
        $this->filesystem = $filesystem;
97
    }
98
99
    /**
100
     * @return OutputInterface
101
     */
102
    public function getOutput()
103
    {
104
        return $this->output;
105
    }
106
107
    /**
108
     * @param OutputInterface $output
109 7
     * @return AssetsInstaller
110
     */
111 7
    public function setOutput($output)
112 7
    {
113
        $this->output = $output;
114
        return $this;
115
    }
116
117
    /**
118
     * @param InputInterface $input
119 7
     * @return AssetsInstaller
120
     */
121 7
    public function setInput($input)
122 7
    {
123
        $this->input = $input;
124
        return $this;
125
    }
126
127
    /**
128
     * Install modules assets with the given $modules.
129
     * $modules should within this format:
130
     *
131
     * [module_name] => module_public_directory
132
     *
133
     * @param array     $modules An array of modules
134 5
     * @param string    $expectedMethod Expected install method
135
     */
136 5
    public function install($modules, $expectedMethod = self::METHOD_RELATIVE_SYMLINK)
137 5
    {
138 5
        $publicDir      = $this->getModuleAssetDir();
139 5
        $loadedModules  = $this->scanInstalledModules();
140 5
        $modules        = array_merge($modules, $loadedModules);
141 5
        $rows           = [];
142
        $exitCode       = 0;
143 5
        $copyUsed       = false;
144 5
145 5
        foreach ($modules as $name => $originDir) {
146
            $targetDir = $publicDir.DIRECTORY_SEPARATOR.$name;
147 5
            $message = $name;
148 5
            try {
149 2
                $this->filesystem->remove($targetDir);
150 3
                if (self::METHOD_RELATIVE_SYMLINK == $expectedMethod) {
151 2
                    $method = $this->relativeSymlinkWithFallback($originDir, $targetDir);
152 2
                } elseif (self::METHOD_ABSOLUTE_SYMLINK == $expectedMethod) {
153
                    $expectedMethod = self::METHOD_ABSOLUTE_SYMLINK;
154 1
                    $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
155 1
                } else {
156
                    $expectedMethod = self::METHOD_COPY;
157
                    $method = $this->hardCopy($originDir, $targetDir);
158 5
                }
159 1
160
                if (self::METHOD_COPY === $method) {
161
                    $copyUsed = true;
162 5
                }
163 5
164
                if ($method === $expectedMethod) {
165 5
                    $rows[] = array(sprintf('<fg=green;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'OK' : "\xE2\x9C\x94" /* HEAVY CHECK MARK (U+2714) */), $message, $method);
166
                } else {
167
                    $rows[] = array(sprintf('<fg=yellow;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'WARNING' : '!'), $message, $method);
168
                }
169 5
            } catch (\Exception $e) { // @codeCoverageIgnoreStart
170
                $exitCode = 1;
171
                $rows[] = array(sprintf('<fg=red;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'ERROR' : "\xE2\x9C\x98" /* HEAVY BALLOT X (U+2718) */), $message, $e->getMessage());
172
            }// @codeCoverageIgnoreEnd
173
        }
174 5
175 5
        // render this output only on cli environment
176
        if ($this->isCli()) {
177 5
            $this->renderInstallOutput($copyUsed, $rows, $exitCode);
178
        }
179 1
    }
180
181 1
    public function uninstall($modules)
182 1
    {
183 1
        $assetDir = $this->getModuleAssetDir();
184 1
        foreach ($modules as $name) {
185 1
            $publicPath = $assetDir.DIRECTORY_SEPARATOR.$name;
186 1
            if (is_dir($publicPath) || is_link($publicPath)) {
187
                $this->filesystem->remove($publicPath);
188
                $this->log("Removed module assets: <info>${name}</info>");
189
            }
190
        }
191
    }
192
193
    /**
194
     *
195
     */
196
    public function fixDirPermissions()
197 1
    {
198 1
        /* @var ModuleOptions $options */
199 1
        $app        = $this->application;
200
        $options    = $app->getServiceManager()->get('Core/Options');
201 1
202 1
        $logDir     = $options->getLogDir();
203 1
        $cacheDir   = $options->getCacheDir();
204
        $configDir  = realpath(Application::getConfigDir());
205 1
206
        $dirs = [
207
            $configDir.'/autoload',
208
            $cacheDir,
209 1
            $logDir,
210
            $logDir.'/tracy',
211
        ];
212
        foreach ($dirs as $dir) {
213 1
            try {
214 1
                if (!is_dir($dir)) {
215 1
                    $this->mkdir($dir, 0777, true);
0 ignored issues
show
Unused Code introduced by
The call to Yawik\Composer\AssetsInstaller::mkdir() has too many arguments starting with 511. ( Ignorable by Annotation )

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

215
                    $this->/** @scrutinizer ignore-call */ 
216
                           mkdir($dir, 0777, true);

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...
216 1
                }
217 1
                $this->chmod($dir);
218
            } catch (\Exception $exception) {
219
                $this->logError($exception->getMessage());
220
            }
221 6
        }
222
    }
223
224
    public function getPublicDir()
225
    {
226 6
        return $this->getRootDir().'/public';
227
    }
228
229
    public function getModuleAssetDir()
230
    {
231
        return $this->getPublicDir().'/modules';
232
    }
233
234 6
    /**
235
     * @return string
236
     */
237
    public function getRootDir()
238
    {
239
        return dirname(realpath(Application::getConfigDir()));
240
    }
241
242
    /**
243
     * @param $message
244
     */
245
    public function logDebug($message)
246
    {
247
        $this->doLog(LogLevel::DEBUG, $message);
248
    }
249
250
    /**
251
     * @param $message
252
     */
253
    public function logError($message)
254
    {
255
        $this->doLog(LogLevel::ERROR, $message);
256
    }
257
258
    public function log($message)
259
    {
260
        $this->doLog(LogLevel::INFO, $message);
261 1
    }
262 1
263 1
    private function chmod($dir)
264
    {
265 1
        if (is_dir($dir) || is_file($dir)) {
266
            $this->filesystem->chmod($dir, 0777);
267
            $this->log(sprintf('<info>chmod: <comment>%s</comment> with 0777</info>', $dir));
268
        }
269 5
    }
270 5
271
    private function mkdir($dir)
272 5
    {
273
        $this->filesystem->mkdir($dir, 0777);
274 5
        $this->log(sprintf('<info>mkdir: </info><comment>%s</comment>', $dir));
275 5
    }
276
277
    public function renderInstallOutput($copyUsed, $rows, $exitCode)
278 5
    {
279
        $io = new SymfonyStyle($this->input, $this->output);
280
        $io->newLine();
281 5
282 1
        $io->section('Yawik Assets Installed!');
283
284 5
        if ($rows) {
285
            $io->table(array('', 'Module', 'Method / Error'), $rows);
286 5
        }
287
288
        if (0 !== $exitCode) {
289
            $io->error('Some errors occurred while installing assets.');
290 5
        } else {
291
            if ($copyUsed) {
292
                $io->note('Some assets were installed via copy. If you make changes to these assets you have to run this command again.');
293
            }
294
            $io->success($rows ? 'All assets were successfully installed.' : 'No assets were provided by any bundle.');
295
        }
296 5
    }
297 5
298 5
    public function isCli()
299 5
    {
300
        return php_sapi_name() === 'cli';
301 5
    }
302
303 5
    private function scanInstalledModules()
304 5
    {
305 5
        /* @var ModuleManager $manager */
306 5
        $app            = $this->application;
307 5
        $manager        = $app->getServiceManager()->get('ModuleManager');
308 5
        $modules        = $manager->getLoadedModules(true);
309
        $moduleAssets   = array();
310
311 5
        foreach ($modules as $module) {
312 5
            try {
313 5
                $className = get_class($module);
314
                $moduleName = substr($className, 0, strpos($className, '\\'));
315
                $r = new \ReflectionClass($className);
316 5
                $file = $r->getFileName();
317 5
                $dir = null;
318
                if ($module instanceof AssetProviderInterface) {
319
                    $dir = $module->getPublicDir();
320 5
                } else {
321
                    $testDir = substr($file, 0, stripos($file, 'src'.DIRECTORY_SEPARATOR.'Module.php')).'/public';
322
                    if (is_dir($testDir)) {
323 5
                        $dir = $testDir;
324
                    }
325
                }
326
                if (is_dir($dir)) {
327
                    $moduleAssets[$moduleName] = realpath($dir);
328 1
                }
329
            } catch (\Exception $e) { // @codeCoverageIgnore
330
                $this->logError($e->getMessage()); // @codeCoverageIgnore
331 1
            } // @codeCoverageIgnore
332
        }
333
        return $moduleAssets;
334
    }
335
336
    private function doLog($level, $message)
337
    {
338
        $message = str_replace(getcwd().DIRECTORY_SEPARATOR, '', $message);
339
        if ($this->logger instanceof LoggerInterface) {
0 ignored issues
show
introduced by
$this->logger is always a sub-type of Psr\Log\LoggerInterface.
Loading history...
340
            $this->logger->log($level, $message);
341 2
        }
342 2
        if ($this->isCli()) {
343
            switch ($level) {
344
                case LogLevel::DEBUG:
345
                    $outputLevel = OutputInterface::VERBOSITY_VERY_VERBOSE;
346
                    break;
347
                case LogLevel::ERROR:
348
                    $message = '<error>'.$message.'</error>';
349
                    $outputLevel = OutputInterface::OUTPUT_NORMAL;
350 2
                    break;
351
                case LogLevel::INFO:
352
                default:
353
                    $outputLevel = OutputInterface::OUTPUT_NORMAL;
354
                    break;
355
            }
356
            $this->doWrite($message, $outputLevel);
357
        }
358
    }
359
360
    private function doWrite($message, $outputLevel = 0)
361 2
    {
362 2
        $message = sprintf(
363
            '<info>[yawik]</info> %s',
364
            $message
365
        );
366
        $this->output->writeln($message, $outputLevel);
367
    }
368
369
    /**
370 2
     * Try to create absolute symlink.
371
     *
372
     * Falling back to hard copy.
373
     */
374
    private function absoluteSymlinkWithFallback($originDir, $targetDir)
375
    {
376
        try {
377
            $this->symlink($originDir, $targetDir);
378
            $method = self::METHOD_ABSOLUTE_SYMLINK;
379
        } catch (\Exception $e) { // @codeCoverageIgnore
380 4
            // fall back to copy
381 2
            $method = $this->hardCopy($originDir, $targetDir); // @codeCoverageIgnore
382 2
        } // @codeCoverageIgnore
383
384 4
        return $method;
385
    }
386
387
    /**
388
     * Try to create relative symlink.
389
     *
390
     * Falling back to absolute symlink and finally hard copy.
391
     */
392
    private function relativeSymlinkWithFallback($originDir, $targetDir)
393
    {
394 4
        try {
395
            $this->symlink($originDir, $targetDir, true);
396
            $method = self::METHOD_RELATIVE_SYMLINK;
397
        }
398
        // @codeCoverageIgnoreStart
399
        catch (\Exception $e) {
400
            $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
401 1
        }
402
        // @codeCoverageIgnoreEnd
403 1
404
        return $method;
405 1
    }
406
407
    /**
408
     * Creates symbolic link.
409
     *
410
     * @throws \Exception if link can not be created
411
     */
412
    private function symlink($originDir, $targetDir, $relative = false)
413
    {
414
        if ($relative) {
415
            $this->filesystem->mkdir(dirname($targetDir));
416
            $originDir = $this->filesystem->makePathRelative($originDir, realpath(dirname($targetDir)));
417
        }
418
        $this->filesystem->symlink($originDir, $targetDir);
419
        // @codeCoverageIgnoreStart
420
        if (!file_exists($targetDir)) {
421
            throw new \Exception(
422
                sprintf('Symbolic link "%s" was created but appears to be broken.', $targetDir),
423
                0,
424
                null
425
            );
426
        }
427
        // @codeCoverageIgnoreEnd
428
    }
429
430
    /**
431
     * Copies origin to target.
432
     */
433
    private function hardCopy($originDir, $targetDir)
434
    {
435
        $this->filesystem->mkdir($targetDir, 0777);
436
        // We use a custom iterator to ignore VCS files
437
        $this->filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir));
438
439
        return self::METHOD_COPY;
440
    }
441
}
442