Completed
Push — fix/plugin_manager_flush ( b4bd0f )
by Kiyotaka
09:33
created

PluginService   F

Complexity

Total Complexity 108

Size/Duplication

Total Lines 902
Duplicated Lines 1.33 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
dl 12
loc 902
rs 1.698
c 0
b 0
f 0
wmc 108
lcom 1
cbo 16

34 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 23 1
A install() 0 42 3
A installWithCode() 6 27 5
A preInstall() 0 6 1
A postInstall() 0 36 3
A generateProxyAndUpdateSchema() 0 36 5
A createTempDir() 0 12 2
A deleteDirs() 0 9 3
A unpackPluginArchive() 0 17 3
B checkPluginArchiveContent() 0 27 8
B readConfig() 0 27 7
A checkSymbolName() 0 7 2
A deleteFile() 0 5 1
A checkSamePlugin() 0 8 3
A calcPluginDir() 0 4 1
A createPluginDir() 0 7 2
A registerPlugin() 0 21 2
A callPluginManagerMethod() 0 10 3
A uninstall() 0 32 4
A unregisterPlugin() 0 10 2
A disable() 0 4 1
B regenerateProxy() 0 33 8
A enable() 0 31 5
A update() 0 34 4
A updatePlugin() 0 21 3
A getPluginRequired() 0 13 3
A findDependentPluginNeedDisable() 0 4 1
B findDependentPlugin() 0 32 7
A getDependentByCode() 6 29 5
A parseToComposerCommand() 0 11 2
A copyAssets() 0 10 2
A removeAssets() 0 10 2
A checkPluginExist() 0 10 2
A isEnable() 0 12 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PluginService 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 PluginService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) LOCKON CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.lockon.co.jp/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Eccube\Service;
15
16
use Doctrine\Common\Collections\Criteria;
17
use Doctrine\ORM\EntityManager;
18
use Doctrine\ORM\EntityManagerInterface;
19
use Eccube\Common\Constant;
20
use Eccube\Common\EccubeConfig;
21
use Eccube\Entity\Plugin;
22
use Eccube\Exception\PluginException;
23
use Eccube\Repository\PluginRepository;
24
use Eccube\Service\Composer\ComposerServiceInterface;
25
use Eccube\Util\CacheUtil;
26
use Eccube\Util\StringUtil;
27
use Symfony\Component\DependencyInjection\ContainerInterface;
28
use Symfony\Component\Filesystem\Filesystem;
29
30
class PluginService
31
{
32
    /**
33
     * @var EccubeConfig
34
     */
35
    protected $eccubeConfig;
36
37
    /**
38
     * @var EntityManager
39
     */
40
    protected $entityManager;
41
42
    /**
43
     * @var PluginRepository
44
     */
45
    protected $pluginRepository;
46
47
    /**
48
     * @var EntityProxyService
49
     */
50
    protected $entityProxyService;
51
52
    /**
53
     * @var SchemaService
54
     */
55
    protected $schemaService;
56
57
    /**
58
     * @var ComposerServiceInterface
59
     */
60
    protected $composerService;
61
62
    const VENDOR_NAME = 'ec-cube';
63
64
    /**
65
     * Plugin type/library of ec-cube
66
     */
67
    const ECCUBE_LIBRARY = 1;
68
69
    /**
70
     * Plugin type/library of other (except ec-cube)
71
     */
72
    const OTHER_LIBRARY = 2;
73
74
    /**
75
     * @var string %kernel.project_dir%
76
     */
77
    private $projectRoot;
78
79
    /**
80
     * @var string %kernel.environment%
81
     */
82
    private $environment;
83
84
    /**
85
     * @var ContainerInterface
86
     */
87
    protected $container;
88
89
    /** @var CacheUtil */
90
    protected $cacheUtil;
91
92
    /**
93
     * @var PluginApiService
94
     */
95
    private $pluginApiService;
96
97
    /**
98
     * PluginService constructor.
99
     *
100
     * @param EntityManagerInterface $entityManager
101
     * @param PluginRepository $pluginRepository
102
     * @param EntityProxyService $entityProxyService
103
     * @param SchemaService $schemaService
104
     * @param EccubeConfig $eccubeConfig
105
     * @param ContainerInterface $container
106
     * @param CacheUtil $cacheUtil
107
     * @param ComposerServiceInterface $composerService
108
     * @param PluginApiService $pluginApiService
109
     */
110
    public function __construct(
111
        EntityManagerInterface $entityManager,
112
        PluginRepository $pluginRepository,
113
        EntityProxyService $entityProxyService,
114
        SchemaService $schemaService,
115
        EccubeConfig $eccubeConfig,
116
        ContainerInterface $container,
117
        CacheUtil $cacheUtil,
118
        ComposerServiceInterface $composerService,
119
        PluginApiService $pluginApiService
120
    ) {
121
        $this->entityManager = $entityManager;
0 ignored issues
show
Documentation Bug introduced by
$entityManager is of type object<Doctrine\ORM\EntityManagerInterface>, but the property $entityManager was declared to be of type object<Doctrine\ORM\EntityManager>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
122
        $this->pluginRepository = $pluginRepository;
123
        $this->entityProxyService = $entityProxyService;
124
        $this->schemaService = $schemaService;
125
        $this->eccubeConfig = $eccubeConfig;
126
        $this->projectRoot = $eccubeConfig->get('kernel.project_dir');
127
        $this->environment = $eccubeConfig->get('kernel.environment');
128
        $this->container = $container;
129
        $this->cacheUtil = $cacheUtil;
130
        $this->composerService = $composerService;
131
        $this->pluginApiService = $pluginApiService;
132
    }
133
134
    /**
135
     * ファイル指定してのプラグインインストール
136
     *
137
     * @param string $path   path to tar.gz/zip plugin file
138
     * @param int    $source
139
     *
140
     * @return boolean
141
     *
142
     * @throws PluginException
143
     * @throws \Exception
144
     */
145
    public function install($path, $source = 0)
146
    {
147
        $pluginBaseDir = null;
148
        $tmp = null;
149
        try {
150
            // プラグイン配置前に実施する処理
151
            $this->preInstall();
152
            $tmp = $this->createTempDir();
153
154
            // 一旦テンポラリに展開
155
            $this->unpackPluginArchive($path, $tmp);
156
            $this->checkPluginArchiveContent($tmp);
157
158
            $config = $this->readConfig($tmp);
159
            // テンポラリのファイルを削除
160
            $this->deleteFile($tmp);
161
162
            // 重複していないかチェック
163
            $this->checkSamePlugin($config['code']);
164
165
            $pluginBaseDir = $this->calcPluginDir($config['code']);
166
            // 本来の置き場所を作成
167
            $this->createPluginDir($pluginBaseDir);
168
169
            // 問題なければ本当のplugindirへ
170
            $this->unpackPluginArchive($path, $pluginBaseDir);
171
172
            // リソースファイルをコピー
173
            $this->copyAssets($config['code']);
174
            // プラグイン配置後に実施する処理
175
            $this->postInstall($config, $source);
176
        } catch (PluginException $e) {
177
            $this->deleteDirs([$tmp, $pluginBaseDir]);
178
            throw $e;
179
        } catch (\Exception $e) {
180
            // インストーラがどんなExceptionを上げるかわからないので
181
            $this->deleteDirs([$tmp, $pluginBaseDir]);
182
            throw $e;
183
        }
184
185
        return true;
186
    }
187
188
    /**
189
     * @param $code string sプラグインコード
190
     *
191
     * @throws PluginException
192
     */
193
    public function installWithCode($code)
194
    {
195
        $pluginDir = $this->calcPluginDir($code);
196
        $this->checkPluginArchiveContent($pluginDir);
197
        $config = $this->readConfig($pluginDir);
198
199
        if (isset($config['source']) && $config['source']) {
200
            // 依存プラグインが有効になっていない場合はエラー
201
            $requires = $this->getPluginRequired($config);
202 View Code Duplication
            $notInstalledOrDisabled = array_filter($requires, function ($req) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
203
                $code = preg_replace('/^ec-cube\//', '', $req['name']);
204
                /** @var Plugin $DependPlugin */
205
                $DependPlugin = $this->pluginRepository->findOneBy(['code' => $code]);
206
207
                return $DependPlugin ? $DependPlugin->isEnabled() == false : true;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
208
            });
209
210
            if (!empty($notInstalledOrDisabled)) {
211
                $names = array_map(function ($p) { return $p['name']; }, $notInstalledOrDisabled);
212
                throw new PluginException(implode(', ', $names).'を有効化してください。');
213
            }
214
        }
215
216
        $this->checkSamePlugin($config['code']);
217
        $this->copyAssets($config['code']);
218
        $this->postInstall($config, $config['source']);
219
    }
