Completed
Push — master ( e25986...ffddd1 )
by Craig
07:06
created

BundleSyncHelper::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 21
nc 1
nop 10
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/*
4
 * This file is part of the Zikula package.
5
 *
6
 * Copyright Zikula Foundation - http://zikula.org/
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zikula\ExtensionsModule\Helper;
13
14
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
15
use Symfony\Component\Finder\Finder;
16
use Symfony\Component\HttpFoundation\Session\SessionInterface;
17
use vierbergenlars\SemVer\expression;
18
use vierbergenlars\SemVer\version;
19
use Zikula\Bundle\CoreBundle\Bundle\Helper\BootstrapHelper;
20
use Zikula\Bundle\CoreBundle\Bundle\MetaData;
21
use Zikula\Bundle\CoreBundle\Bundle\Scanner;
22
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaHttpKernelInterface;
23
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaKernel;
24
use Zikula\Common\Translator\TranslatorInterface;
25
use Zikula\Core\Event\GenericEvent;
26
use Zikula\Core\Exception\FatalErrorException;
27
use Zikula\ExtensionsModule\Api\ExtensionApi;
28
use Zikula\ExtensionsModule\Entity\ExtensionEntity;
29
use Zikula\ExtensionsModule\Entity\Repository\ExtensionDependencyRepository;
30
use Zikula\ExtensionsModule\Entity\Repository\ExtensionRepository;
31
use Zikula\ExtensionsModule\Entity\Repository\ExtensionVarRepository;
32
use Zikula\ExtensionsModule\ExtensionEvents;
33
34
/**
35
 * Helper functions for the extensions bundle
36
 */
37
class BundleSyncHelper
38
{
39
    /**
40
     * @var ZikulaHttpKernelInterface
41
     */
42
    private $kernel;
43
44
    /**
45
     * @var ExtensionRepository
46
     */
47
    private $extensionRepository;
48
49
    /**
50
     * @var ExtensionVarRepository
51
     */
52
    private $extensionVarRepository;
53
54
    /**
55
     * @var ExtensionDependencyRepository
56
     */
57
    private $extensionDependencyRepository;
58
59
    /**
60
     * @var TranslatorInterface
61
     */
62
    private $translator;
63
64
    /**
65
     * @var EventDispatcherInterface
66
     */
67
    private $dispatcher;
68
69
    /**
70
     * @var ExtensionStateHelper
71
     */
72
    private $extensionStateHelper;
73
74
    /**
75
     * @var BootstrapHelper
76
     */
77
    private $bootstrapHelper;
78
79
    /**
80
     * @var ComposerValidationHelper
81
     */
82
    private $composerValidationHelper;
83
84
    /**
85
     * @var SessionInterface
86
     */
87
    protected $session;
88
89
    /**
90
     * BundleSyncHelper constructor.
91
     *
92
     * @param ZikulaHttpKernelInterface $kernel
93
     * @param ExtensionRepository $extensionRepository
94
     * @param ExtensionVarRepository $extensionVarRepository
95
     * @param ExtensionDependencyRepository $extensionDependencyRepository
96
     * @param TranslatorInterface $translator
97
     * @param EventDispatcherInterface $dispatcher
98
     * @param ExtensionStateHelper $extensionStateHelper
99
     * @param ComposerValidationHelper $composerValidationHelper
100
     * @param SessionInterface $session
101
     */
102
    public function __construct(
103
        ZikulaHttpKernelInterface $kernel,
104
        ExtensionRepository $extensionRepository,
105
        ExtensionVarRepository $extensionVarRepository,
106
        ExtensionDependencyRepository $extensionDependencyRepository,
107
        TranslatorInterface $translator,
108
        EventDispatcherInterface $dispatcher,
109
        ExtensionStateHelper $extensionStateHelper,
110
        BootstrapHelper $bootstrapHelper,
111
        ComposerValidationHelper $composerValidationHelper,
112
        SessionInterface $session
113
    ) {
114
        $this->kernel = $kernel;
115
        $this->extensionRepository = $extensionRepository;
116
        $this->extensionVarRepository = $extensionVarRepository;
117
        $this->extensionDependencyRepository = $extensionDependencyRepository;
118
        $this->translator = $translator;
119
        $this->dispatcher = $dispatcher;
120
        $this->extensionStateHelper = $extensionStateHelper;
121
        $this->bootstrapHelper = $bootstrapHelper;
122
        $this->composerValidationHelper = $composerValidationHelper;
123
        $this->session = $session;
124
    }
125
126
    /**
127
     * Scan the file system for bundles.
128
     *
129
     * This function scans the file system for bundles and returns an array with all (potential) bundles found.
130
     *
131
     * @param array $directories
132
     * @return array Thrown if the user doesn't have admin permissions over the bundle
133
     * @throws \Exception
134
     */
135
    public function scanForBundles(array $directories = [])
136
    {
137
        $directories = empty($directories) ? ['system', 'modules'] : $directories;
138
139
        // sync the filesystem and the bundles table
140
        $this->bootstrapHelper->load();
141
142
        // Get all bundles on filesystem
143
        $bundles = [];
144
145
        $scanner = new Scanner();
146
        $scanner->scan($directories, 5);
147
        $newModules = $scanner->getModulesMetaData();
148
149
        // scan for all bundle-type bundles (psr-4) in either /system or /bundles
150
        /** @var MetaData $bundleMetaData */
151
        foreach ($newModules as $name => $bundleMetaData) {
152
            foreach ($bundleMetaData->getPsr4() as $ns => $path) {
153
                $this->kernel->getAutoloader()->addPsr4($ns, $path);
154
            }
155
156
            $bundleClass = $bundleMetaData->getClass();
157
158
            /** @var $bundle \Zikula\Core\AbstractModule */
159
            $bundle = new $bundleClass();
160
            $bundleMetaData->setTranslator($this->translator);
161
            $bundleMetaData->setDirectoryFromBundle($bundle);
162
            $bundleVersionArray = $bundleMetaData->getFilteredVersionInfoArray();
163
            $bundleVersionArray['capabilities'] = serialize($bundleVersionArray['capabilities']);
164
            $bundleVersionArray['securityschema'] = serialize($bundleVersionArray['securityschema']);
165
            $bundleVersionArray['dependencies'] = serialize($bundleVersionArray['dependencies']);
166
167
            $finder = new Finder();
168
            $finder->files()->in($bundle->getPath())->depth(0)->name('composer.json');
169
            foreach ($finder as $splFileInfo) {
170
                // there will only be one loop here
171
                $this->composerValidationHelper->check($splFileInfo);
172
                if ($this->composerValidationHelper->isValid()) {
173
                    $bundles[$bundle->getName()] = $bundleVersionArray;
174
                    $bundles[$bundle->getName()]['oldnames'] = isset($bundleVersionArray['oldnames']) ? $bundleVersionArray['oldnames'] : '';
175 View Code Duplication
                } else {
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...
176
                    $this->session->getFlashBag()->add('error', $this->translator->__f('Cannot load %extension because the composer file is invalid.', ['%extension' => $bundle->getName()]));
177
                    foreach ($this->composerValidationHelper->getErrors() as $error) {
178
                        $this->session->getFlashBag()->add('error', $error);
179
                    }
180
                }
181
            }
182
        }
183
184
        $this->validate($bundles);
185
186
        return $bundles;
187
    }
188
189
    /**
190
     * Validate the extensions and ensure there are no duplicate names, displaynames or urls.
191
     *
192
     * @param array $extensions
193
     * @throws FatalErrorException
194
     */
195
    private function validate(array $extensions)
196
    {
197
        $modulenames = [];
198
        $displaynames = [];
199
        $urls = [];
200
201
        // check for duplicate name, displayname or url
202
        foreach ($extensions as $dir => $modInfo) {
203
            $fields = ['name', 'displayname', 'url'];
204
            foreach ($fields as $field) {
205
                if (isset($modulenames[strtolower($modInfo[$field])])) {
206
                    throw new FatalErrorException($this->translator->__f('Fatal Error: Two extensions share the same %field. [%ext1%] and [%ext2%]', [
207
                        '%field' => $field,
208
                        '%ext1%' => $modInfo['name'],
209
                        '%ext2%' => $modulenames[strtolower($modInfo['name'])]
210
                    ]));
211
                }
212
            }
213
214
            $modulenames[strtolower($modInfo['name'])] = $dir;
215
            $displaynames[strtolower($modInfo['displayname'])] = $dir;
216
            $urls[strtolower($modInfo['url'])] = $dir;
217
        }
218
    }
219
220
    /**
221
     * Sync extensions in the filesystem and the database.
222
     * @param array $extensionsFromFile
223
     * @param bool $forceDefaults
224
     * @return array $upgradedExtensions[<name>] = <version>
225
     */
226
    public function syncExtensions(array $extensionsFromFile, $forceDefaults = false)
227
    {
228
        // Get all extensions in DB, indexed by name
229
        $extensionsFromDB = $this->extensionRepository->getIndexedArrayCollection('name');
230
231
        // see if any extensions have changed since last regeneration
232
        $this->syncUpdatedExtensions($extensionsFromFile, $extensionsFromDB, $forceDefaults);
233
234
        // See if any extensions have been lost since last sync
235
        $this->syncLostExtensions($extensionsFromFile, $extensionsFromDB);
236
237
        // See any extensions have been gained since last sync,
238
        // or if any current extensions have been upgraded
239
        $upgradedExtensions = $this->syncAddedExtensions($extensionsFromFile, $extensionsFromDB);
240
241
        // Clear and reload the dependencies table with all current dependencies
242
        $this->extensionDependencyRepository->reloadExtensionDependencies($extensionsFromFile);
243
244
        return $upgradedExtensions;
245
    }
246
247
    /**
248
     * Sync extensions that are already in the Database.
249
     *  - update from old names
250
     *  - update compatibility
251
     *  - update user settings (or reset to defaults)
252
     *  - ensure current core compatibility
253
     *
254
     * @param array $extensionsFromFile
255
     * @param array $extensionsFromDB
256
     * @param bool $forceDefaults
257
     */
258
    private function syncUpdatedExtensions(array $extensionsFromFile, array &$extensionsFromDB, $forceDefaults = false)
259
    {
260
        foreach ($extensionsFromFile as $name => $extensionFromFile) {
261
            foreach ($extensionsFromDB as $dbname => $extensionFromDB) {
262
                if (isset($extensionFromDB['name']) && in_array($extensionFromDB['name'], (array)$extensionFromFile['oldnames'])) {
263
                    // migrate its modvars
264
                    $this->extensionVarRepository->updateName($dbname, $name);
265
                    // rename the module register
266
                    $this->extensionRepository->updateName($dbname, $name);
267
                    // replace the old module with the new one in the $extensionsFromDB array
268
                    $extensionsFromDB[$name] = $extensionFromDB;
269
                    unset($extensionsFromDB[$dbname]);
270
                }
271
            }
272
273
            // If extension was previously determined to be incompatible with the core. return to original state
274
            if (isset($extensionsFromDB[$name]) && $extensionsFromDB[$name]['state'] > 10) {
275
                $extensionsFromDB[$name]['state'] = $extensionsFromDB[$name]['state'] - ExtensionApi::INCOMPATIBLE_CORE_SHIFT;
276
                $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], $extensionsFromDB[$name]['state']);
277
            }
278
279
            // update the DB information for this extension to reflect user settings (e.g. url)
280
            if (isset($extensionsFromDB[$name]['id'])) {
281
                $extensionFromFile['id'] = $extensionsFromDB[$name]['id'];
282
                if ($extensionsFromDB[$name]['state'] != ExtensionApi::STATE_UNINITIALISED && $extensionsFromDB[$name]['state'] != ExtensionApi::STATE_INVALID) {
283
                    unset($extensionFromFile['version']);
284
                }
285
                if (!$forceDefaults) {
286
                    unset($extensionFromFile['displayname']);
287
                    unset($extensionFromFile['description']);
288
                    unset($extensionFromFile['url']);
289
                }
290
291
                unset($extensionFromFile['oldnames']);
292
                unset($extensionFromFile['dependencies']);
293
                $extensionFromFile['capabilities'] = unserialize($extensionFromFile['capabilities']);
294
                $extensionFromFile['securityschema'] = unserialize($extensionFromFile['securityschema']);
295
                $extension = $this->extensionRepository->find($extensionFromFile['id']);
296
                $extension->merge($extensionFromFile);
297
                $this->extensionRepository->persistAndFlush($extension);
298
            }
299
300
            // check extension core requirement is compatible with current core
301
            $coreCompatibility = isset($extensionFromFile['corecompatibility'])
302
                ? $extensionFromFile['corecompatibility']
303
                : $this->formatCoreCompatibilityString($extensionFromFile['core_min'], $extensionFromFile['core_max']);
304
            $isCompatible = $this->isCoreCompatible($coreCompatibility);
305
            if (isset($extensionsFromDB[$name])) {
306
                if (!$isCompatible) {
307
                    // extension is incompatible with current core
308
                    $extensionsFromDB[$name]['state'] = $extensionsFromDB[$name]['state'] + ExtensionApi::INCOMPATIBLE_CORE_SHIFT;
309
                    $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], $extensionsFromDB[$name]['state']);
310
                }
311
                if (isset($extensionsFromDB[$name]['state'])) {
312
                    $extensionFromFile['state'] = $extensionsFromDB[$name]['state'];
313
                }
314
            }
315
        }
316
    }
317
318
    /**
319
     * Remove extensions from the DB that have been removed from the filesystem.
320
     *
321
     * @param array $extensionsFromFile
322
     * @param array $extensionsFromDB
323
     */
324
    private function syncLostExtensions(array $extensionsFromFile, array &$extensionsFromDB)
325
    {
326
        foreach ($extensionsFromDB as $name => $unusedVariable) {
327
            if (array_key_exists($name, $extensionsFromFile)) {
328
                continue;
329
            }
330
331
            $lostModule = $this->extensionRepository->get($name); // must obtain Entity because value from $extensionsFromDB is only an array
332
            if (!$lostModule) {
333
                throw new \RuntimeException($this->translator->__f('Error! Could not load data for module %s.', [$name]));
334
            }
335
            $lostModuleState = $lostModule->getState();
336
            if (($lostModuleState == ExtensionApi::STATE_INVALID)
337
                || ($lostModuleState == ExtensionApi::STATE_INVALID + ExtensionApi::INCOMPATIBLE_CORE_SHIFT)) {
338
                // extension was invalid and subsequently removed from file system,
339
                // or extension was incompatible with core and subsequently removed, delete it
340
                $this->extensionRepository->removeAndFlush($lostModule);
341
            } elseif (($lostModuleState == ExtensionApi::STATE_UNINITIALISED)
342
                || ($lostModuleState == ExtensionApi::STATE_UNINITIALISED + ExtensionApi::INCOMPATIBLE_CORE_SHIFT)) {
343
                // extension was uninitialised and subsequently removed from file system, delete it
344
                $this->extensionRepository->removeAndFlush($lostModule);
345
            } else {
346
                // Set state of module to 'missing'
347
                // This state cannot be reached in with an ACTIVE bundle. - ACTIVE bundles are part of the pre-compiled Kernel.
348
                // extensions that are inactive can be marked as missing.
349
                $this->extensionStateHelper->updateState($lostModule->getId(), ExtensionApi::STATE_MISSING);
350
            }
351
352
            unset($extensionsFromDB[$name]);
353
        }
354
    }
