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

daycry / doctrine / 19702960321

26 Nov 2025 12:00PM UTC coverage: 76.782% (-1.1%) from 77.852%
19702960321

push

github

daycry
Features

Acknowledged. Here’s a concise PR description you can use.

Title
Standardize docs (English + style), harden DataTables builder, and expand test coverage

Summary

Aligns all documentation to English with consistent structure and operator tables.
Strengthens DataTables builder filtering and search logic to prevent invalid DQL.
Adds targeted tests covering operator parsing, LIKE synonyms, numeric column handling, and combined filters.
Cleans up non-doc files from docs.
Changes

Docs:
datatables.md: rewritten in English; unified sections; operator table (Mode/Pattern/Description); examples and best practices.
search_modes.md: aligned style; added Key Concepts/Notes; included LIKE synonyms.
debug_toolbar.md: standardized with Key Concepts, Setup, Usage, Notes.
installation.md: added Key Concepts and Notes.
configuration.md: added Key Concepts, Publish Configuration, and Notes.
second_level_cache.md: added Key Concepts and Notes; clarified backend reuse and TTL.
README: reference note under “Using DataTables” pointing to datatables.md.
Removed docs/DATATABLES_FIX.md and docs/TEST_COVERAGE.md (non-doc content).
Code (existing feature refinements referenced in tests/documentation):
DataTables builder: operator parsing extracted and validated; numeric/invalid field guard via isValidDQLField; withSearchableColumns support; consistent case-insensitive handling for LIKE/equality.
Tests:
DataTableTest.php: added
Invalid operator fallback ([XYZ]am → LIKE %am%).
LIKE synonyms ([LIKE]am, [%%]am).
Global search skips numeric column identifiers.
Case-insensitive behavior with OR, IN, ><.
Combined global LIKE + per-column operator filter.
Full suite passes: 74 tests, 227 assertions, 5 skipped.
Motivation

Ensure documentation clarity and consistency for contributors/users.
Prevent runtime DQL errors from malformed DataTables params.
Improve reliability with explicit tests for edge cases and operator handling.
Impact

No breaking changes to public APIs.
Docum... (continued)

46 of 66 new or added lines in 4 files covered. (69.7%)

2 existing lines in 1 file now uncovered.

377 of 491 relevant lines covered (76.78%)

19.32 hits per line

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

68.55
/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 Symfony\Component\Cache\Adapter\AdapterInterface as Psr6AdapterInterface;
22
use Symfony\Component\Cache\Adapter\ArrayAdapter;
23
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
24
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
25
use Symfony\Component\Cache\Adapter\RedisAdapter;
26

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

38

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

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

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

69
        $devMode = (ENVIRONMENT === 'development');
82✔
70

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

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

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

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

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

108
        $config = new Configuration();
82✔
109

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

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

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

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

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

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

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

140
            $config->setSecondLevelCacheEnabled(true);
4✔
141
            $config->setSecondLevelCacheConfiguration($slcConfig);
4✔
142
        }
143

144
        switch ($doctrineConfig->metadataConfigurationMethod) {
82✔
145
            case 'xml':
82✔
146
                $config->setMetadataDriverImpl(new XmlDriver($doctrineConfig->entities, XmlDriver::DEFAULT_FILE_EXTENSION, $doctrineConfig->isXsdValidationEnabled));
×
147
                break;
×
148

149
            case 'attribute':
82✔
150
            default:
151
                $config->setMetadataDriverImpl(new AttributeDriver($doctrineConfig->entities));
82✔
152
                break;
82✔
153
        }
154

155
        // INTEGRACIÓN DEL COLLECTOR Y MIDDLEWARE
156
        $collector  = service('doctrineCollector');
82✔
157
        $dbalConfig = new \Doctrine\DBAL\Configuration();
82✔
158
        $middleware = new DoctrineQueryMiddleware($collector);
82✔
159
        $dbalConfig->setMiddlewares([$middleware]);
82✔
160

161
        /** @var Database $dbConfig */
162
        $dbConfig = config('Database');
82✔
163
        if ($dbGroup === null) {
82✔
164
            $dbGroup = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup;
80✔
165
        }
166
        // Database connection information
167
        $connectionOptions = $this->convertDbConfig($dbConfig->{$dbGroup});
82✔
168
        $connection        = DriverManager::getConnection($connectionOptions, $dbalConfig);
82✔
169
        // Create EntityManager con la conexión original (middleware ya captura queries)
170
        $this->em = new EntityManager($connection, $config);
82✔
171

172
        $this->em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('set', 'string');
82✔
173
        $this->em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
82✔
174

175
        // Si la Toolbar está activa, registra el collector manualmente
176
        // (El método addCollector no existe, así que se elimina esta línea)
177
        // if (function_exists('service') && service('toolbar')) {
178
        //     service('toolbar')->addCollector($collector);
179
        // }
180
    }
181

182
    /**
183
     * Reopen the EntityManager with the current connection and configuration.
184
     *
185
     * @return void
186
     */
187
    public function reOpen()
188
    {
189
        $this->em = new EntityManager($this->em->getConnection(), $this->em->getConfiguration(), $this->em->getEventManager());
2✔
190
    }
191

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

228
                default:
229
                    $connectionOptions = [
22✔
230
                        'driver'   => strtolower($db->DBDriver),
22✔
231
                        'user'     => $db->username,
22✔
232
                        'password' => $db->password,
22✔
233
                        'host'     => $db->hostname,
22✔
234
                        'dbname'   => $db->database,
22✔
235
                        'charset'  => $db->charset,
22✔
236
                        'port'     => $db->port,
22✔
237
                    ];
22✔
238
                    // Soporte para SSL y opciones avanzadas
239
                    $sslOptions = ['sslmode', 'sslcert', 'sslkey', 'sslca', 'sslcapath', 'sslcipher', 'sslcrl', 'sslverify', 'sslcompression'];
22✔
240

241
                    foreach ($sslOptions as $opt) {
22✔
242
                        if (isset($db->{$opt})) {
22✔
243
                            $connectionOptions[$opt] = $db->{$opt};
×
244
                        }
245
                    }
246
                    // Opciones personalizadas
247
                    if (isset($db->options) && is_array($db->options)) {
22✔
248
                        foreach ($db->options as $key => $value) {
×
249
                            $connectionOptions[$key] = $value;
×
250
                        }
251
                    }
252
            }
253
        }
254

255
        return $connectionOptions;
82✔
256
    }
257

258
    /**
259
     * Convert CodeIgniter PDO config to Doctrine's connection options.
260
     *
261
     * @param mixed $db
262
     *
263
     * @return array
264
     *
265
     * @throws Exception
266
     * @codeCoverageIgnore
267
     */
268
    protected function convertDbConfigPdo($db)
269
    {
270
        $connectionOptions = [];
×
271

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

300
        return $connectionOptions;
×
301
    }
302

303
    /**
304
     * Create PSR-6 cache pool for Doctrine SLC based on configured adapter.
305
     */
306
    protected function createSecondLevelCachePool(\Config\Cache $cacheConfig): Psr6AdapterInterface
307
    {
308
        $ttl = $cacheConfig->ttl;
4✔
309

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