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

daycry / doctrine / 19929298611

26 Nov 2025 04:39PM UTC coverage: 76.061% (+0.2%) from 75.815%
19929298611

push

github

daycry
- CS Fixer

54 of 66 new or added lines in 6 files covered. (81.82%)

4 existing lines in 1 file now uncovered.

448 of 589 relevant lines covered (76.06%)

17.99 hits per line

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

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

3
namespace Daycry\Doctrine;
4

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

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

40
    /**
41
     * Shared cache backend clients to avoid duplicate connections.
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
     * Doctrine constructor.
57
     *
58
     * @param DoctrineConfig|null $doctrineConfig Doctrine configuration
59
     * @param Cache|null          $cacheConfig    Cache configuration
60
     * @param string|null         $dbGroup        Database group name
61
     *
62
     * @throws Exception
63
     */
64
    public function __construct(?DoctrineConfig $doctrineConfig = null, ?Cache $cacheConfig = null, ?string $dbGroup = null)
65
    {
66
        if ($doctrineConfig === null) {
90✔
67
            /** @var DoctrineConfig $doctrineConfig */
68
            $doctrineConfig = config('Doctrine');
2✔
69
        }
70

71
        if ($cacheConfig === null) {
90✔
72
            /** @var Cache $cacheConfig */
73
            $cacheConfig = config('Cache');
52✔
74
        }
75

76
        $devMode = (ENVIRONMENT === 'development');
90✔
77

78
        // Validate entity paths exist (prevent silent misconfiguration)
79
        foreach ($doctrineConfig->entities as $entityPath) {
90✔
80
            if (! is_dir($entityPath)) {
90✔
81
                // Throwing helps surface misconfiguration early; adjust to log() if preferred
82
                throw new Exception('Doctrine entity path does not exist: ' . $entityPath);
×
83
            }
84
        }
85

86
        switch ($cacheConfig->handler) {
90✔
87
            case 'file':
90✔
88
                $this->sharedFilesystemPath = $cacheConfig->file['storePath'] . DIRECTORY_SEPARATOR . 'doctrine';
72✔
89
                $cacheQuery                 = new PhpFilesAdapter($cacheConfig->prefix . $doctrineConfig->queryCacheNamespace, $cacheConfig->ttl, $this->sharedFilesystemPath);
72✔
90
                $cacheResult                = new PhpFilesAdapter($cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace, $cacheConfig->ttl, $this->sharedFilesystemPath);
72✔
91
                $cacheMetadata              = new PhpFilesAdapter($cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace, $cacheConfig->ttl, $this->sharedFilesystemPath);
72✔
92
                break;
72✔
93

94
            case 'redis':
18✔
95
                $redisLib                = new Redis($cacheConfig);
6✔
96
                $this->sharedRedisClient = $redisLib->getInstance();
6✔
97
                $this->sharedRedisClient->select($cacheConfig->redis['database']);
6✔
98
                $cacheQuery    = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->queryCacheNamespace, $cacheConfig->ttl);
6✔
99
                $cacheResult   = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace, $cacheConfig->ttl);
6✔
100
                $cacheMetadata = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace, $cacheConfig->ttl);
6✔
101
                break;
6✔
102

103
            case 'memcached':
12✔
104
                $memcachedLib                = new Memcached($cacheConfig);
6✔
105
                $this->sharedMemcachedClient = $memcachedLib->getInstance();
6✔
106
                $cacheQuery                  = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->queryCacheNamespace, $cacheConfig->ttl);
6✔
107
                $cacheResult                 = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace, $cacheConfig->ttl);
6✔
108
                $cacheMetadata               = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace, $cacheConfig->ttl);
6✔
109
                break;
6✔
110

111
            default:
112
                $cacheQuery = $cacheResult = $cacheMetadata = new ArrayAdapter($cacheConfig->ttl);
6✔
113
        }
114

115
        $config = new Configuration();
90✔
116

117
        $config->setProxyDir($doctrineConfig->proxies);
90✔
118
        $config->setProxyNamespace($doctrineConfig->proxiesNamespace);
90✔
119
        $config->setAutoGenerateProxyClasses($doctrineConfig->setAutoGenerateProxyClasses);
90✔
120

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

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

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

133
        // Second-Level Cache (SLC): uses the framework cache backend
134
        if (! empty($doctrineConfig->secondLevelCache)) {
90✔
135
            $regionsConfig = new RegionsConfiguration(
4✔
136
                (int) ($cacheConfig->ttl ?? 3600),
4✔
137
                60,
4✔
138
            );
4✔
139

140
            $psr6Pool = $this->createSecondLevelCachePool($cacheConfig);
4✔
141

142
            $slcConfig = new ORMCacheConfiguration();
4✔
143
            $slcConfig->setRegionsConfiguration($regionsConfig);
4✔
144
            $cacheFactory = new DefaultCacheFactory($regionsConfig, $psr6Pool);
4✔
145
            $slcConfig->setCacheFactory($cacheFactory);
4✔
146

147
            // Optional SLC statistics logger (hits/misses/puts)
148
            if (! empty($doctrineConfig->secondLevelCacheStatistics)) {
4✔
149
                $slcConfig->setCacheLogger(new StatisticsCacheLogger());
×
150
            }
151

152
            $config->setSecondLevelCacheEnabled(true);
4✔
153
            $config->setSecondLevelCacheConfiguration($slcConfig);
4✔
154
        }
