Completed
Push — master ( 7e70c2...f499f2 )
by ANTHONIUS
03:01
created

AssetsInstaller::setFilesystem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 0
cts 0
cp 0
crap 2
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 Symfony\Component\Console\Input\StringInput;
16
use Symfony\Component\Console\Output\ConsoleOutput;
17
use Symfony\Component\Filesystem\Filesystem;
18
use Symfony\Component\Console\Style\SymfonyStyle;
19
use Symfony\Component\Finder\Finder;
20
use Zend\ModuleManager\ModuleManager;
21
22
/**
23
 * Class AssetsInstaller
24
 * @package Yawik\Composer
25
 * @author  Anthonius Munthi <[email protected]>
26
 * @TODO    Create more documentation for methods
27
 */
28
class AssetsInstaller
29
{
30
    use LogTrait;
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 Application
43
     */
44
    private $application;
45
46
    public function __construct()
47
    {
48
        umask(0000);
49
        $this->filesystem = new Filesystem();
50
        // @codeCoverageIgnoreStart
51
        if (!class_exists('Core\\Application')) {
52
            include_once __DIR__.'/../../../autoload.php';
53
        }
54
        // @codeCoverageIgnoreEnd
55
56
        $this->input       = new StringInput('');
57
        $this->output      = new ConsoleOutput($this->input);
0 ignored issues
show
Bug introduced by
$this->input of type Symfony\Component\Console\Input\StringInput is incompatible with the type integer expected by parameter $verbosity of Symfony\Component\Consol...leOutput::__construct(). ( Ignorable by Annotation )

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

57
        $this->output      = new ConsoleOutput(/** @scrutinizer ignore-type */ $this->input);
Loading history...
58
        $this->application = Application::init();
59
    }
60
61
    public function setFilesystem(Filesystem $filesystem)
62
    {
63
        $this->filesystem = $filesystem;
64
    }
65
66
    /**
67
     * Install modules assets with the given $modules.
68 7
     * $modules should within this format:
69
     *
70 7
     * [module_name] => module_public_directory
71 7
     *
72
     * @param array     $modules An array of modules
73
     * @param string    $expectedMethod Expected install method
74
     */
75
    public function install($modules, $expectedMethod = self::METHOD_RELATIVE_SYMLINK)
76
    {
77
        $publicDir      = $this->getModuleAssetDir();
78 7
        $loadedModules  = $this->scanInstalledModules();
79 7
        $modules        = array_merge($modules, $loadedModules);
80
        $rows           = [];
81
        $exitCode       = 0;
82
        $copyUsed       = false;
83
84
        foreach ($modules as $name => $originDir) {
85
            $targetDir = $publicDir.DIRECTORY_SEPARATOR.$name;
86
            $message = $name;
87
            try {
88
                $this->filesystem->remove($targetDir);
89
                if (self::METHOD_RELATIVE_SYMLINK == $expectedMethod) {
90
                    $method = $this->relativeSymlinkWithFallback($originDir, $targetDir);
91
                } elseif (self::METHOD_ABSOLUTE_SYMLINK == $expectedMethod) {
92 1
                    $expectedMethod = self::METHOD_ABSOLUTE_SYMLINK;
93
                    $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
94 1
                } else {
95 1
                    $expectedMethod = self::METHOD_COPY;
96
                    $method = $this->hardCopy($originDir, $targetDir);
97
                }
98
99
                if (self::METHOD_COPY === $method) {
100
                    $copyUsed = true;
101
                }
102
103
                if ($method === $expectedMethod) {
104
                    $rows[] = array(sprintf('<fg=green;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'OK' : "\xE2\x9C\x94" /* HEAVY CHECK MARK (U+2714) */), $message, $method);
105
                } else {
106
                    $rows[] = array(sprintf('<fg=yellow;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'WARNING' : '!'), $message, $method);
107
                }
108
            } catch (\Exception $e) { // @codeCoverageIgnoreStart
109 7
                $exitCode = 1;
110
                $rows[] = array(sprintf('<fg=red;options=bold>%s</>', '\\' === DIRECTORY_SEPARATOR ? 'ERROR' : "\xE2\x9C\x98" /* HEAVY BALLOT X (U+2718) */), $message, $e->getMessage());
111 7
            }// @codeCoverageIgnoreEnd
112 7
        }
113
114
        // render this output only on cli environment
115
        if ($this->isCli()) {
116
            $this->renderInstallOutput($copyUsed, $rows, $exitCode);
117
        }
118
    }
119 7
120
    /**
121 7
     * Uninstall modules
122 7
     *
123
     * @param array $modules A list of modules to uninstall
124
     */
125
    public function uninstall($modules)
126
    {
127
        $assetDir = $this->getModuleAssetDir();
128
        foreach ($modules as $name) {
129
            $publicPath = $assetDir.DIRECTORY_SEPARATOR.$name;
130
            if (is_dir($publicPath) || is_link($publicPath)) {
131
                $this->filesystem->remove($publicPath);
132
                $this->log("Removed module assets: <info>${name}</info>");
133
            }
134 5
        }
135
    }
136 5
137 5
    public function getPublicDir()
138 5
    {
139 5
        return $this->getRootDir().'/public';
140 5
    }
141 5
142
    public function getModuleAssetDir()
143 5
    {
144 5
        return $this->getPublicDir().'/modules';
145 5
    }
146
147 5
    /**
148 5
     * @return string
149 2
     */
150 3
    public function getRootDir()
151 2
    {
152 2
        return dirname(realpath(Application::getConfigDir()));
153
    }
154 1
155 1
    public function renderInstallOutput($copyUsed, $rows, $exitCode)
156
    {
157
        $io = new SymfonyStyle($this->input, $this->output);
158 5
        $io->newLine();
159 1
160
        $io->section('Yawik Assets Installed!');
161
162 5
        if ($rows) {
163 5
            $io->table(array('', 'Module', 'Method / Error'), $rows);
164
        }
165 5
166
        if (0 !== $exitCode) {
167
            $io->error('Some errors occurred while installing assets.');
168
        } else {
169 5
            if ($copyUsed) {
170
                $io->note('Some assets were installed via copy. If you make changes to these assets you have to run this command again.');
171
            }
172
            $io->success($rows ? 'All assets were successfully installed.' : 'No assets were provided by any bundle.');
173
        }
174 5
    }
175 5
176
    private function scanInstalledModules()
177 5
    {
178
        /* @var ModuleManager $manager */
179 1
        $app            = $this->application;
180
        $manager        = $app->getServiceManager()->get('ModuleManager');
181 1
        $modules        = $manager->getLoadedModules(true);
182 1
        $moduleAssets   = array();
183 1
184 1
        foreach ($modules as $module) {
185 1
            try {
186 1
                $className = get_class($module);
187
                $moduleName = substr($className, 0, strpos($className, '\\'));
188
                $r = new \ReflectionClass($className);
189
                $file = $r->getFileName();
190
                $dir = null;
191
                if ($module instanceof AssetProviderInterface) {
192
                    $dir = $module->getPublicDir();
193
                } else {
194
                    $testDir = substr($file, 0, stripos($file, 'src'.DIRECTORY_SEPARATOR.'Module.php')).'/public';
195
                    if (is_dir($testDir)) {
196
                        $dir = $testDir;
197 1
                    }
198 1
                }
199 1
                if (is_dir($dir)) {
200
                    $moduleAssets[$moduleName] = realpath($dir);
201 1
                }
202 1
            } catch (\Exception $e) { // @codeCoverageIgnore
203 1
                $this->logError($e->getMessage()); // @codeCoverageIgnore
204
            } // @codeCoverageIgnore
205 1
        }
206
        return $moduleAssets;
207
    }
208
209 1
    /**
210
     * Try to create absolute symlink.
211
     *
212
     * Falling back to hard copy.
213 1
     */
214 1
    private function absoluteSymlinkWithFallback($originDir, $targetDir)
215 1
    {
216 1
        try {
217 1
            $this->symlink($originDir, $targetDir);
218
            $method = self::METHOD_ABSOLUTE_SYMLINK;
219
        } catch (\Exception $e) { // @codeCoverageIgnore
220
            // fall back to copy
221 6
            $method = $this->hardCopy($originDir, $targetDir); // @codeCoverageIgnore
222
        } // @codeCoverageIgnore
223
224
        return $method;
225
    }
226 6
227
    /**
228
     * Try to create relative symlink.
229
     *
230
     * Falling back to absolute symlink and finally hard copy.
231
     */
232
    private function relativeSymlinkWithFallback($originDir, $targetDir)
233
    {
234 6
        try {
235
            $this->symlink($originDir, $targetDir, true);
236
            $method = self::METHOD_RELATIVE_SYMLINK;
237
        }
238
        // @codeCoverageIgnoreStart
239
        catch (\Exception $e) {
240
            $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
241
        }
242
        // @codeCoverageIgnoreEnd
243
244
        return $method;
245
    }
246
247
    /**
248
     * Creates symbolic link.
249
     *
250
     * @throws \Exception if link can not be created
251
     */
252
    private function symlink($originDir, $targetDir, $relative = false)
253
    {
254
        if ($relative) {
255
            $this->filesystem->mkdir(dirname($targetDir));
256
            $originDir = $this->filesystem->makePathRelative($originDir, realpath(dirname($targetDir)));
257
        }
258
        $this->filesystem->symlink($originDir, $targetDir);
259
        // @codeCoverageIgnoreStart
260
        if (!file_exists($targetDir)) {
261 1
            throw new \Exception(
262 1
                sprintf('Symbolic link "%s" was created but appears to be broken.', $targetDir),
263 1
                0,
264
                null
265 1
            );
266
        }
267
        // @codeCoverageIgnoreEnd
268
    }
269 5
270 5
    /**
271
     * Copies origin to target.
272 5
     */
273
    private function hardCopy($originDir, $targetDir)
274 5
    {
275 5
        $this->filesystem->mkdir($targetDir, 0777);
276
        // We use a custom iterator to ignore VCS files
277
        $this->filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir));
278 5
279
        return self::METHOD_COPY;
280
    }
281
}
282