Failed Conditions
Push — sf/package-api ( 831849 )
by Kiyotaka
05:53
created

PluginService::getDependentByCode()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 29

Duplication

Lines 6
Ratio 20.69 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 2
dl 6
loc 29
rs 9.1448
c 0
b 0
f 0
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\Application;
20
use Eccube\Common\Constant;
21
use Eccube\Common\EccubeConfig;
22
use Eccube\Entity\Plugin;
23
use Eccube\Exception\PluginException;
24
use Eccube\Repository\PluginRepository;
25
use Eccube\Service\Composer\ComposerApiService;
26
use Eccube\Service\Composer\ComposerServiceInterface;
27
use Eccube\Util\CacheUtil;
28
use Eccube\Util\StringUtil;
29
use Symfony\Component\DependencyInjection\ContainerInterface;
30
use Symfony\Component\Filesystem\Filesystem;
31
32
class PluginService
33
{
34
    /**
35
     * @var EccubeConfig
36
     */
37
    protected $eccubeConfig;
38
39
    /**
40
     * @var EntityManager
41
     */
42
    protected $entityManager;
43
44
    /**
45
     * @var PluginRepository
46
     */
47
    protected $pluginRepository;
48
49
    /**
50
     * @var Application
51
     */
52
    protected $app;
53
54
    /**
55
     * @var EntityProxyService
56
     */
57
    protected $entityProxyService;
58
59
    /**
60
     * @var SchemaService
61
     */
62
    protected $schemaService;
63
64
    /**
65
     * @var ComposerServiceInterface
66
     */
67
    protected $composerService;
68
69
    const VENDOR_NAME = 'ec-cube';
70
71
    /**
72
     * Plugin type/library of ec-cube
73
     */
74
    const ECCUBE_LIBRARY = 1;
75
76
    /**
77
     * Plugin type/library of other (except ec-cube)
78
     */
79
    const OTHER_LIBRARY = 2;
80
81
    /**
82
     * @var string %kernel.project_dir%
83
     */
84
    private $projectRoot;
85
86
    /**
87
     * @var string %kernel.environment%
88
     */
89
    private $environment;
90
91
    /**
92
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
93
     */
94
    protected $container;
95
96
    /** @var CacheUtil */
97
    protected $cacheUtil;
98
99
    /**
100
     * PluginService constructor.
101
     *
102
     * @param EntityManagerInterface $entityManager
103
     * @param PluginRepository $pluginRepository
104
     * @param EntityProxyService $entityProxyService
105
     * @param SchemaService $schemaService
106
     * @param EccubeConfig $eccubeConfig
107
     * @param ContainerInterface $container
108
     * @param CacheUtil $cacheUtil
109
     * @param ComposerApiService $composerService
110
     */
111
    public function __construct(
112
        EntityManagerInterface $entityManager,
113
        PluginRepository $pluginRepository,
114
        EntityProxyService $entityProxyService,
115
        SchemaService $schemaService,
116
        EccubeConfig $eccubeConfig,
117
        ContainerInterface $container,
118
        CacheUtil $cacheUtil,
119
        ComposerApiService $composerService
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
    }
132
133
    /**
134
     * ファイル指定してのプラグインインストール
135
     *
136
     * @param string $path   path to tar.gz/zip plugin file
137
     * @param int    $source
138
     *
139
     * @return boolean
140
     *
141
     * @throws PluginException
142
     * @throws \Exception
143
     */
144
    public function install($path, $source = 0)
145
    {
146
        $pluginBaseDir = null;
147
        $tmp = null;
148
        try {
149
            // プラグイン配置前に実施する処理
150
            $this->preInstall();
151
            $tmp = $this->createTempDir();
152
153
            // 一旦テンポラリに展開
154
            $this->unpackPluginArchive($path, $tmp);
155
            $this->checkPluginArchiveContent($tmp);
156
157
            $config = $this->readConfig($tmp);
158
            // テンポラリのファイルを削除
159
            $this->deleteFile($tmp);
160
161
            // 重複していないかチェック
162
            $this->checkSamePlugin($config['code']);
163
164
            $pluginBaseDir = $this->calcPluginDir($config['code']);
165
            // 本来の置き場所を作成
166
            $this->createPluginDir($pluginBaseDir);
167
168
            // 問題なければ本当のplugindirへ
169
            $this->unpackPluginArchive($path, $pluginBaseDir);
170
171
            // Check dependent plugin
172
            // Don't install ec-cube library
173
//            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
174
//            if (!empty($dependents)) {
175
//                $package = $this->parseToComposerCommand($dependents);
176
            //FIXME: how to working with ComposerProcessService or ComposerApiService ?
177
//                $this->composerService->execRequire($package);
178
//            }
179
180
            // プラグイン配置後に実施する処理
181
            $this->postInstall($config, $source);
182
            // リソースファイルをコピー
183
            $this->copyAssets($pluginBaseDir, $config['code']);
184
        } catch (PluginException $e) {
185
            $this->deleteDirs([$tmp, $pluginBaseDir]);
186
            throw $e;
187
        } catch (\Exception $e) {
188
            // インストーラがどんなExceptionを上げるかわからないので
189
            $this->deleteDirs([$tmp, $pluginBaseDir]);
190
            throw $e;
191
        }
192
193
        return true;
194
    }
195
196
    // インストール事前処理
197
    public function preInstall()
198
    {
199
        // キャッシュの削除
200
        // FIXME: Please fix clearCache function (because it's clear all cache and this file just upload)
201
//        $this->cacheUtil->clearCache();
202
    }
203
204
    // インストール事後処理
205
    public function postInstall($config, $source)
206
    {
207
        // Proxyのクラスをロードせずにスキーマを更新するために、
208
        // インストール時には一時的なディレクトリにProxyを生成する
209
        $tmpProxyOutputDir = sys_get_temp_dir().'/proxy_'.StringUtil::random(12);
210
        @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...
211
212
        try {
213
            // dbにプラグイン登録
214
            $plugin = $this->registerPlugin($config, $source);
215
216
            // プラグインmetadata定義を追加
217
            $entityDir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode().'/Entity';
218
            if (file_exists($entityDir)) {
219
                $ormConfig = $this->entityManager->getConfiguration();
220
                $chain = $ormConfig->getMetadataDriverImpl();
221
                $driver = $ormConfig->newDefaultAnnotationDriver([$entityDir], false);
222
                $namespace = 'Plugin\\'.$config['code'].'\\Entity';
223
                $chain->addDriver($driver, $namespace);
224
                $ormConfig->addEntityNamespace($plugin->getCode(), $namespace);
225
            }
226
227
            // インストール時には一時的に利用するProxyを生成してからスキーマを更新する
228
            $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir);
229
            $this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);
230
        } finally {
231
            foreach (glob("${tmpProxyOutputDir}/*") as  $f) {
232
                unlink($f);
233
            }
234
            rmdir($tmpProxyOutputDir);
235
        }
236
    }
237
238
    public function createTempDir()
239
    {
240
        $tempDir = $this->projectRoot.'/var/cache/'.$this->environment.'/Plugin';
241
        @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...
242
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
243
244
        if (!mkdir($d, 0777)) {
245
            throw new PluginException($php_errormsg.$d);
246
        }
247
248
        return $d;
249
    }
250
251
    public function deleteDirs($arr)
252
    {
253
        foreach ($arr as $dir) {
254
            if (file_exists($dir)) {
255
                $fs = new Filesystem();
256
                $fs->remove($dir);
257
            }
258
        }
259
    }
260
261
    /**
262
     * @param string $archive
263
     * @param string $dir
264
     *
265
     * @throws PluginException
266
     */
267
    public function unpackPluginArchive($archive, $dir)
268
    {
269
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
270
        try {
271
            if ($extension == 'zip') {
272
                $zip = new \ZipArchive();
273
                $zip->open($archive);
274
                $zip->extractTo($dir);
275
                $zip->close();
276
            } else {
277
                $phar = new \PharData($archive);
278
                $phar->extractTo($dir, null, true);
279
            }
280
        } catch (\Exception $e) {
281
            throw new PluginException(trans('pluginservice.text.error.upload_failure'));
282
        }
283
    }
284
285
    /**
286
     * @param $dir
287
     * @param array $config_cache
288
     *
289
     * @throws PluginException
290
     */
291
    public function checkPluginArchiveContent($dir, array $config_cache = [])
292
    {
293
        try {
294
            if (!empty($config_cache)) {
295
                $meta = $config_cache;
296
            } else {
297
                $meta = $this->readConfig($dir);
298
            }
299
        } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
300
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
301
        }
302
303
        if (!is_array($meta)) {
304
            throw new PluginException('config.yml not found or syntax error');
305
        }
306
        if (!isset($meta['code']) || !$this->checkSymbolName($meta['code'])) {
307
            throw new PluginException('config.yml code empty or invalid_character(\W)');
308
        }
309
        if (!isset($meta['name'])) {
310
            // nameは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
311
            throw new PluginException('config.yml name empty');
312
        }
313
        if (!isset($meta['version'])) {
314
            // versionは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
315
            throw new PluginException('config.yml version invalid_character(\W) ');
316
        }
317
        if (isset($meta['orm.path'])) {
318
            if (!is_array($meta['orm.path'])) {
319
                throw new PluginException('config.yml orm.path invalid_character(\W) ');
320
            }
321
        }
322
        if (isset($meta['service'])) {
323
            if (!is_array($meta['service'])) {
324
                throw new PluginException('config.yml service invalid_character(\W) ');
325
            }
326
        }
327
    }
328
329
    /**
330
     * @param $pluginDir
331
     *
332
     * @return array
333
     *
334
     * @throws PluginException
335
     */
336
    public function readConfig($pluginDir)
337
    {
338
        $composerJsonPath = $pluginDir.DIRECTORY_SEPARATOR.'composer.json';
339
        if (file_exists($composerJsonPath) === false) {
340
            throw new PluginException("${composerJsonPath} not found.");
341
        }
342
343
        $json = json_decode(file_get_contents($composerJsonPath), true);
344
        if ($json === null) {
345
            throw new PluginException("Invalid json format. [${composerJsonPath}]");
346
        }
347
348
        if (!isset($json['version'])) {
349
            throw new PluginException("`version` is not defined in ${composerJsonPath}");
350
        }
351
352
        if (!isset($json['extra']['code'])) {
353
            throw new PluginException("`extra.code` is not defined in ${composerJsonPath}");
354
        }
355
356
        return [
357
            'code' => $json['extra']['code'],
358
            'name' => isset($json['description']) ? $json['description'] : $json['extra']['code'],
359
            'version' => $json['version'],
360
        ];
361
    }
362
363
    public function checkSymbolName($string)
364
    {
365
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
366
        // plugin_nameやplugin_codeに使える文字のチェック
367
        // a-z A-Z 0-9 _
368
        // ディレクトリ名などに使われれるので厳しめ
369
    }
370
371
    /**
372
     * @param string $path
373
     */
374
    public function deleteFile($path)
375
    {
376
        $f = new Filesystem();
377
        $f->remove($path);
378
    }
379
380
    public function checkSamePlugin($code)
381
    {
382
        $repo = $this->pluginRepository->findOneBy(['code' => $code]);
383
        if ($repo) {
384
            throw new PluginException('plugin already installed.');
385
        }
386
    }
387
388
    public function calcPluginDir($name)
389
    {
390
        return $this->projectRoot.'/app/Plugin/'.$name;
391
    }
392
393
    /**
394
     * @param string $d
395
     *
396
     * @throws PluginException
397
     */
398
    public function createPluginDir($d)
399
    {
400
        $b = @mkdir($d);
401
        if (!$b) {
402
            throw new PluginException($php_errormsg);
403
        }
404
    }
405
406
    /**
407
     * @param $meta
408
     * @param int $source
409
     *
410
     * @return Plugin
411
     *
412
     * @throws PluginException
413
     * @throws \Doctrine\DBAL\ConnectionException
414
     */
415
    public function registerPlugin($meta, $source = 0)
416
    {
417
        $em = $this->entityManager;
418
        $em->getConnection()->beginTransaction();
419
        try {
420
            $p = new Plugin();
421
            // インストール直後はプラグインは有効にしない
422
            $p->setName($meta['name'])
423
                ->setEnabled(false)
424
                ->setVersion($meta['version'])
425
                ->setSource($source)
426
                ->setCode($meta['code']);
427
428
            $em->persist($p);
429
            $em->flush();
430
431
            $this->callPluginManagerMethod($meta, 'install');
432
433
            $em->flush();
434
            $em->getConnection()->commit();
435
        } catch (\Exception $e) {
436
            $em->getConnection()->rollback();
437
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
438
        }
439
440
        return $p;
441
    }
442
443
    /**
444
     * @param $meta
445
     * @param string $method
446
     */
447
    public function callPluginManagerMethod($meta, $method)
