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

aplus-framework / session / 14274562555

04 Apr 2025 09:29PM UTC coverage: 93.732% (+0.04%) from 93.695%
14274562555

push

github

natanfelles
Do not remove the handler object if it is created externally

15 of 18 new or added lines in 3 files covered. (83.33%)

972 of 1037 relevant lines covered (93.73%)

39.68 hits per line

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

86.72
/src/SaveHandlers/RedisHandler.php
1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of Aplus Framework Session Library.
4
 *
5
 * (c) Natan Felles <natanfelles@gmail.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Framework\Session\SaveHandlers;
11

12
use Framework\Log\LogLevel;
13
use Framework\Session\SaveHandler;
14
use Redis;
15
use RedisException;
16
use SensitiveParameter;
17

18
/**
19
 * Class RedisHandler.
20
 *
21
 * @package session
22
 */
23
class RedisHandler extends SaveHandler
24
{
25
    protected ?Redis $redis;
26

27
    /**
28
     * Prepare configurations to be used by the RedisHandler.
29
     *
30
     * @param array<string,mixed> $config Custom configs
31
     *
32
     * The custom configs are:
33
     *
34
     * ```php
35
     * $configs = [
36
     *     // A custom prefix prepended in the keys
37
     *     'prefix' => '',
38
     *     // The Redis host
39
     *     'host' => '127.0.0.1',
40
     *     // The Redis host port
41
     *     'port' => 6379,
42
     *     // The connection timeout
43
     *     'timeout' => 0.0,
44
     *     // Optional auth password
45
     *     'password' => null,
46
     *     // Optional database to select
47
     *     'database' => null,
48
     *     // Maximum attempts to try lock a session id
49
     *     'lock_attempts' => 60,
50
     *     // Interval between the lock attempts in microseconds
51
     *     'lock_sleep' => 1_000_000,
52
     *     // TTL to the lock (valid for the current session only)
53
     *     'lock_ttl' => 600,
54
     *     // The maxlifetime (TTL) used for cache item expiration
55
     *     'maxlifetime' => null, // Null to use the ini value of session.gc_maxlifetime
56
     *     // Match IP?
57
     *     'match_ip' => false,
58
     *     // Match User-Agent?
59
     *     'match_ua' => false,
60
     * ];
61
     * ```
62
     */
63
    protected function prepareConfig(#[SensitiveParameter] array $config) : void
64
    {
65
        $this->config = \array_replace([
62✔
66
            'prefix' => '',
62✔
67
            'host' => '127.0.0.1',
62✔
68
            'port' => 6379,
62✔
69
            'timeout' => 0.0,
62✔
70
            'password' => null,
62✔
71
            'database' => null,
62✔
72
            'lock_attempts' => 60,
62✔
73
            'lock_sleep' => 1_000_000,
62✔
74
            'lock_ttl' => 600,
62✔
75
            'maxlifetime' => null,
62✔
76
            'match_ip' => false,
62✔
77
            'match_ua' => false,
62✔
78
        ], $config);
62✔
79
    }
80

81
    public function setRedis(Redis $redis) : static
82
    {
83
        $this->setByExternal = true;
2✔
84
        $this->redis = $redis;
2✔
85
        return $this;
2✔
86
    }
87

88
    public function getRedis() : ?Redis
89
    {
90
        return $this->redis ?? null;
3✔
91
    }
92

93
    /**
94
     * Get a key for Redis, using the optional
95
     * prefix, match IP and match User-Agent configs.
96
     *
97
     * @param string $id The session id
98
     *
99
     * @return string The final key
100
     */
101
    protected function getKey(string $id) : string
102
    {
103
        return $this->config['prefix'] . $id . $this->getKeySuffix();
63✔
104
    }
105

106
    public function open($path, $name) : bool
107
    {
108
        if (isset($this->redis)) {
63✔
109
            return true;
6✔
110
        }
111
        $this->redis = new Redis();
63✔
112
        try {
113
            @$this->redis->connect(
63✔
114
                $this->config['host'],
63✔
115
                $this->config['port'],
63✔
116
                $this->config['timeout']
63✔
117
            );
63✔
118
        } catch (RedisException) {
2✔
119
            $this->log(
2✔
120
                'Session (redis): Could not connect to server '
2✔
121
                . $this->config['host'] . ':' . $this->config['port']
2✔
122
            );
2✔
123
            return false;
2✔
124
        }
125
        if (isset($this->config['password'])) {
63✔
126
            try {
127
                $this->redis->auth($this->config['password']);
2✔
128
            } catch (RedisException) {
2✔
129
                $this->log('Session (redis): Authentication failed');
2✔
130
                return false;
2✔
131
            }
132
        }
133
        if (isset($this->config['database'])
63✔
134
            && !$this->redis->select($this->config['database'])
63✔
135
        ) {
136
            $this->log(
2✔
137
                "Session (redis): Could not select the database '{$this->config['database']}'"
2✔
138
            );
2✔
139
            return false;
2✔
140
        }
141
        return true;
63✔
142
    }
143

144
    public function read($id) : string
145
    {
146
        if (!isset($this->redis) || !$this->lock($id)) {
63✔
147
            return '';
2✔
148
        }
149
        if (!isset($this->sessionId)) {
63✔
150
            $this->sessionId = $id;
63✔
151
        }
152
        $data = $this->redis->get($this->getKey($id));
63✔
153
        \is_string($data) ? $this->sessionExists = true : $data = '';
63✔
154
        $this->setFingerprint($data);
63✔
155
        return $data;
63✔
156
    }
157

158
    public function write($id, $data) : bool
159
    {
160
        if (!isset($this->redis)) {
24✔
161
            return false;
2✔
162
        }
163
        if ($id !== $this->sessionId) {
24✔
164
            if (!$this->unlock() || !$this->lock($id)) {
4✔
165
                return false;
2✔
166
            }
167
            $this->sessionExists = false;
2✔
168
            $this->sessionId = $id;
2✔
169
        }
170
        if ($this->lockId === false) {
22✔
171
            return false;
×
172
        }
173
        $maxlifetime = $this->getMaxlifetime();
22✔
174
        $this->redis->expire($this->lockId, $this->config['lock_ttl']);
22✔
175
        if ($this->sessionExists === false || !$this->hasSameFingerprint($data)) {
22✔
176
            if ($this->redis->set($this->getKey($id), $data, $maxlifetime)) {
22✔
177
                $this->setFingerprint($data);
22✔
178
                $this->sessionExists = true;
22✔
179
                return true;
22✔
180
            }
181
            return false;
×
182
        }
183
        return $this->redis->expire($this->getKey($id), $maxlifetime);
×
184
    }
185

186
    public function updateTimestamp($id, $data) : bool
187
    {
188
        return $this->redis->setex($this->getKey($id), $this->getMaxlifetime(), $data);
6✔
189
    }
190

191
    public function close() : bool
192
    {
193
        if (!isset($this->redis)) {
62✔
194
            return true;
2✔
195
        }
196
        if ($this->setByExternal === false) {
62✔
197
            try {
198
                if ($this->redis->ping()) {
62✔
199
                    if ($this->lockId) {
62✔
200
                        $this->redis->del($this->lockId);
62✔
201
                    }
202
                    if (!$this->redis->close()) {
62✔
203
                        return false;
62✔
204
                    }
205
                }
NEW
206
            } catch (RedisException $e) {
×
NEW
207
                $this->log('Session (redis): Got RedisException on close: ' . $e->getMessage());
×
208
            }
209
            $this->redis = null;
62✔
210
        }
211
        return true;
62✔
212
    }
213

214
    public function destroy($id) : bool
215
    {
216
        if (!$this->lockId) {
58✔
217
            return false;
2✔
218
        }
219
        $result = $this->redis->del($this->getKey($id));
58✔
220
        if ($result !== 1) {
58✔
221
            $this->log(
48✔
222
                'Session (redis): Expected to delete 1 key, deleted ' . $result,
48✔
223
                LogLevel::DEBUG
48✔
224
            );
48✔
225
        }
226
        return true;
58✔
227
    }
228

229
    public function gc($max_lifetime) : false | int
230
    {
231
        return 0;
2✔
232
    }
233

234
    protected function lock(string $id) : bool
235
    {
236
        $ttl = $this->config['lock_ttl'];
63✔
237
        if ($this->lockId && $this->redis->get($this->lockId)) {
63✔
238
            return $this->redis->expire($this->lockId, $ttl);
4✔
239
        }
240
        $lockId = $this->getKey($id) . ':lock';
63✔
241
        $attempt = 0;
63✔
242
        while ($attempt < $this->config['lock_attempts']) {
63✔
243
            $attempt++;
63✔
244
            $oldTtl = $this->redis->ttl($lockId);
63✔
245
            if (\is_int($oldTtl) && $oldTtl > 0) {
63✔
246
                \usleep($this->config['lock_sleep']);
×
247
                continue;
×
248
            }
249
            if (!$this->redis->setex($lockId, $ttl, (string) \time())) {
63✔
250
                $this->log('Session (redis): Error while trying to lock ' . $lockId);
×
251
                return false;
×
252
            }
253
            $this->lockId = $lockId;
63✔
254
            break;
63✔
255
        }
256
        if ($attempt === $this->config['lock_attempts']) {
63✔
257
            $this->log(
×
258
                "Session (redis): Unable to lock {$lockId} after {$attempt} attempts"
×
259
            );
×
260
            return false;
×
261
        }
262
        if (isset($oldTtl) && $oldTtl === -1) {
63✔
263
            $this->log(
×
264
                'Session (redis): Lock for ' . $this->getKey($id) . ' had not TTL',
×
265
                LogLevel::DEBUG
×
266
            );
×
267
        }
268
        return true;
63✔
269
    }
270

271
    protected function unlock() : bool
272
    {
273
        if ($this->lockId === false) {
4✔
274
            return true;
2✔
275
        }
276
        if (!$this->redis->del($this->lockId)) {
4✔
277
            $this->log('Session (redis): Error while trying to unlock ' . $this->lockId);
2✔
278
            return false;
2✔
279
        }
280
        $this->lockId = false;
2✔
281
        return true;
2✔
282
    }
283
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc