Completed
Push — master ( 89c3e8...c161b3 )
by Craig
05:56 queued 40s
created

BundleSyncHelper::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 10
dl 0
loc 22
rs 9.9332
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
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Zikula package.
7
 *
8
 * Copyright Zikula Foundation - https://ziku.la/
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 Zikula\ExtensionsModule\Helper;
15
16
use Composer\Semver\Semver;
17
use RuntimeException;
18
use Symfony\Component\ErrorHandler\Error\FatalError;
19
use Symfony\Component\Finder\Finder;
20
use Symfony\Component\HttpFoundation\Session\SessionInterface;
21
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
22
use Symfony\Contracts\Translation\TranslatorInterface;
23
use Zikula\Bundle\CoreBundle\AbstractBundle;
24
use Zikula\Bundle\CoreBundle\Composer\MetaData;
25
use Zikula\Bundle\CoreBundle\Event\GenericEvent;
26
use Zikula\Bundle\CoreBundle\Helper\BundlesSchemaHelper;
27
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaHttpKernelInterface;
28
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaKernel;
29
use Zikula\ExtensionsModule\Constant;
30
use Zikula\ExtensionsModule\Entity\ExtensionEntity;
31
use Zikula\ExtensionsModule\Entity\Repository\ExtensionDependencyRepository;
32
use Zikula\ExtensionsModule\Entity\RepositoryInterface\ExtensionRepositoryInterface;
33
use Zikula\ExtensionsModule\Entity\RepositoryInterface\ExtensionVarRepositoryInterface;
34
use Zikula\ExtensionsModule\ExtensionEvents;
35
36
/**
37
 * Helper functions for the extensions bundle
38
 */
39
class BundleSyncHelper
40
{
41
    /**
42
     * @var ZikulaHttpKernelInterface
43
     */
44
    private $kernel;
45
46
    /**
47
     * @var ExtensionRepositoryInterface
48
     */
49
    private $extensionRepository;
50
51
    /**
52
     * @var ExtensionVarRepositoryInterface
53
     */
54
    private $extensionVarRepository;
55
56
    /**
57
     * @var ExtensionDependencyRepository
58
     */
59
    private $extensionDependencyRepository;
60
61
    /**
62
     * @var TranslatorInterface
63
     */
64
    private $translator;
65
66
    /**
67
     * @var EventDispatcherInterface
68
     */
69
    private $dispatcher;
70
71
    /**
72
     * @var ExtensionStateHelper
73
     */
74
    private $extensionStateHelper;
75
76
    /**
77
     * @var BundlesSchemaHelper
78
     */
79
    private $bundlesSchemaHelper;
80
81
    /**
82
     * @var ComposerValidationHelper
83
     */
84
    private $composerValidationHelper;
85
86
    /**
87
     * @var SessionInterface
88
     */
89
    protected $session;
90
91
    public function __construct(
92
        ZikulaHttpKernelInterface $kernel,
93
        ExtensionRepositoryInterface $extensionRepository,
94
        ExtensionVarRepositoryInterface $extensionVarRepository,
95
        ExtensionDependencyRepository $extensionDependencyRepository,
96
        TranslatorInterface $translator,
97
        EventDispatcherInterface $dispatcher,
98
        ExtensionStateHelper $extensionStateHelper,
99
        BundlesSchemaHelper $bundlesSchemaHelper,
100
        ComposerValidationHelper $composerValidationHelper,
101
        SessionInterface $session
102
    ) {
103
        $this->kernel = $kernel;
104
        $this->extensionRepository = $extensionRepository;
105
        $this->extensionVarRepository = $extensionVarRepository;
106
        $this->extensionDependencyRepository = $extensionDependencyRepository;
107
        $this->translator = $translator;
108
        $this->dispatcher = $dispatcher;
109
        $this->extensionStateHelper = $extensionStateHelper;
110
        $this->bundlesSchemaHelper = $bundlesSchemaHelper;
111
        $this->composerValidationHelper = $composerValidationHelper;
112
        $this->session = $session;
113
    }
114
115
    /**
116
     * Scan the extensions directory for bundles and returns an array with all (potential) bundles found.
117
     */
118
    public function scanForBundles($includeCore = false): array
119
    {
120
        // sync the extensions directory and the bundles table
121
        $this->bundlesSchemaHelper->load();
122
        $scanner = $this->bundlesSchemaHelper->getScanner();
123
        foreach ($scanner->getInvalid() as $invalidName) {
124
            $this->session->getFlashBag()->add(
125
                'warning',
126
                $this->translator->trans(
127
                    'WARNING: %extension% has an invalid composer.json file which could not be decoded.',
128
                    ['%extension%' => $invalidName]
129
                )
130
            );
131
        }
132
        $extensions = $scanner->getExtensionsMetaData();
133
134
        $bundles = [];
135
        $srcDir = $this->kernel->getProjectDir() . '/src/';
136
        /** @var MetaData $bundleMetaData */
137
        foreach ($extensions as $name => $bundleMetaData) {
138
            foreach ($bundleMetaData->getPsr4() as $ns => $path) {
139
                $this->kernel->getAutoloader()->addPsr4($ns, $srcDir . $path);
140
            }
141
142
            $bundleClass = $bundleMetaData->getClass();
143
144
            /** @var $bundle \Zikula\Bundle\CoreBundle\AbstractBundle */
145
            $bundle = new $bundleClass();
146
            $bundleMetaData->setTranslator($this->translator);
147
            $bundleVersionArray = $bundleMetaData->getFilteredVersionInfoArray();
148
149
            $finder = new Finder();
150
            $finder->files()->in($bundle->getPath())->depth(0)->name('composer.json');
151
            foreach ($finder as $splFileInfo) {
152
                // there will only be one loop here
153
                $this->composerValidationHelper->check($splFileInfo);
154
                if ($this->composerValidationHelper->isValid()) {
155
                    $bundles[$bundle->getName()] = $bundleVersionArray;
156
                    $bundles[$bundle->getName()]['oldnames'] = $bundleVersionArray['oldnames'] ?? '';
157
                } else {
158
                    $this->session->getFlashBag()->add(
159
                        'error',
160
                        $this->translator->trans(
161
                            'Cannot load %extension% because the composer file is invalid.',
162
                            ['%extension%' => $bundle->getName()]
163
                        )
164
                    );
165
                    foreach ($this->composerValidationHelper->getErrors() as $error) {
166
                        $this->session->getFlashBag()->add('error', $error);
167
                    }
168
                }
169
            }
170
        }
171
172
        if ($includeCore) {
173
            $this->appendCoreExtensionsMetaData($bundles);
174
        }
175
        $this->validate($bundles);
176
177
        return $bundles;
178
    }
179
180
    private function appendCoreExtensionsMetaData(array &$extensions): void
181
    {
182
        foreach (ZikulaKernel::$coreExtension as $systemModule => $bundleClass) {
183
            $bundle = $this->kernel->getBundle($systemModule);
184
            if ($bundle instanceof AbstractBundle) {
185
                $extensions[$systemModule] = $bundle->getMetaData()->getFilteredVersionInfoArray();
186
            }
187
        }
188
    }
189
190
    /**
191
     * Validate the extensions and ensure there are no duplicate names, display names or urls.
192
     *
193
     * @throws FatalError
194
     */
195
    private function validate(array $extensions = []): void
196
    {
197
        $fieldNames = ['name', 'displayname', 'url'];
198
        $moduleValues = [
199
            'name' => [],
200
            'displayname' => [],
201
            'url' => []
202
        ];
203
204
        // check for duplicate name, display name or url
205
        foreach ($extensions as $dir => $modInfo) {
206
            foreach ($fieldNames as $fieldName) {
207
                $key = mb_strtolower($modInfo[$fieldName]);
208
                if (!empty($moduleValues[$fieldName][$key]) && !empty($modInfo[$fieldName])) {
209
                    $message = $this->translator->trans('Fatal error: Two extensions share the same %field%. [%ext1%] and [%ext2%]', [
210
                        '%field%' => $fieldName,
211
                        '%ext1%' => $modInfo['name'],
212
                        '%ext2%' => $moduleValues[$fieldName][$key]
213
                    ]);
214
                    throw new FatalError($message, 500, error_get_last());
215
                }
216
                $moduleValues[$fieldName][$key] = $dir;
217
            }
218
        }
219
    }
220
221
    /**
222
     * Sync extensions in the filesystem and the extensions table.
223
     *
224
     * @return array $upgradedExtensions[<name>] = <version>
225
     */
226
    public function syncExtensions(array $extensionsFromFile, bool $forceDefaults = false): array
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
    private function syncUpdatedExtensions(
255
        array $extensionsFromFile,
256
        array &$extensionsFromDB,
257
        bool $forceDefaults = false
258
    ): void {
259
        foreach ($extensionsFromFile as $name => $extensionFromFile) {
260
            foreach ($extensionsFromDB as $dbname => $extensionFromDB) {
261
                if (isset($extensionFromDB['name']) && in_array($extensionFromDB['name'], (array)$extensionFromFile['oldnames'], true)) {
262
                    // migrate its modvars
263
                    $this->extensionVarRepository->updateName($dbname, $name);
264
                    // rename the extension register
265
                    $this->extensionRepository->updateName($dbname, $name);
266
                    // replace the old extension with the new one in the $extensionsFromDB array
267
                    $extensionsFromDB[$name] = $extensionFromDB;
268
                    unset($extensionsFromDB[$dbname]);
269
                }
270
            }
271
272
            // If extension was previously determined to be incompatible with the core. return to original state
273
            if (isset($extensionsFromDB[$name]) && $extensionsFromDB[$name]['state'] > 10) {
274
                $extensionsFromDB[$name]['state'] -= Constant::INCOMPATIBLE_CORE_SHIFT;
275
                $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], $extensionsFromDB[$name]['state']);
276
            }
277
278
            // update the DB information for this extension to reflect user settings (e.g. url)
279
            if (isset($extensionsFromDB[$name]['id'])) {
280
                $extensionFromFile['id'] = $extensionsFromDB[$name]['id'];
281
                if (Constant::STATE_UNINITIALISED !== $extensionsFromDB[$name]['state'] && Constant::STATE_INVALID !== $extensionsFromDB[$name]['state']) {
282
                    unset($extensionFromFile['version']);
283
                }
284
                if (!$forceDefaults) {
285
                    unset($extensionFromFile['displayname'], $extensionFromFile['description'], $extensionFromFile['url']);
286
                }
287
288
                unset($extensionFromFile['oldnames'], $extensionFromFile['dependencies']);
289
290
                /** @var ExtensionEntity $extension */
291
                $extension = $this->extensionRepository->find($extensionFromFile['id']);
292
                $extension->merge($extensionFromFile);
293
                $this->extensionRepository->persistAndFlush($extension);
294
            }
295
296
            // check extension core requirement is compatible with current core
297
            $coreCompatibility = $extensionFromFile['coreCompatibility'];
298
            if (isset($extensionsFromDB[$name])) {
299
                if (!Semver::satisfies(ZikulaKernel::VERSION, $coreCompatibility)) {
300
                    // extension is incompatible with current core
301
                    $extensionsFromDB[$name]['state'] += Constant::INCOMPATIBLE_CORE_SHIFT;
302
                    $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], $extensionsFromDB[$name]['state']);
303
                }
304
                if (isset($extensionsFromDB[$name]['state'])) {
305
                    $extensionFromFile['state'] = $extensionsFromDB[$name]['state'];
306
                }
307
            }
308
        }
309
    }
310
311
    /**
312
     * Remove extensions from the DB that have been removed from the filesystem.
313
     */
314
    private function syncLostExtensions(array $extensionsFromFile, array &$extensionsFromDB): void
315
    {
316
        foreach ($extensionsFromDB as $extensionName => $unusedVariable) {
317
            if ($this->kernel::isCoreExtension($extensionName) || array_key_exists($extensionName, $extensionsFromFile)) {
318
                continue;
319
            }
320
321
            $lostExtension = $this->extensionRepository->get($extensionName); // must obtain Entity because value from $extensionsFromDB is only an array
322
            if (!$lostExtension) {
323
                throw new RuntimeException($this->translator->trans('Error! Could not load data for %extension%.', ['%extension%' => $extensionName]));
324
            }
325
            $lostExtensionState = $lostExtension->getState();
326
            if ((Constant::STATE_INVALID === $lostExtensionState)
327
                || ($lostExtensionState === Constant::STATE_INVALID + Constant::INCOMPATIBLE_CORE_SHIFT)) {
328
                // extension was invalid and subsequently removed from file system,
329
                // or extension was incompatible with core and subsequently removed, delete it
330
                $this->extensionRepository->removeAndFlush($lostExtension);
331
            } elseif ((Constant::STATE_UNINITIALISED === $lostExtensionState)
332
                || ($lostExtensionState === Constant::STATE_UNINITIALISED + Constant::INCOMPATIBLE_CORE_SHIFT)) {
333
                // extension was uninitialised and subsequently removed from file system, delete it
334
                $this->extensionRepository->removeAndFlush($lostExtension);
335
            } else {
336
                // Set state of extension to 'missing'
337
                // This state cannot be reached in with an ACTIVE bundle. - ACTIVE bundles are part of the pre-compiled Kernel.
338
                // extensions that are inactive can be marked as missing.
339
                $this->extensionStateHelper->updateState($lostExtension->getId(), Constant::STATE_MISSING);
340
            }
341
342
            unset($extensionsFromDB[$extensionName]);
343
        }
344
    }
345
346
    /**
347
     * Add extensions to the DB that have been added to the filesystem.
348
     *  - add uninitialized extensions
349
     *  - update missing or invalid extensions
350
     *
351
     * @return array $upgradedExtensions[<name>] => <version>
352
     */
353
    private function syncAddedExtensions(array $extensionsFromFile, array $extensionsFromDB): array
354
    {
355
        $upgradedExtensions = [];
356
357
        foreach ($extensionsFromFile as $name => $extensionFromFile) {
358
            if (empty($extensionsFromDB[$name])) {
359
                $extensionFromFile['state'] = Constant::STATE_UNINITIALISED;
360
                if (!$extensionFromFile['version']) {
361
                    // set state to invalid if we can't determine a version
362
                    $extensionFromFile['state'] = Constant::STATE_INVALID;
363
                } else {
364
                    $coreCompatibility = $extensionFromFile['coreCompatibility'];
365
                    // shift state if extension is incompatible with core version
366
                    $extensionFromFile['state'] = Semver::satisfies(ZikulaKernel::VERSION, $coreCompatibility)
367
                        ? $extensionFromFile['state']
368
                        : $extensionFromFile['state'] + Constant::INCOMPATIBLE_CORE_SHIFT;
369
                }
370
371
                // unset vars that don't matter
372
                unset($extensionFromFile['oldnames'], $extensionFromFile['dependencies']);
373
374
                // insert new extension to db
375
                $newExtension = new ExtensionEntity();
376
                $newExtension->merge($extensionFromFile);
377
                $vetoEvent = new GenericEvent($newExtension);
378
                $this->dispatcher->dispatch($vetoEvent, ExtensionEvents::INSERT_VETO);
379
                if (!$vetoEvent->isPropagationStopped()) {
380
                    $this->extensionRepository->persistAndFlush($newExtension);
381
                }
382
            } else {
383
                // extension is in the db already
384
                if ((Constant::STATE_MISSING === $extensionsFromDB[$name]['state'])
385
                    || ($extensionsFromDB[$name]['state'] === Constant::STATE_MISSING + Constant::INCOMPATIBLE_CORE_SHIFT)) {
386
                    // extension was lost, now it is here again
387
                    $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], Constant::STATE_INACTIVE);
388
                } elseif (((Constant::STATE_INVALID === $extensionsFromDB[$name]['state'])
389
                        || ($extensionsFromDB[$name]['state'] === Constant::STATE_INVALID + Constant::INCOMPATIBLE_CORE_SHIFT))
390
                    && $extensionFromFile['version']) {
391
                    $coreCompatibility = $extensionFromFile['coreCompatibility'];
392
                    if (Semver::satisfies(ZikulaKernel::VERSION, $coreCompatibility)) {
393
                        // extension was invalid, now it is valid
394
                        $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], Constant::STATE_UNINITIALISED);
395
                    }
396
                }
397
398
                if ($extensionsFromDB[$name]['version'] !== $extensionFromFile['version']) {
399
                    if (Constant::STATE_UNINITIALISED !== $extensionsFromDB[$name]['state'] &&
400
                        Constant::STATE_INVALID !== $extensionsFromDB[$name]['state']) {
401
                        $this->extensionStateHelper->updateState($extensionsFromDB[$name]['id'], Constant::STATE_UPGRADED);
402
                        $upgradedExtensions[$name] = $extensionFromFile['version'];
403
                    }
404
                }
405
            }
406
        }
407
408
        return $upgradedExtensions;
409
    }
410
}
411