Completed
Pull Request — master (#7)
by Anton
01:51
created

Plugin::copyExtraFiles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
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\EventDispatcher\EventSubscriberInterface;
16
use Composer\Installer\PackageEvents;
17
use Composer\IO\IOInterface;
18
use Composer\Plugin\PluginInterface;
19
use Composer\Script\Event;
20
use Composer\Script\ScriptEvents;
21
use Symfony\Component\Filesystem\Exception\IOException;
22
use Symfony\Component\Filesystem\Filesystem;
23
use Symfony\Component\Finder\Finder;
24
25
/**
26
 * Class Plugin
27
 *
28
 * @package Bluz\Composer\Installers
29
 */
30
class Plugin implements PluginInterface, EventSubscriberInterface
31
{
32
    const PERMISSION_CODE = 0755;
33
    const REPEAT = 5;
34
    const DIRECTORIES = [
35
        'application',
36
        'data',
37
        'public',
38
        'tests'
39
    ];
40
41
    /**
42
     * @var Installer
43
     */
44
    protected $installer;
45
46
    /**
47
     * @var string
48
     */
49
    protected $environment;
50
51
    /**
52
     * @var Filesystem
53
     */
54
    protected $filesystem;
55
56
    /**
57
     * Create instance, define constants
58
     */
59
    public function __construct()
60
    {
61
        defined('PATH_ROOT') ?: define('PATH_ROOT', realpath($_SERVER['DOCUMENT_ROOT']));
62
        defined('DS') ?: define('DS', DIRECTORY_SEPARATOR);
63
    }
64
65
    /**
66
     * Called after the plugin is loaded
67
     *
68
     * It setup composer installer
69
     *
70
     * {@inheritDoc}
71
     */
72
    public function activate(Composer $composer, IOInterface $io)
73
    {
74
        $this->installer = new Installer($io, $composer);
75
        $composer->getInstallationManager()->addInstaller($this->installer);
76
    }
77
78
    /**
79
     * Registered events after the plugin is loaded
80
     *
81
     * {@inheritDoc}
82
     */
83
    public static function getSubscribedEvents(): array
84
    {
85
        return [
86
            // copy files to working directory
87
            PackageEvents::POST_PACKAGE_INSTALL => 'copyFiles',
88
            // copy new files
89
            PackageEvents::POST_PACKAGE_UPDATE => 'copyFiles',
90
            // removed unchanged files
91
            PackageEvents::PRE_PACKAGE_UPDATE => 'removeFiles',
92
            // removed all files
93
            PackageEvents::PRE_PACKAGE_UNINSTALL => 'removeFiles',
94
            // copy extra files from root composer.json
95
            ScriptEvents::POST_UPDATE_CMD => 'copyExtraFiles',
96
            // remove extra files from root composer.json
97
            ScriptEvents::PRE_UPDATE_CMD => 'removeExtraFiles'
98
        ];
99
    }
100
101
102
    /**
103
     * Hook which is called after install package
104
     * It copies bluz module
105
     */
106
    public function copyFiles()
107
    {
108
        if (file_exists($this->installer->getVendorPath())) {
109
            $this->copyModule();
110
        }
111
    }
112
113
    /**
114
     * Hook which is called before update package
115
     * It checks bluz module
116
     */
117
    public function removeFiles()
118
    {
119
        if (file_exists($this->installer->getVendorPath())) {
120
            $this->removeModule();
121
        }
122
123
    }
124
125
    /**
126
     * Copy extra files from compose.json of project
127
     *
128
     * @param Event $event
129
     *
130
     * @return void
131
     */
132
    public function copyExtraFiles(Event $event)
133
    {
134
        $extras = $event->getComposer()->getPackage()->getExtra();
135
        if (array_key_exists('copy-files', $extras)) {
136
            $this->installer->getIo()->write(
137
                sprintf('  - Copied additional file(s)'),
138
                true
139
            );
140
            $this->copyExtras($extras['copy-files']);
141
        }
142
    }
143
144
    /**
145
     * Remove extra files from compose.json of project
146
     *
147
     * @param Event $event
148
     *
149
     * @return void
150
     */
151
    public function removeExtraFiles(Event $event)
152
    {
153
        $extras = $event->getComposer()->getPackage()->getExtra();
154
        if (array_key_exists('copy-files', $extras)) {
155
            $this->removeExtras($extras['copy-files']);
156
        }
157
    }
158
159
    /**
160
     * Get Filesystem
161
     *
162
     * @return Filesystem
163
     */
164
    protected function getFilesystem()
165
    {
166
        if (!$this->filesystem) {
167
            $this->filesystem = new Filesystem();
168
        }
169
        return $this->filesystem;
170
    }
171
172
    /**
173
     * getExtra
174
     *
175
     * @return array
176
     */
177
    protected function getExtraFiles() : array
178
    {
179
        $moduleJson = json_decode(file_get_contents($this->installer->getVendorPath() . DS . 'composer.json'), true);
180
181
        if (isset($moduleJson, $moduleJson['extra'], $moduleJson['extra']['copy-files'])) {
182
            return $moduleJson['extra']['copy-files'];
183
        }
184
        return [];
185
    }
186
187
    /**
188
     * Copy Module files
189
     *
190
     * @return void
191
     * @throws \InvalidArgumentException
192
     */
193
    protected function copyModule()
194
    {
195
        $this->copyExtras($this->getExtraFiles());
196
197
        foreach (self::DIRECTORIES as $directory) {
198
            $this->copy(
199
                $this->installer->getVendorPath() . DS . $directory . DS,
200
                PATH_ROOT . DS . $directory . DS
201
            );
202
        }
203
204
        $this->installer->getIo()->write(
205
            sprintf(
206
                '  - Copied <comment>%s</comment> module to application',
207
                basename($this->installer->getVendorPath())
208
            ),
209
            true
210
        );
211
    }
212
213
    /**
214
     * copyExtras
215
     *
216
     * @param  array $files
217
     *
218
     * @return void
219
     * @throws \InvalidArgumentException
220
     */
221
    protected function copyExtras($files)
222
    {
223
        foreach ($files as $source => $target) {
224
            $this->copy(
225
                dirname($this->installer->getVendorPath(), 2) . DS . $source,
226
                PATH_ROOT . DS . $target
227
            );
228
        }
229
    }
230
231
    /**
232
     * It recursively copies the files and directories
233
     *
234
     * @param $source
235
     * @param $target
236
     *
237
     * @return void
238
     * @throws \InvalidArgumentException
239
     */
240
    protected function copy($source, $target)
241
    {
242
        // skip, if not exists
243
        if (!file_exists($source)) {
244
            return;
245
        }
246
        // skip, if target exists
247
        if (is_file($target)) {
248
            $this->installer->getIo()->write(
249
                sprintf('  - File <comment>%s</comment> already exists', $target),
250
                true,
251
                IOInterface::VERBOSE
252
            );
253
            return;
254
        }
255
256
        // Check the renaming of file for direct moving (file-to-file)
257
        $isRenameFile = substr($target, -1) !== '/' && !is_dir($source);
258
259
        if (file_exists($target) && !is_dir($target) && !$isRenameFile) {
260
            throw new \InvalidArgumentException('Destination directory is not a directory');
261
        }
262
263
        try {
264
            if ($isRenameFile) {
265
                $this->getFilesystem()->mkdir(dirname($target));
266
            } else {
267
                $this->getFilesystem()->mkdir($target);
268
            }
269
        } catch (IOException $e) {
270
            throw new \InvalidArgumentException(
271
                sprintf('Could not create directory `%s`', $target)
272
            );
273
        }
274
275
        if (false === file_exists($source)) {
276
            throw new \InvalidArgumentException(
277
                sprintf('Source directory or file `%s` does not exist', $source)
278
            );
279
        }
280
281
        if (is_dir($source)) {
282
            $finder = new Finder;
283
            $finder->files()->in($source);
284
285
            foreach ($finder as $file) {
286
                try {
287
                    $this->getFilesystem()->copy($file, $target . DS . $file->getRelativePathname());
288
                } catch (IOException $e) {
289
                    throw new \InvalidArgumentException(
290
                        sprintf('Could not copy `%s`', $file->getBaseName())
291
                    );
292
                }
293
            }
294
        } else {
295
            try {
296
                if ($isRenameFile) {
297
                    $this->getFilesystem()->copy($source, $target);
298
                } else {
299
                    $this->getFilesystem()->copy($source, $target . '/' . basename($source));
300
                }
301
            } catch (IOException $e) {
302
                throw new \InvalidArgumentException(sprintf('Could not copy `%s`', $source));
303
            }
304
        }
305
306
        $this->installer->getIo()->write(
307
            sprintf('  - Copied file(s) from <comment>%s</comment> to <comment>%s</comment>', $source, $target),
308
            true,
309
            IOInterface::VERBOSE
310
        );
311
    }
312
313
    /**
314
     * It recursively removes the files and empty directories
315
     * @return void
316
     */
317
    protected function removeModule()
318
    {
319
        $this->removeExtras($this->getExtraFiles());
320
321
        foreach (self::DIRECTORIES as $directory) {
322
            $this->remove($directory);
323
        }
324
325
        $this->installer->getIo()->write(
326
            sprintf(
327
                '  - Removed <comment>%s</comment> module from application',
328
                basename($this->installer->getVendorPath())
329
            ),
330
            true
331
        );
332
    }
333
334
    /**
335
     * removeExtras
336
     *
337
     * @param  array $files
338
     *
339
     * @return void
340
     */
341
    protected function removeExtras($files)
342
    {
343
        foreach ($files as $source => $target) {
344
            $this->installer->getIo()->write(
345
                sprintf('  - Skipped additional file(s) <comment>%s</comment>', $target),
346
                true
347
            );
348
        }
349
    }
350
351
    /**
352
     * It recursively removes the files and directories
353
     * @param $directory
354
     * @return void
355
     */
356
    protected function remove($directory)
357
    {
358
        $sourcePath = $this->installer->getVendorPath() . DS . $directory;
359
360
        if (!is_dir($sourcePath)) {
361
            return;
362
        }
363
        foreach ($iterator = new \RecursiveIteratorIterator(
364
            new \RecursiveDirectoryIterator(
365
                $sourcePath,
366
                \RecursiveDirectoryIterator::SKIP_DOTS
367
            ),
368
            \RecursiveIteratorIterator::CHILD_FIRST
369
        ) as $item) {
370
            // path to copied file
371
            $current = PATH_ROOT . DS . $directory . DS . $iterator->getSubPathName();
372
373
            // remove empty directories
374
            if (is_dir($current)) {
375
                if (count(scandir($current, SCANDIR_SORT_ASCENDING)) === 2) {
376
                    rmdir($current);
377
                    $this->installer->getIo()->write(
378
                        "  - Removed directory `{$iterator->getSubPathName()}`",
379
                        true,
380
                        IOInterface::VERBOSE
381
                    );
382
                } else {
383
                    $this->installer->getIo()->write(
384
                        sprintf(
385
                            '  - <comment>Skipped directory `%s`</comment>',
386
                            $directory . DS . $iterator->getSubPathName()
387
                        ),
388
                        true,
389
                        IOInterface::VERBOSE
390
                    );
391
                }
392
                continue;
393
            }
394
395
            // skip already removed files
396
            if (!is_file($current)) {
397
                continue;
398
            }
399
400
            if (md5_file($item) === md5_file($current)) {
401
                // remove file
402
                unlink($current);
403
                $this->installer->getIo()->write(
404
                    "  - Removed file `{$iterator->getSubPathName()}`",
405
                    true,
406
                    IOInterface::VERBOSE
407
                );
408
            } else {
409
                // or skip changed files
410
                $this->installer->getIo()->write(
411
                    "  - <comment>File `{$iterator->getSubPathName()}` has changed</comment>",
412
                    true,
413
                    IOInterface::VERBOSE
414
                );
415
            }
416
        }
417
    }
418
}
419