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

daycry / doctrine / 24675033129

20 Apr 2026 03:26PM UTC coverage: 78.027% (+1.9%) from 76.142%
24675033129

push

github

daycry
Fixes & improvements

Fixes & improvements

122 of 157 new or added lines in 8 files covered. (77.71%)

51 existing lines in 5 files now uncovered.

522 of 669 relevant lines covered (78.03%)

10.3 hits per line

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

70.41
/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\Debug\Toolbar\Collectors\DoctrineCollector;
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 Symfony\Component\Cache\Adapter\AdapterInterface as Psr6AdapterInterface;
27
use Symfony\Component\Cache\Adapter\ArrayAdapter;
28
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
29
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
30
use Symfony\Component\Cache\Adapter\RedisAdapter;
31

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

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

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

53
    protected ?string $sharedFilesystemPath = null;
54

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

66
        if ($cacheConfig === null) {
46✔
67
            /** @var Cache $cacheConfig */
68
            $cacheConfig = config('Cache');
26✔
69
        }
70

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

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

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

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

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

105
        $config = new Configuration();
46✔
106

107
        $useNativeLazyObjects = $doctrineConfig->proxyFactory;
46✔
108

109
        if (\PHP_VERSION_ID >= 80400 && $useNativeLazyObjects) {
46✔
UNCOV
110
            $config->enableNativeLazyObjects(true);
×
111
        }
112

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

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

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

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

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

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

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

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

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

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

161
        switch ($doctrineConfig->metadataConfigurationMethod) {
46✔
162
            case 'xml':
46✔
UNCOV
163
                $config->setMetadataDriverImpl(new XmlDriver($doctrineConfig->entities, XmlDriver::DEFAULT_FILE_EXTENSION, $doctrineConfig->isXsdValidationEnabled));
×
UNCOV
164
                break;
×
165

166
            case 'attribute':
46✔
167
            default:
168
                $config->setMetadataDriverImpl(new AttributeDriver($doctrineConfig->entities));
46✔
169
                break;
46✔
170
        }
171

172
        // Register custom DQL functions (beberlei/doctrineextensions + user-defined)
173
        foreach ($doctrineConfig->customStringFunctions as $name => $class) {
46✔
174
            $config->addCustomStringFunction($name, $class);
46✔
175
        }
176

177
        foreach ($doctrineConfig->customNumericFunctions as $name => $class) {
46✔
178
            $config->addCustomNumericFunction($name, $class);
46✔
179
        }
180

181
        foreach ($doctrineConfig->customDatetimeFunctions as $name => $class) {
46✔
182
            $config->addCustomDatetimeFunction($name, $class);
46✔
183
        }
184

185
        // Register JSON DQL functions (scienta/doctrine-json-functions)
186
        foreach ($doctrineConfig->customJsonFunctions as $name => $class) {
46✔
187
            $config->addCustomStringFunction($name, $class);
46✔
188
        }
189

190
        // INTEGRACIÓN DEL COLLECTOR Y MIDDLEWARE
191
        /** @var DoctrineCollector $collector */
192
        $collector  = service('doctrineCollector') ?? new DoctrineCollector();
46✔
193
        $dbalConfig = new \Doctrine\DBAL\Configuration();
46✔
194
        $middleware = new DoctrineQueryMiddleware($collector);
46✔
195
        $dbalConfig->setMiddlewares([$middleware]);
46✔
196

197
        /** @var Database $dbConfig */
198
        $dbConfig = config('Database');
46✔
199
        if ($dbGroup === null) {
46✔
200
            $dbGroup = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup;
42✔
201
        }
202
        // Database connection information
203
        $connectionOptions = $this->convertDbConfig($dbConfig->{$dbGroup});
46✔
204
        $connection        = DriverManager::getConnection($connectionOptions, $dbalConfig);
46✔
205
        // Create EntityManager con la conexión original (middleware ya captura queries)
206
        $this->em = new EntityManager($connection, $config);
46✔
207

208
        $this->registerTypeMappings($doctrineConfig);
46✔
209
    }
210

211
    /**
212
     * Reopen the EntityManager with the current connection and configuration.
213
     */
214
    public function reOpen(): void
1✔
215
    {
216
        $this->em = new EntityManager($this->em->getConnection(), $this->em->getConfiguration());
1✔
217
        $this->registerTypeMappings(config('Doctrine'));
1✔
218
    }
219

220
    /**
221
     * Register custom DBAL type mappings on the current connection's platform.
222
     */
223
    protected function registerTypeMappings(DoctrineConfig $doctrineConfig): void
46✔
224
    {
225
        $platform = $this->em->getConnection()->getDatabasePlatform();
46✔
226

227
        foreach ($doctrineConfig->customTypeMappings as $dbType => $doctrineType) {
46✔
228
            $platform->registerDoctrineTypeMapping($dbType, $doctrineType);
46✔
229
        }
230
    }
231

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

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

286
                    foreach ($sslOptions as $opt) {
13✔
287
                        if (isset($db->{$opt})) {
13✔
UNCOV
288
                            $connectionOptions[$opt] = $db->{$opt};
×
289
                        }
290
                    }