355
356
    /**
357
     * Add extensions to the DB that have been added to the filesystem.
358
     *  - add uninitialized extensions
359
     *  - update missing or invalid extensions
360
     *
361
     * @param array $extensionsFromFile
362
     * @param array $extensionsFromDB
363
     * @return array $upgradedExtensions[<name>] => <version>
364
     */
365
    private function syncAddedExtensions(array $extensionsFromFile, array $extensionsFromDB)
366
    {
367
        $upgradedExtensions = [];
368
369
        foreach ($extensionsFromFile as $name => $extensionFromFile) {
370
            if (empty($extensionsFromDB[$name])) {
371
                $extensionFromFile['state'] = ExtensionApi::STATE_UNINITIALISED;
372
                if (!$extensionFromFile['version']) {
373
                    // set state to invalid if we can't determine a version
374
                    $extensionFromFile['state'] = ExtensionApi::STATE_INVALID;
375
                } else {
376
                    $coreCompatibility = isset($extensionFromFile['corecompatibility'])
377
                        ? $extensionFromFile['corecompatibility']
378
                        : $this->formatCoreCompatibilityString($extensionFromFile['core_min'], $extensionFromFile['core_max']);
379
                    // shift state if module is incompatible with core version
380
                    $extensionFromFile['state'] = $this->isCoreCompatible($coreCompatibility)
381
                        ? $extensionFromFile['state']
382
                        : $extensionFromFile['state'] + ExtensionApi::INCOMPATIBLE_CORE_SHIFT;
383
                }
384
385
                // unset vars that don't matter
386
                unset($extensionFromFile['oldnames']);
387
                unset($extensionFromFile['dependencies']);
388
389
                // unserialize vars
390
                $extensionFromFile['capabilities'] = unserialize($extensionFromFile['capabilities']);
391
                $extensionFromFile['securityschema'] = unserialize($extensionFromFile['securityschema']);
392
393
                // insert new module to db
394
                $newExtension = new ExtensionEntity();
395
                $newExtension->merge($extensionFromFile);
396
                $vetoEvent = new GenericEvent($newExtension);
397
                $this->dispatcher->dispatch(ExtensionEvents::INSERT_VETO, $vetoEvent);
398
                if (!$vetoEvent->isPropagationStopped()) {
399
                    $this->extensionRepository->persistAndFlush($newExtension);
400
                }
401
            } else {
402
                // extension is in the db already
403
                if (($extensionsFromDB[$name]['state'] == ExtensionApi::STATE_MISSING)
404
                    || ($extensionsFromDB[$name]['state'] == ExtensionApi::STATE_MISSING + ExtensionApi::INCOMPATIBLE_CORE_SHIFT)) {
405
                    // extension was lost, now it is here again
406
                    $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], ExtensionApi::STATE_INACTIVE);
407
                } elseif ((($extensionsFromDB[$name]['state'] == ExtensionApi::STATE_INVALID)
408
                        || ($extensionsFromDB[$name]['state'] == ExtensionApi::STATE_INVALID + ExtensionApi::INCOMPATIBLE_CORE_SHIFT))
409
                    && $extensionFromFile['version']) {
410
                    $coreCompatibility = isset($extensionFromFile['corecompatibility'])
411
                        ? $extensionFromFile['corecompatibility']
412
                        : $this->formatCoreCompatibilityString($extensionFromFile['core_min'], $extensionFromFile['core_max']);
413
                    $isCompatible = $this->isCoreCompatible($coreCompatibility);
414
                    if ($isCompatible) {
415
                        // extension was invalid, now it is valid
416
                        $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], ExtensionApi::STATE_UNINITIALISED);
417
                    }
