Completed
Push — master ( e86446...0e66aa )
by Michael
13:31 queued 07:43
created

StaticsMergerPlugin   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 411
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 83.23%

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 9
dl 0
loc 411
ccs 139
cts 167
cp 0.8323
rs 6.8
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A activate() 0 22 3
A getInstallPath() 0 6 2
A getPackageBasePath() 0 7 2
A getSubscribedEvents() 0 21 1
A verifyEnvironment() 0 4 1
A getYarnExecutablePath() 0 4 1
B staticsCompile() 0 38 4
B symlinkStatics() 0 29 4
C processFiles() 0 34 7
B processSymlink() 0 41 6
A getStaticPackages() 0 8 2
A getStaticMaps() 0 11 3
C staticsCleanup() 0 33 8
A tryCleanup() 0 8 2
A getFullDirectoryListing() 0 12 1
B getRelativePath() 0 32 6
A getRootThemeDir() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like StaticsMergerPlugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StaticsMergerPlugin, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types = 1);
3
4
namespace Jh\StaticsMerger;
5
6
use Composer\Composer;
7
use Composer\EventDispatcher\EventSubscriberInterface;
8
use Composer\IO\IOInterface;
9
use Composer\Plugin\PluginInterface;
10
use Composer\Script\ScriptEvents;
11
use Composer\Util\Filesystem;
12
use Composer\Package\PackageInterface;
13
use Symfony\Component\Process\Exception\LogicException;
14
use Symfony\Component\Process\Exception\ProcessFailedException;
15
use Symfony\Component\Process\Exception\RuntimeException;
16
use Symfony\Component\Process\ExecutableFinder;
17
use Symfony\Component\Process\Process;
18
19
/**
20
 * Composer Plugin for merging static assets with the Jh Magento Skeleton
21
 * @author Michael Woodward <[email protected]>
22
 */