220
221
    // インストール事前処理
222
    public function preInstall()
223
    {
224
        // キャッシュの削除
225
        // FIXME: Please fix clearCache function (because it's clear all cache and this file just upload)
226
//        $this->cacheUtil->clearCache();
227
    }
228
229
    // インストール事後処理
230
    public function postInstall($config, $source)
231
    {
232
        // dbにプラグイン登録
233
234
        $this->entityManager->getConnection()->beginTransaction();
235
236
        try {
237
            $Plugin = $this->pluginRepository->findByCode($config['code']);
238
239
            if (!$Plugin) {
240
                $Plugin = new Plugin();
241
                // インストール直後はプラグインは有効にしない
242
                $Plugin->setName($config['name'])
243
                    ->setEnabled(false)
244
                    ->setVersion($config['version'])
245
                    ->setSource($source)
246
                    ->setCode($config['code']);
247
                $this->entityManager->persist($Plugin);
248
                $this->entityManager->flush();
249
            }
250
251
            $this->generateProxyAndUpdateSchema($Plugin, $config);
252
253
            $this->callPluginManagerMethod($config, 'install');
254
255
            $Plugin->setInitialized(true);
256
            $this->entityManager->persist($Plugin);
257
            $this->entityManager->flush();
258
259
            $this->entityManager->flush();
260
            $this->entityManager->getConnection()->commit();
261
        } catch (\Exception $e) {
262
            $this->entityManager->getConnection()->rollback();
263
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
264
        }
265
    }
266
267
    public function generateProxyAndUpdateSchema(Plugin $plugin, $config, $uninstall = false)
268
    {
269
        if ($plugin->isEnabled()) {
270
            $generatedFiles = $this->regenerateProxy($plugin, false);
271
            $this->schemaService->updateSchema($generatedFiles, $this->projectRoot.'/app/proxy/entity');
272
        } else {
273
            // Proxyのクラスをロードせずにスキーマを更新するために、
274
            // インストール時には一時的なディレクトリにProxyを生成する
275
            $tmpProxyOutputDir = sys_get_temp_dir().'/proxy_'.StringUtil::random(12);
276
            @mkdir($tmpProxyOutputDir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
277
278
            try {
279
                if (!$uninstall) {
280
                    // プラグインmetadata定義を追加
281
                    $entityDir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode().'/Entity';
282
                    if (file_exists($entityDir)) {
283
                        $ormConfig = $this->entityManager->getConfiguration();
284
                        $chain = $ormConfig->getMetadataDriverImpl();
285
                        $driver = $ormConfig->newDefaultAnnotationDriver([$entityDir], false);
286
                        $namespace = 'Plugin\\'.$config['code'].'\\Entity';
287
                        $chain->addDriver($driver, $namespace);
288
                        $ormConfig->addEntityNamespace($plugin->getCode(), $namespace);
289
                    }
290
                }
291
292
                // 一時的に利用するProxyを生成してからスキーマを更新する
293
                $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir, $uninstall);
294
                $this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);
295
            } finally {
296
                foreach (glob("${tmpProxyOutputDir}/*") as  $f) {
297
                    unlink($f);
298
                }
299
                rmdir($tmpProxyOutputDir);
300
            }
301
        }
302
    }
303
304
    public function createTempDir()