448
    {
449
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
450
        if (class_exists($class)) {
451
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
452
            if (method_exists($installer, $method)) {
453
                // FIXME appを削除.
454
                $installer->$method($meta, $this->app, $this->container);
455
            }
456
        }
457
    }
458
459
    /**
460
     * @param Plugin $plugin
461
     * @param bool $force
462
     *
463
     * @return bool
464
     *
465
     * @throws \Exception
466
     */
467
    public function uninstall(Plugin $plugin, $force = true)
468
    {
469
        $pluginDir = $this->calcPluginDir($plugin->getCode());
470
        $this->cacheUtil->clearCache();
471
        $config = $this->readConfig($pluginDir);
472
        $this->callPluginManagerMethod($config, 'disable');
473
        $this->callPluginManagerMethod($config, 'uninstall');
474
        $this->disable($plugin);
475
        $this->unregisterPlugin($plugin);
476
477
        // スキーマを更新する
478
        //FIXME: Update schema before no affect
479
        $this->schemaService->updateSchema([], $this->projectRoot.'/app/proxy/entity');
480
481
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
482
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
483
        $this->schemaService->dropTable($namespace);
484
485
        if ($force) {
486
            $this->deleteFile($pluginDir);
487
            $this->removeAssets($plugin->getCode());
488
        }
489
490
        return true;
491
    }
492
493
    public function unregisterPlugin(Plugin $p)
494
    {
495
        try {
496
            $em = $this->entityManager;
497
            $em->remove($p);
498
            $em->flush();
499
        } catch (\Exception $e) {
500
            throw $e;
501
        }
502
    }
503
504
    public function disable(Plugin $plugin)
505
    {
506
        return $this->enable($plugin, false);
507
    }
508
509
    /**
510
     * Proxyを再生成します.
511
     *
512
     * @param Plugin $plugin プラグイン
513
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
514
     * @param string|null $outputDir 出力先
515
     *
516
     * @return array 生成されたファイルのパス
517
     */
518
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null)
519
    {
520
        if (is_null($outputDir)) {
521
            $outputDir = $this->projectRoot.'/app/proxy/entity';
522
        }
523
        @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...
524
525
        $enabledPluginCodes = array_map(
526
            function ($p) { return $p->getCode(); },
527
            $this->pluginRepository->findAllEnabled()
528
        );
529
530
        $excludes = [];
531
        if ($temporary || $plugin->isEnabled()) {
532
            $enabledPluginCodes[] = $plugin->getCode();
533
        } else {
534
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
535
            if ($index >= 0) {
536
                array_splice($enabledPluginCodes, $index, 1);
537
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
538
            }
539
        }
540
541
        $enabledPluginEntityDirs = array_map(function ($code) {
542
            return $this->projectRoot."/app/Plugin/${code}/Entity";
543
        }, $enabledPluginCodes);
544
545
        return $this->entityProxyService->generate(
546
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
547
            $excludes,
548
            $outputDir
549
        );
550
    }
551
552
    public function enable(Plugin $plugin, $enable = true)
553
    {
554
        $em = $this->entityManager;
555
        try {
556
            $pluginDir = $this->calcPluginDir($plugin->getCode());
557
            $config = $this->readConfig($pluginDir);
558
            $em->getConnection()->beginTransaction();
559
            $plugin->setEnabled($enable ? true : false);
560
            $em->persist($plugin);
561
562
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
563
564
            // Proxyだけ再生成してスキーマは更新しない
565
            $this->regenerateProxy($plugin, false);
566
567
            $em->flush();
568
            $em->getConnection()->commit();
569
        } catch (\Exception $e) {
570
            $em->getConnection()->rollback();
571
            throw $e;
572
        }
573
574
        return true;
575
    }
576
577
    /**
578
     * Update plugin
579
     *
580
     * @param Plugin $plugin
581
     * @param string $path
582
     *
583
     * @return bool
584
     *
585
     * @throws PluginException
586
     * @throws \Exception
587
     */
588
    public function update(Plugin $plugin, $path)
