Completed
Branch 2.x (133470)
by Julián
06:39
created

Native   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 4
dl 0
loc 362
rs 8.3396
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 3
A getConfiguration() 0 4 1
B verifyIniSettings() 0 22 6
A configureIniSettings() 0 8 1
A getNewSessionId() 0 8 1
A getId() 0 4 2
A setId() 0 8 2
B start() 0 29 5
A configure() 0 14 2
A initialize() 0 20 4
A loadData() 0 14 3
A shouldRegenerateId() 0 4 2
A regenerateId() 0 22 3
B close() 0 25 4
A destroy() 0 19 3
A isStarted() 0 4 1
A isDestroyed() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Native often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Native, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * sessionware (https://github.com/juliangut/sessionware).
5
 * PSR7 compatible session management.
6
 *
7
 * @license BSD-3-Clause
8
 * @link https://github.com/juliangut/sessionware
9
 * @author Julián Gutiérrez <[email protected]>
10
 */
11
12
declare(strict_types=1);
13
14
namespace Jgut\Sessionware\Manager;
15
16
use Jgut\Sessionware\Configuration;
17
use Jgut\Sessionware\Handler\Handler;
18
use Jgut\Sessionware\Handler\Native as NativeHandler;
19
use Jgut\Sessionware\Traits\NativeSessionTrait;
20
21
/**
22
 * Native PHP session manager.
23
 */
24
class Native implements Manager
25
{
26
    use NativeSessionTrait;
27
28
    /**
29
     * @var Configuration
30
     */
31
    protected $configuration;
32
33
    /**
34
     * Session handler.
35
     *
36
     * @var Handler
37
     */
38
    protected $sessionHandler;
39
40
    /**
41
     * @var string
42
     */
43
    protected $sessionId;
44
45
    /**
46
     * @var bool
47
     */
48
    protected $sessionStarted = false;
49
50
    /**
51
     * Session has been destroyed.
52
     *
53
     * @var bool
54
     */
55
    protected $sessionDestroyed = false;
56
57
    /**
58
     * Session manager constructor.
59
     *
60
     * @param Configuration $configuration
61
     * @param Handler|null  $sessionHandler
62
     *
63
     * @throws \RuntimeException
64
     */
65
    public function __construct(Configuration $configuration, Handler $sessionHandler = null)
66
    {
67
        if (session_status() === PHP_SESSION_DISABLED) {
68
            // @codeCoverageIgnoreStart
69
            throw new \RuntimeException('PHP sessions are disabled');
70
            // @codeCoverageIgnoreEnd
71
        }
72
73
        $this->configuration = $configuration;
74
75
        if ($sessionHandler === null) {
76
            $sessionHandler = new NativeHandler();
77
        }
78
        $sessionHandler->setConfiguration($configuration);
79
80
        $this->sessionHandler = $sessionHandler;
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function getConfiguration() : Configuration
87
    {
88
        return $this->configuration;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function getId() : string
95
    {
96
        return !empty($this->sessionId) ? $this->sessionId : '';
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     *
102
     * @throws \RuntimeException
103
     */
104
    public function setId(string $sessionId)
105
    {
106
        if ($this->sessionStarted) {
107
            throw new \RuntimeException('Session identifier cannot be manually altered once session is started');
108
        }
109
110
        $this->sessionId = trim($sessionId);
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     *
116
     * @throws \RuntimeException
117
     *
118
     * @return array
119
     */
120
    public function start() : array
121
    {
122
        $this->verifyIniSettings();
123
124
        if ($this->sessionDestroyed) {
125
            throw new \RuntimeException('Cannot start a session that has been previously destroyed');
126
        }
127
128
        if ($this->sessionStarted || session_status() === PHP_SESSION_ACTIVE) {
129
            throw new \RuntimeException('Session has already been started. Check "session.auto_start" ini setting');
130
        }
131
132
        if (headers_sent($file, $line)) {
133
            throw new \RuntimeException(
134
                sprintf(
135
                    'PHP session failed to start because headers have already been sent by "%s" at line %d.',
136
                    $file,
137
                    $line
138
                )
139
            );
140
        }
141
142
        $this->configure();
143
        $this->initialize();
144
145
        $this->sessionStarted = true;
146
147
        return $this->loadData();
148
    }
149
150
    /**
151
     * Configure session settings.
152
     */
153
    protected function configure()
154
    {
155
        // Use better session serializer when available
156
        if ($this->getStringIniSetting('serialize_handler') !== 'php_serialize') {
157
            // @codeCoverageIgnoreStart
158
            $this->setIniSetting('serialize_handler', 'php_serialize');
159
            // @codeCoverageIgnoreEnd
160
        }
161
162
        $this->setIniSetting('gc_maxlifetime', (string) $this->configuration->getLifetime());
163
164
        session_set_save_handler($this->sessionHandler, false);
165
        session_module_name('user');
166
    }
167
168
    /**
169
     * Initialize session.
170
     *
171
     * @throws \RuntimeException
172
     */
173
    final protected function initialize()
174
    {
175
        if (!empty($this->sessionId)) {
176
            session_id($this->sessionId);
177
        }
178
179
        session_name($this->configuration->getName());
180
181
        session_start();
182
183
        if (session_status() !== PHP_SESSION_ACTIVE) {
184
            // @codeCoverageIgnoreStart
185
            throw new \RuntimeException('PHP session failed to start');
186
            // @codeCoverageIgnoreEnd
187
        }
188
189
        if (empty($this->sessionId)) {
190
            $this->sessionId = session_id();
191
        }
192
    }
193
194
    /**
195
     * Retrieve session saved data.
196
     *
197
     * @return array
198
     *
199
     * @SuppressWarnings(PHPMD.Superglobals)
200
     */
201
    final protected function loadData()
202
    {
203
        $keyPattern = '/^' . $this->configuration->getName() . '\./';
204
        $data = [];
205
        foreach ($_SESSION as $key => $value) {
206
            if (preg_match($keyPattern, $key)) {
207
                $data[preg_replace($keyPattern, '', $key)] = $value;
208
            }
209
        }
210
211
        $_SESSION = null;
212
213
        return $data;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function shouldRegenerateId() : bool
220
    {
221
        return !empty($this->sessionId) && strlen($this->sessionId) !== Configuration::SESSION_ID_LENGTH;
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     *
227
     * @throws \RuntimeException
228
     *
229
     * @SuppressWarnings(PMD.Superglobals)
230
     */
231
    public function regenerateId()
232
    {
233
        if (!$this->sessionStarted) {
234
            throw new \RuntimeException('Cannot regenerate id a not started session');
235
        }
236
237
        $_SESSION = null;
238
        session_unset();
239
        session_destroy();
240
241
        $this->sessionStarted = false;
242
243
        if (session_status() === PHP_SESSION_ACTIVE) {
244
            // @codeCoverageIgnoreStart
245
            throw new \RuntimeException('PHP session failed to regenerate id');
246
            // @codeCoverageIgnoreEnd
247
        }
248
249
        $this->sessionId = $this->getNewSessionId(Configuration::SESSION_ID_LENGTH);
250
251
        $this->start();
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     *
257
     * @throws \RuntimeException
258
     *
259
     * @SuppressWarnings(PMD.Superglobals)
260
     */
261
    public function close(array $data = [])
262
    {
263
        if (!$this->sessionStarted) {
264
            throw new \RuntimeException('Cannot end a not started session');
265
        }
266
267
        $keyPrefix = $this->configuration->getName();
268
        $sessionData = [];
269
        foreach ($data as $key => $value) {
270
            $sessionData[$keyPrefix . '.' . $key] = $value;
271
        }
272
        $_SESSION = $sessionData;
273
274
        session_write_close();
275
276
        $_SESSION = null;
277
278
        $this->sessionStarted = false;
279
280
        if (session_status() === PHP_SESSION_ACTIVE) {
281
            // @codeCoverageIgnoreStart
282
            throw new \RuntimeException('PHP session failed to finish');
283
            // @codeCoverageIgnoreEnd
284
        }
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     *
290
     * @throws \RuntimeException
291
     *
292
     * @SuppressWarnings(PMD.Superglobals)
293
     */
294
    public function destroy()
295
    {
296
        if (!$this->sessionStarted) {
297
            throw new \RuntimeException('Cannot destroy a not started session');
298
        }
299
300
        unset($_SESSION);
301
        session_unset();
302
        session_destroy();
303
304
        $this->sessionStarted = false;
305
        $this->sessionDestroyed = true;
306
307
        if (session_status() === PHP_SESSION_ACTIVE) {
308
            // @codeCoverageIgnoreStart
309
            throw new \RuntimeException('PHP session failed to finish');
310
            // @codeCoverageIgnoreEnd
311
        }
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317
    public function isStarted() : bool
318
    {
319
        return $this->sessionStarted;
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325
    public function isDestroyed() : bool
326
    {
327
        return $this->sessionDestroyed;
328
    }
329
330
    /**
331
     * Verify session ini settings.
332
     *
333
     * @throws \RuntimeException
334
     */
335
    final protected function verifyIniSettings()
336
    {
337
        if ($this->hasBoolIniSetting('use_trans_sid') !== false) {
338
            throw new \RuntimeException('"session.use_trans_sid" ini setting must be set to false');
339
        }
340
341
        if ($this->hasBoolIniSetting('use_cookies') !== true) {
342
            throw new \RuntimeException('"session.use_cookies" ini setting must be set to true');
343
        }
344
345
        if ($this->hasBoolIniSetting('use_only_cookies') !== true) {
346
            throw new \RuntimeException('"session.use_only_cookies" ini setting must be set to true');
347
        }
348
349
        if ($this->hasBoolIniSetting('use_strict_mode') !== false) {
350
            throw new \RuntimeException('"session.use_strict_mode" ini setting must be set to false');
351
        }
352
353
        if ($this->getStringIniSetting('cache_limiter') !== '') {
354
            throw new \RuntimeException('"session.cache_limiter" ini setting must be set to empty string');
355
        }
356
    }
357
358
    /**
359
     * Ini settings configuration helper.
360
     */
361
    final public function configureIniSettings()
362
    {
363
        $this->setIniSetting('use_trans_sid', '0');
364
        $this->setIniSetting('use_cookies', '1');
365
        $this->setIniSetting('use_only_cookies', '1');
366
        $this->setIniSetting('use_strict_mode', '0');
367
        $this->setIniSetting('cache_limiter', '');
368
    }
369
370
    /**
371
     * Generates cryptographically secure session identifier.
372
     *
373
     * @param int $length
374
     *
375
     * @return string
376
     */
377
    private function getNewSessionId(int $length) : string
378
    {
379
        return substr(
380
            preg_replace('/[^a-zA-Z0-9-]+/', '', base64_encode(random_bytes($length))),
381
            0,
382
            $length
383
        );
384
    }
385
}
386