305
    {
306
        $tempDir = $this->projectRoot.'/var/cache/'.$this->environment.'/Plugin';
307
        @mkdir($tempDir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
308
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
309
310
        if (!mkdir($d, 0777)) {
311
            throw new PluginException(trans('admin.store.plugin.mkdir.error', ['%dir_name%' => $d]));
312
        }
313
314
        return $d;
315
    }
316
317
    public function deleteDirs($arr)
318
    {
319
        foreach ($arr as $dir) {
320
            if (file_exists($dir)) {
321
                $fs = new Filesystem();
322
                $fs->remove($dir);
323
            }
324
        }
325
    }
326
327
    /**
328
     * @param string $archive
329
     * @param string $dir
330
     *
331
     * @throws PluginException
332
     */
333
    public function unpackPluginArchive($archive, $dir)
334
    {
335
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
336
        try {
337
            if ($extension == 'zip') {
338
                $zip = new \ZipArchive();
339
                $zip->open($archive);
340
                $zip->extractTo($dir);
341
                $zip->close();
342
            } else {
343
                $phar = new \PharData($archive);
344
                $phar->extractTo($dir, null, true);
345
            }
346
        } catch (\Exception $e) {
347
            throw new PluginException(trans('pluginservice.text.error.upload_failure'));
348
        }
349
    }
350
351
    /**
352
     * @param $dir
353
     * @param array $config_cache
354
     *
355
     * @throws PluginException
356
     */
357
    public function checkPluginArchiveContent($dir, array $config_cache = [])
358
    {
359
        try {
360
            if (!empty($config_cache)) {
361
                $meta = $config_cache;
362
            } else {
363
                $meta = $this->readConfig($dir);
364
            }
365
        } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
366
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
367
        }
368
369
        if (!is_array($meta)) {
370
            throw new PluginException('config.yml not found or syntax error');
371
        }
372
        if (!isset($meta['code']) || !$this->checkSymbolName($meta['code'])) {
373
            throw new PluginException('config.yml code empty or invalid_character(\W)');
374
        }
375
        if (!isset($meta['name'])) {
376
            // nameは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
377
            throw new PluginException('config.yml name empty');
378
        }
379
        if (!isset($meta['version'])) {
380
            // versionは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
381
            throw new PluginException('config.yml version invalid_character(\W) ');
382
        }
383
    }
384
385
    /**
386
     * @param $pluginDir
387
     *
388
     * @return array
389
     *
390
     * @throws PluginException
391
     */
392
    public function readConfig($pluginDir)
393
    {
394
        $composerJsonPath = $pluginDir.DIRECTORY_SEPARATOR.'composer.json';
395
        if (file_exists($composerJsonPath) === false) {
396
            throw new PluginException("${composerJsonPath} not found.");
397
        }
398
399
        $json = json_decode(file_get_contents($composerJsonPath), true);
400
        if ($json === null) {
401
            throw new PluginException("Invalid json format. [${composerJsonPath}]");
402
        }
403
404
        if (!isset($json['version'])) {
405
            throw new PluginException("`version` is not defined in ${composerJsonPath}");
406
        }
407
408
        if (!isset($json['extra']['code'])) {
409
            throw new PluginException("`extra.code` is not defined in ${composerJsonPath}");
410
        }
411
412
        return [
413
            'code' => $json['extra']['code'],
414
            'name' => isset($json['description']) ? $json['description'] : $json['extra']['code'],
415
            'version' => $json['version'],
416
            'source' => isset($json['extra']['id']) ? $json['extra']['id'] : false,
417
        ];
418
    }
419
420
    public function checkSymbolName($string)
421
    {
422
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
423
        // plugin_nameやplugin_codeに使える文字のチェック
424
        // a-z A-Z 0-9 _
425
        // ディレクトリ名などに使われれるので厳しめ
426
    }
427
428
    /**
429
     * @param string $path
430
     */
431
    public function deleteFile($path)
432
    {
433
        $f = new Filesystem();
434
        $f->remove($path);
435
    }
436
437
    public function checkSamePlugin($code)
438
    {
439
        /** @var Plugin $Plugin */
440
        $Plugin = $this->pluginRepository->findOneBy(['code' => $code]);
441
        if ($Plugin && $Plugin->isInitialized()) {
442
            throw new PluginException('plugin already installed.');
443
        }
444
    }
445
446
    public function calcPluginDir($code)
447
    {
448
        return $this->projectRoot.'/app/Plugin/'.$code;
449
    }
450
451
    /**
452
     * @param string $d
453
     *
454
     * @throws PluginException
455
     */
456
    public function createPluginDir($d)
457
    {
458
        $b = @mkdir($d);
459
        if (!$b) {
460
            throw new PluginException(trans('admin.store.plugin.mkdir.error', ['%dir_name%' => $d]));
461
        }
462
    }
463
464
    /**
465
     * @param $meta
466
     * @param int $source
467
     *
468
     * @return Plugin
469
     *
470
     * @throws PluginException
471
     */
472
    public function registerPlugin($meta, $source = 0)
