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

daycry / doctrine / 19709863448

26 Nov 2025 04:00PM UTC coverage: 75.815% (-1.0%) from 76.782%
19709863448

push

github

daycry
- Improvements

69 of 96 new or added lines in 3 files covered. (71.88%)

442 of 583 relevant lines covered (75.81%)

18.15 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\Configuration;
14
use Doctrine\ORM\EntityManager;
15
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
16
use Doctrine\ORM\Mapping\Driver\XmlDriver;
17
use Exception;
18
use Doctrine\ORM\Cache\CacheConfiguration as ORMCacheConfiguration;
19
use Doctrine\ORM\Cache\DefaultCacheFactory;
20
use Doctrine\ORM\Cache\RegionsConfiguration;
21
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
22
use Symfony\Component\Cache\Adapter\AdapterInterface as Psr6AdapterInterface;
23
use Symfony\Component\Cache\Adapter\ArrayAdapter;
24
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
25
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
26
use Symfony\Component\Cache\Adapter\RedisAdapter;
27

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

39

40
    /**
41
     * Shared cache backend clients to avoid duplicate connections.
42
     */
43
    /** @var object|null Redis client instance if available */
44
    protected $sharedRedisClient = null;
45
    /** @var object|null Memcached client instance if available */
46
    protected $sharedMemcachedClient = null;
47
    protected ?string $sharedFilesystemPath = null;
48

49
    /**
50
     * Doctrine constructor.
51
     *
52
     * @param DoctrineConfig|null $doctrineConfig Doctrine configuration
53
     * @param Cache|null          $cacheConfig    Cache configuration
54
     * @param string|null         $dbGroup        Database group name
55
     *
56
     * @throws Exception
57
     */
58
    public function __construct(?DoctrineConfig $doctrineConfig = null, ?Cache $cacheConfig = null, ?string $dbGroup = null)
59
    {
60
        if ($doctrineConfig === null) {
90✔
61
            /** @var DoctrineConfig $doctrineConfig */
62
            $doctrineConfig = config('Doctrine');
2✔
63
        }
64

65
        if ($cacheConfig === null) {
90✔
66
            /** @var Cache $cacheConfig */
67
            $cacheConfig = config('Cache');
52✔
68
        }
69

70
        $devMode = (ENVIRONMENT === 'development');
90✔
71

72
        // Validate entity paths exist (prevent silent misconfiguration)
73
        foreach ($doctrineConfig->entities as $entityPath) {
90✔
74
            if (! is_dir($entityPath)) {
90✔
75
                // Throwing helps surface misconfiguration early; adjust to log() if preferred
76
                throw new Exception('Doctrine entity path does not exist: ' . $entityPath);
×
77
            }
78
        }
79

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

88
            case 'redis':
18✔
89
                $redisLib = new Redis($cacheConfig);
6✔
90
                $this->sharedRedisClient = $redisLib->getInstance();
6✔
91
                $this->sharedRedisClient->select($cacheConfig->redis['database']);
6✔
92
                $cacheQuery    = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->queryCacheNamespace, $cacheConfig->ttl);
6✔
93
                $cacheResult   = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace, $cacheConfig->ttl);
6✔
94
                $cacheMetadata = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace, $cacheConfig->ttl);
6✔
95
                break;
6✔
96

97
            case 'memcached':
12✔
98
                $memcachedLib  = new Memcached($cacheConfig);
6✔
99
                $this->sharedMemcachedClient = $memcachedLib->getInstance();
6✔
100
                $cacheQuery    = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->queryCacheNamespace, $cacheConfig->ttl);
6✔
101
                $cacheResult   = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace, $cacheConfig->ttl);
6✔
102
                $cacheMetadata = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace, $cacheConfig->ttl);
6✔
103
                break;
6✔
104

105
            default:
106
                $cacheQuery = $cacheResult = $cacheMetadata = new ArrayAdapter($cacheConfig->ttl);
6✔
107
        }
108

109
        $config = new Configuration();
90✔
110

