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

codeigniter4 / settings / 20033287059

08 Dec 2025 03:25PM UTC coverage: 81.911%. First build
20033287059

Pull #157

github

web-flow
Merge e972c1655 into 7fb10c218
Pull Request #157: Implement a getAll function

0 of 29 new or added lines in 3 files covered. (0.0%)

403 of 492 relevant lines covered (81.91%)

13.34 hits per line

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

93.02
/src/Handlers/DatabaseHandler.php
1
<?php
2

3
namespace CodeIgniter\Settings\Handlers;
4

5
use CodeIgniter\Database\BaseBuilder;
6
use CodeIgniter\Database\BaseConnection;
7
use CodeIgniter\Database\Exceptions\DatabaseException;
8
use CodeIgniter\I18n\Time;
9
use CodeIgniter\Settings\Config\Settings;
10
use RuntimeException;
11

12
/**
13
 * Provides database persistence for Settings.
14
 * Uses ArrayHandler for storage to minimize database calls.
15
 */
16
class DatabaseHandler extends ArrayHandler
17
{
18
    /**
19
     * The DB connection for the Settings.
20
     */
21
    private BaseConnection $db;
22

23
    /**
24
     * The Query Builder for the Settings table.
25
     */
26
    private BaseBuilder $builder;
27

28
    /**
29
     * Array of contexts that have been stored.
30
     *
31
     * @var list<null>|list<string>
32
     */
33
    private array $hydrated = [];
34

35
    private Settings $config;
36

37
    /**
38
     * Stores the configured database table.
39
     */
40
    public function __construct()
41
    {
42
        $this->config  = config('Settings');
20✔
43
        $this->db      = db_connect($this->config->database['group']);
20✔
44
        $this->builder = $this->db->table($this->config->database['table']);
20✔
45

46
        $this->setupDeferredWrites($this->config->database['deferWrites'] ?? false);
20✔
47
    }
48

49
    /**
50
     * Checks whether this handler has a value set.
51
     */
52
    public function has(string $class, string $property, ?string $context = null): bool
53
    {
54
        $this->hydrate($context);
16✔
55

56
        return $this->hasStored($class, $property, $context);
16✔
57
    }
58

59
    /**
60
     * Attempt to retrieve a value from the database.
61
     * To boost performance, all of the values are
62
     * read and stored the first call for each contexts
63
     * and then retrieved from storage.
64
     *
65
     * @return mixed
66
     */
67
    public function get(string $class, string $property, ?string $context = null)
68
    {
69
        return $this->getStored($class, $property, $context);
8✔
70
    }
71

72
    /**
73
     * Retrieve all values from the database.
74
     *
75
     * @return mixed|null
76
     */
77
    public function getAll(?string $class, ?string $context = null)
78
    {
NEW
79
        return $this->getAllStored($class, $context);
×
80
    }
81

82
    /**
83
     * Stores values into the database for later retrieval.
84
     *
85
     * @param mixed $value
86
     *
87
     * @return void
88
     *
89
     * @throws RuntimeException For database failures
90
     */
91
    public function set(string $class, string $property, $value = null, ?string $context = null)
92
    {
93
        if ($this->deferWrites) {
17✔
94
            $this->markPending($class, $property, $value, $context);
4✔
95
        } else {
96
            $this->persist($class, $property, $value, $context);
16✔
97
        }
98

99
        // Update storage after persistence check
100
        $this->setStored($class, $property, $value, $context);
17✔
101
    }
102

103
    /**
104
     * Persists a single property to the database.
105
     *
106
     * @param mixed $value
107
     *
108
     * @throws RuntimeException For database failures
109
     */
110
    private function persist(string $class, string $property, $value, ?string $context): void
111
    {
112
        $time     = Time::now()->format('Y-m-d H:i:s');
16✔
113
        $type     = gettype($value);
16✔
114
        $prepared = $this->prepareValue($value);
16✔
115

116
        // If it was stored then we need to update
117
        if ($this->has($class, $property, $context)) {
16✔
118
            $result = $this->builder
2✔
119
                ->where('class', $class)
2✔
120
                ->where('key', $property)
2✔
121
                ->where('context', $context)
2✔
122
                ->update([
2✔
123
                    'value'      => $prepared,
2✔
124
                    'type'       => $type,
2✔
125
                    'context'    => $context,
2✔
126
                    'updated_at' => $time,
2✔
127
                ]);
2✔
128
            // ...otherwise insert it
129
        } else {
130
            $result = $this->builder
15✔
131
                ->insert([
15✔
132
                    'class'      => $class,
15✔
133
                    'key'        => $property,
15✔
134
                    'value'      => $prepared,
15✔
135
                    'type'       => $type,
15✔
136
                    'context'    => $context,
15✔
137
                    'created_at' => $time,
15✔
138
                    'updated_at' => $time,
15✔
139
                ]);
15✔
140
        }
141

142
        if ($result !== true) {
16✔
143
            throw new RuntimeException($this->db->error()['message'] ?? 'Error writing to the database.');
×
144
        }
145
    }
146

147
    /**
148
     * Deletes the record from persistent storage, if found,
149
     * and from the local cache.
150
     *
151
     * @return void
152
     */
153
    public function forget(string $class, string $property, ?string $context = null)
154
    {
155
        $this->hydrate($context);
4✔
156

157
        if ($this->deferWrites) {
4✔
158
            $this->markPending($class, $property, null, $context, true);
2✔
159
        } else {
160
            $this->persistForget($class, $property, $context);
2✔
161
        }
162

163
        // Delete from local storage
164
        $this->forgetStored($class, $property, $context);
4✔
165
    }
166

167
    /**
168
     * Deletes a single property from the database.
169
     *
170
     * @throws RuntimeException For database failures
171
     */
172
    private function persistForget(string $class, string $property, ?string $context): void
173
    {
174
        $result = $this->builder
2✔
175
            ->where('class', $class)
2✔
176
            ->where('key', $property)
2✔
177
            ->where('context', $context)
2✔
178
            ->delete();
2✔
179

180
        if (! $result) {
2✔
181
            throw new RuntimeException($this->db->error()['message'] ?? 'Error writing to the database.');
×
182
        }
183
    }
184

185
    /**
186
     * Deletes all records from persistent storage, if found,
187
     * and from the local cache.
188
     *
189
     * @return void
190
     */
191
    public function flush()
192
    {
193
        $this->builder->truncate();
1✔
194

195
        parent::flush();
1✔
196
    }
197

198
    /**
199
     * Fetches values from the database in bulk to minimize calls.
200
     * General (null) is always fetched once, contexts are fetched
201
     * in their entirety for each new request.
202
     *
203
     * @throws RuntimeException For database failures
204
     */
205
    private function hydrate(?string $context): void
206
    {
207
        // Check for completion
208
        if (in_array($context, $this->hydrated, true)) {
18✔
209
            return;
11✔
210
        }
211

212
        if ($context === null) {
18✔
213
            $this->hydrated[] = null;
17✔
214

215
            $query = $this->builder->where('context', null);
17✔
216
        } else {
217
            $query = $this->builder->where('context', $context);
2✔
218

219
            // If general has not been hydrated we will do that at the same time
220
            if (! in_array(null, $this->hydrated, true)) {
2✔
221
                $this->hydrated[] = null;
1✔
222
                $query->orWhere('context', null);
1✔
223
            }
224

225
            $this->hydrated[] = $context;
2✔
226
        }
227

228
        if (is_bool($result = $query->get())) {
18✔
229
            throw new RuntimeException($this->db->error()['message'] ?? 'Error reading from database.');
×
230
        }
231

232
        foreach ($result->getResultObject() as $row) {
18✔
233
            $this->setStored($row->class, $row->key, $this->parseValue($row->value, $row->type), $row->context);
4✔
234
        }
235
    }
236

237
    /**
238
     * Persists all pending properties to the database.
239
     * Called automatically at the end of request via post_system
240
     * event when deferWrites is enabled.
241
     *
242
     * @return void
243
     */
244
    public function persistPendingProperties()
245
    {
246
        if ($this->pendingProperties === []) {
5✔
247
            return;
×
248
        }
249

250
        $time = Time::now()->format('Y-m-d H:i:s');
5✔
251

252
        // Separate deletes from upserts and prepare for database operations
253
        $deletes = [];
5✔
254
        $upserts = [];
5✔
255

256
        foreach ($this->pendingProperties as $info) {
5✔
257
            if ($info['delete']) {
5✔
258
                // Prepare delete row with correct database column names
259
                $deletes[] = [
1✔
260
                    'class'   => $info['class'],
1✔
261
                    'key'     => $info['property'],
1✔
262
                    'context' => $info['context'],
1✔
263
                ];
1✔
264
            } else {
265
                // Prepare upsert row with correct database column names
266
                $upserts[] = [
4✔
267
                    'class'      => $info['class'],
4✔
268
                    'key'        => $info['property'],
4✔
269
                    'value'      => $this->prepareValue($info['value']),
4✔
270
                    'type'       => gettype($info['value']),
4✔
271
                    'context'    => $info['context'],
4✔
272
                    'created_at' => $time,
4✔
273
                    'updated_at' => $time,
4✔
274
                ];
4✔
275
            }
276
        }
277

278
        try {
279
            $this->db->transStart();
5✔
280

281
            // Handle upserts: fetch existing records matching our pending data
282
            if ($upserts !== []) {
5✔
283
                // Build query to fetch only the specific records we need
284
                $this->buildOrWhereConditions($upserts, 'class', 'key', 'context');
4✔
285

286
                $existing = $this->builder->get()->getResultArray();
4✔
287

288
                // Build a map of existing records for quick lookup
289
                $existingMap = [];
4✔
290

291
                foreach ($existing as $row) {
4✔
292
                    $key               = $this->buildCompositeKey($row['class'], $row['key'], $row['context']);
3✔
293
                    $existingMap[$key] = $row['id'];
3✔
294
                }
295

296
                // Separate into inserts and updates
297
                $inserts = [];
4✔
298
                $updates = [];
4✔
299

300
                foreach ($upserts as $row) {
4✔
301
                    $key = $this->buildCompositeKey($row['class'], $row['key'], $row['context']);
4✔
302

303
                    if (isset($existingMap[$key])) {
4✔
304
                        // Record exists - prepare for update
305
                        $updates[] = [
3✔
306
                            'id'         => $existingMap[$key],
3✔
307
                            'value'      => $row['value'],
3✔
308
                            'type'       => $row['type'],
3✔
309
                            'updated_at' => $row['updated_at'],
3✔
310
                        ];
3✔
311
                    } else {
312
                        // New record - prepare for insert
313
                        $inserts[] = $row;
2✔
314
                    }
315
                }
316

317
                // Batch insert new records
318
                if ($inserts !== []) {
4✔
319
                    $this->builder->insertBatch($inserts);
2✔
320
                }
321

322
                // Batch update existing records
323
                if ($updates !== []) {
4✔
324
                    $this->builder->updateBatch($updates, 'id');
3✔
325
                }
326
            }
327

328
            // Batch delete all delete operations
329
            if ($deletes !== []) {
5✔
330
                $this->buildOrWhereConditions($deletes, 'class', 'key', 'context');
1✔
331

332
                $this->builder->delete();
1✔
333
            }
334

335
            $this->db->transComplete();
5✔
336

337
            if ($this->db->transStatus() === false) {
5✔
338
                log_message('error', 'Failed to persist pending properties to database.');
×
339
            }
340

341
            $this->pendingProperties = [];
5✔
342
        } catch (DatabaseException $e) {
×
343
            log_message('error', 'Failed to persist pending properties: ' . $e->getMessage());
×
344

345
            $this->pendingProperties = [];
×
346
        }
347
    }
348

349
    /**
350
     * Builds a composite key for lookup purposes.
351
     */
352
    private function buildCompositeKey(string $class, string $key, ?string $context): string
353
    {
354
        return $class . '::' . $key . ($context === null ? '' : '::' . $context);
4✔
355
    }
356

357
    /**
358
     * Builds OR WHERE conditions for multiple rows.
359
     */
360
    private function buildOrWhereConditions(array $rows, string $classKey, string $keyKey, string $contextKey): void
361
    {
362
        foreach ($rows as $row) {
5✔
363
            $this->builder->orGroupStart();
5✔
364

365
            $this->builder
5✔
366
                ->where($classKey, $row[$classKey])
5✔
367
                ->where($keyKey, $row[$keyKey])
5✔
368
                ->where($contextKey, $row[$contextKey]);
5✔
369

370
            $this->builder->groupEnd();
5✔
371
        }
372
    }
373
}
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

© 2025 Coveralls, Inc