589
    {
590
        $pluginBaseDir = null;
591
        $tmp = null;
592
        try {
593
            $this->cacheUtil->clearCache();
594
            $tmp = $this->createTempDir();
595
596
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
597
            $this->checkPluginArchiveContent($tmp);
598
599
            $config = $this->readConfig($tmp);
600
601
            if ($plugin->getCode() != $config['code']) {
602
                throw new PluginException('new/old plugin code is different.');
603
            }
604
605
            $pluginBaseDir = $this->calcPluginDir($config['code']);
606
            $this->deleteFile($tmp); // テンポラリのファイルを削除
607
608
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
609
610
            // Check dependent plugin
611
            // Don't install ec-cube library
612
            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
613
            if (!empty($dependents)) {
614
                $package = $this->parseToComposerCommand($dependents);
615
                $this->composerService->execRequire($package);
616
            }
617
618
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
619
        } catch (PluginException $e) {
620
            $this->deleteDirs([$tmp]);
621
            throw $e;
622
        } catch (\Exception $e) {
623
            // catch exception of composer
624
            $this->deleteDirs([$tmp]);
625
            throw $e;
626
        }
627
628
        return true;
629
    }
630
631
    /**
632
     * Update plugin
633
     *
634
     * @param Plugin $plugin
635
     * @param array  $meta     Config data
636
     *
637
     * @throws \Exception
638
     */
639
    public function updatePlugin(Plugin $plugin, $meta)
640
    {
641
        $em = $this->entityManager;
642
        try {
643
            $em->getConnection()->beginTransaction();
644
            $plugin->setVersion($meta['version'])
645
                ->setName($meta['name']);
646
647
            $em->persist($plugin);
648
            $this->callPluginManagerMethod($meta, 'update');
649
            $em->flush();
650
            $em->getConnection()->commit();
651
        } catch (\Exception $e) {
652
            $em->getConnection()->rollback();
653
            throw $e;
654
        }
655
    }
656
657
    /**
658
     * Get array require by plugin
659
     * Todo: need define dependency plugin mechanism
660
     *
661
     * @param array|Plugin $plugin format as plugin from api
662
     *
663
     * @return array|mixed
664
     * @throws PluginException
665
     */
666
    public function getPluginRequired($plugin)
667
    {
668
        $pluginCode = $plugin instanceof Plugin ? $plugin->getCode() : $plugin['code'];
669
        $pluginVersion = $plugin instanceof Plugin ? $plugin->getVersion() : $plugin['version'];
670
671
        $results = [];
672
673
        $this->composerService->foreachRequires('ec-cube/'.$pluginCode, $pluginVersion, function($package) use(&$results) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Eccube\Service\Composer\ComposerServiceInterface as the method foreachRequires() does only exist in the following implementations of said interface: Eccube\Service\Composer\ComposerApiService.

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...
674
            $results[] = $package;
675
        }, 'eccube-plugin');
676
677
        return $results;
678
    }
679
680
    /**
681
     * Get plugin information
682
     *
683
     * @param array $plugin
684
     *
685
     * @return array|null
686
     */
687
    public function buildInfo($plugin)
688
    {
689
        $this->supportedVersion($plugin);
690
691
        return $plugin;
692
    }
693
694
    /**
695
     * Check support version
696
     *
697
     * @param $plugin
698
     */
699
    public function supportedVersion(&$plugin)
700
    {
701
        // Check the eccube version that the plugin supports.
702
        $plugin['version_check'] = false;
703
        if (in_array(Constant::VERSION, $plugin['supported_versions'])) {
704
            // Match version
705
            $plugin['version_check'] = true;
706
        }
707
    }
708
709
    /**
710
     * Find the dependent plugins that need to be disabled
711
     *
712
     * @param string $pluginCode
713
     *
714
     * @return array plugin code
715
     */
716
    public function findDependentPluginNeedDisable($pluginCode)
717
    {
718
        return $this->findDependentPlugin($pluginCode, true);
719
    }
720
721
    /**
722
     * Find the other plugin that has requires on it.
723
     * Check in both dtb_plugin table and <PluginCode>/composer.json
724
     *
725
     * @param string $pluginCode
726
     * @param bool   $enableOnly
727
     *
728
     * @return array plugin code
729
     */
730
    public function findDependentPlugin($pluginCode, $enableOnly = false)
731
    {
732
        $criteria = Criteria::create()
733
            ->where(Criteria::expr()->neq('code', $pluginCode));
734
        if ($enableOnly) {
735
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
736
        }
737
        /**
738
         * @var Plugin[]
739
         */
740
        $plugins = $this->pluginRepository->matching($criteria);
741
        $dependents = [];
742
        foreach ($plugins as $plugin) {
743
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
744
            $fileName = $dir.'/composer.json';
745
            if (!file_exists($fileName)) {
746
                continue;
747
            }
748
            $jsonText = file_get_contents($fileName);
749
            if ($jsonText) {
750
                $json = json_decode($jsonText, true);
751
                if (!isset($json['require'])) {
752
                    continue;
753
                }
754
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
755
                    $dependents[] = $plugin->getCode();
756
                }
757
            }
758
        }
