1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\App;
6:
7: use Atk4\Ui\Exception;
8:
9: class SessionManager
10: {
11: /** @var string Session container key. */
12: protected $rootNamespace = '__atk_session';
13:
14: /** @var array<string, array<string, array<string, mixed>>>|null */
15: private static $readCache;
16:
17: protected function isSessionActive(): bool
18: {
19: $status = session_status();
20: if ($status === \PHP_SESSION_DISABLED) {
21: throw new Exception('Session support is disabled');
22: }
23:
24: return $status === \PHP_SESSION_ACTIVE;
25: }
26:
27: protected function createStartSessionOptions(): array
28: {
29: return [];
30: }
31:
32: protected function startSession(bool $readAndCloseImmediately): void
33: {
34: $options = $this->createStartSessionOptions();
35: if ($readAndCloseImmediately) {
36: $options['read_and_close'] = true;
37: }
38:
39: $res = session_start($options);
40:
41: if (!$res) {
42: throw new Exception('Failed to start a session');
43: }
44: }
45:
46: protected function closeSession(bool $writeBeforeClose): void
47: {
48: if ($writeBeforeClose) {
49: $res = session_write_close();
50: } else {
51: $res = session_abort();
52: }
53:
54: if (!$res) {
55: throw new Exception('Failed to close a session');
56: }
57: }
58:
59: /**
60: * @template T
61: *
62: * @param \Closure(): T $fx
63: *
64: * @return T
65: */
66: public function atomicSession(\Closure $fx, bool $readAndCloseImmediately = false)
67: {
68: $wasActive = $this->isSessionActive();
69:
70: if (!$wasActive) {
71: $this->startSession($readAndCloseImmediately);
72: }
73:
74: $e = null;
75: try {
76: self::$readCache = $_SESSION;
77: if (!isset(self::$readCache[$this->rootNamespace])) {
78: self::$readCache[$this->rootNamespace] = [];
79: }
80:
81: $res = $fx();
82:
83: if (!$readAndCloseImmediately) {
84: self::$readCache = $_SESSION;
85: }
86:
87: return $res;
88: } catch (\Throwable $e) {
89: throw $e;
90: } finally {
91: if (!$readAndCloseImmediately && $e !== null) {
92: self::$readCache = null;
93: }
94:
95: if (!$wasActive) {
96: if (!$readAndCloseImmediately) {
97: $this->closeSession($e === null);
98: }
99:
100: unset($_SESSION);
101: }
102: }
103: }
104:
105: /**
106: * @param bool $found
107: *
108: * @return mixed
109: */
110: protected function recallWithCache(string $namespace, string $key, &$found)
111: {
112: $found = false;
113:
114: if (self::$readCache === null) {
115: $this->atomicSession(static function (): void {}, true);
116: }
117:
118: if (isset(self::$readCache[$this->rootNamespace][$namespace])
119: && array_key_exists($key, self::$readCache[$this->rootNamespace][$namespace])) {
120: $res = self::$readCache[$this->rootNamespace][$namespace][$key];
121: $found = true;
122:
123: return $res;
124: }
125:
126: return null;
127: }
128:
129: /**
130: * Returns session data for this object. If not previously set, then
131: * $defaultValue is returned.
132: *
133: * @param mixed $defaultValue
134: *
135: * @return mixed Previously memorized data or $defaultValue
136: */
137: public function recall(string $namespace, string $key, $defaultValue = null)
138: {
139: $res = $this->recallWithCache($namespace, $key, $found);
140: if ($found) {
141: return $res;
142: }
143:
144: if ($defaultValue instanceof \Closure) {
145: $defaultValue = $defaultValue($key);
146: }
147:
148: return $defaultValue;
149: }
150:
151: /**
152: * Remember data in object-relevant session data.
153: *
154: * @param mixed $value
155: *
156: * @return mixed $value
157: */
158: public function memorize(string $namespace, string $key, $value)
159: {
160: return $this->atomicSession(function () use ($namespace, $key, $value) {
161: $_SESSION[$this->rootNamespace][$namespace][$key] = $value;
162:
163: return $value;
164: });
165: }
166:
167: /**
168: * Similar to memorize, but if value for key exist, will return it.
169: *
170: * @param mixed $defaultValue
171: *
172: * @return mixed Previously memorized data or $defaultValue
173: */
174: public function learn(string $namespace, string $key, $defaultValue = null)
175: {
176: $res = $this->recallWithCache($namespace, $key, $found);
177: if ($found) {
178: return $res;
179: }
180:
181: return $this->atomicSession(function () use ($namespace, $key, $defaultValue) {
182: $res = $this->recallWithCache($namespace, $key, $found);
183: if ($found) {
184: return $res;
185: }
186:
187: if ($defaultValue instanceof \Closure) {
188: $defaultValue = $defaultValue($key);
189: }
190:
191: return $this->memorize($namespace, $key, $defaultValue);
192: });
193: }
194:
195: /**
196: * Forget session data for $key. If $key is omitted will forget all
197: * associated session data.
198: */
199: public function forget(string $namespace, string $key = null): void
200: {
201: $this->atomicSession(function () use ($namespace, $key) {
202: if ($key === null) {
203: unset($_SESSION[$this->rootNamespace][$namespace]);
204: } else {
205: unset($_SESSION[$this->rootNamespace][$namespace][$key]);
206: }
207: });
208: }
209: }
210: