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

daycry / doctrine / 27071527216

06 Jun 2026 07:20PM UTC coverage: 93.219% (+0.8%) from 92.41%
27071527216

Pull #17

github

web-flow
Merge 7d5aa15dc into d99db3aca
Pull Request #17: Development

210 of 218 new or added lines in 9 files covered. (96.33%)

866 of 929 relevant lines covered (93.22%)

18.87 hits per line

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

90.78
/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 Daycry\Doctrine\Logging\QueryLoggerMiddleware;
17
use Doctrine\Common\EventManager;
18
use Doctrine\DBAL\DriverManager;
19
use Doctrine\DBAL\Tools\DsnParser;
20
use Doctrine\DBAL\Types\Type;
21
use Doctrine\ORM\Cache\CacheConfiguration as ORMCacheConfiguration;
22
use Doctrine\ORM\Cache\DefaultCacheFactory;
23
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
24
use Doctrine\ORM\Cache\RegionsConfiguration;
25
use Doctrine\ORM\Configuration;
26
use Doctrine\ORM\EntityManager;
27
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
28
use Doctrine\ORM\Mapping\Driver\XmlDriver;
29
use Psr\Log\LoggerInterface;
30
use RuntimeException;
31
use Symfony\Component\Cache\Adapter\AdapterInterface as Psr6AdapterInterface;
32
use Symfony\Component\Cache\Adapter\ArrayAdapter;
33
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
34
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
35
use Symfony\Component\Cache\Adapter\RedisAdapter;
36

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

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

53
    /**
54
     * @var object|null Memcached client instance if available
55
     */
56
    protected $sharedMemcachedClient;
57

58
    protected ?string $sharedFilesystemPath = null;
59

60
    /**
61
     * @throws CacheException
62
     * @throws ConfigException
63
     */
64
    public function __construct(?DoctrineConfig $doctrineConfig = null, ?Cache $cacheConfig = null, ?string $dbGroup = null)
111✔
65
    {
66
        if ($doctrineConfig === null) {
111✔
67
            /** @var DoctrineConfig $doctrineConfig */
68
            $doctrineConfig = config('Doctrine');
1✔
69
        }
70

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

76
        foreach ($doctrineConfig->entities as $entityPath) {
111✔
77
            if (! is_dir($entityPath)) {
111✔
78
                throw new ConfigException('Doctrine entity path does not exist: ' . $entityPath);
1✔
79
            }
80
        }
81

82
        /** @var Database $dbConfig */
83
        $dbConfig = config('Database');
110✔
84
        if ($dbGroup === null) {
110✔
85
            $dbGroup = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup;
54✔
86
        }
87
        // Suffix cache namespaces with the (non-default) DB group so that multiple
88
        // groups sharing one cache backend never collide on the same keys.
89
        $cacheGroupSuffix = $dbGroup === $dbConfig->defaultGroup ? '' : '_' . strtolower($dbGroup);
110✔
90

91
        switch ($cacheConfig->handler) {
110✔
92
            case 'file':
110✔
93
                $this->sharedFilesystemPath = $cacheConfig->file['storePath'] . DIRECTORY_SEPARATOR . 'doctrine';
97✔
94
                $cacheQuery                 = new PhpFilesAdapter($cacheConfig->prefix . $doctrineConfig->queryCacheNamespace . $cacheGroupSuffix, $cacheConfig->ttl, $this->sharedFilesystemPath);
97✔
95
                $cacheResult                = new PhpFilesAdapter($cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace . $cacheGroupSuffix, $cacheConfig->ttl, $this->sharedFilesystemPath);
97✔
96
                $cacheMetadata              = new PhpFilesAdapter($cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace . $cacheGroupSuffix, $cacheConfig->ttl, $this->sharedFilesystemPath);
97✔
97
                break;
97✔
98

99
            case 'redis':
13✔
100
                $redisLib                = new Redis($cacheConfig);
3✔
101
                $this->sharedRedisClient = $redisLib->getInstance();
3✔
102
                $cacheQuery              = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->queryCacheNamespace . $cacheGroupSuffix, $cacheConfig->ttl);
3✔
103
                $cacheResult             = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace . $cacheGroupSuffix, $cacheConfig->ttl);
