• 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

108
        $serverList = [];
×
109

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

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

125
            return false;
×
126
        }
127

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

135
                continue;
×
136
            }
137

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

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

150
            return false;
×
151
        }
152

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

UNCOV
155
        return true;
×
156
    }
157

158
    /**
159
     * Reads the session data from the session storage, and returns the results.
160
     *
161
     * @param string $id The session ID.
162
     */
163
    public function read($id): false|string
164
    {
165
        if (isset($this->memcached) && $this->lockSession($id)) {
×
166
            if (! isset($this->sessionID)) {
×
167
                $this->sessionID = $id;
×
168
            }
169

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

172
            $this->fingerprint = md5($data);
×
173

174
            return $data;
×
175
        }
176

177
        return '';
×
178
    }
179

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

192
        if ($this->sessionID !== $id) {
×
193
            if (! $this->releaseLock() || ! $this->lockSession($id)) {
×
194
                return false;
×
195
            }
196

197
            $this->fingerprint = md5('');
×
198
            $this->sessionID   = $id;
×
199
        }
200

201
        if (isset($this->lockKey)) {
×
202
            $this->memcached->replace($this->lockKey, Time::now()->getTimestamp(), 300);
×
203

204
            if ($this->fingerprint !== ($fingerprint = md5($data))) {
×
205
                if ($this->memcached->set($this->keyPrefix . $id, $data, $this->sessionExpiration)) {
×
206
                    $this->fingerprint = $fingerprint;
×
207

208
                    return true;
×
209
                }
210

211
                return false;
×
212
            }
213

214
            return $this->memcached->touch($this->keyPrefix . $id, $this->sessionExpiration);
×
215
        }
216

217
        return false;
×
218
    }
219

220
    /**
221
     * Closes the current session.
222
     */
223
    public function close(): bool
224
    {
225
        if (isset($this->memcached)) {
×
226
            if (isset($this->lockKey)) {
×
227
                $this->memcached->delete($this->lockKey);
×
228
            }
229

230
            return true;
×
231
        }
232

233
        return false;
×
234
    }
235

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

246
            return $this->destroyCookie();
×
247
        }
248

249
        return false;
×
250
    }
251

252
    /**
253
     * Cleans up expired sessions.
254
     *
255
     * @param int $max_lifetime Sessions that have not updated
256
     *                          for the last max_lifetime seconds will be removed.
257
     */
258
    public function gc($max_lifetime): int
259
    {
260
        return 1;
×
261
    }
262

263
    /**
264
     * Acquires an emulated lock.
265
     *
266
     * @param string $sessionID Session ID.
267
     */
268
    protected function lockSession(string $sessionID): bool
269
    {
270
        if (isset($this->lockKey)) {
×
271
            return $this->memcached->replace($this->lockKey, Time::now()->getTimestamp(), 300);
×
272
        }
273

274
        $lockKey = $this->keyPrefix . $sessionID . ':lock';
×
275
        $attempt = 0;
×
276

277
        do {
278
            if ($this->memcached->get($lockKey) !== false) {
×
279
                sleep(1);
×
280

281
                continue;
×
282
            }
283

284
            if (! $this->memcached->set($lockKey, Time::now()->getTimestamp(), 300)) {
×
285
                $this->logger->error(
×
286
                    'Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID,
×
287
                );
×
288

289
                return false;
×
290
            }
291

292
            $this->lockKey = $lockKey;
×
293
            break;
×
294
        } while (++$attempt < 30);
×
295

296
        if ($attempt === 30) {
×
297
            $this->logger->error(
×
298
                'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.',
×
299
            );
×
300

301
            return false;
×
302
        }
303

304
        $this->lock = true;
×
305

306
        return true;
×
307
    }
308

309
    /**
310
     * Releases a previously acquired lock.
311
     */
312
    protected function releaseLock(): bool
313
    {
314
        if (isset($this->memcached, $this->lockKey) && $this->lock) {
×
315
            if (
316
                ! $this->memcached->delete($this->lockKey)
×
317
                && $this->memcached->getResultCode() !== Memcached::RES_NOTFOUND
×
318
            ) {
319
                $this->logger->error(
×
320
                    'Session: Error while trying to free lock for ' . $this->lockKey,
×
321
                );
×
322

323
                return false;
×
324
            }
325

326
            $this->lockKey = null;
×
327
            $this->lock    = false;
×
328
        }
329

330
        return true;
×
331
    }
332
}
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