Completed
Push — master ( 710352...a85e25 )
by Craig
04:57
created

PersistedBundleHelper::markExtensionUnavailable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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