Completed
Push — master ( 10a28c...325b74 )
by Anton
13s
created

Session::start()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 0
dl 0
loc 12
ccs 5
cts 6
cp 0.8333
crap 3.0416
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Bluz Framework Component
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link      https://github.com/bluzphp/framework
7
 */
8
9
declare(strict_types=1);
10
11
namespace Bluz\Session;
12
13
use Bluz\Common\Exception\ComponentException;
14
use Bluz\Common\Options;
15
16
/**
17
 * Session
18
 *
19
 * @package  Bluz\Session
20
 * @author   Anton Shevchuk
21
 * @link     https://github.com/bluzphp/framework/wiki/Session
22
 */
23
class Session
24
{
25
    use Options;
26
27
    /**
28
     * @var string value returned by session_name()
29
     */
30
    protected $name;
31
32
    /**
33
     * @var string namespace
34
     */
35
    protected $namespace = 'bluz';
36
37
    /**
38
     * @var \SessionHandlerInterface Session save handler
39
     */
40
    protected $adapter;
41
42
    /**
43
     * Attempt to set the session name
44
     *
45
     * If the session has already been started, or if the name provided fails
46
     * validation, an exception will be raised.
47
     *
48
     * @param  string $name
49
     *
50
     * @throws SessionException
51
     * @return Session
52
     */
53
    public function setName($name)
54
    {
55
        if ($this->sessionExists()) {
56
            throw new SessionException(
57
                'Cannot set session name after a session has already started'
58
            );
59
        }
60
61
        if (!preg_match('/^[a-zA-Z0-9]+$/', $name)) {
62
            throw new SessionException(
63
                'Name provided contains invalid characters; must be alphanumeric only'
64
            );
65
        }
66
67
        $this->name = $name;
68
        session_name($name);
69
        return $this;
70
    }
71
72
    /**
73
     * Get session name
74
     *
75
     * Proxies to {@link session_name()}.
76
     *
77
     * @return string
78
     */
79
    public function getName()
80
    {
81
        if (null === $this->name) {
82
            // If we're grabbing via session_name(), we don't need our
83
            // validation routine; additionally, calling setName() after
84
            // session_start() can lead to issues, and often we just need the name
85
            // in order to do things such as setting cookies.
86
            $this->name = session_name();
87
        }
88
        return $this->name;
89
    }
90
91
    /**
92
     * Set Namespace
93
     *
94
     * @param  string $namespace
95
     *
96
     * @return Session
97
     */
98
    public function setNamespace($namespace)
99
    {
100
        $this->namespace = $namespace;
101
        return $this;
102
    }
103
104
    /**
105
     * Get Namespace
106
     *
107
     * @return string
108
     */
109 6
    public function getNamespace()
110
    {
111 6
        return $this->namespace;
112
    }
113
114
    /**
115
     * Set session ID
116
     *
117
     * Can safely be called in the middle of a session.
118
     *
119
     * @param  string $id
120
     *
121
     * @return Session
122
     * @throws SessionException
123
     */
124
    public function setId($id)
125
    {
126
        if ($this->sessionExists()) {
127
            throw new SessionException(
128
                'Session has already been started, to change the session ID call regenerateId()'
129
            );
130
        }
131
        session_id($id);
132
        return $this;
133
    }
134
135
    /**
136
     * Get session ID
137
     *
138
     * Proxies to {@link session_id()}
139
     *
140
     * @return string
141
     */
142
    public function getId()
143
    {
144
        return session_id();
145
    }
146
147
    /**
148
     * Regenerate id
149
     *
150
     * Regenerate the session ID, using session save handler's
151
     * native ID generation Can safely be called in the middle of a session.
152
     *
153
     * @param  bool $deleteOldSession
154
     *
155
     * @return bool
156
     */
157
    public function regenerateId($deleteOldSession = true)
158
    {
159
        if ($this->sessionExists()) {
160
            if (session_id() !== '') {
161
                return session_regenerate_id((bool)$deleteOldSession);
162
            }
163
        }
164
        return false;
165
    }
166
167
    /**
168
     * Returns true if session ID is set
169
     *
170
     * @return bool
171
     */
172 11
    public function cookieExists()
0 ignored issues
show
Coding Style introduced by
cookieExists uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
173
    {
174 11
        return isset($_COOKIE[session_name()]);
175
    }
176
177
    /**
178
     * Does a session started and is it currently active?
179
     *
180
     * @return bool
181
     */
182 13
    public function sessionExists()
183
    {
184 13
        return session_status() === PHP_SESSION_ACTIVE;
185
    }
186
187
    /**
188
     * Start session
189
     *
190
     * if No session currently exists, attempt to start it. Calls
191
     * {@link isValid()} once session_start() is called, and raises an
192
     * exception if validation fails.
193
     *
194
     * @return bool
195
     * @throws \Bluz\Common\Exception\ComponentException
196
     * @throws SessionException
197
     */
198 13
    public function start()
199
    {
200 13
        if ($this->sessionExists()) {
201 13
            return true;
202
        }
203
204 1
        if ($this->initAdapter()) {
205 1
            return session_start();
206
        }
207
208
        throw new ComponentException('Invalid adapter settings');
209
    }
210
211
    /**
212
     * Destroy/end a session
213
     *
214
     * @return void
215
     */
216
    public function destroy()
0 ignored issues
show
Coding Style introduced by
destroy uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
217
    {
218
        if (!$this->cookieExists() || !$this->sessionExists()) {
219
            return;
220
        }
221
222
        session_destroy();
223
224
        // send expire cookies
225
        $this->expireSessionCookie();
226
227
        // clear session data
228
        unset($_SESSION[$this->getNamespace()]);
229
    }
230
231
    /**
232
     * Set session save handler object
233
     *
234
     * @param  \SessionHandlerInterface $saveHandler
235
     *
236
     * @return Session
237
     */
238 578
    public function setAdapter($saveHandler)
239
    {
240 578
        $this->adapter = $saveHandler;
241 578
        return $this;
242
    }
243
244
    /**
245
     * Get SaveHandler Object
246
     *
247
     * @return \SessionHandlerInterface
248
     */
249
    public function getAdapter()
250
    {
251
        return $this->adapter;
252
    }
253
254
    /**
255
     * Register Save Handler with ext/session
256
     *
257
     * Since ext/session is coupled to this particular session manager
258
     * register the save handler with ext/session.
259
     *
260
     * @return bool
261
     * @throws ComponentException
262
     */
263 1
    protected function initAdapter()
264
    {
265 1
        if (null === $this->adapter || 'files' === $this->adapter) {
266
            // try to apply settings
267 1
            if ($settings = $this->getOption('settings', 'files')) {
268 1
                $this->setSavePath($settings['save_path']);
269
            }
270 1
            return true;
271
        }
272
        if (is_string($this->adapter)) {
273
            $adapterClass = '\\Bluz\\Session\\Adapter\\' . ucfirst($this->adapter);
274
            if (!class_exists($adapterClass) || !is_subclass_of($adapterClass, '\SessionHandlerInterface')) {
275
                throw new ComponentException("Class for session adapter `{$this->adapter}` not found");
276
            }
277
            $settings = $this->getOption('settings', $this->adapter) ?: [];
278
279
            $this->adapter = new $adapterClass($settings);
280
            return session_set_save_handler($this->adapter);
281
        }
282
        return true;
283
    }
284
285
    /**
286
     * Set the session cookie lifetime
287
     *
288
     * If a session already exists, destroys it (without sending an expiration
289
     * cookie), regenerates the session ID, and restarts the session.
290
     *
291
     * @param  integer $ttl TTL in seconds
292
     *
293
     * @return void
294
     */
295
    public function setSessionCookieLifetime($ttl)
296
    {
297
        // Set new cookie TTL
298
        session_set_cookie_params($ttl);
299
300
        if ($this->sessionExists()) {
301
            // There is a running session so we'll regenerate id to send a new cookie
302
            $this->regenerateId();
303
        }
304
    }
305
306
    /**
307
     * Expire the session cookie
308
     *
309
     * Sends a session cookie with no value, and with an expiry in the past.
310
     *
311
     * @return void
312
     */
313
    public function expireSessionCookie()
0 ignored issues
show
Coding Style introduced by
expireSessionCookie uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
314
    {
315
        if (ini_get('session.use_cookies')) {
316
            $params = session_get_cookie_params();
317
            setcookie(
318
                $this->getName(),
319
                '',
320
                $_SERVER['REQUEST_TIME'] - 42000,
321
                $params['path'],
322
                $params['domain'],
323
                $params['secure'],
324
                $params['httponly']
325
            );
326
        }
327
    }
328
329
    /**
330
     * Set session save path
331
     *
332
     * @param  string $savePath
333
     *
334
     * @return Session
335
     * @throws ComponentException
336
     */
337 1
    protected function setSavePath($savePath)
338
    {
339 1
        if (!is_dir($savePath)
340 1
            || !is_writable($savePath)
341
        ) {
342
            throw new ComponentException('Session path is not writable');
343
        }
344 1
        session_save_path($savePath);
345 1
        return $this;
346
    }
347
348
    /**
349
     * Set key/value pair
350
     *
351
     * @param  string $key
352
     * @param  mixed  $value
353
     *
354
     * @return void
355
     */
356 7
    public function set($key, $value)
0 ignored issues
show
Coding Style introduced by
set uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
357
    {
358 7
        $this->start();
359
        // check storage
360 7
        if (!isset($_SESSION[$this->getNamespace()])) {
361 7
            $_SESSION[$this->getNamespace()] = [];
362
        }
363 7
        $_SESSION[$this->namespace][$key] = $value;
364 7
    }
365
366
    /**
367
     * Get value by key
368
     *
369
     * @param  string $key
370
     *
371
     * @return mixed
372
     */
373 12
    public function get($key)
0 ignored issues
show
Coding Style introduced by
get uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
374
    {
375 12
        if ($this->contains($key)) {
376 5
            return $_SESSION[$this->namespace][$key];
377
        }
378 12
        return null;
379
    }
380
381
    /**
382
     * Isset
383
     *
384
     * @param  string $key
385
     *
386
     * @return bool
387
     */
388 12
    public function contains($key)
0 ignored issues
show
Coding Style introduced by
contains uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
389
    {
390 12
        if ($this->cookieExists()) {
391 12
            $this->start();
392
        } elseif (!$this->sessionExists()) {
393
            return false;
394
        }
395 12
        return isset($_SESSION[$this->namespace][$key]);
396
    }
397
398
    /**
399
     * Unset
400
     *
401
     * @param  string $key
402
     *
403
     * @return void
404
     */
405 1
    public function delete($key)
0 ignored issues
show
Coding Style introduced by
delete uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
406
    {
407 1
        if ($this->contains($key)) {
408 1
            unset($_SESSION[$this->namespace][$key]);
409
        }
410 1
    }
411
}
412