Completed
Pull Request — master (#14)
by Michael
02:20
created

StaticsMergerPlugin   C

Complexity

Total Complexity 56

Size/Duplication

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