291
                    // Opciones personalizadas
292
                    if (isset($db->options) && is_array($db->options)) {
13✔
UNCOV
293
                        foreach ($db->options as $key => $value) {
×
UNCOV
294
                            $connectionOptions[$key] = $value;
×
295
                        }
296
                    }
297
            }
298
        }
299

300
        return $connectionOptions;
46✔
301
    }
302

303
    /**
304
     * Convert CodeIgniter PDO config to Doctrine's connection options.
305
     *
306
     * @return array<string, mixed>
307
     *
308
     * @throws ConfigException
309
     * @codeCoverageIgnore
310
     */
NEW
311
    protected function convertDbConfigPdo(mixed $db): array
×
312
    {
313
        if (substr($db->hostname, 0, 7) === 'sqlite:') {
×
314
            $connectionOptions = [
×
315
                'driver'   => 'pdo_sqlite',
×
316
                'user'     => $db->username,
×
317
                'password' => $db->password,
×
318
                'path'     => preg_replace('/\Asqlite:/', '', $db->hostname),
×
319
            ];
×
320
        } elseif (substr($db->dsn, 0, 7) === 'sqlite:') {
×
321
            $connectionOptions = [
×
322
                'driver'   => 'pdo_sqlite',
×
323
                'user'     => $db->username,
×
324
                'password' => $db->password,
×
325
                'path'     => preg_replace('/\Asqlite:/', '', $db->dsn),
×
326
            ];
×
327
        } elseif (substr($db->dsn, 0, 6) === 'mysql:') {
×
328
            $connectionOptions = [
×
UNCOV
329
                'driver'   => 'pdo_mysql',
×
330
                'user'     => $db->username,
×
UNCOV
331
                'password' => $db->password,
×
UNCOV
332
                'host'     => $db->hostname,
×
333
                'dbname'   => $db->database,
×
UNCOV
334
                'charset'  => $db->charset,
×
UNCOV
335
                'port'     => $db->port,
×
UNCOV
336
            ];
×
337
        } else {
NEW
338
            throw new ConfigException('Your Database Configuration is not confirmed by CodeIgniter Doctrine');
×
339
        }
340

UNCOV
341
        return $connectionOptions;
×
342
    }
343

344
    /**
345
     * Create PSR-6 cache pool for Doctrine SLC based on configured adapter.
346
     */
347
    protected function createSecondLevelCachePool(Cache $cacheConfig, int $ttl, ?DoctrineConfig $doctrineConfig = null): Psr6AdapterInterface
3✔
348
    {
349
        switch ($cacheConfig->handler) {
3✔
350
            case 'file':
3✔
351
                $dir = $this->sharedFilesystemPath ?? ($cacheConfig->file['storePath'] . DIRECTORY_SEPARATOR . 'doctrine');
3✔
352

353
                return new PhpFilesAdapter($cacheConfig->prefix . 'doctrine_slc', $ttl, $dir);
3✔
354

UNCOV
355
            case 'redis':
×
356
                $client = $this->sharedRedisClient;
×
UNCOV
357
                if ($client === null) {
×
NEW
358
                    $redisLib                = new Redis($cacheConfig);
×
NEW
359
                    $client                  = $redisLib->getInstance();
×
360
                    $this->sharedRedisClient = $client;
×
361
                }
362

UNCOV
363
                return new RedisAdapter($client, $cacheConfig->prefix . 'doctrine_slc', $ttl);
×
364

365
            case 'memcached':
×
UNCOV
366
                $client = $this->sharedMemcachedClient;
×
367
                if ($client === null) {
×
UNCOV
368
                    $memcachedLib                = new Memcached($cacheConfig);
×
369
                    $client                      = $memcachedLib->getInstance();
×
UNCOV
370
                    $this->sharedMemcachedClient = $client;
×
371
                }
372

UNCOV
373
                return new MemcachedAdapter($client, $cacheConfig->prefix . 'doctrine_slc', $ttl);
×
374

UNCOV
375
            case 'array':
×
376
            default:
UNCOV
377
                return new ArrayAdapter($ttl);
×
378
        }
379
    }
380

381
    /**
382
     * Return Second-Level Cache logger if enabled.
383
     * Consumers can inspect the logger for stats.
384
     */
385
    public function getSecondLevelCacheLogger(): ?StatisticsCacheLogger
4✔
386
    {
387
        $cfg = $this->em?->getConfiguration()?->getSecondLevelCacheConfiguration();
4✔
388
        if ($cfg === null) {
4✔
389
            return null;
4✔
390
        }
UNCOV
391
        $logger = $cfg->getCacheLogger();
×
392

393
        return $logger instanceof StatisticsCacheLogger ? $logger : null;
×
394
    }
395

396
    /**
397
     * Reset Second-Level Cache statistics counters if available.
398
     */
399
    public function resetSecondLevelCacheStatistics(): void
×
400
    {
401
        $logger = $this->getSecondLevelCacheLogger();
×
UNCOV
402
        if ($logger === null) {
×
UNCOV
403
            return;
×
404
        }
405

NEW
406
        $logger->clearStats();
×
407
    }
408
}
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