Completed
Push — master ( 0e66aa...8a361e )
by Michael
21:56 queued 10:31
created

StaticsMergerPlugin   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 82.25%

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 9
dl 0
loc 416
ccs 139
cts 169
cp 0.8225
rs 6.5957
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 6 2
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
    /*
129
     * @throws \RuntimeException When environment is invalid
130
     */
131
    public function verifyEnvironment()
132
    {
133
        if (!is_executable($this->getYarnExecutablePath())) {
134
            throw new \RuntimeException('Yarn is not installed or executable!');
135
        }
136
    }
137
138
    private function getYarnExecutablePath() : string
139
    {
140
        return (new ExecutableFinder)->find('yarn', '');
141
    }
142
143
    /**
144
     * @throws \RuntimeException When Yarn install fails or crossbow fails
145
     * @throws LogicException From process
146
     * @throws RuntimeException From process
147
     */
148
    public function staticsCompile()
149
    {
150
        $cwd = getcwd();
151
152
        foreach ($this->getStaticPackages() as $package) {
153
            chdir($this->getInstallPath($package));
154
155
            $this->io->write(sprintf('<info>Installing dependencies for "%s"', $package->getPrettyName()));
156
            $dependencyProcess = new Process(sprintf('%s && npm rebuild node-sass', $this->getYarnExecutablePath()));
157
158
            try {
159
                $dependencyProcess->setTimeout(300)->mustRun();
160
            } catch (ProcessFailedException $e) {
161
                $this->io->write($dependencyProcess->getOutput());
162
                $this->io->write($dependencyProcess->getErrorOutput());
163
                $this->io->write(
164
                    sprintf('<error>Failed to install dependencies for "%s" </error>', $package->getPrettyName())
165
                );
166
                return false;
167
            }
168
169
            $this->io->write(sprintf('<info>Building statics assets for "%s"', $package->getPrettyName()));
170
            $buildProcess = new Process('node_modules/.bin/cb release');
171
172
            try {
173
                $buildProcess->setTimeout(300)->mustRun();
174
            } catch (ProcessFailedException $e) {
175
                $this->io->write($buildProcess->getOutput());
176
                $this->io->write($buildProcess->getErrorOutput());
177
                $this->io->write(
178
                    sprintf('<error>Static package "%s" failed to build </error>', $package->getPrettyName())
179
                );
180
                return false;
181
            }
182
        }
183
184
        chdir($cwd);
185
    }
186
187 17
    public function symlinkStatics()
188
    {
189 17
        foreach ($this->getStaticPackages() as $package) {
190 16
            $packageSource = $this->getInstallPath($package);
191
192 16
            foreach ($this->getStaticMaps($package->getName()) as $mappingDir => $mappings) {
193 16
                $destinationTheme = $this->getRootThemeDir($mappingDir);
194
195
                // Add slash to paths
196 16
                $packageSource    = rtrim($packageSource, '/');
197 16
                $destinationTheme = rtrim($destinationTheme, '/');
198
199
                // If theme doesn't exist - Create it
200 16
                $this->filesystem->ensureDirectoryExists($destinationTheme);
201
202
                // Process files from package
203 16
                if ($mappings) {
204 15
                    $this->processFiles($packageSource, $destinationTheme, $mappings);
205
                } else {
206 1
                    $this->io->write(
207
                        sprintf(
208 1
                            '<error>%s requires at least one file mapping, has none!<error>',
209 16
                            $package->getPrettyName()
210
                        )
211
                    );
212
                }
213
            }
214
        }
215 17
    }
216
217
    /**
218
     * Processes defined file mappings and symlinks resulting files to destination theme
219
     */
220 15
    public function processFiles(string $packageSource, string $destinationTheme, array $files = [])