111
        $config->setProxyDir($doctrineConfig->proxies);
90✔
112
        $config->setProxyNamespace($doctrineConfig->proxiesNamespace);
90✔
113
        $config->setAutoGenerateProxyClasses($doctrineConfig->setAutoGenerateProxyClasses);
90✔
114

115
        if ($doctrineConfig->queryCache) {
90✔
116
            $config->setQueryCache($cacheQuery);
90✔
117
        }
118

119
        if ($doctrineConfig->resultsCache) {
90✔
120
            $config->setResultCache($cacheResult);
90✔
121
        }
122

123
        if ($doctrineConfig->metadataCache) {
90✔
124
            $config->setMetadataCache($cacheMetadata);
90✔
125
        }
126

127
        // Second-Level Cache (SLC): uses the framework cache backend
128
        if (!empty($doctrineConfig->secondLevelCache)) {
90✔
129
            $regionsConfig = new RegionsConfiguration(
4✔
130
                (int) ($cacheConfig->ttl ?? 3600),
4✔
131
                60
4✔
132
            );
4✔
133

134
            $psr6Pool = $this->createSecondLevelCachePool($cacheConfig);
4✔
135

136
            $slcConfig = new ORMCacheConfiguration();
4✔
137
            $slcConfig->setRegionsConfiguration($regionsConfig);
4✔
138
            $cacheFactory = new DefaultCacheFactory($regionsConfig, $psr6Pool);
4✔
139
            $slcConfig->setCacheFactory($cacheFactory);
4✔
140

141
            // Optional SLC statistics logger (hits/misses/puts)
142
            if (!empty($doctrineConfig->secondLevelCacheStatistics)) {
4✔
NEW
143
                $slcConfig->setCacheLogger(new StatisticsCacheLogger());
×
144
            }
145

146
            $config->setSecondLevelCacheEnabled(true);
4✔
147
            $config->setSecondLevelCacheConfiguration($slcConfig);
4✔
148
        }
149

150
        switch ($doctrineConfig->metadataConfigurationMethod) {
90✔
151
            case 'xml':
90✔
152
                $config->setMetadataDriverImpl(new XmlDriver($doctrineConfig->entities, XmlDriver::DEFAULT_FILE_EXTENSION, $doctrineConfig->isXsdValidationEnabled));
×
153
                break;
×
154

155
            case 'attribute':
90✔
156
            default:
157
                $config->setMetadataDriverImpl(new AttributeDriver($doctrineConfig->entities));
90✔
158
                break;
90✔
159
        }
160

161
        // INTEGRACIÓN DEL COLLECTOR Y MIDDLEWARE
162
        $collector  = service('doctrineCollector');
90✔
163
        $dbalConfig = new \Doctrine\DBAL\Configuration();
90✔
164
        $middleware = new DoctrineQueryMiddleware($collector);
90✔
165
        $dbalConfig->setMiddlewares([$middleware]);
90✔
166

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

178
        $this->em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('set', 'string');
90✔
179
        $this->em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
90✔
180

181
        // Si la Toolbar está activa, registra el collector manualmente
182
        // (El método addCollector no existe, así que se elimina esta línea)
183
        // if (function_exists('service') && service('toolbar')) {
184
        //     service('toolbar')->addCollector($collector);
185
        // }
186
    }
187

188
    /**
189
     * Reopen the EntityManager with the current connection and configuration.
190
     *
191
     * @return void
192
     */
193
    public function reOpen()
194
    {
195
        $this->em = new EntityManager($this->em->getConnection(), $this->em->getConfiguration(), $this->em->getEventManager());
2✔
196
    }
197

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

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

247
                    foreach ($sslOptions as $opt) {
30✔
248
                        if (isset($db->{$opt})) {
30✔
249
                            $connectionOptions[$opt] = $db->{$opt};
×
250
                        }
251
                    }
252
                    // Opciones personalizadas
253
                    if (isset($db->options) && is_array($db->options)) {
30✔
254
                        foreach ($db->options as $key => $value) {
×
255
                            $connectionOptions[$key] = $value;
×
256
                        }
257
                    }