3✔
104
                $cacheMetadata           = new RedisAdapter($this->sharedRedisClient, $cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace . $cacheGroupSuffix, $cacheConfig->ttl);
3✔
105
                break;
3✔
106

107
            case 'memcached':
10✔
108
                $memcachedLib                = new Memcached($cacheConfig);
3✔
109
                $this->sharedMemcachedClient = $memcachedLib->getInstance();
3✔
110
                $cacheQuery                  = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->queryCacheNamespace . $cacheGroupSuffix, $cacheConfig->ttl);
3✔
111
                $cacheResult                 = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->resultsCacheNamespace . $cacheGroupSuffix, $cacheConfig->ttl);
3✔
112
                $cacheMetadata               = new MemcachedAdapter($this->sharedMemcachedClient, $cacheConfig->prefix . $doctrineConfig->metadataCacheNamespace . $cacheGroupSuffix, $cacheConfig->ttl);
3✔
113
                break;
3✔
114

115
            default:
116
                $cacheQuery = $cacheResult = $cacheMetadata = new ArrayAdapter($cacheConfig->ttl);
7✔
117
        }
118

119
        $config = new Configuration();
110✔
120

121
        $useNativeLazyObjects = $doctrineConfig->proxyFactory;
110✔
122

123
        if (\PHP_VERSION_ID >= 80400 && $useNativeLazyObjects) {
110✔
124
            $config->enableNativeLazyObjects(true);
110✔
125
        }
126

127
        if (! $config->isNativeLazyObjectsEnabled()) {
110✔
128
            $config->setProxyDir($doctrineConfig->proxies);
×
129
            $config->setProxyNamespace($doctrineConfig->proxiesNamespace);
×
130
            $config->setAutoGenerateProxyClasses($doctrineConfig->setAutoGenerateProxyClasses);
×
131
        }
132

133
        if ($doctrineConfig->queryCache) {
110✔
134
            $config->setQueryCache($cacheQuery);
110✔
135
        }
136

137
        if ($doctrineConfig->resultsCache) {
110✔
138
            $config->setResultCache($cacheResult);
110✔
139
        }
140

141
        if ($doctrineConfig->metadataCache) {
110✔
142
            $config->setMetadataCache($cacheMetadata);
110✔
143
        }
144

145
        // Second-Level Cache (SLC): uses the framework cache backend
146
        if ($doctrineConfig->secondLevelCache) {
110✔
147
            $slcTtl = $doctrineConfig->secondLevelCacheTtl;
11✔
148
            if ($slcTtl === null) {
11✔
149
                $slcTtl = $cacheConfig->ttl > 0 ? $cacheConfig->ttl : 3600;
10✔
150
            }
151

152
            // Symfony Cache adapters interpret 0 as no-expiration
153
            // Doctrine RegionsConfiguration expects lifetime seconds for regions
154
            $regionsConfig = new RegionsConfiguration(
11✔
155
                (int) $slcTtl,
11✔
156
                60,
11✔
157
            );
11✔
158

159
            $psr6Pool = $this->createSecondLevelCachePool($cacheConfig, (int) $slcTtl, $cacheGroupSuffix);
11✔
160

161
            $slcConfig = new ORMCacheConfiguration();
11✔
162
            $slcConfig->setRegionsConfiguration($regionsConfig);
11✔
163
            $cacheFactory = new DefaultCacheFactory($regionsConfig, $psr6Pool);
11✔
164
            $slcConfig->setCacheFactory($cacheFactory);
11✔
165

166
            // Optional SLC statistics logger (hits/misses/puts)
167
            if ($doctrineConfig->secondLevelCacheStatistics) {
11✔
168
                $slcConfig->setCacheLogger(new StatisticsCacheLogger());
3✔
169
            }
170

171
            $config->setSecondLevelCacheEnabled(true);
11✔
172
            $config->setSecondLevelCacheConfiguration($slcConfig);
11✔
173
        }
174

175
        match ($doctrineConfig->metadataConfigurationMethod) {
110✔
176
            'xml'   => $config->setMetadataDriverImpl(new XmlDriver($doctrineConfig->entities, XmlDriver::DEFAULT_FILE_EXTENSION, $doctrineConfig->isXsdValidationEnabled)),
110✔
177
            default => $config->setMetadataDriverImpl(new AttributeDriver($doctrineConfig->entities)),
110✔
178
        };
110✔
179

180
        // Register custom DQL functions (beberlei/doctrineextensions + user-defined)
181
        foreach ($doctrineConfig->customStringFunctions as $name => $class) {
110✔
182
            $config->addCustomStringFunction($name, $class);
110✔
183
        }
184

185
        foreach ($doctrineConfig->customNumericFunctions as $name => $class) {
110✔
186
            $config->addCustomNumericFunction($name, $class);
110✔
187
        }
188

189
        foreach ($doctrineConfig->customDatetimeFunctions as $name => $class) {
110✔
190
            $config->addCustomDatetimeFunction($name, $class);
110✔
191
        }
192

193
        // Register JSON DQL functions (scienta/doctrine-json-functions)
194
        foreach ($doctrineConfig->customJsonFunctions as $name => $class) {
110✔
195
            $config->addCustomStringFunction($name, $class);
110✔
196
        }
197

198
        // Register SQL Filters (soft-delete, multi-tenant, …) on the configuration.
199
        foreach ($doctrineConfig->sqlFilters as $name => $class) {
110✔
200
            $config->addFilter($name, $class);
1✔
201
        }
202

203
        // Default repository class for entities that don't declare their own.
204
        if ($doctrineConfig->defaultRepositoryClass !== null) {
110✔
205
            $config->setDefaultRepositoryClassName($doctrineConfig->defaultRepositoryClass);
1✔
206
        }
207

208
        // DBAL middleware chain: user-defined middlewares (retry/logging/metrics)
209
        // wrap the driver first (outermost); the toolbar capture middleware is
210
        // applied last so it sees the final SQL closest to the driver. The toolbar
211
        // middleware is only wired when query instrumentation is enabled (CI_DEBUG):
212
        // in production the toolbar never renders, so wrapping every statement is
213
        // pure dead weight.
214
        $dbalConfig  = new \Doctrine\DBAL\Configuration();
110✔
215
        $middlewares = [];
110✔
216

217
        foreach ($doctrineConfig->dbalMiddlewares as $userMiddleware) {
110✔
218
            $middlewares[] = is_string($userMiddleware) ? new $userMiddleware() : $userMiddleware;
1✔
219
        }
220

221
        // Production query logging (slow-query log) — independent of CI_DEBUG.
222
        if ($doctrineConfig->queryLogging) {
110✔
223
            $middlewares[] = new QueryLoggerMiddleware(
4✔
224
                $this->logger(),
4✔
225
                $doctrineConfig->slowQueryThreshold,
4✔
226
                $doctrineConfig->queryLogLevel,
4✔
227
            );
4✔
228
        }
229

230
        if ($this->shouldInstrumentQueries()) {
110✔
231
            $collector     = Services::doctrineCollector();
109✔
232
            $middlewares[] = new DoctrineQueryMiddleware($collector);
109✔
233
        }
234

235
        if ($middlewares !== []) {
110✔
236
            $dbalConfig->setMiddlewares($middlewares);
109✔
237
        }
238

239
        // Database connection information ($dbGroup and $dbConfig resolved above).
240
        $connectionOptions = $this->convertDbConfig($dbConfig->{$dbGroup});
110✔
241
        $connection        = DriverManager::getConnection($connectionOptions, $dbalConfig);
110✔
242
        // Create EntityManager con la conexión original (middleware ya captura queries)
243
        $this->em = new EntityManager($connection, $config, $this->buildEventManager($doctrineConfig));