221
    {
222 15
        foreach ($files as $file) {
223
            // Ensure we have correct json
224 15
            if (isset($file['src']) && isset($file['dest'])) {
225 15
                $src    = sprintf("%s/%s", $packageSource, $file['src']);
226 15
                $dest   = rtrim($file['dest'], '/');
227
228
                // Check if it's a glob
229 15
                if (strpos($src, '*') !== false) {
230 2
                    $files = array_filter(glob($src), 'is_file');
231 2
                    foreach ($files as $globFile) {
232
                        //strip the full path
233
                        //and just get path relative to package
234 2
                        $fileSource = str_replace(sprintf("%s/", $packageSource), "", $globFile);
235
236 2
                        $dest = ltrim(sprintf("%s/%s", $dest, basename($fileSource)), '/');
237
238 2
                        $this->processSymlink($packageSource, $fileSource, $destinationTheme, $dest);
239 2
                        $dest = $file['dest'];
240
                    }
241
                } else {
242 15
                    if (!$dest) {
243 1
                        $this->io->write(
244 1
                            sprintf('<error>Full path is required for: "%s" </error>', $file['src'])
245
                        );
246 1
                        return false;
247
                    }
248
249 15
                    $this->processSymlink($packageSource, $file['src'], $destinationTheme, $dest);
250
                }
251
            }
252
        }
253 14
    }
254
255
    /**
256
     * Process symlink, checks given source and destination paths
257
     */
258 15
    public function processSymlink(
259
        string $packageSrc,
260
        string $relativeSourcePath,
261
        string $destinationTheme,
262
        string $relativeDestinationPath
263
    ) {
264 15
        $sourcePath         = sprintf("%s/%s", $packageSrc, $relativeSourcePath);
265 15
        $destinationPath    = sprintf("%s/%s", $destinationTheme, $relativeDestinationPath);
266
267 15
        if (!file_exists($sourcePath)) {
268 1
            $this->io->write(
269 1
                sprintf('<error>The static package does not contain directory: "%s" </error>', $relativeSourcePath)
270
            );
271 1
            return;
272
        }
273
274 14
        if (file_exists($destinationPath) && !is_link($destinationPath)) {
275 1
            $this->io->write(
276
                sprintf(
277 1
                    '<error>Your static path: "%s" is currently not a symlink, please remove first </error>',
278
                    $destinationPath
279
                )
280
            );
281 1
            return;
282
        }
283
284
        //if it's a link, remove it and recreate it
285
        //assume we are updating the static package
286 13
        if (is_link($destinationPath)) {
287 1
            unlink($destinationPath);
288
        } else {
289
            //file doesn't already exist
290
            //lets make sure the parent directory does
291 13
            $this->filesystem->ensureDirectoryExists(dirname($destinationPath));
292
        }
293
294 13
        $relativeSourcePath = $this->getRelativePath($destinationPath, $sourcePath);
295 13
        if (!@\symlink($relativeSourcePath, $destinationPath)) {
296 1
            $this->io->write(sprintf('<error>Failed to symlink %s to %s</error>', $sourcePath, $destinationPath));
297
        }
298 13
    }
299
300
    /**
301
     * Get filtered packages array
302
     */
303 19
    public function getStaticPackages() : array
304
    {
305 19
        $packages = $this->composer->getRepositoryManager()->getLocalRepository()->getPackages();
306
307
        return array_filter($packages, function (PackageInterface $package) {
308 19
            return $package->getType() == static::PACKAGE_TYPE && $this->getStaticMaps($package->getName());
309 19
        });
310
    }
311
312
    /**
313
     * Get a single static package's maps or all static maps
314
     */
315 21
    public function getStaticMaps($packageName = null) : array
316
    {
317 21
        if ($packageName === null) {
318 1
            return $this->staticMaps;
319 20
        } elseif (array_key_exists($packageName, $this->staticMaps)) {
320 19
            return $this->staticMaps[$packageName];
321
        } else {
322 1
            $this->io->write(sprintf('<error>Mappings for %s are not defined</error>', $packageName));
323 1
            return [];
324
        }
325
    }
326
327
    /**
328
     * Isolated event that runs on PRE hooks to cleanup mapped packages
329
     */
