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

codeigniter4 / CodeIgniter4 / 21507617415

30 Jan 2026 07:11AM UTC coverage: 85.382% (-0.1%) from 85.527%
21507617415

push

github

web-flow
feat: FrankenPHP Worker Mode (#9889)

Co-authored-by: John Paul E. Balandan, CPA <paulbalandan@gmail.com>
Co-authored-by: neznaika0 <ozornick.ks@gmail.com>

153 of 243 new or added lines in 19 files covered. (62.96%)

1 existing line in 1 file now uncovered.

22119 of 25906 relevant lines covered (85.38%)

205.24 hits per line

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

67.59
/system/Session/Handlers/RedisHandler.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Session\Handlers;
15

16
use CodeIgniter\I18n\Time;
17
use CodeIgniter\Session\Exceptions\SessionException;
18
use CodeIgniter\Session\PersistsConnection;
19
use Config\Session as SessionConfig;
20
use Redis;
21
use RedisException;
22

23
/**
24
 * Session handler using Redis for persistence.
25
 */
26
class RedisHandler extends BaseHandler
27
{
28
    use PersistsConnection;
29

30
    private const DEFAULT_PORT     = 6379;
31
    private const DEFAULT_PROTOCOL = 'tcp';
32

33
    /**
34
     * phpRedis instance.
35
     *
36
     * @var Redis|null
37
     */
38
    protected $redis;
39

40
    /**
41
     * Key prefix.
42
     *
43
     * @var string
44
     */
45
    protected $keyPrefix = 'ci_session:';
46

47
    /**
48
     * Lock key.
49
     *
50
     * @var string|null
51
     */
52
    protected $lockKey;
53

54
    /**
55
     * Key exists flag.
56
     *
57
     * @var bool
58
     */
59
    protected $keyExists = false;
60

61
    /**
62
     * Number of seconds until the session ends.
63
     *
64
     * @var int
65
     */
66
    protected $sessionExpiration = 7200;
67

68
    /**
69
     * Time (microseconds) to wait if lock cannot be acquired.
70
     */
71
    private int $lockRetryInterval = 100_000;
72

73
    /**
74
     * Maximum number of lock acquisition attempts.
75
     */
76
    private int $lockMaxRetries = 300;
77

78
    /**
79
     * @param string $ipAddress User's IP address.
80
     *
81
     * @throws SessionException
82
     */
83
    public function __construct(SessionConfig $config, string $ipAddress)
84
    {
85
        parent::__construct($config, $ipAddress);
22✔
86

87
        // Store Session configurations.
88
        $this->sessionExpiration = ($config->expiration === 0)
22✔
89
            ? (int) ini_get('session.gc_maxlifetime')
×
90
            : $config->expiration;
22✔
91

92
        // Add session cookie name for multiple session cookies.
93
        $this->keyPrefix .= $config->cookieName . ':';
22✔
94

95
        $this->setSavePath();
22✔
96

97
        if ($this->matchIP === true) {
22✔
98
            $this->keyPrefix .= $this->ipAddress . ':';
×
99
        }
100

101
        $this->lockRetryInterval = $config->lockWait ?? $this->lockRetryInterval;
22✔
102
        $this->lockMaxRetries    = $config->lockAttempts ?? $this->lockMaxRetries;
22✔
103
    }
104

105
    protected function setSavePath(): void
106
    {
107
        if ($this->savePath === '') {
22✔
108
            throw SessionException::forEmptySavepath();
×
109
        }
110

111
        $url   = parse_url($this->savePath);
22✔
112
        $query = [];
22✔
113

114
        if ($url === false) {
22✔
115
            // Unix domain socket like `unix:///var/run/redis/redis.sock?persistent=1`.
116
            if (preg_match('#unix://(/[^:?]+)(\?.+)?#', $this->savePath, $matches)) {
1✔
117
                $host = $matches[1];
1✔
118
                $port = 0;
1✔
119

120
                if (isset($matches[2])) {
1✔
121
                    parse_str(ltrim($matches[2], '?'), $query);
×
122
                }
123
            } else {
124
                throw SessionException::forInvalidSavePathFormat($this->savePath);
×
125
            }
126
        } else {
127
            // Also accepts `/var/run/redis.sock` for backward compatibility.
128
            if (isset($url['path']) && $url['path'][0] === '/') {
21✔
129
                $host = $url['path'];
1✔
130
                $port = 0;
1✔
131
            } else {
132
                // TCP connection.
133
                if (! isset($url['host'])) {
20✔
134
                    throw SessionException::forInvalidSavePathFormat($this->savePath);
×
135
                }
136

137
                $protocol = $url['scheme'] ?? self::DEFAULT_PROTOCOL;
20✔
138
                $host     = $protocol . '://' . $url['host'];
20✔
139
                $port     = $url['port'] ?? self::DEFAULT_PORT;
20✔
140
            }
141

142
            if (isset($url['query'])) {
21✔
143
                parse_str($url['query'], $query);
9✔
144
            }
145
        }
146

147
        $persistent = isset($query['persistent']) ? filter_var($query['persistent'], FILTER_VALIDATE_BOOL) : null;
22✔
148
        $password   = $query['auth'] ?? null;
22✔
149
        $database   = isset($query['database']) ? (int) $query['database'] : 0;
22✔
150
        $timeout    = isset($query['timeout']) ? (float) $query['timeout'] : 0.0;
22✔
151
        $prefix     = $query['prefix'] ?? null;
22✔
152

153
        $this->savePath = [
22✔
154
            'host'       => $host,
22✔
155
            'port'       => $port,
22✔
156
            'password'   => $password,
22✔
157
            'database'   => $database,
22✔
158
            'timeout'    => $timeout,
22✔
159
            'persistent' => $persistent,
22✔
160
        ];
22✔
161

162
        if ($prefix !== null) {
22✔
163
            $this->keyPrefix = $prefix;
×
164
        }
165
    }
166

167
    /**
168
     * Re-initialize existing session, or creates a new one.
169
     *
170
     * @param string $path The path where to store/retrieve the session.
171
     * @param string $name The session name.
172
     *
173
     * @throws RedisException
174
     */
175
    public function open($path, $name): bool
176
    {
177
        if (empty($this->savePath)) {
9✔
178
            return false;
×
179
        }
180

181
        if ($this->hasPersistentConnection()) {
9✔
182
            $redis = $this->getPersistentConnection();
2✔
183

184
            try {
185
                $pingReply = $redis->ping();
2✔
186

187
                if (in_array($pingReply, [true, '+PONG'], true)) {
2✔
188
                    $this->redis = $redis;
2✔
189

190
                    return true;
2✔
191
                }
NEW
192
            } catch (RedisException) {
×
NEW
193
                $this->setPersistentConnection(null);
×
194
            }
195
        }
196

197
        $redis = new Redis();
9✔
198

199
        $funcConnection = isset($this->savePath['persistent']) && $this->savePath['persistent'] === true
9✔
200
            ? 'pconnect'
×
201
            : 'connect';
9✔
202

203
        if ($redis->{$funcConnection}($this->savePath['host'], $this->savePath['port'], $this->savePath['timeout']) === false) {
9✔
204
            $this->logger->error('Session: Unable to connect to Redis with the configured settings.');
×
205
        } elseif (isset($this->savePath['password']) && ! $redis->auth($this->savePath['password'])) {
9✔
206
            $this->logger->error('Session: Unable to authenticate to Redis instance.');
×
207
        } elseif (isset($this->savePath['database']) && ! $redis->select($this->savePath['database'])) {
9✔
208
            $this->logger->error(
×
209
                'Session: Unable to select Redis database with index ' . $this->savePath['database'],
×
210
            );
×
211
        } else {
212
            $this->setPersistentConnection($redis);
9✔
213
            $this->redis = $redis;
9✔
214

215
            return true;
9✔
216
        }
217

218
        return false;
×
219
    }
220

221
    /**
222
     * Reads the session data from the session storage, and returns the results.
223
     *
224
     * @param string $id The session ID.
225
     *
226
     * @throws RedisException
227
     */
228
    public function read($id): false|string
229
    {
230
        if (isset($this->redis) && $this->lockSession($id)) {
4✔
231
            if (! isset($this->sessionID)) {
4✔
232
                $this->sessionID = $id;
4✔
233
            }
234

235
            $data = $this->redis->get($this->keyPrefix . $id);
4✔
236

237
            if (is_string($data)) {
4✔
238
                $this->keyExists = true;
2✔
239
            } else {
240
                $data = '';
2✔
241
            }
242

243
            $this->fingerprint = md5($data);
4✔
244

245
            return $data;
4✔
246
        }
247

248
        return false;
×
249
    }
250

251
    /**
252
     * Writes the session data to the session storage.
253
     *
254
     * @param string $id   The session ID.
255
     * @param string $data The encoded session data.
256
     *
257
     * @throws RedisException
258
     */
259
    public function write($id, $data): bool
260
    {
261
        if (! isset($this->redis)) {
1✔
262
            return false;
×
263
        }
264

265
        if ($this->sessionID !== $id) {
1✔
266
            if (! $this->releaseLock() || ! $this->lockSession($id)) {
×
267
                return false;
×
268
            }
269

270
            $this->keyExists = false;
×
271
            $this->sessionID = $id;
×
272
        }
273

274
        if (isset($this->lockKey)) {
1✔
275
            $this->redis->expire($this->lockKey, 300);
1✔
276

277
            if ($this->fingerprint !== ($fingerprint = md5($data)) || $this->keyExists === false) {
1✔
278
                if ($this->redis->set($this->keyPrefix . $id, $data, $this->sessionExpiration)) {
1✔
279
                    $this->fingerprint = $fingerprint;
1✔
280
                    $this->keyExists   = true;
1✔
281

282
                    return true;
1✔
283
                }
284

285
                return false;
×
286
            }
287

288
            return $this->redis->expire($this->keyPrefix . $id, $this->sessionExpiration);
×
289
        }
290

291
        return false;
×
292
    }
293

294
    /**
295
     * Closes the current session.
296
     */
297
    public function close(): bool
298
    {
299
        if (isset($this->redis)) {
7✔
300
            try {
301
                $pingReply = $this->redis->ping();
7✔
302

303
                if (in_array($pingReply, [true, '+PONG'], true) && isset($this->lockKey) && ! $this->releaseLock()) {
7✔
304
                    return false;
7✔
305
                }
306
            } catch (RedisException $e) {
×
307
                $this->logger->error('Session: Got RedisException on close(): ' . $e->getMessage());
×
308
            }
309

310
            return true;
7✔
311
        }
312

313
        return true;
×
314
    }
315

316
    /**
317
     * Destroys a session.
318
     *
319
     * @param string $id The session ID being destroyed.
320
     *
321
     * @throws RedisException
322
     */
323
    public function destroy($id): bool
324
    {
325
        if (isset($this->redis, $this->lockKey)) {
×
326
            if (($result = $this->redis->del($this->keyPrefix . $id)) !== 1) {
×
327
                $this->logger->debug(
×
328
                    'Session: Redis::del() expected to return 1, got ' . var_export($result, true) . ' instead.',
×
329
                );
×
330
            }
331

332
            return $this->destroyCookie();
×
333
        }
334

335
        return false;
×
336
    }
337

338
    /**
339
     * Cleans up expired sessions.
340
     *
341
     * @param int $max_lifetime Sessions that have not updated
342
     *                          for the last max_lifetime seconds will be removed.
343
     */
344
    public function gc($max_lifetime): int
345
    {
346
        return 1;
1✔
347
    }
348

349
    /**
350
     * Acquires an emulated lock.
351
     *
352
     * @param string $sessionID Session ID.
353
     *
354
     * @throws RedisException
355
     */
356
    protected function lockSession(string $sessionID): bool
357
    {
358
        $lockKey = $this->keyPrefix . $sessionID . ':lock';
4✔
359

360
        // PHP 7 reuses the SessionHandler object on regeneration,
361
        // so we need to check here if the lock key is for the
362
        // correct session ID.
363
        if ($this->lockKey === $lockKey) {
4✔
364
            // If there is the lock, make the ttl longer.
365
            return $this->redis->expire($this->lockKey, 300);
×
366
        }
367

368
        $attempt = 0;
4✔
369

370
        do {
371
            $result = $this->redis->set(
4✔
372
                $lockKey,
4✔
373
                (string) Time::now()->getTimestamp(),
4✔
374
                // NX -- Only set the key if it does not already exist.
375
                // EX seconds -- Set the specified expire time, in seconds.
376
                ['nx', 'ex' => 300],
4✔
377
            );
4✔
378

379
            if (! $result) {
4✔
380
                usleep($this->lockRetryInterval);
×
381

382
                continue;
×
383
            }
384

385
            $this->lockKey = $lockKey;
4✔
386
            break;
4✔
387
        } while (++$attempt < $this->lockMaxRetries);
×
388

389
        if ($attempt === 300) {
4✔
390
            $this->logger->error(
×
391
                'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID
×
392
                . ' after 300 attempts, aborting.',
×
393
            );
×
394

395
            return false;
×
396
        }
397

398
        $this->lock = true;
4✔
399

400
        return true;
4✔
401
    }
402

403
    /**
404
     * Releases a previously acquired lock.
405
     *
406
     * @throws RedisException
407
     */
408
    protected function releaseLock(): bool
409
    {
410
        if (isset($this->redis, $this->lockKey) && $this->lock) {
4✔
411
            if (! $this->redis->del($this->lockKey)) {
4✔
412
                $this->logger->error('Session: Error while trying to free lock for ' . $this->lockKey);
×
413

414
                return false;
×
415
            }
416

417
            $this->lockKey = null;
4✔
418
            $this->lock    = false;
4✔
419
        }
420

421
        return true;
4✔
422
    }
423
}
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