StaticsMergerPlugin   C
last analyzed

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