759
760
        return $dependents;
761
    }
762
763
    /**
764
     * Get dependent plugin by code
765
     * It's base on composer.json
766
     * Return the plugin code and version in the format of the composer
767
     *
768
     * @param string   $pluginCode
769
     * @param int|null $libraryType
770
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
771
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
772
     *                      default : return all library/plugin
773
     *
774
     * @return array format [packageName1 => version1, packageName2 => version2]
775
     */
776
    public function getDependentByCode($pluginCode, $libraryType = null)
777
    {
778
        $pluginDir = $this->calcPluginDir($pluginCode);
779
        $jsonFile = $pluginDir.'/composer.json';
780
        if (!file_exists($jsonFile)) {
781
            return [];
782
        }
783
        $jsonText = file_get_contents($jsonFile);
784
        $json = json_decode($jsonText, true);
785
        $dependents = [];
786
        if (isset($json['require'])) {
787
            $require = $json['require'];
788
            switch ($libraryType) {
789 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...
790
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
791
                    break;
792
793 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...
794
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
795
                    break;
796
797
                default:
798
                    $dependents = $json['require'];
799
                    break;
800
            }
801
        }
802
803
        return $dependents;
804
    }
805
806
    /**
807
     * Format array dependent plugin to string
808
     * It is used for commands.
809
     *
810
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
811
     * @param bool  $getVersion
812
     *
813
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
814
     */
815
    public function parseToComposerCommand(array $packages, $getVersion = true)
816
    {
817
        $result = array_keys($packages);
818
        if ($getVersion) {
819
            $result = array_map(function ($package, $version) {
820
                return $package.':'.$version;
821
            }, array_keys($packages), array_values($packages));
822
        }
823
824
        return implode(' ', $result);
825
    }
826
827
    /**
828
     * リソースファイル等をコピー
829
     * コピー元となるファイルの置き場所は固定であり、
830
     * [プラグインコード]/Resource/assets
831
     * 配下に置かれているファイルが所定の位置へコピーされる
832
     *
833
     * @param string $pluginBaseDir
834
     * @param $pluginCode
835
     */
836
    public function copyAssets($pluginBaseDir, $pluginCode)
837
    {
838
        $assetsDir = $pluginBaseDir.'/Resource/assets';
839
840
        // プラグインにリソースファイルがあれば所定の位置へコピー
841
        if (file_exists($assetsDir)) {
842
            $file = new Filesystem();
843
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
844
        }
845
    }
846
847
    /**
848
     * コピーしたリソースファイル等を削除
849
     *
850
     * @param string $pluginCode
851
     */
852
    public function removeAssets($pluginCode)
853
    {
854
        $assetsDir = $this->projectRoot.'/app/Plugin/'.$pluginCode;
855
856
        // コピーされているリソースファイルがあれば削除
857
        if (file_exists($assetsDir)) {
858
            $file = new Filesystem();
859
            $file->remove($assetsDir);
860
        }
861
    }
862
863
    /**
864
     * Is update
865
     *
866
     * @param string $pluginVersion
867
     * @param string $remoteVersion
868
     *
869
     * @return boolean
870
     */
871
    public function isUpdate($pluginVersion, $remoteVersion)
872
    {
873
        return version_compare($pluginVersion, $remoteVersion, '<');
874
    }
875
876
    /**
877
     * Plugin is exist check
878
     *
879
     * @param array  $plugins    get from api
880
     * @param string $pluginCode
881
     *
882
     * @return false|int|string
883
     */
884
    public function checkPluginExist($plugins, $pluginCode)
885
    {
886
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
887
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
888
        }
889
        // Find plugin in array
890
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
891
892
        return $index;
893
    }
894
895
    /**
896
     * @param string $code
897
     *
898
     * @return bool
899
     */
900
    private function isEnable($code)
901
    {
902
        $Plugin = $this->pluginRepository->findOneBy([
903
            'enabled' => Constant::ENABLED,
904
            'code' => $code,
905
        ]);
906
        if ($Plugin) {
907
            return true;
908
        }
909
910
        return false;
911
    }
912
}
913