• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / doctrine / 25509251920

07 May 2026 04:40PM UTC coverage: 92.41% (+3.5%) from 88.939%
25509251920

Pull #15

github

web-flow
Merge 2270784ae into 4b36cebe6
Pull Request #15: Audit follow-up: bug fixes, refactors, tests and full docs revamp

108 of 116 new or added lines in 10 files covered. (93.1%)

3 existing lines in 1 file now uncovered.

694 of 751 relevant lines covered (92.41%)

16.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.47
/src/Doctrine.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Daycry\Doctrine;
6

7
use CodeIgniter\Cache\Exceptions\CacheException;
8
use CodeIgniter\Exceptions\ConfigException;
9
use Config\Cache;
10
use Config\Database;
11
use Daycry\Doctrine\Config\Doctrine as DoctrineConfig;
12
use Daycry\Doctrine\Config\Services;
13
use Daycry\Doctrine\Debug\Toolbar\Collectors\DoctrineQueryMiddleware;
14
use Daycry\Doctrine\Libraries\Memcached;
15
use Daycry\Doctrine\Libraries\Redis;
16
use Doctrine\DBAL\DriverManager;
17
use Doctrine\DBAL\Tools\DsnParser;
18
use Doctrine\ORM\Cache\CacheConfiguration as ORMCacheConfiguration;
19
use Doctrine\ORM\Cache\DefaultCacheFactory;
20
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
21
use Doctrine\ORM\Cache\RegionsConfiguration;
22
use Doctrine\ORM\Configuration;
23
use Doctrine\ORM\EntityManager;
24
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
25
use Doctrine\ORM\Mapping\Driver\XmlDriver;
26
use RuntimeException;
27
use Symfony\Component\Cache\Adapter\AdapterInterface as Psr6AdapterInterface;
28
use Symfony\Component\Cache\Adapter\ArrayAdapter;
29
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
30
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
31
use Symfony\Component\Cache\Adapter\RedisAdapter;
32

33
/**
34
 * Doctrine integration for CodeIgniter 4.
35
 * Handles EntityManager, DBAL connection, and cache configuration.
36
 */
37
class Doctrine
38
{
39
    /**
40
     * The Doctrine EntityManager instance.
41
     */
42
    public ?EntityManager $em = null;
43

44
    /**
45
     * @var object|null Redis client instance if available
46
     */
47
    protected $sharedRedisClient;
48

49
    /**
50
     * @var object|null Memcached client instance if available
51
     */
52
    protected $sharedMemcachedClient;
53

54
    protected ?string $sharedFilesystemPath = null;
55

56
    /**
57
     * @throws CacheException
58
     * @throws ConfigException
59
     */
60
    public function __construct(?DoctrineConfig $doctrineConfig = null, ?Cache $cacheConfig = null, ?string $dbGroup = null)
85✔
61
    {
62
        if ($doctrineConfig === null) {
85✔
63
            /** @var DoctrineConfig $doctrineConfig */
64
            $doctrineConfig = config('Doctrine');
1✔
65
        }
66

67
        if ($cacheConfig === null) {
85✔
68
            /** @var Cache $cacheConfig */
69
            $cacheConfig = config('Cache');
50✔
70
        }
71

72
        foreach ($doctrineConfig->entities as $entityPath) {
85✔
73
            if (! is_dir($entityPath)) {
85✔
74
                throw new ConfigException('Doctrine entity path does not exist: ' . $entityPath);
1✔
75
            }
76
        }
77

78
        switch ($cacheConfig->handler) {
84✔
79
            case 'file':
84✔
80
                $this->sharedFilesystemPath = $cacheConfig->file['storePath'] . DIRECTORY_SEPARATOR . 'doctrine';
71✔
81
                $cacheQuery                 = new PhpFilesAdapter($cacheConfig->prefix . $doctrineConfig->queryCacheNamespace, $cacheConfig->ttl, $this->sharedFilesystemPath);
71✔
82
                $cacheResult                = new PhpFilesAdapter($cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace, $cacheConfig->ttl, $this->sharedFilesystemPath);
71✔
83
                $cacheMetadata              = new PhpFilesAdapter($cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace, $cacheConfig->ttl, $this->sharedFilesystemPath);
71✔
84
                break;
71✔
85

86
            case 'redis':
13✔
87
                $redisLib                = new Redis($cacheConfig);
3✔
88
                $this->sharedRedisClient = $redisLib->getInstance();
3✔
89
                $cacheQuery              = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->queryCacheNamespace, $cacheConfig->ttl);
3✔
90
                $cacheResult             = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace, $cacheConfig->ttl);
3✔
91
                $cacheMetadata           = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace, $cacheConfig->ttl);