418
                }
419
420
                if ($extensionsFromDB[$name]['version'] != $extensionFromFile['version']) {
421
                    if ($extensionsFromDB[$name]['state'] != ExtensionApi::STATE_UNINITIALISED &&
422
                        $extensionsFromDB[$name]['state'] != ExtensionApi::STATE_INVALID) {
423
                        $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], ExtensionApi::STATE_UPGRADED);
424
                        $upgradedExtensions[$name] = $extensionFromFile['version'];
425
                    }
426
                }
427
            }
428
        }
429
430
        return $upgradedExtensions;
431
    }
432
433
    /**
434
     * Determine if $min and $max values are compatible with Current Core version
435
     *
436
     * @param string $compatibilityString Semver
437
     * @return bool
438
     */
439
    private function isCoreCompatible($compatibilityString)
440
    {
441
        $coreVersion = new version(ZikulaKernel::VERSION);
442
        $requiredVersionExpression = new expression($compatibilityString);
443
444
        return $requiredVersionExpression->satisfiedBy($coreVersion);
445
    }
446
447
    /**
448
     * Format a compatibility string suitable for semver comparison using vierbergenlars/php-semver
449
     *
450
     * @param null $coreMin
451
     * @param null $coreMax
452
     * @return string
453
     */
454
    private function formatCoreCompatibilityString($coreMin = null, $coreMax = null)
455
    {
456
        $coreMin = !empty($coreMin) ? $coreMin : '1.4.0';
457
        $coreMax = !empty($coreMax) ? $coreMax : '2.9.99';
458
459
        return $coreMin . ' - ' . $coreMax;
460
    }
461
}
462