473
    {
474
        try {
475
            $p = new Plugin();
476
            // インストール直後はプラグインは有効にしない
477
            $p->setName($meta['name'])
478
                ->setEnabled(false)
479
                ->setVersion($meta['version'])
480
                ->setSource($source)
481
                ->setCode($meta['code']);
482
483
            $this->entityManager->persist($p);
484
            $this->entityManager->flush($p);
485
486
            $this->pluginApiService->pluginInstalled($p);
487
        } catch (\Exception $e) {
488
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
489
        }
490
491
        return $p;
492
    }
493
494
    /**
495
     * @param $meta
496
     * @param string $method
497
     */
498
    public function callPluginManagerMethod($meta, $method)
499
    {
500
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
501
        if (class_exists($class)) {
502
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
503
            if (method_exists($installer, $method)) {
504
                $installer->$method($meta, $this->container);
505
            }
506
        }
507
    }
508
509
    /**
510
     * @param Plugin $plugin
511
     * @param bool $force
512
     *
513
     * @return bool
514
     *
515
     * @throws \Exception
516
     */
517
    public function uninstall(Plugin $plugin, $force = true)
518
    {
519
        $pluginDir = $this->calcPluginDir($plugin->getCode());
520
        $this->cacheUtil->clearCache();
521
        $config = $this->readConfig($pluginDir);
522
523
        if ($plugin->isEnabled()) {
524
            $this->disable($plugin);
525
        }
526
527
        // 初期化されていない場合はPluginManager#uninstall()は実行しない
528
        if ($plugin->isInitialized()) {
529
            $this->callPluginManagerMethod($config, 'uninstall');
530
        }
531
        $this->unregisterPlugin($plugin);
532
533
        // スキーマを更新する
534
        $this->generateProxyAndUpdateSchema($plugin, $config, true);
535
536
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
537
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
538
        $this->schemaService->dropTable($namespace);
539
540
        if ($force) {
541
            $this->deleteFile($pluginDir);
542
            $this->removeAssets($plugin->getCode());
543
        }
544
545
        $this->pluginApiService->pluginUninstalled($plugin);
546
547
        return true;
548
    }
549
550
    public function unregisterPlugin(Plugin $p)
551
    {
552
        try {
553
            $em = $this->entityManager;
554
            $em->remove($p);
555
            $em->flush();
556
        } catch (\Exception $e) {
557
            throw $e;
558
        }
559
    }
560
561
    public function disable(Plugin $plugin)
562
    {
563
        return $this->enable($plugin, false);
564
    }
565
566
    /**
567
     * Proxyを再生成します.
568
     *
569
     * @param Plugin $plugin プラグイン
570
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
571
     * @param string|null $outputDir 出力先
572
     * @param bool $uninstall プラグイン削除の場合はtrue
573
     *
574
     * @return array 生成されたファイルのパス
575
     */