258
            }
259
        }
260

261
        return $connectionOptions;
90✔
262
    }
263

264
    /**
265
     * Convert CodeIgniter PDO config to Doctrine's connection options.
266
     *
267
     * @param mixed $db
268
     *
269
     * @return array
270
     *
271
     * @throws Exception
272
     * @codeCoverageIgnore
273
     */
274
    protected function convertDbConfigPdo($db)
275
    {
276
        $connectionOptions = [];
×
277

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

306
        return $connectionOptions;
×
307
    }
308

309
    /**
310
     * Create PSR-6 cache pool for Doctrine SLC based on configured adapter.
311
     */
312
    protected function createSecondLevelCachePool(\Config\Cache $cacheConfig): Psr6AdapterInterface
313
    {
314
        $ttl = $cacheConfig->ttl;
4✔
315

316
        switch ($cacheConfig->handler) {
4✔
317
            case 'file':
4✔
318
                $dir = $this->sharedFilesystemPath ?? ($cacheConfig->file['storePath'] . DIRECTORY_SEPARATOR . 'doctrine');
4✔
319
                return new PhpFilesAdapter($cacheConfig->prefix . 'doctrine_slc', $ttl, $dir);
4✔
320
            case 'redis':
×
321
                $client = $this->sharedRedisClient;
×
322
                if ($client === null) {
×
323
                    $redisLib = new \Daycry\Doctrine\Libraries\Redis($cacheConfig);
×
324
                    $client = $redisLib->getInstance();
×
325
                    $client->select($cacheConfig->redis['database']);
×
326
                    $this->sharedRedisClient = $client;
×
327
                }
328
                return new RedisAdapter($client, $cacheConfig->prefix . 'doctrine_slc', $ttl);
×
329
            case 'memcached':
×
330
                $client = $this->sharedMemcachedClient;
×
331
                if ($client === null) {
×
332
                    $memcachedLib = new \Daycry\Doctrine\Libraries\Memcached($cacheConfig);
×
333
                    $client = $memcachedLib->getInstance();
×
334
                    $this->sharedMemcachedClient = $client;
×
335
                }
336
                return new MemcachedAdapter($client, $cacheConfig->prefix . 'doctrine_slc', $ttl);
×
337
            case 'array':
×
338
            default:
339
                return new ArrayAdapter($ttl);
×
340
        }
341
    }
342

343
    /**
344
     * Return Second-Level Cache logger if enabled.
345
     * Consumers can inspect the logger for stats.
346
     */
347
    public function getSecondLevelCacheLogger(): ?\Doctrine\ORM\Cache\Logging\CacheLogger
348
    {
349
        $cfg = $this->em?->getConfiguration()?->getSecondLevelCacheConfiguration();
8✔
350
        if ($cfg === null) {
8✔
351
            return null;
8✔
352
        }
NEW
353
        $logger = $cfg->getCacheLogger();
×
NEW
354
        return $logger instanceof StatisticsCacheLogger ? $logger : null;
×
355
    }
356

357
    /**
358
     * Reset Second-Level Cache statistics counters if available.
359
     */
360
    public function resetSecondLevelCacheStatistics(): void
361
    {
NEW
362
        $logger = $this->getSecondLevelCacheLogger();
×
NEW
363
        if ($logger === null) {
×
NEW
364
            return;
×
365
        }
366
        // Prefer clearStats() in Doctrine ORM 3.x
NEW
367
        if (method_exists($logger, 'clearStats')) {
×
NEW
368
            $logger->clearStats();
×
NEW
369
            return;
×
370
        }
371
        // Fallback: zero known public properties (legacy stubs)
NEW
372
        foreach (['cacheHits', 'cacheMisses', 'cachePuts'] as $prop) {
×
NEW
373
            if (property_exists($logger, $prop)) {
×
NEW
374
                $logger->$prop = 0;
×
375
            }
376
        }
377
    }
378
}
379
    
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