155

156
        switch ($doctrineConfig->metadataConfigurationMethod) {
90✔
157
            case 'xml':
90✔
158
                $config->setMetadataDriverImpl(new XmlDriver($doctrineConfig->entities, XmlDriver::DEFAULT_FILE_EXTENSION, $doctrineConfig->isXsdValidationEnabled));
×
159
                break;
×
160

161
            case 'attribute':
90✔
162
            default:
163
                $config->setMetadataDriverImpl(new AttributeDriver($doctrineConfig->entities));
90✔
164
                break;
90✔
165
        }
166

167
        // INTEGRACIÓN DEL COLLECTOR Y MIDDLEWARE
168
        $collector  = service('doctrineCollector');
90✔
169
        $dbalConfig = new \Doctrine\DBAL\Configuration();
90✔
170
        $middleware = new DoctrineQueryMiddleware($collector);
90✔
171
        $dbalConfig->setMiddlewares([$middleware]);
90✔
172

173
        /** @var Database $dbConfig */
174
        $dbConfig = config('Database');
90✔
175
        if ($dbGroup === null) {
90✔
176
            $dbGroup = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup;
88✔
177
        }
178
        // Database connection information
179
        $connectionOptions = $this->convertDbConfig($dbConfig->{$dbGroup});
90✔
180
        $connection        = DriverManager::getConnection($connectionOptions, $dbalConfig);
90✔
181
        // Create EntityManager con la conexión original (middleware ya captura queries)
182
        $this->em = new EntityManager($connection, $config);
90✔
183

184
        $this->em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('set', 'string');
90✔
185
        $this->em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
90✔
186

187
        // Si la Toolbar está activa, registra el collector manualmente
188
        // (El método addCollector no existe, así que se elimina esta línea)
189
        // if (function_exists('service') && service('toolbar')) {
190
        //     service('toolbar')->addCollector($collector);
191
        // }
192
    }
193

194
    /**
195
     * Reopen the EntityManager with the current connection and configuration.
196
     *
197
     * @return void
198
     */
199
    public function reOpen()
200
    {
201
        $this->em = new EntityManager($this->em->getConnection(), $this->em->getConfiguration(), $this->em->getEventManager());
2✔
202
    }
203

204
    /**
205
     * Convert CodeIgniter database config array to Doctrine's connection options.
206
     *
207
     * @param object $db
208
     *
209
     * @return array
210
     *
211
     * @throws Exception
212
     */
213
    public function convertDbConfig($db)
214
    {
215
        $connectionOptions = [];
90✔
216
        $db                = (is_array($db)) ? json_decode(json_encode($db)) : $db;
90✔
217
        if ($db->DSN) {
90✔
218
            $driverMapper = ['MySQLi' => 'mysqli', 'Postgre' => 'pgsql', 'OCI8' => 'oci8', 'SQLSRV' => 'sqlsrv', 'SQLite3' => 'sqlite3'];
56✔
219
            if (str_contains($db->DSN, 'SQLite')) {
56✔
220
                $db->DSN = strtolower($db->DSN);
4✔
221
            }
222
            $dsnParser         = new DsnParser($driverMapper);
56✔
223
            $connectionOptions = $dsnParser->parse($db->DSN);
56✔
224
        } else {
225
            switch (strtolower($db->DBDriver)) {
34✔
226
                case 'sqlite3':
34✔
227
                    if ($db->database === ':memory:') {
4✔
228
                        $connectionOptions = [
2✔
229
                            'driver' => strtolower($db->DBDriver),
2✔
230
                            'memory' => true,
2✔
231
                        ];
2✔
232
                    } else {
233
                        $connectionOptions = [
2✔
234
                            'driver' => strtolower($db->DBDriver),
2✔
235
                            'path'   => $db->database,
2✔
236
                        ];
2✔
237
                    }
238
                    break;
4✔
239

240
                default:
241
                    $connectionOptions = [
30✔
242
                        'driver'   => strtolower($db->DBDriver),
30✔
243
                        'user'     => $db->username,
30✔
244
                        'password' => $db->password,
30✔
245
                        'host'     => $db->hostname,
30✔
246
                        'dbname'   => $db->database,
30✔
247
                        'charset'  => $db->charset,
30✔
248
                        'port'     => $db->port,
30✔
249
                    ];
30✔
250
                    // Soporte para SSL y opciones avanzadas
251
                    $sslOptions = ['sslmode', 'sslcert', 'sslkey', 'sslca', 'sslcapath', 'sslcipher', 'sslcrl', 'sslverify', 'sslcompression'];
30✔
252

253
                    foreach ($sslOptions as $opt) {
30✔
254
                        if (isset($db->{$opt})) {
30✔
255
                            $connectionOptions[$opt] = $db->{$opt};
×
256
                        }
257
                    }
258
                    // Opciones personalizadas
259
                    if (isset($db->options) && is_array($db->options)) {
30✔
260
                        foreach ($db->options as $key => $value) {
×
261
                            $connectionOptions[$key] = $value;
×
262
                        }
263
                    }
264
            }
265
        }
266

267
        return $connectionOptions;
90✔
268
    }