330 6
    public function staticsCleanup()
331
    {
332 6
        foreach ($this->getStaticPackages() as $package) {
333 6
            foreach ($this->getStaticMaps($package->getName()) as $mappingDir => $mappings) {
334 6
                $themeRootDir = $this->getRootThemeDir($mappingDir);
335
336 6
                if (!is_dir($themeRootDir)) {
337 1
                    continue;
338
                }
339
340
                // Get contents and sort
341 5
                $contents   = $this->getFullDirectoryListing($themeRootDir);
342 5
                $strLengths = array_map('strlen', $contents);
343 5
                array_multisort($strLengths, SORT_DESC, $contents);
344
345
                // Exception error message
346 5
                $errorMsg = sprintf("<error>Failed to remove %s from %s</error>", $package->getName(), $themeRootDir);
347
348 5
                foreach ($contents as $content) {
349
                    // Remove packages symlinked files/dirs
350 5
                    if (is_link($content)) {
351 5
                        $this->tryCleanup($content, $errorMsg);
352 5
                        continue;
353
                    }
354
355
                    // Remove empty folders
356 5
                    if (is_dir($content) && $this->filesystem->isDirEmpty($content)) {
357 6
                        $this->tryCleanup($content, $errorMsg);
358
                    }
359
                }
360
            }
361
        }
362 6
    }
363
364
    /**
365
     * Try to cleanup a file/dir, output on exception
366
     */
367 5
    private function tryCleanup(string $path, string $errorMsg)
368
    {
369
        try {
370 5
            $this->filesystem->remove($path);
371 2
        } catch (\RuntimeException $ex) {
372 2
            $this->io->write($errorMsg);
373
        }
374 5
    }
375
376
    /**
377
     * Get full directory listing without dots
378
     */
379 5
    private function getFullDirectoryListing(string $path) : array
380
    {
381 5
        $listings   = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
382 5
        $listingArr = array_keys(\iterator_to_array($listings));
383
384
        // Remove dots :)
385 5
        $listingArr = array_map(function ($listing) {
386 5
            return rtrim($listing, '\/\.');
387 5
        }, $listingArr);
388
389 5
        return array_unique($listingArr);
390
    }
391
392
    /**
393
     * This is utility method for symlink creation.
394
     * @see http://stackoverflow.com/a/2638272/485589
395
     */
396 16
    public function getRelativePath(string $from, string $to) : string
397
    {
398
        // some compatibility fixes for Windows paths
399 16
        $from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
400 16
        $to   = is_dir($to) ? rtrim($to, '\/') . '/' : $to;
401 16
        $from = str_replace('\\', '/', $from);
402 16
        $to   = str_replace('\\', '/', $to);
403
404 16
        $from     = explode('/', $from);
405 16
        $to       = explode('/', $to);
406 16
        $relPath  = $to;
407
408 16
        foreach ($from as $depth => $dir) {
409
            // find first non-matching dir
410 16
            if ($dir === $to[$depth]) {
411
                // ignore this directory
412 16
                array_shift($relPath);
413
            } else {
414
                // get number of remaining dirs to $from
415 15
                $remaining = count($from) - $depth;
416 15
                if ($remaining > 1) {
417
                    // add traversals up to first matching dir
418 14
                    $padLength = (count($relPath) + $remaining - 1) * -1;
419 14
                    $relPath = array_pad($relPath, $padLength, '..');
420 14
                    break;
421
                } else {
422 16
                    $relPath[0] = './' . $relPath[0];
423
                }
424
            }
425
        }
426 16
        return implode('/', $relPath);
427
    }
428
429 17
    private function getRootThemeDir(string $mappingDir) : string
430
    {
431 17
        return sprintf(
432 17
            '%s%s/app/design/frontend/%s/web',
433
            getcwd(),
434 17
            $this->mageDir ? '/' . $this->mageDir : '',
435
            ucwords($mappingDir)
436
        );
437
    }
438
}
439