3✔
92
                break;
3✔
93

94
            case 'memcached':
10✔
95
                $memcachedLib                = new Memcached($cacheConfig);
3✔
96
                $this->sharedMemcachedClient = $memcachedLib->getInstance();
3✔
97
                $cacheQuery                  = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->queryCacheNamespace, $cacheConfig->ttl);
3✔
98
                $cacheResult                 = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace, $cacheConfig->ttl);
3✔
99
                $cacheMetadata               = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace, $cacheConfig->ttl);
3✔
100
                break;
3✔
101

102
            default:
103
                $cacheQuery = $cacheResult = $cacheMetadata = new ArrayAdapter($cacheConfig->ttl);
7✔
104
        }
105

106
        $config = new Configuration();
84✔
107

108
        $useNativeLazyObjects = $doctrineConfig->proxyFactory;
84✔
109

110
        if (\PHP_VERSION_ID >= 80400 && $useNativeLazyObjects) {
84✔
111
            $config->enableNativeLazyObjects(true);
84✔
112
        }
113

114
        if (! $config->isNativeLazyObjectsEnabled()) {
84✔
UNCOV
115
            $config->setProxyDir($doctrineConfig->proxies);
×
UNCOV
116
            $config->setProxyNamespace($doctrineConfig->proxiesNamespace);
×
UNCOV
117
            $config->setAutoGenerateProxyClasses($doctrineConfig->setAutoGenerateProxyClasses);
×
118
        }
119

120
        if ($doctrineConfig->queryCache) {
84✔
121
            $config->setQueryCache($cacheQuery);
84✔
122
        }
123

124
        if ($doctrineConfig->resultsCache) {
84✔
125
            $config->setResultCache($cacheResult);
84✔
126
        }
127

128
        if ($doctrineConfig->metadataCache) {
84✔
129
            $config->setMetadataCache($cacheMetadata);
84✔
130
        }
131

132
        // Second-Level Cache (SLC): uses the framework cache backend
133
        if ($doctrineConfig->secondLevelCache) {
84✔
134
            $slcTtl = $doctrineConfig->secondLevelCacheTtl;
11✔
135
            if ($slcTtl === null) {
11✔
136
                $slcTtl = $cacheConfig->ttl > 0 ? $cacheConfig->ttl : 3600;
10✔
137
            }
138

139
            // Symfony Cache adapters interpret 0 as no-expiration
140
            // Doctrine RegionsConfiguration expects lifetime seconds for regions
141
            $regionsConfig = new RegionsConfiguration(
11✔
142
                (int) $slcTtl,
11✔
143
                60,
11✔
144
            );
11✔
145

146
            $psr6Pool = $this->createSecondLevelCachePool($cacheConfig, (int) $slcTtl, $doctrineConfig);
11✔
147

148
            $slcConfig = new ORMCacheConfiguration();
11✔
149
            $slcConfig->setRegionsConfiguration($regionsConfig);
11✔
150
            $cacheFactory = new DefaultCacheFactory($regionsConfig, $psr6Pool);
11✔
151
            $slcConfig->setCacheFactory($cacheFactory);
11✔
152

153
            // Optional SLC statistics logger (hits/misses/puts)
154
            if ($doctrineConfig->secondLevelCacheStatistics) {
11✔
155
                $slcConfig->setCacheLogger(new StatisticsCacheLogger());
3✔
156
            }
157

158
            $config->setSecondLevelCacheEnabled(true);
11✔
159
            $config->setSecondLevelCacheConfiguration($slcConfig);
11✔
160
        }
161

162
        match ($doctrineConfig->metadataConfigurationMethod) {
84✔
163
            'xml'   => $config->setMetadataDriverImpl(new XmlDriver($doctrineConfig->entities, XmlDriver::DEFAULT_FILE_EXTENSION, $doctrineConfig->isXsdValidationEnabled)),
84✔
164
            default => $config->setMetadataDriverImpl(new AttributeDriver($doctrineConfig->entities)),
84✔
165
        };
84✔
166

167
        // Register custom DQL functions (beberlei/doctrineextensions + user-defined)
168
        foreach ($doctrineConfig->customStringFunctions as $name => $class) {
84✔
169
            $config->addCustomStringFunction($name, $class);
84✔
170
        }
171

