Completed
Push — master ( 58b9d0...1fb47b )
by Axel
08:56 queued 03:29
created

PersistedBundleHelper::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Zikula package.
7
 *
8
 * Copyright Zikula - 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\Bundle\CoreBundle\Helper;
15
16
use function Composer\Autoload\includeFile;
17
use Doctrine\DBAL\Configuration;
18
use Doctrine\DBAL\Connection;
19
use Doctrine\DBAL\DriverManager;
20
use Exception;
21
use PDO;
22
use Zikula\Bundle\CoreBundle\Exception\StaleCacheException;
23
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaHttpKernelInterface;
24
use Zikula\ExtensionsModule\Constant;
25
26
class PersistedBundleHelper
27
{
28
    /**
29
     * @var array the active/inactive state of each extension (extension state !== bundle state)
30
     */
31
    private $extensionStateMap = [];
32
33
    /**
34
     * @var string
35
     */
36
    private $databaseUrl;
37
38
    public function __construct(string $databaseUrl = '')
39
    {
40
        $this->databaseUrl = $databaseUrl;
41
    }
42
43
    public function getPersistedBundles(ZikulaHttpKernelInterface $kernel, array &$bundles): void
44
    {
45
        try {
46
            $this->doGetPersistedBundles($kernel, $bundles);
47
        } catch (StaleCacheException $exception) {
48
            throw $exception;
49
        } catch (Exception $exception) {
50
            // fail silently on purpose
51
        }
52
    }
53
54
    private function doGetPersistedBundles(ZikulaHttpKernelInterface $kernel, array &$bundles): void
55
    {
56
        $conn = $this->getConnection();
57
        $conn->connect();
58
        $res = $conn->executeQuery('SELECT bundleclass, autoload, bundletype FROM bundles');
59
        $unavailableExtensions = 0;
60
        foreach ($res->fetchAll(PDO::FETCH_NUM) as [$class, $autoload, $type]) {
61
            $extensionIsActive = $this->extensionIsActive($conn, $class, $type);
62
            if (!$extensionIsActive) {
63
                continue;
64
            }
65
            try {
66
                $autoload = unserialize($autoload);
67
                $this->addAutoloaders($kernel, $autoload);
68
69
                if (class_exists($class)) {
70
                    $bundles[$class] = ['all' => true];
71
                } else {
72
                    $unavailableExtensions += $this->markExtensionUnavailable($conn, $class);
73
                }
74
            } catch (Exception $exception) {
75
                // unable to autoload $prefix / $path
76
            }
77
        }
78
        $conn->close();
79
        if ($unavailableExtensions > 0) {
80
            // clear the cache & start over
81
            throw new StaleCacheException('An extension has been removed without uninstalling.');
82
        }
83
    }
84
85
    private function getConnection(): Connection
86
    {
87
        $connectionParams = [
88
            'url' => $this->databaseUrl ?? ''
89
        ];
90
91
        return DriverManager::getConnection($connectionParams, new Configuration());
92
    }
93
94
    /**
95
     * Determine if an extension is active.
96
     */
97
    private function extensionIsActive(Connection $conn, string $class, string $type): ?bool
98
    {
99
        $extensionName = $this->extensionNameFromClass($class);
100
        if (isset($this->extensionStateMap[$extensionName])) {
101
            // used cached value
102
            $state = $this->extensionStateMap[$extensionName];
103
        } else {
104
            // load all values into class var for lookup
105
            $sql = 'SELECT m.name, m.state, m.id FROM extensions as m';
106
            $rows = $conn->executeQuery($sql);
107
            foreach ($rows as $row) {
108
                $this->extensionStateMap[$row['name']] = [
109
                    'state' => (int)$row['state'],
110
                    'id'    => (int)$row['id'],
111
                ];
112
            }
113
114
            $state = $this->extensionStateMap[$extensionName] ?? ['state' => ('T' === $type) ? Constant::STATE_INACTIVE : Constant::STATE_UNINITIALISED];
115
        }
116
117
        return in_array($state['state'], [Constant::STATE_ACTIVE, Constant::STATE_UPGRADED, Constant::STATE_TRANSITIONAL], true);
118
    }
119
120
    private function markExtensionUnavailable(Connection $conn, string $class): int
121
    {
122
        $extensionName = $this->extensionNameFromClass($class);
123
        $id = $this->extensionStateMap[$extensionName]['id'];
124
125
        return $conn->executeUpdate('UPDATE extensions set state = ? where id = ?', [Constant::STATE_MISSING, $id]);
126
    }
127
128
    private function extensionNameFromClass(string $class): string
129
    {
130
        $extensionNameArray = explode('\\', $class);
131
132
        return array_pop($extensionNameArray);
133
    }
134
135
    /**
136
     * Add autoloaders to kernel or include files from json.
137
     */
138
    public function addAutoloaders(ZikulaHttpKernelInterface $kernel, array $autoload = []): void
139
    {
140
        $srcDir = $kernel->getProjectDir() . '/src/';
141
        if (isset($autoload['psr-0'])) {
142
            foreach ($autoload['psr-0'] as $prefix => $path) {
143
                $kernel->getAutoloader()->add($prefix, $srcDir . $path);
144
            }
145
        }
146
        if (isset($autoload['psr-4'])) {
147
            foreach ($autoload['psr-4'] as $prefix => $path) {
148
                $kernel->getAutoloader()->addPsr4($prefix, $srcDir . $path);
149
            }
150
        }
151
        if (isset($autoload['classmap'])) {
152
            $kernel->getAutoloader()->addClassMap($autoload['classmap']);
153
        }
154
        if (isset($autoload['files'])) {
155
            foreach ($autoload['files'] as $path) {
156
                includeFile($path);
157
            }
158
        }
159
    }
160
}
161