Completed
Push — master ( 614aa2...fd4688 )
by ANTHONIUS
03:04
created

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