110✔
244

245
        $this->registerTypeMappings($doctrineConfig);
110✔
246
        $this->enableConfiguredFilters($doctrineConfig);
110✔
247
    }
248

249
    /**
250
     * Whether DBAL queries should be instrumented for the Debug Toolbar.
251
     *
252
     * Defaults to CodeIgniter's debug flag: the toolbar only renders when
253
     * CI_DEBUG is true, so production skips the per-query capture overhead.
254
     * Override in a subclass to force instrumentation on or off.
255
     */
256
    protected function shouldInstrumentQueries(): bool
109✔
257
    {
258
        return defined('CI_DEBUG') && CI_DEBUG;
109✔
259
    }
260

261
    /**
262
     * PSR-3 logger used for production query logging. Defaults to CodeIgniter's
263
     * logger service; override in a subclass to route logs elsewhere.
264
     */
NEW
265
    protected function logger(): LoggerInterface
×
266
    {
NEW
267
        return service('logger');
×
268
    }
269

270
    /**
271
     * Get the EntityManager. Throws if it has not been initialized.
272
     */
273
    public function getEm(): EntityManager
110✔
274
    {
275
        if ($this->em === null) {
110✔
276
            throw new RuntimeException('EntityManager has not been initialized.');
2✔
277
        }
278

279
        return $this->em;
110✔
280
    }
281

282
    /**
283
     * Reopen the EntityManager, recovering from a stale or dropped database
284
     * connection (e.g. "MySQL server has gone away" after an idle timeout in a
285
     * long-running CLI worker or queue consumer).
286
     *
287
     * The existing DBAL connection is closed first so DBAL re-establishes the
288
     * underlying socket lazily on the next query; a fresh EntityManager is then
289
     * built over that connection with the same configuration, and custom type
290
     * mappings are re-applied.
291
     *
292
     * Note: closing the connection discards an in-memory SQLite database. This
293
     * method is intended for server-backed connections (MySQL/Postgres/…), which
294
     * is the documented worker-recovery use case.
295
     */
296
    public function reOpen(): void
4✔
297
    {
298
        $em         = $this->getEm();
4✔
299
        $connection = $em->getConnection();
3✔
300

301
        if ($connection->isConnected()) {
3✔
302
            $connection->close();
3✔
303
        }
304

305
        /** @var DoctrineConfig $doctrineConfig */
306
        $doctrineConfig = config('Doctrine');
3✔
307
        $this->em       = new EntityManager($connection, $em->getConfiguration(), $this->buildEventManager($doctrineConfig));
3✔
308
        $this->registerTypeMappings($doctrineConfig);
3✔
309
        $this->enableConfiguredFilters($doctrineConfig);
3✔
310
    }
311

312
    /**
313
     * Build an EventManager from the configured listeners/subscribers, or null
314
     * when none are configured (so the EntityManager keeps its default).
315
     * Class-strings are instantiated with no constructor arguments; ready-made
316
     * instances are used as-is.
317
     */
318
    protected function buildEventManager(DoctrineConfig $doctrineConfig): ?EventManager
110✔
319
    {
320
        if ($doctrineConfig->eventListeners === [] && $doctrineConfig->eventSubscribers === []) {
110✔
321
            return null;
109✔
322
        }
323

324
        $eventManager = new EventManager();
1✔
325

326
        foreach ($doctrineConfig->eventListeners as $event => $listeners) {
1✔
327
            foreach ($listeners as $listener) {
1✔
328
                $eventManager->addEventListener($event, is_string($listener) ? new $listener() : $listener);
1✔
329
            }
330
        }
331

332
        foreach ($doctrineConfig->eventSubscribers as $subscriber) {
1✔
333
            $eventManager->addEventSubscriber(is_string($subscriber) ? new $subscriber() : $subscriber);
1✔
334
        }
335

336
        return $eventManager;
1✔
337
    }
338

