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

codeigniter4 / CodeIgniter4 / 21178922612

20 Jan 2026 04:20PM UTC coverage: 85.402% (-0.1%) from 85.526%
21178922612

Pull #9889

github

web-flow
Merge 63185abef into c7e784fe4
Pull Request #9889: feat: FrankenPHP Worker Mode

154 of 238 new or added lines in 19 files covered. (64.71%)

1 existing line in 1 file now uncovered.

22119 of 25900 relevant lines covered (85.4%)

205.29 hits per line

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

0.0
/system/Session/Handlers/MemcachedHandler.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 Memcached;
21
use ReturnTypeWillChange;
22

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

30
    /**
31
     * Memcached instance.
32
     *
33
     * @var Memcached|null
34
     */
35
    protected $memcached;
36

37
    /**
38
     * Key prefix.
39
     *
40
     * @var string
41
     */
42
    protected $keyPrefix = 'ci_session:';
43

44
    /**
45
     * Lock key
46
     *
47
     * @var string|null
48
     */
49
    protected $lockKey;
50

51
    /**
52
     * Number of seconds until the session ends.
53
     *
54
     * @var int
55
     */
56
    protected $sessionExpiration = 7200;
57

58
    /**
59
     * @throws SessionException
60
     */
61
    public function __construct(SessionConfig $config, string $ipAddress)
62
    {
63
        parent::__construct($config, $ipAddress);
×
64

65
        $this->sessionExpiration = $config->expiration;
×
66

67
        if ($this->savePath !== '') {
×
68
            throw SessionException::forEmptySavepath();
×
69
        }
70

71
        // Add session cookie name for multiple session cookies.
72
        $this->keyPrefix .= $config->cookieName . ':';
×
73

74
        if ($this->matchIP === true) {
×
75
            $this->keyPrefix .= $this->ipAddress . ':';
×
76
        }
77

78
        ini_set('memcached.sess_prefix', $this->keyPrefix);
×
79
    }
80

81
    /**
82
     * Re-initialize existing session, or creates a new one.
83
     *
84
     * @param string $path The path where to store/retrieve the session.
85
     * @param string $name The session name.
86
     */
87
    public function open($path, $name): bool
88
    {
NEW
89
        if ($this->hasPersistentConnection()) {
×
NEW
90
            $memcached = $this->getPersistentConnection();
×
NEW
91
            $version   = $memcached->getVersion();
×
92

NEW
93
            if (is_array($version)) {
×
NEW
94
                foreach ($version as $serverVersion) {
×
NEW
95
                    if ($serverVersion !== false) {
×
NEW
96
                        $this->memcached = $memcached;
×
97

NEW
98
                        return true;
×
99
                    }
100
                }
101
            }
102

NEW
103
            $this->setPersistentConnection(null);
×
104
        }
105

106
        $this->memcached = new Memcached();
×
107
        $this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); // required for touch() usage
×
108

109
        $serverList = [];
×
110

111
        foreach ($this->memcached->getServerList() as $server) {
×
112
            $serverList[] = $server['host'] . ':' . $server['port'];
×
113
        }
114

115
        if (
116
            preg_match_all(
×
117
                '#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#',
×
118
                $this->savePath,
×
119
                $matches,
×
120
                PREG_SET_ORDER,
×
121
            ) < 1
×
122
        ) {
123
            $this->memcached = null;
×
124
            $this->logger->error('Session: Invalid Memcached save path format: ' . $this->savePath);
×
125

126
            return false;
×
127
        }
128

129
        foreach ($matches as $match) {
×
130
            // If Memcached already has this server (or if the port is invalid), skip it
131
            if (in_array($match[1] . ':' . $match[2], $serverList, true)) {
×
132
                $this->logger->debug(
×
133
                    'Session: Memcached server pool already has ' . $match[1] . ':' . $match[2],
×
134
                );
×
135

136
                continue;
×
137
            }
138

139
            if (! $this->memcached->addServer($match[1], (int) $match[2], $match[3] ?? 0)) {
×
140
                $this->logger->error(
×
141
                    'Could not add ' . $match[1] . ':' . $match[2] . ' to Memcached server pool.',
×
142
                );
×
143
            } else {
144
                $serverList[] = $match[1] . ':' . $match[2];
×
145
            }
146
        }
147

148
        if ($serverList === []) {
×
149
            $this->logger->error('Session: Memcached server pool is empty.');
×
150

151
            return false;
×
152
        }
153

NEW
154
        $this->setPersistentConnection($this->memcached);
×
155

UNCOV
156
        return true;
×
157
    }
158

159
    /**
160
     * Reads the session data from the session storage, and returns the results.
161
     *
162
     * @param string $id The session ID.
163
     *
164
     * @return false|string Returns an encoded string of the read data.
165
     *                      If nothing was read, it must return false.
166
     */
167
    #[ReturnTypeWillChange]