576
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null, $uninstall = false)
577
    {
578
        if (is_null($outputDir)) {
579
            $outputDir = $this->projectRoot.'/app/proxy/entity';
580
        }
581
        @mkdir($outputDir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
582
583
        $enabledPluginCodes = array_map(
584
            function ($p) { return $p->getCode(); },
585
            $temporary ? $this->pluginRepository->findAll() : $this->pluginRepository->findAllEnabled()
586
        );
587
588
        $excludes = [];
589
        if (!$uninstall && ($temporary || $plugin->isEnabled())) {
590
            $enabledPluginCodes[] = $plugin->getCode();
591
        } else {
592
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
593
            if ($index !== false && $index >= 0) {
594
                array_splice($enabledPluginCodes, $index, 1);
595
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
596
            }
597
        }
598
599
        $enabledPluginEntityDirs = array_map(function ($code) {
600
            return $this->projectRoot."/app/Plugin/${code}/Entity";
601
        }, $enabledPluginCodes);
602
603
        return $this->entityProxyService->generate(
604
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
605
            $excludes,
606
            $outputDir
607
        );
608
    }
609
610
    public function enable(Plugin $plugin, $enable = true)
611
    {
612
        $em = $this->entityManager;
613
        try {
614
            $pluginDir = $this->calcPluginDir($plugin->getCode());
615
            $config = $this->readConfig($pluginDir);
616
            $em->getConnection()->beginTransaction();
617
618
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
619
620
            $plugin->setEnabled($enable ? true : false);
621
            $em->persist($plugin);
622
623
            // Proxyだけ再生成してスキーマは更新しない
624
            $this->regenerateProxy($plugin, false);
625
626
            $em->flush();
627
            $em->getConnection()->commit();
628
629
            if ($enable) {
630
                $this->pluginApiService->pluginEnabled($plugin);
631
            } else {
632
                $this->pluginApiService->pluginDisabled($plugin);
633
            }
634
        } catch (\Exception $e) {
635
            $em->getConnection()->rollback();
636
            throw $e;
637
        }
638
639
        return true;
640
    }
641
642
    /**
643
     * Update plugin
644
     *
645
     * @param Plugin $plugin
646
     * @param string $path
647
     *
648
     * @return bool
649
     *
650
     * @throws PluginException
651
     * @throws \Exception
652
     */
653
    public function update(Plugin $plugin, $path)
654
    {
655
        $pluginBaseDir = null;
656
        $tmp = null;
657
        try {
658
            $this->cacheUtil->clearCache();
659
            $tmp = $this->createTempDir();
660
661
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
662
            $this->checkPluginArchiveContent($tmp);
663
664
            $config = $this->readConfig($tmp);
665
666
            if ($plugin->getCode() != $config['code']) {
667
                throw new PluginException('new/old plugin code is different.');
668
            }
669
670
            $pluginBaseDir = $this->calcPluginDir($config['code']);
671
            $this->deleteFile($tmp); // テンポラリのファイルを削除
672
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
673
674
            $this->copyAssets($plugin->getCode());
675
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
676
        } catch (PluginException $e) {
677
            $this->deleteDirs([$tmp]);
678
            throw $e;
679
        } catch (\Exception $e) {
680
            // catch exception of composer
681
            $this->deleteDirs([$tmp]);
682
            throw $e;
683
        }
684
685
        return true;
686
    }
687
688
    /**
689
     * Update plugin
690
     *
691
     * @param Plugin $plugin
692
     * @param array  $meta     Config data
693
     *
694
     * @throws \Exception
695
     */
696
    public function updatePlugin(Plugin $plugin, $meta)
697
    {
698
        $em = $this->entityManager;
699
        try {
700
            $em->getConnection()->beginTransaction();
701
            $plugin->setVersion($meta['version'])
702
                ->setName($meta['name']);
703
704
            $em->persist($plugin);
705
706
            if ($plugin->isInitialized()) {
707
                $this->callPluginManagerMethod($meta, 'update');
708
            }
709
            $this->copyAssets($plugin->getCode());
710
            $em->flush();
711
            $em->getConnection()->commit();
712
        } catch (\Exception $e) {
713
            $em->getConnection()->rollback();
714
            throw $e;
715
        }
716
    }
717
718
    /**
719
     * Get array require by plugin
720
     * Todo: need define dependency plugin mechanism
721
     *
722
     * @param array|Plugin $plugin format as plugin from api
723
     *
724
     * @return array|mixed
725
     *
726
     * @throws PluginException
727
     */
728
    public function getPluginRequired($plugin)
729
    {
730
        $pluginCode = $plugin instanceof Plugin ? $plugin->getCode() : $plugin['code'];
731
        $pluginVersion = $plugin instanceof Plugin ? $plugin->getVersion() : $plugin['version'];
732
733
        $results = [];
734
735
        $this->composerService->foreachRequires('ec-cube/'.$pluginCode, $pluginVersion, function ($package) use (&$results) {
736
            $results[] = $package;
737
        }, 'eccube-plugin');
738
739
        return $results;
740
    }
741
742
    /**
743
     * Find the dependent plugins that need to be disabled
744
     *
745
     * @param string $pluginCode
746
     *
747
     * @return array plugin code
748
     */
749
    public function findDependentPluginNeedDisable($pluginCode)
750
    {
751
        return $this->findDependentPlugin($pluginCode, true);
752
    }
753
754
    /**
755
     * Find the other plugin that has requires on it.
756
     * Check in both dtb_plugin table and <PluginCode>/composer.json
757
     *
758
     * @param string $pluginCode
759
     * @param bool   $enableOnly
760
     *
761
     * @return array plugin code
762
     */
763
    public function findDependentPlugin($pluginCode, $enableOnly = false)
764
    {
765
        $criteria = Criteria::create()
766
            ->where(Criteria::expr()->neq('code', $pluginCode));
767
        if ($enableOnly) {
768
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
769
        }
770
        /**
771
         * @var Plugin[]
772
         */
773
        $plugins = $this->pluginRepository->matching($criteria);
774
        $dependents = [];
775
        foreach ($plugins as $plugin) {
776
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
777
            $fileName = $dir.'/composer.json';
778
            if (!file_exists($fileName)) {
779
                continue;
780
            }
781
            $jsonText = file_get_contents($fileName);
782
            if ($jsonText) {
783
                $json = json_decode($jsonText, true);
784
                if (!isset($json['require'])) {
785
                    continue;
786
                }
787
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
788
                    $dependents[] = $plugin->getCode();
789
                }
790
            }
791
        }
792
793
        return $dependents;
794
    }