339
    /**
340
     * Enable the SQL filters listed in Config\Doctrine::$enabledFilters on the
341
     * current EntityManager. FilterCollection is per-EntityManager, so this must
342
     * run on construction and again after reOpen() rebuilds the EM.
343
     */
344
    protected function enableConfiguredFilters(DoctrineConfig $doctrineConfig): void
110✔
345
    {
346
        if ($doctrineConfig->enabledFilters === []) {
110✔
347
            return;
109✔
348
        }
349

350
        $filters = $this->getEm()->getFilters();
1✔
351

352
        foreach ($doctrineConfig->enabledFilters as $name) {
1✔
353
            // Only enable filters that were actually registered, to avoid a
354
            // misconfigured name throwing during construction.
355
            if (array_key_exists($name, $doctrineConfig->sqlFilters)) {
1✔
356
                $filters->enable($name);
1✔
357
            }
358
        }
359
    }
360

361
    /**
362
     * Register custom DBAL type mappings on the current connection's platform.
363
     */
364
    protected function registerTypeMappings(DoctrineConfig $doctrineConfig): void
110✔
365
    {
366
        // Register custom DBAL Types first (process-global, idempotent).
367
        foreach ($doctrineConfig->customTypes as $name => $class) {
110✔
368
            if (! Type::hasType($name)) {
1✔
369
                Type::addType($name, $class);
1✔
370
            }
371
        }
372

373
        $platform = $this->getEm()->getConnection()->getDatabasePlatform();
110✔
374

375
        foreach ($doctrineConfig->customTypeMappings as $dbType => $doctrineType) {
110✔
376
            $platform->registerDoctrineTypeMapping($dbType, $doctrineType);
110✔
377
        }
378
    }
379

380
    /**
381
     * Convert CodeIgniter database config to Doctrine's connection options.
382
     *
383
     * @param array<string, mixed>|object $db
384
     *
385
     * @return array<string, mixed>
386
     *
387
     * @throws ConfigException
388
     */
389
    public function convertDbConfig(array|object $db): array
110✔
390
    {
391
        $db = (is_array($db)) ? (object) $db : $db;
110✔
392
        if (! empty($db->DSN)) {
110✔
393
            $driverMapper = ['MySQLi' => 'mysqli', 'Postgre' => 'pgsql', 'OCI8' => 'oci8', 'SQLSRV' => 'sqlsrv', 'SQLite3' => 'sqlite3'];
45✔
394
            if (str_contains((string) $db->DSN, 'SQLite')) {
45✔
395
                // Lower-case only the scheme (everything before the first ':'); the
396
                // rest of the DSN — notably a case-sensitive SQLite file path — must
397
                // be preserved verbatim.
398
                $db->DSN = preg_replace_callback('~^[^:]+~', static fn (array $m): string => strtolower($m[0]), (string) $db->DSN);
3✔
399
            }
400
            $dsnParser         = new DsnParser($driverMapper);
45✔
401
            $connectionOptions = $dsnParser->parse($db->DSN);
45✔
402
        } else {
403
            switch (strtolower($db->DBDriver)) {
66✔
404
                case 'sqlite3':
66✔
405
                    if ($db->database === ':memory:') {
56✔
406
                        $connectionOptions = [
55✔
407
                            'driver' => strtolower($db->DBDriver),
55✔
408
                            'memory' => true,
55✔
409
                        ];
55✔
410
                    } else {
411
                        $connectionOptions = [
1✔
412
                            'driver' => strtolower($db->DBDriver),
1✔
413
                            'path'   => $db->database,
1✔
414
                        ];
1✔
415
                    }
416
                    break;
56✔
417

418
                default:
419
                    $driverMap = [
10✔
420
                        'mysqli'  => 'mysqli',
10✔
421
                        'postgre' => 'pdo_pgsql',
10✔
422
                        'oci8'    => 'oci8',
10✔
423
                        'sqlsrv'  => 'sqlsrv',
10✔
424
                    ];
10✔
425
                    $connectionOptions = [
10✔
426
                        'driver'   => $driverMap[strtolower($db->DBDriver)] ?? strtolower($db->DBDriver),
10✔
427
                        'user'     => $db->username,
10✔
428
                        'password' => $db->password,
10✔
429
                        'host'     => $db->hostname,
10✔
430
                        'dbname'   => $db->database,
10✔
431
                        'charset'  => $db->charset,
10✔
432
                        'port'     => $db->port,
10✔
433
                    ];
10✔
434
                    // Soporte para SSL y opciones avanzadas
435
                    $sslOptions = ['sslmode', 'sslcert', 'sslkey', 'sslca', 'sslcapath', 'sslcipher', 'sslcrl', 'sslverify', 'sslcompression'];
10✔
436

437
                    foreach ($sslOptions as $opt) {
10✔
438
                        if (isset($db->{$opt})) {
10✔
439
                            $connectionOptions[$opt] = $db->{$opt};
×
440
                        }
441
                    }
442
                    // Opciones personalizadas
443
                    if (isset($db->options) && is_array($db->options)) {
10✔
444
                        foreach ($db->options as $key => $value) {
×
445
                            $connectionOptions[$key] = $value;
×
446
                        }
447
                    }
448
            }
449
        }
450

451
        return $connectionOptions;
110✔
452
    }