23
class StaticsMergerPlugin implements PluginInterface, EventSubscriberInterface
24
{
25
    /**
26
     * Package Type to install
27
     */
28
    const PACKAGE_TYPE = 'static';
29
30
    /**
31
     * @var Composer $composer
32
     */
33
    protected $composer;
34
35
    /**
36
     * @var IOInterface $io
37
     */
38
    protected $io;
39
40
    /**
41
     * @var Filesystem
42
     */
43
    protected $filesystem;
44
45
    /**
46
     * @var string
47
     */
48
    protected $vendorDir;
49
50
    /**
51
     * @var array
52
     */
53
    protected $packageExtra = [];
54
55
    /**
56
     * @var array
57
     */
58
    protected $staticMaps = [];
59
60
    /**
61
     * @var string
62
     */
63
    protected $mageDir = '';
64
65
    /**
66
     * @throws \RuntimeException On composer config failure
67
     */
68 24
    public function activate(Composer $composer, IOInterface $io) : bool
69
    {
70 24
        $this->composer     = $composer;
71 24
        $this->io           = $io;
72 24
        $this->vendorDir    = rtrim($composer->getConfig()->get('vendor-dir'), '/');
73 24
        $this->filesystem   = new Filesystem();
74 24
        $this->packageExtra = $this->composer->getPackage()->getExtra();
75
76 24
        if (!array_key_exists('static-map', $this->packageExtra)) {
77 1
            $this->io->write('<info>No static maps defined</info>');
78 1
            return false;
79
        }
80
81 23
        if (!array_key_exists('magento-root-dir', $this->packageExtra)) {
82 1
            $this->io->write('<info>Magento root dir not defined, assumed current working directory</info>');
83
        } else {
84 22
            $this->mageDir = rtrim($this->packageExtra['magento-root-dir'], '/');
85
        }
86
87 23
        $this->staticMaps  = $this->packageExtra['static-map'];
88 23
        return true;
89
    }
90
91 16
    public function getInstallPath(PackageInterface $package) : string
92
    {
93 16
        $targetDir = $package->getTargetDir();
94
95 16
        return $this->getPackageBasePath($package) . ($targetDir ? '/'.$targetDir : '');
96
    }
97
98 16
    protected function getPackageBasePath(PackageInterface $package) : string
99
    {
100 16
        $this->filesystem->ensureDirectoryExists($this->vendorDir);
101 16
        $this->vendorDir = realpath($this->vendorDir);
102
103 16
        return ($this->vendorDir ? $this->vendorDir.'/' : '') . $package->getPrettyName();
104
    }
105
106 1
    public static function getSubscribedEvents() : array
107
    {
108
        return [
109 1
            ScriptEvents::PRE_INSTALL_CMD => [
110
                ['verifyEnvironment', 1],
111
                ['staticsCleanup', 0]
112 1
            ],
113 1
            ScriptEvents::PRE_UPDATE_CMD => [
114
                ['verifyEnvironment', 1],
115
                ['staticsCleanup', 0]
116
            ],
117 1
            ScriptEvents::POST_INSTALL_CMD => [
118
                ['staticsCompile', 1],
119
                ['symlinkStatics', 0]
120
            ],
121 1
            ScriptEvents::POST_UPDATE_CMD => [
122
                ['staticsCompile', 1],
123
                ['symlinkStatics', 0]
124
            ]
125
        ];
126
    }
127
128
    public function verifyEnvironment() : bool
129
    {
130
        return is_executable($this->getYarnExecutablePath());
131
    }
132
133
    private function getYarnExecutablePath() : string
134
    {
135
        return (new ExecutableFinder)->find('yarn', '');
136
    }
137
138
    /**
139
     * @throws \RuntimeException When Yarn install fails or crossbow fails
140
     * @throws LogicException From process
141
     * @throws RuntimeException From process
142
     */
143
    public function staticsCompile()
144
    {
145
        $cwd = getcwd();
146
147
        foreach ($this->getStaticPackages() as $package) {
148
            chdir($this->getInstallPath($package));
149
150
            $this->io->write(sprintf('<info>Installing dependencies for "%s"', $package->getPrettyName()));
151
            $dependencyProcess = new Process($this->getYarnExecutablePath());
152
153
            try {
154
                $dependencyProcess->mustRun();
155
            } catch (ProcessFailedException $e) {
156
                $this->io->write($dependencyProcess->getOutput());
157
                $this->io->write($dependencyProcess->getErrorOutput());
158
                $this->io->write(
159
                    sprintf('<error>Failed to install dependencies for "%s" </error>', $package->getPrettyName())
160
                );
161
                return false;
162
            }
163
164
            $this->io->write(sprintf('<info>Building statics assets for "%s"', $package->getPrettyName()));
165
            $buildProcess = new Process('node_modules/.bin/cb release');
166
167
            try {
168
                $buildProcess->mustRun();
169
            } catch (ProcessFailedException $e) {
170
                $this->io->write($buildProcess->getOutput());
171
                $this->io->write($buildProcess->getErrorOutput());
172
                $this->io->write(
173
                    sprintf('<error>Static package "%s" failed to build </error>', $package->getPrettyName())
174
                );
175
                return false;
176
            }
177
        }
178
179
        chdir($cwd);
180
    }
181
182 17
    public function symlinkStatics()
183
    {
184 17
        foreach ($this->getStaticPackages() as $package) {
185 16
            $packageSource = $this->getInstallPath($package);
186
187 16
            foreach ($this->getStaticMaps($package->getName()) as $mappingDir => $mappings) {
188 16
                $destinationTheme = $this->getRootThemeDir($mappingDir);
189
190
                // Add slash to paths
191 16
                $packageSource    = rtrim($packageSource, '/');
192 16
                $destinationTheme = rtrim($destinationTheme, '/');
193
194
                // If theme doesn't exist - Create it
195 16
                $this->filesystem->ensureDirectoryExists($destinationTheme);
196
197
                // Process files from package
198 16
                if ($mappings) {
199 15
                    $this->processFiles($packageSource, $destinationTheme, $mappings);
200
                } else {
201 1
                    $this->io->write(
202
                        sprintf(
203 1
                            '<error>%s requires at least one file mapping, has none!<error>',
204 16
                            $package->getPrettyName()
205
                        )
206
                    );
207
                }
208
            }
209
        }
210 17
    }
211
212
    /**
213
     * Processes defined file mappings and symlinks resulting files to destination theme
214
     */
215 15
    public function processFiles(string $packageSource, string $destinationTheme, array $files = [])
216
    {
217 15
        foreach ($files as $file) {
218
            // Ensure we have correct json
219 15
            if (isset($file['src']) && isset($file['dest'])) {
220 15
                $src    = sprintf("%s/%s", $packageSource, $file['src']);
221 15
                $dest   = rtrim($file['dest'], '/');
222
223
                // Check if it's a glob
224 15
                if (strpos($src, '*') !== false) {
225 2
                    $files = array_filter(glob($src), 'is_file');
226 2
                    foreach ($files as $globFile) {
227
                        //strip the full path
228
                        //and just get path relative to package
229 2
                        $fileSource = str_replace(sprintf("%s/", $packageSource), "", $globFile);
230
231 2
                        $dest = ltrim(sprintf("%s/%s", $dest, basename($fileSource)), '/');
232
233 2
                        $this->processSymlink($packageSource, $fileSource, $destinationTheme, $dest);
234 2
                        $dest = $file['dest'];
235
                    }
236
                } else {
237 15
                    if (!$dest) {
238 1
                        $this->io->write(
239 1
                            sprintf('<error>Full path is required for: "%s" </error>', $file['src'])
240
                        );
241 1
                        return false;
242
                    }
243
244 15
                    $this->processSymlink($packageSource, $file['src'], $destinationTheme, $dest);
245
                }
246
            }
247
        }
248 14
    }
249
250
    /**
251
     * Process symlink, checks given source and destination paths
252
     */
253 15
    public function processSymlink(
254
        string $packageSrc,
255
        string $relativeSourcePath,
256
        string $destinationTheme,
257
        string $relativeDestinationPath
258
    ) {
259 15
        $sourcePath         = sprintf("%s/%s", $packageSrc, $relativeSourcePath);
260 15
        $destinationPath    = sprintf("%s/%s", $destinationTheme, $relativeDestinationPath);
261
262 15
        if (!file_exists($sourcePath)) {
263 1
            $this->io->write(
264 1
                sprintf('<error>The static package does not contain directory: "%s" </error>', $relativeSourcePath)
265
            );
266 1
            return;
267
        }
268
269 14
        if (file_exists($destinationPath) && !is_link($destinationPath)) {
270 1
            $this->io->write(
271
                sprintf(
272 1
                    '<error>Your static path: "%s" is currently not a symlink, please remove first </error>',
273
                    $destinationPath
274
                )
275
            );
276 1
            return;
277
        }
278
279
        //if it's a link, remove it and recreate it
280
        //assume we are updating the static package
281 13
        if (is_link($destinationPath)) {
282 1
            unlink($destinationPath);
283
        } else {
284
            //file doesn't already exist
285
            //lets make sure the parent directory does
286 13
            $this->filesystem->ensureDirectoryExists(dirname($destinationPath));
287
        }
288
289 13
        $relativeSourcePath = $this->getRelativePath($destinationPath, $sourcePath);
290 13
        if (!@\symlink($relativeSourcePath, $destinationPath)) {
291 1
            $this->io->write(sprintf('<error>Failed to symlink %s to %s</error>', $sourcePath, $destinationPath));
292
        }
293 13
    }
294
295
    /**
296
     * Get filtered packages array
297
     */
298 19
    public function getStaticPackages() : array
299
    {
300 19
        $packages = $this->composer->getRepositoryManager()->getLocalRepository()->getPackages();
301
302
        return array_filter($packages, function (PackageInterface $package) {
303 19
            return $package->getType() == static::PACKAGE_TYPE && $this->getStaticMaps($package->getName());
304 19
        });
305
    }
306
307
    /**
308
     * Get a single static package's maps or all static maps
309
     */
310 21
    public function getStaticMaps($packageName = null) : array
311
    {
312 21
        if ($packageName === null) {
313 1
            return $this->staticMaps;
314 20
        } elseif (array_key_exists($packageName, $this->staticMaps)) {
315 19
            return $this->staticMaps[$packageName];
316
        } else {
317 1
            $this->io->write(sprintf('<error>Mappings for %s are not defined</error>', $packageName));
318 1
            return [];
319
        }
320
    }
321
322
    /**
323
     * Isolated event that runs on PRE hooks to cleanup mapped packages
324
     */
325 6
    public function staticsCleanup()
326
    {
327 6
        foreach ($this->getStaticPackages() as $package) {
328 6
            foreach ($this->getStaticMaps($package->getName()) as $mappingDir => $mappings) {
329 6
                $themeRootDir = $this->getRootThemeDir($mappingDir);
330
331 6
                if (!is_dir($themeRootDir)) {
332 1
                    continue;
333
                }
334
335
                // Get contents and sort
336 5
                $contents   = $this->getFullDirectoryListing($themeRootDir);
337 5
                $strLengths = array_map('strlen', $contents);
338 5
                array_multisort($strLengths, SORT_DESC, $contents);
339
340
                // Exception error message
341 5
                $errorMsg = sprintf("<error>Failed to remove %s from %s</error>", $package->getName(), $themeRootDir);
342
343 5
                foreach ($contents as $content) {
344
                    // Remove packages symlinked files/dirs
345 5
                    if (is_link($content)) {
346 5
                        $this->tryCleanup($content, $errorMsg);
347 5
                        continue;
348
                    }
349
350
                    // Remove empty folders
351 5
                    if (is_dir($content) && $this->filesystem->isDirEmpty($content)) {
352 6
                        $this->tryCleanup($content, $errorMsg);
353
                    }
354
                }
355
            }
356
        }
357 6
    }
358
359
    /**
360
     * Try to cleanup a file/dir, output on exception
361
     */
362 5
    private function tryCleanup(string $path, string $errorMsg)
363
    {
364
        try {
365 5
            $this->filesystem->remove($path);
366 2
        } catch (\RuntimeException $ex) {
367 2
            $this->io->write($errorMsg);
368
        }
369 5
    }
370
371
    /**
372
     * Get full directory listing without dots
373
     */
374 5
    private function getFullDirectoryListing(string $path) : array
375
    {
376 5
        $listings   = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
377 5
        $listingArr = array_keys(\iterator_to_array($listings));
378
379
        // Remove dots :)
380 5
        $listingArr = array_map(function ($listing) {
381 5
            return rtrim($listing, '\/\.');
382 5
        }, $listingArr);
383
384 5
        return array_unique($listingArr);
385
    }
386
387
    /**
388
     * This is utility method for symlink creation.
389
     * @see http://stackoverflow.com/a/2638272/485589
390
     */
391 16
    public function getRelativePath(string $from, string $to) : string
392
    {
393
        // some compatibility fixes for Windows paths
394 16
        $from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
395 16
        $to   = is_dir($to) ? rtrim($to, '\/') . '/' : $to;
396 16
        $from = str_replace('\\', '/', $from);
397 16
        $to   = str_replace('\\', '/', $to);
398
399 16
        $from     = explode('/', $from);
400 16
        $to       = explode('/', $to);
401 16
        $relPath  = $to;
402
403 16
        foreach ($from as $depth => $dir) {
404
            // find first non-matching dir
405 16
            if ($dir === $to[$depth]) {
406
                // ignore this directory
407 16
                array_shift($relPath);
408
            } else {
409
                // get number of remaining dirs to $from
410 15
                $remaining = count($from) - $depth;
411 15
                if ($remaining > 1) {
412
                    // add traversals up to first matching dir
413 14
                    $padLength = (count($relPath) + $remaining - 1) * -1;
414 14
                    $relPath = array_pad($relPath, $padLength, '..');
415 14
                    break;
416
                } else {
417 16
                    $relPath[0] = './' . $relPath[0];
418
                }
419
            }
420
        }
421 16
        return implode('/', $relPath);
422
    }
423
424 17
    private function getRootThemeDir(string $mappingDir) : string
425
    {
426 17
        return sprintf(
427 17
            '%s%s/app/design/frontend/%s/web',
428
            getcwd(),
429 17
            $this->mageDir ? '/' . $this->mageDir : '',
430
            ucwords($mappingDir)
431
        );
432
    }
433
}
434