795
796
    /**
797
     * Get dependent plugin by code
798
     * It's base on composer.json
799
     * Return the plugin code and version in the format of the composer
800
     *
801
     * @param string   $pluginCode
802
     * @param int|null $libraryType
803
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
804
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
805
     *                      default : return all library/plugin
806
     *
807
     * @return array format [packageName1 => version1, packageName2 => version2]
808
     */
809
    public function getDependentByCode($pluginCode, $libraryType = null)
810
    {
811
        $pluginDir = $this->calcPluginDir($pluginCode);
812
        $jsonFile = $pluginDir.'/composer.json';
813
        if (!file_exists($jsonFile)) {
814
            return [];
815
        }
816
        $jsonText = file_get_contents($jsonFile);
817
        $json = json_decode($jsonText, true);
818
        $dependents = [];
819
        if (isset($json['require'])) {
820
            $require = $json['require'];
821
            switch ($libraryType) {
822 View Code Duplication
                case self::ECCUBE_LIBRARY:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
823
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
824
                    break;
825
826 View Code Duplication
                case self::OTHER_LIBRARY:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
827
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
828
                    break;
829
830
                default:
831
                    $dependents = $json['require'];
832
                    break;
833
            }
834
        }
835
836
        return $dependents;
837
    }
838
839
    /**
840
     * Format array dependent plugin to string
841
     * It is used for commands.
842
     *
843
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
844
     * @param bool  $getVersion
845
     *
846
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
847
     */
848
    public function parseToComposerCommand(array $packages, $getVersion = true)
849
    {
850
        $result = array_keys($packages);
851
        if ($getVersion) {
852
            $result = array_map(function ($package, $version) {
853
                return $package.':'.$version;
854
            }, array_keys($packages), array_values($packages));
855
        }
856
857
        return implode(' ', $result);
858
    }
859
860
    /**
861
     * リソースファイル等をコピー
862
     * コピー元となるファイルの置き場所は固定であり、
863
     * [プラグインコード]/Resource/assets
864
     * 配下に置かれているファイルが所定の位置へコピーされる
865
     *
866
     * @param $pluginCode
867
     */
868
    public function copyAssets($pluginCode)
869
    {
870
        $assetsDir = $this->calcPluginDir($pluginCode).'/Resource/assets';
871
872
        // プラグインにリソースファイルがあれば所定の位置へコピー
873
        if (file_exists($assetsDir)) {
874
            $file = new Filesystem();
875
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
876
        }
877
    }
878
879
    /**
880
     * コピーしたリソースファイル等を削除
881
     *
882
     * @param string $pluginCode
883
     */
884
    public function removeAssets($pluginCode)
885
    {
886
        $assetsDir = $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets';
887
888
        // コピーされているリソースファイルがあれば削除
889
        if (file_exists($assetsDir)) {
890
            $file = new Filesystem();
891
            $file->remove($assetsDir);
892
        }
893
    }
894
895
    /**
896
     * Plugin is exist check
897
     *
898
     * @param array  $plugins    get from api
899
     * @param string $pluginCode
900
     *
901
     * @return false|int|string
902
     */
903
    public function checkPluginExist($plugins, $pluginCode)
904
    {
905
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
906
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
907
        }
908
        // Find plugin in array
909
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
910
911
        return $index;
912
    }
913
914
    /**
915
     * @param string $code
916
     *
917
     * @return bool
918
     */
919
    private function isEnable($code)
920
    {
921
        $Plugin = $this->pluginRepository->findOneBy([
922
            'enabled' => Constant::ENABLED,
923
            'code' => $code,
924
        ]);
925
        if ($Plugin) {
926
            return true;
927
        }
928
929
        return false;
930
    }
931
}
932