Completed
Branch master (36649e)
by ANTHONIUS
06:30
created

AssetsInstaller::renderInstallOutput()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 6
nop 3
dl 0
loc 18
ccs 9
cts 10
cp 0.9
crap 5.025
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 Psr\Log\LoggerInterface;
16
use Psr\Log\LogLevel;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Filesystem\Filesystem;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Symfony\Component\Console\Style\SymfonyStyle;
21
use Symfony\Component\Finder\Finder;
22
use Zend\ModuleManager\ModuleManager;
23
24
/**
25
 * Class AssetsInstaller
26
 * @package Yawik\Composer
27
 * @author  Anthonius Munthi <[email protected]>
28
 * @TODO    Create more documentation for methods
29
 */
30
class AssetsInstaller
31
{
32
    const METHOD_COPY               = 'copy';
33
    const METHOD_ABSOLUTE_SYMLINK   = 'absolute symlink';
34
    const METHOD_RELATIVE_SYMLINK   = 'relative symlink';
35
36
    /**
37
     * @var Filesystem
38
     */
39
    private $filesystem;
40
41
    /**
42
     * @var OutputInterface
43
     */
44
    private $output;
45
46
    /**
47
     * @var InputInterface
48
     */
49
    private $input;
50
51
    /**
52
     * @var LoggerInterface
53
     */
54
    private $logger;
55
56 6
    public function __construct(Filesystem $filesystem = null)
57
    {
58 6
        if (is_null($filesystem)) {
59 6
            $filesystem = new Filesystem();
60
        }
61 6
        $this->filesystem = $filesystem;
62 6
    }
63
64
    /**
65
     * Set a logger to use
66
     * @param LoggerInterface $logger
67
     */
68
    public function setLogger(LoggerInterface $logger)
69
    {
70
        $this->logger = $logger;
71
72
        return;
73
    }
74
75
    public function setFilesystem(Filesystem $filesystem)
76
    {
77
        $this->filesystem = $filesystem;
78
    }
79
80
    /**
81
     * @return OutputInterface
82
     */
83
    public function getOutput()
84
    {
85
        return $this->output;
86
    }
87
88
    /**
89
     * @param OutputInterface $output
90
     * @return AssetsInstaller
91
     */
92 6
    public function setOutput($output)
93
    {
94 6
        $this->output = $output;
95 6
        return $this;
96
    }
97
98
    /**
99
     * @param InputInterface $input
100
     * @return AssetsInstaller
101
     */
102 6
    public function setInput($input)
103
    {
104 6
        $this->input = $input;
105 6
        return $this;
106
    }
107
108
    /**
109
     * Install modules assets with the given $modules.
110
     * $modules should within this format:
111
     *
112
     * [module_name] => module_public_directory
113
     *
114
     * @param array     $modules An array of modules
115
     * @param string    $expectedMethod Expected install method
116
     */
117 5
    public function install($modules, $expectedMethod = self::METHOD_RELATIVE_SYMLINK)
118
    {
119 5
        $publicDir      = $this->getModuleAssetDir();
120 5
        $loadedModules  = $this->scanInstalledModules();
121 5
        $modules        = array_merge($modules, $loadedModules);
122 5
        $rows           = [];
123 5
        $exitCode       = 0;
124 5
        $copyUsed       = false;
125
126 5
        foreach ($modules as $name => $originDir) {
127 5
            $targetDir = $publicDir.DIRECTORY_SEPARATOR.$name;
128 5
            $message = $name;
129
            try {
130 5
                $this->filesystem->remove($targetDir);
131 5
                if (self::METHOD_RELATIVE_SYMLINK == $expectedMethod) {
132 2
                    $method = $this->relativeSymlinkWithFallback($originDir, $targetDir);
133 3
                } elseif (self::METHOD_ABSOLUTE_SYMLINK == $expectedMethod) {
134 2
                    $expectedMethod = self::METHOD_ABSOLUTE_SYMLINK;
135 2
                    $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
136
                } else {
137 1
                    $expectedMethod = self::METHOD_COPY;
138 1
                    $method = $this->hardCopy($originDir, $targetDir);
139
                }
140
141 5
                if (self::METHOD_COPY === $method) {
142 1
                    $copyUsed = true;
143
                }
144
145 5
                if ($method === $expectedMethod) {
146 5
                    $rows[] = array(sprintf('<fg=green;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'OK' : "\xE2\x9C\x94" /* HEAVY CHECK MARK (U+2714) */), $message, $method);
147
                } else {
148 5
                    $rows[] = array(sprintf('<fg=yellow;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'WARNING' : '!'), $message, $method);
149
                }
150
            } catch (\Exception $e) {
151
                $exitCode = 1;
152 5
                $rows[] = array(sprintf('<fg=red;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'ERROR' : "\xE2\x9C\x98" /* HEAVY BALLOT X (U+2718) */), $message, $e->getMessage());
153
            }
154
        }
155
156
        // render this output only on cli environment
157 5
        if ($this->isCli()) {
158 5
            $this->renderInstallOutput($copyUsed, $rows, $exitCode);
159
        }
160 5
    }
161
162 1
    public function uninstall($modules)
163
    {
164 1
        $assetDir = $this->getModuleAssetDir();
165 1
        foreach ($modules as $name) {
166 1
            $publicPath = $assetDir.DIRECTORY_SEPARATOR.$name;
167 1
            if (is_dir($publicPath) || is_link($publicPath)) {
168 1
                $this->filesystem->remove($publicPath);
169 1
                $this->log("Removed module assets: <info>${name}</info>");
170
            }
171
        }
172
    }
173
174
    public function getModuleAssetDir()
175
    {
176 6
        return $this->getPublicDir().'/modules';
177
    }
178
179
    private function renderInstallOutput($copyUsed, $rows, $exitCode)
180
    {
181 5
        $io = new SymfonyStyle($this->input, $this->output);
182 5
        $io->newLine();
183
184 5
        $io->section('Yawik Assets Installed!');
185
186 5
        if ($rows) {
187 5
            $io->table(array('', 'Module', 'Method / Error'), $rows);
188
        }
189
190 5
        if (0 !== $exitCode) {
191
            $io->error('Some errors occurred while installing assets.');
192
        } else {
193 5
            if ($copyUsed) {
194 1
                $io->note('Some assets were installed via copy. If you make changes to these assets you have to run this command again.');
195
            }
196 5
            $io->success($rows ? 'All assets were successfully installed.' : 'No assets were provided by any bundle.');
197
        }
198 5
    }
199
200
    private function isCli()
201
    {
202 5
        return php_sapi_name() === 'cli';
203
    }
204
205
    private function getPublicDir()
206
    {
207
        $dirs = [
208 6
            getcwd().'/test/sandbox/public',
209 6
            getcwd().'/public'
210
        ];
211 6
        foreach ($dirs as $dir) {
212 6
            if (is_dir($dir)) {
213 6
                return $dir;
214
            }
215
        }
216
    }
217
218
    private function scanInstalledModules()
219
    {
220
        // @codeCoverageIgnoreStart
221
        if (!class_exists('Core\\Application')) {
222
            include_once __DIR__.'/../../../autoload.php';
223
        }
224
        // @codeCoverageIgnoreEnd
225
226
        /* @var ModuleManager $manager */
227 5
        $app            = Application::init();
228 5
        $manager        = $app->getServiceManager()->get('ModuleManager');
229 5
        $modules        = $manager->getLoadedModules(true);
230 5
        $moduleAssets   = array();
231
232 5
        foreach ($modules as $module) {
233
            try {
234 5
                $className = get_class($module);
235 5
                $moduleName = substr($className, 0, strpos($className, '\\'));
236 5
                $r = new \ReflectionClass($className);
237 5
                $file = $r->getFileName();
238 5
                $dir = null;
239 5
                if ($module instanceof AssetProviderInterface) {
240
                    $dir = $module->getPublicDir();
241
                } else {
242 5
                    $testDir = substr($file, 0, stripos($file, 'src'.DIRECTORY_SEPARATOR.'Module.php')).'/public';
243 5
                    if (is_dir($testDir)) {
244 5
                        $dir = $testDir;
245
                    }
246
                }
247 5
                if (is_dir($dir)) {
248 5
                    $moduleAssets[$moduleName] = realpath($dir);
249
                }
250
            } catch (\Exception $e) {
251 5
                $this->logError($e->getMessage());
252
            }
253
        }
254 5
        return $moduleAssets;
255
    }
256
257
    public function log($message)
258
    {
259 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

259
        $this->/** @scrutinizer ignore-call */ 
260
               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...
260 1
        if ($this->isCli()) {
261 1
            $this->output->writeln($message);
262
        }
263 1
    }
264
265
    /**
266
     * @param $message
267
     */
268
    public function logDebug($message)
269
    {
270
        $this->doLog(LogLevel::DEBUG, $message);
271
        if ($this->isCli()) {
272
            $this->output->writeln($message, OutputInterface::VERBOSITY_VERY_VERBOSE);
273
        }
274
    }
275
276
    /**
277
     * @param $message
278
     */
279
    public function logError($message)
280
    {
281
        $this->doLog(LogLevel::ERROR, $message);
282
        if ($this->isCli()) {
283
            $this->output->writeln("<error>[error] {$message}</error>");
284
        }
285
    }
286
287
    private function doLog($level, $message)
288
    {
289 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...
290
            $this->logger->log($level, $message, ['yawik.assets']);
291
        }
292 1
    }
293
294
    /**
295
     * Try to create absolute symlink.
296
     *
297
     * Falling back to hard copy.
298
     */
299
    private function absoluteSymlinkWithFallback($originDir, $targetDir)
300
    {
301
        try {
302 2
            $this->symlink($originDir, $targetDir);
303 2
            $method = self::METHOD_ABSOLUTE_SYMLINK;
304
        } catch (\Exception $e) {
305
            // @codeCoverageIgnoreStart
306
            // fall back to copy
307
            $method = $this->hardCopy($originDir, $targetDir);
308
            // @codeCoverageIgnoreEnd
309
        }
310
311 2
        return $method;
312
    }
313
314
    /**
315
     * Try to create relative symlink.
316
     *
317
     * Falling back to absolute symlink and finally hard copy.
318
     */
319
    private function relativeSymlinkWithFallback($originDir, $targetDir)
320
    {
321
        try {
322 2
            $this->symlink($originDir, $targetDir, true);
323 2
            $method = self::METHOD_RELATIVE_SYMLINK;
324
        }
325
        // @codeCoverageIgnoreStart
326
        catch (\Exception $e) {
327
            $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
328
        }
329
        // @codeCoverageIgnoreEnd
330
331 2
        return $method;
332
    }
333
334
    /**
335
     * Creates symbolic link.
336
     *
337
     * @throws \Exception if link can not be created
338
     */
339
    private function symlink($originDir, $targetDir, $relative = false)
340
    {
341 4
        if ($relative) {
342 2
            $this->filesystem->mkdir(dirname($targetDir));
343 2
            $originDir = $this->filesystem->makePathRelative($originDir, realpath(dirname($targetDir)));
344
        }
345 4
        $this->filesystem->symlink($originDir, $targetDir);
346
        // @codeCoverageIgnoreStart
347
        if (!file_exists($targetDir)) {
348
            throw new \Exception(
349
                sprintf('Symbolic link "%s" was created but appears to be broken.', $targetDir),
350
                0,
351
                null
352
            );
353
        }
354
        // @codeCoverageIgnoreEnd
355 4
    }
356
357
    /**
358
     * Copies origin to target.
359
     */
360
    private function hardCopy($originDir, $targetDir)
361
    {
362 1
        $this->filesystem->mkdir($targetDir, 0777);
363
        // We use a custom iterator to ignore VCS files
364 1
        $this->filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir));
365
366 1
        return self::METHOD_COPY;
367
    }
368
}
369