453

454
    /**
455
     * Create PSR-6 cache pool for Doctrine SLC based on configured adapter.
456
     */
457
    protected function createSecondLevelCachePool(Cache $cacheConfig, int $ttl, string $groupSuffix = ''): Psr6AdapterInterface
11✔
458
    {
459
        switch ($cacheConfig->handler) {
11✔
460
            case 'file':
11✔
461
                $dir = $this->sharedFilesystemPath ?? ($cacheConfig->file['storePath'] . DIRECTORY_SEPARATOR . 'doctrine');
7✔
462

463
                return new PhpFilesAdapter($cacheConfig->prefix . 'doctrine_slc' . $groupSuffix, $ttl, $dir);
7✔
464

465
            case 'redis':
4✔
466
                $client = $this->sharedRedisClient;
×
467
                if ($client === null) {
×
468
                    $redisLib                = new Redis($cacheConfig);
×
469
                    $client                  = $redisLib->getInstance();
×
470
                    $this->sharedRedisClient = $client;
×
471
                }
472

NEW
473
                return new RedisAdapter($client, $cacheConfig->prefix . 'doctrine_slc' . $groupSuffix, $ttl);
×
474

475
            case 'memcached':
4✔
476
                $client = $this->sharedMemcachedClient;
×
477
                if ($client === null) {
×
478
                    $memcachedLib                = new Memcached($cacheConfig);
×
479
                    $client                      = $memcachedLib->getInstance();
×
480
                    $this->sharedMemcachedClient = $client;
×
481
                }
482

NEW
483
                return new MemcachedAdapter($client, $cacheConfig->prefix . 'doctrine_slc' . $groupSuffix, $ttl);
×
484

485
            case 'array':
4✔
486
            default:
487
                return new ArrayAdapter($ttl);
4✔
488
        }
489
    }
490

491
    /**
492
     * Return Second-Level Cache logger if enabled.
493
     * Consumers can inspect the logger for stats.
494
     */
495
    public function getSecondLevelCacheLogger(): ?StatisticsCacheLogger
10✔
496
    {
497
        $cfg = $this->em?->getConfiguration()?->getSecondLevelCacheConfiguration();
10✔
498
        if ($cfg === null) {
10✔
499
            return null;
4✔
500
        }
501
        $logger = $cfg->getCacheLogger();
6✔
502

503
        return $logger instanceof StatisticsCacheLogger ? $logger : null;
6✔
504
    }
505

506
    /**
507
     * Reset Second-Level Cache statistics counters if available.
508
     */
509
    public function resetSecondLevelCacheStatistics(): void
5✔
510
    {
511
        $logger = $this->getSecondLevelCacheLogger();
5✔
512
        if ($logger === null) {
5✔
513
            return;
3✔
514
        }
515

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

© 2026 Coveralls, Inc