269

270
    /**
271
     * Convert CodeIgniter PDO config to Doctrine's connection options.
272
     *
273
     * @param mixed $db
274
     *
275
     * @return array
276
     *
277
     * @throws Exception
278
     * @codeCoverageIgnore
279
     */
280
    protected function convertDbConfigPdo($db)
281
    {
282
        $connectionOptions = [];
×
283

284
        if (substr($db->hostname, 0, 7) === 'sqlite:') {
×
285
            $connectionOptions = [
×
286
                'driver'   => 'pdo_sqlite',
×
287
                'user'     => $db->username,
×
288
                'password' => $db->password,
×
289
                'path'     => preg_replace('/\Asqlite:/', '', $db->hostname),
×
290
            ];
×
291
        } elseif (substr($db->dsn, 0, 7) === 'sqlite:') {
×
292
            $connectionOptions = [
×
293
                'driver'   => 'pdo_sqlite',
×
294
                'user'     => $db->username,
×
295
                'password' => $db->password,
×
296
                'path'     => preg_replace('/\Asqlite:/', '', $db->dsn),
×
297
            ];
×
298
        } elseif (substr($db->dsn, 0, 6) === 'mysql:') {
×
299
            $connectionOptions = [
×
300
                'driver'   => 'pdo_mysql',
×
301
                'user'     => $db->username,
×
302
                'password' => $db->password,
×
303
                'host'     => $db->hostname,
×
304
                'dbname'   => $db->database,
×
305
                'charset'  => $db->charset,
×
306
                'port'     => $db->port,
×
307
            ];
×
308
        } else {
309
            throw new Exception('Your Database Configuration is not confirmed by CodeIgniter Doctrine');
×
310
        }
311

312
        return $connectionOptions;
×
313
    }
314

315
    /**
316
     * Create PSR-6 cache pool for Doctrine SLC based on configured adapter.
317
     */
318
    protected function createSecondLevelCachePool(Cache $cacheConfig): Psr6AdapterInterface
319
    {
320
        $ttl = $cacheConfig->ttl;
4✔
321

322
        switch ($cacheConfig->handler) {
4✔
323
            case 'file':
4✔
324
                $dir = $this->sharedFilesystemPath ?? ($cacheConfig->file['storePath'] . DIRECTORY_SEPARATOR . 'doctrine');
4✔
325

326
                return new PhpFilesAdapter($cacheConfig->prefix . 'doctrine_slc', $ttl, $dir);
4✔
327

328
            case 'redis':
×
329
                $client = $this->sharedRedisClient;
×
330
                if ($client === null) {
×
NEW
331
                    $redisLib = new Redis($cacheConfig);
×
NEW
332
                    $client   = $redisLib->getInstance();
×
333
                    $client->select($cacheConfig->redis['database']);
×
334
                    $this->sharedRedisClient = $client;
×
335
                }
336

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

339
            case 'memcached':
×
340
                $client = $this->sharedMemcachedClient;
×
341
                if ($client === null) {
×
NEW
342
                    $memcachedLib                = new Memcached($cacheConfig);
×
NEW
343
                    $client                      = $memcachedLib->getInstance();
×
UNCOV
344
                    $this->sharedMemcachedClient = $client;
×
345
                }
346

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

UNCOV
349
            case 'array':
×
350
            default:
351
                return new ArrayAdapter($ttl);
×
352
        }
353
    }
354

355
    /**
356
     * Return Second-Level Cache logger if enabled.
357
     * Consumers can inspect the logger for stats.
358
     */
359
    public function getSecondLevelCacheLogger(): ?CacheLogger
360
    {
361
        $cfg = $this->em?->getConfiguration()?->getSecondLevelCacheConfiguration();
8✔
362
        if ($cfg === null) {
8✔
363
            return null;
8✔
364
        }
365
        $logger = $cfg->getCacheLogger();
×
366

UNCOV
367
        return $logger instanceof StatisticsCacheLogger ? $logger : null;
×
368
    }
369

370
    /**
371
     * Reset Second-Level Cache statistics counters if available.
372
     */
373
    public function resetSecondLevelCacheStatistics(): void
374
    {
375
        $logger = $this->getSecondLevelCacheLogger();
×
376
        if ($logger === null) {
×
377
            return;
×
378
        }
379
        // Prefer clearStats() in Doctrine ORM 3.x
380
        if (method_exists($logger, 'clearStats')) {
×
381
            $logger->clearStats();
×
382

UNCOV
383
            return;
×
384
        }
385

386
        // Fallback: zero known public properties (legacy stubs)
387
        foreach (['cacheHits', 'cacheMisses', 'cachePuts'] as $prop) {
×
388
            if (property_exists($logger, $prop)) {
×
NEW
389
                $logger->{$prop} = 0;
×
390
            }
391
        }
392
    }
393
}
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