172
        foreach ($doctrineConfig->customNumericFunctions as $name => $class) {
84✔
173
            $config->addCustomNumericFunction($name, $class);
84✔
174
        }
175

176
        foreach ($doctrineConfig->customDatetimeFunctions as $name => $class) {
84✔
177
            $config->addCustomDatetimeFunction($name, $class);
84✔
178
        }
179

180
        // Register JSON DQL functions (scienta/doctrine-json-functions)
181
        foreach ($doctrineConfig->customJsonFunctions as $name => $class) {
84✔
182
            $config->addCustomStringFunction($name, $class);
84✔
183
        }
184

185
        // Collector and middleware integration: capture every DBAL query for the toolbar.
186
        $collector  = Services::doctrineCollector();
84✔
187
        $dbalConfig = new \Doctrine\DBAL\Configuration();
84✔
188
        $middleware = new DoctrineQueryMiddleware($collector);
84✔
189
        $dbalConfig->setMiddlewares([$middleware]);
84✔
190

191
        /** @var Database $dbConfig */
192
        $dbConfig = config('Database');
84✔
193
        if ($dbGroup === null) {
84✔
194
            $dbGroup = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup;
39✔
195
        }
196
        // Database connection information
197
        $connectionOptions = $this->convertDbConfig($dbConfig->{$dbGroup});
84✔
198
        $connection        = DriverManager::getConnection($connectionOptions, $dbalConfig);
84✔
199
        // Create EntityManager con la conexión original (middleware ya captura queries)
200
        $this->em = new EntityManager($connection, $config);
84✔
201

202
        $this->registerTypeMappings($doctrineConfig);
84✔
203
    }
204

205
    /**
206
     * Get the EntityManager. Throws if it has not been initialized.
207
     */
208
    public function getEm(): EntityManager
84✔
209
    {
210
        if ($this->em === null) {
84✔
211
            throw new RuntimeException('EntityManager has not been initialized.');
2✔
212
        }
213

214
        return $this->em;
84✔
215
    }
216

217
    /**
218
     * Reopen the EntityManager with the current connection and configuration.
219
     */
220
    public function reOpen(): void
2✔
221
    {
222
        $em       = $this->getEm();
2✔
223
        $this->em = new EntityManager($em->getConnection(), $em->getConfiguration());
1✔
224
        $this->registerTypeMappings(config('Doctrine'));
1✔
225
    }
226

227
    /**
228
     * Register custom DBAL type mappings on the current connection's platform.
229
     */
230
    protected function registerTypeMappings(DoctrineConfig $doctrineConfig): void
84✔
231
    {
232
        $platform = $this->getEm()->getConnection()->getDatabasePlatform();
84✔
233

234
        foreach ($doctrineConfig->customTypeMappings as $dbType => $doctrineType) {
84✔
235
            $platform->registerDoctrineTypeMapping($dbType, $doctrineType);
84✔
236
        }
237
    }
238

239
    /**
240
     * Convert CodeIgniter database config to Doctrine's connection options.
241
     *
242
     * @param array<string, mixed>|object $db
243
     *
244
     * @return array<string, mixed>
245
     *
246
     * @throws ConfigException
247
     */
248
    public function convertDbConfig(array|object $db): array