168
    public function read($id)
169
    {
170
        if (isset($this->memcached) && $this->lockSession($id)) {
×
171
            if (! isset($this->sessionID)) {
×
172
                $this->sessionID = $id;
×
173
            }
174

175
            $data = (string) $this->memcached->get($this->keyPrefix . $id);
×
176

177
            $this->fingerprint = md5($data);
×
178

179
            return $data;
×
180
        }
181

182
        return '';
×
183
    }
184

185
    /**
186
     * Writes the session data to the session storage.
187
     *
188
     * @param string $id   The session ID.
189
     * @param string $data The encoded session data.
190
     */
191
    public function write($id, $data): bool
192
    {
193
        if (! isset($this->memcached)) {
×
194
            return false;
×
195
        }
196

197
        if ($this->sessionID !== $id) {
×
198
            if (! $this->releaseLock() || ! $this->lockSession($id)) {
×
199
                return false;
×
200
            }
201

202
            $this->fingerprint = md5('');
×
203
            $this->sessionID   = $id;
×
204
        }
205

206
        if (isset($this->lockKey)) {
×
207
            $this->memcached->replace($this->lockKey, Time::now()->getTimestamp(), 300);
×
208

209
            if ($this->fingerprint !== ($fingerprint = md5($data))) {
×
210
                if ($this->memcached->set($this->keyPrefix . $id, $data, $this->sessionExpiration)) {
×
211
                    $this->fingerprint = $fingerprint;
×
212

213
                    return true;
×
214
                }
215

216
                return false;
×
217
            }
218

219
            return $this->memcached->touch($this->keyPrefix . $id, $this->sessionExpiration);
×
220
        }
221

222
        return false;
×
223
    }
224

225
    /**
226
     * Closes the current session.
227
     */
228
    public function close(): bool
229
    {
230
        if (isset($this->memcached)) {
×
231
            if (isset($this->lockKey)) {
×
232
                $this->memcached->delete($this->lockKey);
×
233
            }
234

235
            return true;
×
236
        }
237

238
        return false;
×
239
    }
240

241
    /**
242
     * Destroys a session.
243
     *
244
     * @param string $id The session ID being destroyed.
245
     */
246
    public function destroy($id): bool
247
    {
248
        if (isset($this->memcached, $this->lockKey)) {
×
249
            $this->memcached->delete($this->keyPrefix . $id);
×
250

251
            return $this->destroyCookie();
×
252
        }
253

254
        return false;
×
255
    }
256

257
    /**
258
     * Cleans up expired sessions.
259
     *
260
     * @param int $max_lifetime Sessions that have not updated
261
     *                          for the last max_lifetime seconds will be removed.
262
     *
263
     * @return false|int Returns the number of deleted sessions on success, or false on failure.
264
     */
265
    #[ReturnTypeWillChange]
266
    public function gc($max_lifetime)
267
    {
268
        return 1;
×
269
    }
270

271
    /**
272
     * Acquires an emulated lock.
273
     *
274
     * @param string $sessionID Session ID.
275
     */
276
    protected function lockSession(string $sessionID): bool
277
    {
278
        if (isset($this->lockKey)) {
×
279
            return $this->memcached->replace($this->lockKey, Time::now()->getTimestamp(), 300);
×
280
        }
281

282
        $lockKey = $this->keyPrefix . $sessionID . ':lock';
×
283
        $attempt = 0;
×
284

285
        do {
286
            if ($this->memcached->get($lockKey) !== false) {
×
287
                sleep(1);
×
288

289
                continue;
×
290
            }
291

292
            if (! $this->memcached->set($lockKey, Time::now()->getTimestamp(), 300)) {
×
293
                $this->logger->error(
×
294
                    'Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID,
×
295
                );
×
296

297
                return false;
×
298
            }
299

300
            $this->lockKey = $lockKey;
×
301
            break;
×
302
        } while (++$attempt < 30);
×
303

304
        if ($attempt === 30) {
×
305
            $this->logger->error(
×
306
                'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.',
×
307
            );
×
308

309
            return false;
×
310
        }
311

312
        $this->lock = true;
×
313

314
        return true;
×
315
    }
316

317
    /**
318
     * Releases a previously acquired lock.
319
     */
320
    protected function releaseLock(): bool
321
    {
322
        if (isset($this->memcached, $this->lockKey) && $this->lock) {
×
323
            if (
324
                ! $this->memcached->delete($this->lockKey)
×
325
                && $this->memcached->getResultCode() !== Memcached::RES_NOTFOUND
×
326
            ) {
327
                $this->logger->error(
×
328
                    'Session: Error while trying to free lock for ' . $this->lockKey,
×
329
                );
×
330

331
                return false;
×
332
            }
333

334
            $this->lockKey = null;
×
335
            $this->lock    = false;
×
336
        }
337

338
        return true;
×
339
    }
340
}
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