Completed
Push — master ( 2e6970...cfac02 )
by Anton
14s
created

Plugin::copyExtras()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
/**
3
 * Bluz composer plugin
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link https://github.com/bluzphp/composer-plugin
7
 */
8
9
/**
10
 * @namespace
11
 */
12
namespace Bluz\Composer\Installers;
13
14
use Composer\Composer;
15
use Composer\DependencyResolver\Operation\UpdateOperation;
16
use Composer\EventDispatcher\EventSubscriberInterface;
17
use Composer\Installer\PackageEvent;
18
use Composer\Installer\PackageEvents;
19
use Composer\IO\IOInterface;
20
use Composer\Package\PackageInterface;
21
use Composer\Plugin\PluginInterface;
22
use Composer\Script\Event;
23
use Composer\Script\ScriptEvents;
24
use Symfony\Component\Filesystem\Exception\IOException;
25
use Symfony\Component\Filesystem\Filesystem;
26
use Symfony\Component\Finder\Finder;
27
28
/**
29
 * Class Plugin
30
 *
31
 * @package Bluz\Composer\Installers
32
 */
33
class Plugin implements PluginInterface, EventSubscriberInterface
34
{
35
    const PERMISSION_CODE = 0755;
36
    const REPEAT = 5;
37
    const DIRECTORIES = [
38
        'application',
39
        'data',
40
        'public',
41
        'tests'
42
    ];
43
44
    /**
45
     * @var Installer
46
     */
47
    protected $installer;
48
49
    /**
50
     * @var string
51
     */
52
    protected $vendorPath;
53
54
    /**
55
     * @var string
56
     */
57
    protected $packagePath;
58
59
    /**
60
     * @var string
61
     */
62
    protected $environment;
63
64
    /**
65
     * @var Filesystem
66
     */
67
    protected $filesystem;
68
69
    /**
70
     * Create instance, define constants
71
     */
72
    public function __construct()
73
    {
74
        defined('PATH_ROOT') ?: define('PATH_ROOT', realpath($_SERVER['DOCUMENT_ROOT']));
75
        defined('DS') ?: define('DS', DIRECTORY_SEPARATOR);
76
    }
77
78
    /**
79
     * Called after the plugin is loaded
80
     *
81
     * It setup composer installer
82
     *
83
     * {@inheritDoc}
84
     */
85
    public function activate(Composer $composer, IOInterface $io)
86
    {
87
        $this->installer = new Installer($io, $composer);
88
        $this->vendorPath = $composer->getConfig()->get('vendor-dir');
89
        $composer->getInstallationManager()->addInstaller($this->installer);
90
    }
91
92
    /**
93
     * Registered events after the plugin is loaded
94
     *
95
     * {@inheritDoc}
96
     */
97
    public static function getSubscribedEvents(): array
98
    {
99
        return [
100
            // copy extra files from root composer.json
101
            // do it only once after create project
102
            ScriptEvents::POST_CREATE_PROJECT_CMD => 'copyProjectExtraFiles',
103
            // copy module's files to working directory
104
            PackageEvents::POST_PACKAGE_INSTALL => 'copyModuleFiles',
105
            PackageEvents::POST_PACKAGE_UPDATE => 'copyModuleFiles',
106
            // removed unchanged module's files
107
            PackageEvents::PRE_PACKAGE_UPDATE => 'removeModuleFiles',
108
            PackageEvents::PRE_PACKAGE_UNINSTALL => 'removeModuleFiles',
109
        ];
110
    }
111
112
    /**
113
     * extractPackage
114
     *
115
     * @param PackageEvent $event
116
     *
117
     * @return PackageInterface
118
     */
119
    protected function extractPackage(PackageEvent $event)
120
    {
121
        if ($event->getOperation() instanceof UpdateOperation) {
122
            return $event->getOperation()->getTargetPackage();
123
        }
124
        return $event->getOperation()->getPackage();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Composer\DependencyResol...tion\OperationInterface as the method getPackage() does only exist in the following implementations of said interface: Composer\DependencyResol...ration\InstallOperation, Composer\DependencyResol...AliasInstalledOperation, Composer\DependencyResol...iasUninstalledOperation, Composer\DependencyResol...tion\UninstallOperation.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
125
    }
126
127
    /**
128
     * Copy extra files from compose.json of project
129
     *
130
     * @param Event $event
131
     *
132
     * @return void
133
     * @throws \InvalidArgumentException
134
     */
135
    public function copyProjectExtraFiles(Event $event)
136
    {
137
        $extras = $event->getComposer()->getPackage()->getExtra();
138
        if (array_key_exists('copy-files', $extras)) {
139
            $this->installer->getIo()->write(
140
                sprintf('  - Copied additional file(s)'),
141
                true
142
            );
143
            $this->copyExtras($extras['copy-files']);
144
        }
145
    }
146
147
    /**
148
     * Hook which is called after install package
149
     * It copies bluz module
150
     *
151
     * @param PackageEvent $event
152
     *
153
     * @throws \InvalidArgumentException
154
     */
155 View Code Duplication
    public function copyModuleFiles(PackageEvent $event)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
156
    {
157
        $package = $this->extractPackage($event);
158
        $this->packagePath = $this->vendorPath .DS. $package->getName();
159
        if ($package->getType() === 'bluz-module' && file_exists($this->packagePath)) {
160
            if ($package->getExtra() && isset($package->getExtra()['copy-files'])) {
161
                $this->copyExtras($package->getExtra()['copy-files']);
162
            }
163
            $this->copyModule();
164
        }
165
    }
166
167
    /**
168
     * Hook which is called before update package
169
     * It checks bluz module
170
     *
171
     * @param PackageEvent $event
172
     */
173 View Code Duplication
    public function removeModuleFiles(PackageEvent $event)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
174
    {
175
        $package = $this->extractPackage($event);
176
        $this->packagePath = $this->vendorPath .DS. $package->getName();
177
        if ($package->getType() === 'bluz-module' && file_exists($this->packagePath)) {
178
            if ($package->getExtra() && isset($package->getExtra()['copy-files'])) {
179
                $this->removeExtras($package->getExtra()['copy-files']);
180
            }
181
            $this->removeModule();
182
        }
183
    }
184
185
    /**
186
     * Get Filesystem
187
     *
188
     * @return Filesystem
189
     */
190
    protected function getFilesystem()
191
    {
192
        if (!$this->filesystem) {
193
            $this->filesystem = new Filesystem();
194
        }
195
        return $this->filesystem;
196
    }
197
198
    /**
199
     * Copy Module files
200
     *
201
     * @return void
202
     * @throws \InvalidArgumentException
203
     */
204
    protected function copyModule()
205
    {
206
        foreach (self::DIRECTORIES as $directory) {
207
            $this->copy(
208
                $this->packagePath . DS . $directory . DS,
209
                PATH_ROOT . DS . $directory . DS
210
            );
211
        }
212
213
        $this->installer->getIo()->write(
214
            sprintf(
215
                '  - Copied <comment>%s</comment> module to application',
216
                basename($this->packagePath)
217
            ),
218
            true
219
        );
220
    }
221
222
    /**
223
     * copyExtras
224
     *
225
     * @param  array $files
226
     *
227
     * @return void
228
     * @throws \InvalidArgumentException
229
     */
230
    protected function copyExtras($files)
231
    {
232
        foreach ($files as $source => $target) {
233
            $this->copy(
234
                $this->vendorPath . DS . $source,
235
                PATH_ROOT . DS . $target
236
            );
237
        }
238
    }
239
240
    /**
241
     * It recursively copies the files and directories
242
     *
243
     * @param string $source
244
     * @param string $target
245
     *
246
     * @return void
247
     * @throws \InvalidArgumentException
248
     */
249
    protected function copy($source, $target)
250
    {
251
        // skip, if not exists
252
        if (!file_exists($source)) {
253
            return;
254
        }
255
        // skip, if target exists
256
        if (is_file($target) && !is_dir($target)) {
257
            $this->installer->getIo()->write(
258
                sprintf('  - File <comment>%s</comment> already exists', $target),
259
                true,
260
                IOInterface::VERBOSE
261
            );
262
            return;
263
        }
264
265
        // Check the renaming of file for direct moving (file-to-file)
266
        $isRenameFile = substr($target, -1) !== '/' && !is_dir($source);
267
268
        if (file_exists($target) && !is_dir($target) && !$isRenameFile) {
269
            throw new \InvalidArgumentException('Destination directory is not a directory');
270
        }
271
272
        try {
273
            if ($isRenameFile) {
274
                $this->getFilesystem()->mkdir(dirname($target));
275
            } else {
276
                $this->getFilesystem()->mkdir($target);
277
            }
278
        } catch (IOException $e) {
279
            throw new \InvalidArgumentException(
280
                sprintf('Could not create directory `%s`', $target)
281
            );
282
        }
283
284
        if (false === file_exists($source)) {
285
            throw new \InvalidArgumentException(
286
                sprintf('Source directory or file `%s` does not exist', $source)
287
            );
288
        }
289
290
        if (is_dir($source)) {
291
            $finder = new Finder;
292
            $finder->files()->in($source);
293
294
            foreach ($finder as $file) {
295
                try {
296
                    $this->getFilesystem()->copy($file, $target . DS . $file->getRelativePathname());
297
                } catch (IOException $e) {
298
                    throw new \InvalidArgumentException(
299
                        sprintf('Could not copy `%s`', $file->getBaseName())
300
                    );
301
                }
302
            }
303
        } else {
304
            try {
305
                if ($isRenameFile) {
306
                    $this->getFilesystem()->copy($source, $target);
307
                } else {
308
                    $this->getFilesystem()->copy($source, $target . '/' . basename($source));
309
                }
310
            } catch (IOException $e) {
311
                throw new \InvalidArgumentException(sprintf('Could not copy `%s`', $source));
312
            }
313
        }
314
315
        $this->installer->getIo()->write(
316
            sprintf('  - Copied file(s) from <comment>%s</comment> to <comment>%s</comment>', $source, $target),
317
            true,
318
            IOInterface::VERBOSE
319
        );
320
    }
321
322
    /**
323
     * It recursively removes the files and empty directories
324
     * @return void
325
     */
326
    protected function removeModule()
327
    {
328
        foreach (self::DIRECTORIES as $directory) {
329
            $this->remove($directory);
330
        }
331
332
        $this->installer->getIo()->write(
333
            sprintf(
334
                '  - Removed <comment>%s</comment> module from application',
335
                basename($this->packagePath)
336
            ),
337
            true
338
        );
339
    }
340
341
    /**
342
     * removeExtras
343
     *
344
     * @param  array $files
345
     *
346
     * @return void
347
     */
348
    protected function removeExtras($files)
349
    {
350
        foreach ($files as $source => $target) {
351
            $this->installer->getIo()->write(
352
                sprintf('  - Skipped additional file(s) <comment>%s</comment>', $target),
353
                true
354
            );
355
        }
356
    }
357
358
    /**
359
     * It recursively removes the files and directories
360
     * @param $directory
361
     * @return void
362
     */
363
    protected function remove($directory)
364
    {
365
        $sourcePath = $this->packagePath . DS . $directory;
366
367
        if (!is_dir($sourcePath)) {
368
            return;
369
        }
370
        foreach ($iterator = new \RecursiveIteratorIterator(
371
            new \RecursiveDirectoryIterator(
372
                $sourcePath,
373
                \RecursiveDirectoryIterator::SKIP_DOTS
374
            ),
375
            \RecursiveIteratorIterator::CHILD_FIRST
376
        ) as $item) {
377
            // path to copied file
378
            $current = PATH_ROOT . DS . $directory . DS . $iterator->getSubPathName();
379
380
            // remove empty directories
381
            if (is_dir($current)) {
382
                if (count(scandir($current, SCANDIR_SORT_ASCENDING)) === 2) {
383
                    rmdir($current);
384
                    $this->installer->getIo()->write(
385
                        "  - Removed directory `{$iterator->getSubPathName()}`",
386
                        true,
387
                        IOInterface::VERBOSE
388
                    );
389
                } else {
390
                    $this->installer->getIo()->write(
391
                        sprintf(
392
                            '  - <comment>Skipped directory `%s`</comment>',
393
                            $directory . DS . $iterator->getSubPathName()
394
                        ),
395
                        true,
396
                        IOInterface::VERBOSE
397
                    );
398
                }
399
                continue;
400
            }
401
402
            // skip already removed files
403
            if (!is_file($current)) {
404
                continue;
405
            }
406
407
            if (md5_file($item) === md5_file($current)) {
408
                // remove file
409
                unlink($current);
410
                $this->installer->getIo()->write(
411
                    "  - Removed file `{$iterator->getSubPathName()}`",
412
                    true,
413
                    IOInterface::VERBOSE
414
                );
415
            } else {
416
                // or skip changed files
417
                $this->installer->getIo()->write(
418
                    "  - <comment>File `{$iterator->getSubPathName()}` has changed</comment>",
419
                    true,
420
                    IOInterface::VERBOSE
421
                );
422
            }
423
        }
424
    }
425
}
426