84✔
249
    {
250
        $db = (is_array($db)) ? (object) $db : $db;
84✔
251
        if (! empty($db->DSN)) {
84✔
252
            $driverMapper = ['MySQLi' => 'mysqli', 'Postgre' => 'pgsql', 'OCI8' => 'oci8', 'SQLSRV' => 'sqlsrv', 'SQLite3' => 'sqlite3'];
28✔
253
            if (str_contains((string) $db->DSN, 'SQLite')) {
28✔
254
                $db->DSN = strtolower((string) $db->DSN);
2✔
255
            }
256
            $dsnParser         = new DsnParser($driverMapper);
28✔
257
            $connectionOptions = $dsnParser->parse($db->DSN);
28✔
258
        } else {
259
            switch (strtolower($db->DBDriver)) {
56✔
260
                case 'sqlite3':
56✔
261
                    if ($db->database === ':memory:') {
46✔
262
                        $connectionOptions = [
45✔
263
                            'driver' => strtolower($db->DBDriver),
45✔
264
                            'memory' => true,
45✔
265
                        ];
45✔
266
                    } else {
267
                        $connectionOptions = [
1✔
268
                            'driver' => strtolower($db->DBDriver),
1✔
269
                            'path'   => $db->database,
1✔
270
                        ];
1✔
271
                    }
272
                    break;
46✔
273

274
                default:
275
                    $driverMap = [
10✔
276
                        'mysqli'  => 'mysqli',
10✔
277
                        'postgre' => 'pdo_pgsql',
10✔
278
                        'oci8'    => 'oci8',
10✔
279
                        'sqlsrv'  => 'sqlsrv',
10✔
280
                    ];
10✔
281
                    $connectionOptions = [
10✔
282
                        'driver'   => $driverMap[strtolower($db->DBDriver)] ?? strtolower($db->DBDriver),
10✔
283
                        'user'     => $db->username,
10✔
284
                        'password' => $db->password,
10✔
285
                        'host'     => $db->hostname,
10✔
286
                        'dbname'   => $db->database,
10✔
287
                        'charset'  => $db->charset,
10✔
288
                        'port'     => $db->port,
10✔
289
                    ];
10✔
290
                    // Soporte para SSL y opciones avanzadas
291
                    $sslOptions = ['sslmode', 'sslcert', 'sslkey', 'sslca', 'sslcapath', 'sslcipher', 'sslcrl', 'sslverify', 'sslcompression'];
10✔
292

293
                    foreach ($sslOptions as $opt) {
10✔
294
                        if (isset($db->{$opt})) {
10✔
295
                            $connectionOptions[$opt] = $db->{$opt};
×
296
                        }
297
                    }
298
                    // Opciones personalizadas
299
                    if (isset($db->options) && is_array($db->options)) {
10✔
300
                        foreach ($db->options as $key => $value) {
×
301
                            $connectionOptions[$key] = $value;
×
302
                        }
303
                    }
304
            }
305
        }
306

307
        return $connectionOptions;
84✔
308
    }
309

310
    /**
311
     * Create PSR-6 cache pool for Doctrine SLC based on configured adapter.
312
     */
313
    protected function createSecondLevelCachePool(Cache $cacheConfig, int $ttl, ?DoctrineConfig $doctrineConfig = null): Psr6AdapterInterface
11✔
314
    {
315
        switch ($cacheConfig->handler) {
11✔
316
            case 'file':
11✔
317
                $dir = $this->sharedFilesystemPath ?? ($cacheConfig->file['storePath'] . DIRECTORY_SEPARATOR . 'doctrine');
7✔
318

319
                return new PhpFilesAdapter($cacheConfig->prefix . 'doctrine_slc', $ttl, $dir);
7✔
320

321
            case 'redis':
4✔
322
                $client = $this->sharedRedisClient;
×
323
                if ($client === null) {
×
324
                    $redisLib                = new Redis($cacheConfig);
×
325
                    $client                  = $redisLib->getInstance();
×
326
                    $this->sharedRedisClient = $client;
×
327
                }
328

329
                return new RedisAdapter($client, $cacheConfig->prefix . 'doctrine_slc', $ttl);
×
330

331
            case 'memcached':
4✔
332
                $client = $this->sharedMemcachedClient;
×
333
                if ($client === null) {
×
334
                    $memcachedLib                = new Memcached($cacheConfig);
×
335
                    $client                      = $memcachedLib->getInstance();
×
336
                    $this->sharedMemcachedClient = $client;
×
337
                }
338

339
                return new MemcachedAdapter($client, $cacheConfig->prefix . 'doctrine_slc', $ttl);
×
340

341
            case 'array':
4✔
342
            default:
343
                return new ArrayAdapter($ttl);
4✔
344
        }
345
    }
346

347
    /**
348
     * Return Second-Level Cache logger if enabled.
349
     * Consumers can inspect the logger for stats.
350
     */
351
    public function getSecondLevelCacheLogger(): ?StatisticsCacheLogger
10✔
352
    {
353
        $cfg = $this->em?->getConfiguration()?->getSecondLevelCacheConfiguration();
10✔
354
        if ($cfg === null) {
10✔
355
            return null;
4✔
356
        }
357
        $logger = $cfg->getCacheLogger();
6✔
358

359
        return $logger instanceof StatisticsCacheLogger ? $logger : null;
6✔
360
    }
361

362
    /**
363
     * Reset Second-Level Cache statistics counters if available.
364
     */
365
    public function resetSecondLevelCacheStatistics(): void
5✔
366
    {
367
        $logger = $this->getSecondLevelCacheLogger();
5✔
368
        if ($logger === null) {
5✔
369
            return;
3✔
370
        }
371

372
        $logger->clearStats();
2✔
373
    }
374
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc