Lines 70.58% 24 / 34
Methods 87.50% 7 / 8
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 fromRequest 100.00% 2 / 2 100.00% 1 / 1 2
 set 100.00% 1 / 1 100.00% 1 / 1 1
 get 100.00% 1 / 1 100.00% 1 / 1 2
 getTyped 100.00% 2 / 2 100.00% 1 / 1 2
 requireTyped 100.00% 10 / 10 100.00% 1 / 1 3
 matchesType 37.50% 6 / 16 0.00% 0 / 1 78.50
 has 100.00% 1 / 1 100.00% 1 / 1 1
 all 100.00% 1 / 1 100.00% 1 / 1 1
23class RequestContext
24{
25    /** @var array<string, mixed> */
26    private array $data = [];
27
28    /**
29     * Retrieve the context bag attached to a request, if any.
30     *
31     * Works with any {@see ServerRequestInterface}: Lucent's ServerRequest
32     * carries a RequestContext as its 'context' attribute, and other
33     * implementations may do the same. Returns null when the request has no
34     * context bag attached.
35     *
36     * @param ServerRequestInterface $request The request to inspect
37     * @return self|null The attached context bag, or null if none
38     */
39    public static function fromRequest(ServerRequestInterface $request): ?self
40    {
41        $context = $request->getAttribute('context');
42
43        return $context instanceof self ? $context : null;
44    }
45
46    /**
47     * Store a value in the context.
48     *
49     * @param string $key The context key
50     * @param mixed $value The value to store
51     * @return void
52     */
53    public function set(string $key, mixed $value): void
54    {
55        $this->data[$key] = $value;
56    }
57
58    /**
59     * Read a value from the context.
60     *
61     * @param string $key The context key
62     * @param mixed $default Default value if the key is not set
63     * @return mixed
64     */
65    public function get(string $key, mixed $default = null): mixed
66    {
67        return array_key_exists($key, $this->data) ? $this->data[$key] : $default;
68    }
69
70    /**
71     * Read a value from the context, guaranteed to match a given type.
72     *
73     * Returns the stored value when it matches the type, otherwise returns
74     * the default. This is the type-safe counterpart to {@see get()}: it lets
75     * middleware and rules stash objects (a User, a Session, a request id) or
76     * scalars and read them back without a manual instanceof / is_* check at
77     * every call site.
78     *
79     * The type may be a class or interface name (checked with instanceof) or
80     * a builtin type name: string, int, float, bool, array, object, callable,
81     * iterable, numeric, scalar, resource, or null.
82     *
83     * @template T
84     * @param string $key The context key
85     * @param class-string<T>|string $type The expected type of the stored
86     *                                     value (class name or builtin type)
87     * @param T|null $default Default value if the key is not set or holds a
88     *                        value that does not match $type
89     * @return ($default is null ? T|null : T) The stored value when it matches
90     *         $type, otherwise $default. When $default is non-null the return is always T.
91     */
92    public function getTyped(string $key, string $type, mixed $default = null): mixed
93    {
94        $value = $this->data[$key] ?? null;
95
96        return $this->matchesType($value, $type) ? $value : $default;
97    }
98
99    /**
100     * Read a value from the context, guaranteed to match a given type, or
101     * throw if it is missing or of the wrong type.
102     *
103     * This is the fail-fast counterpart to {@see getTyped()}: it is intended
104     * for values the code genuinely cannot proceed without (an authenticated
105     * user, a session, a request id). Instead of returning a default and
106     * forcing the caller to null-check, it throws a descriptive
107     * {@see \RuntimeException} so the failure surfaces at the point of use
108     * with a clear message rather than as a confusing "call to member
109     * function on null" deeper in the stack.
110     *
111     * The type may be a class or interface name (checked with instanceof) or
112     * a builtin type name: string, int, float, bool, array, object, callable,
113     * iterable, numeric, scalar, resource, or null.
114     *
115     * @template T
116     * @param string $key The context key
117     * @param class-string<T>|string $type The expected type of the stored
118     *                                     value (class name or builtin type)
119     * @return T The stored value, guaranteed to match $type
120     * @throws \RuntimeException When the key is not set or holds a value that
121     *         does not match $type
122     */
123    public function requireTyped(string $key, string $type): mixed
124    {
125        $value = $this->getTyped($key, $type);
126
127        if ($value === null && !$this->matchesType(null, $type)) {
128            throw new \RuntimeException(
129                sprintf(
130                    'Context key "%s" is missing or does not hold a value of type "%s".',
131                    $key,
132                    $type
133                )
134            );
135        }
136
137        return $value;
138    }
139
140    /**
141     * Determine whether a value matches a class name or builtin type.
142     *
143     * @param mixed $value The value to check
144     * @param string $type A class/interface name or a builtin type name
145     * @return bool
146     */
147    private function matchesType(mixed $value, string $type): bool
148    {
149        if (class_exists($type) || interface_exists($type)) {
150            return $value instanceof $type;
151        }
152
153        return match ($type) {
154            'string' => is_string($value),
155            'int', 'integer' => is_int($value),
156            'float', 'double' => is_float($value),
157            'bool', 'boolean' => is_bool($value),
158            'array' => is_array($value),
159            'object' => is_object($value),
160            'callable' => is_callable($value),
161            'iterable' => is_iterable($value),
162            'numeric' => is_numeric($value),
163            'scalar' => is_scalar($value),
164            'resource' => is_resource($value),
165            'null' => $value === null,
166            default => false,
167        };
168    }
169
170    /**
171     * Determine whether a key is set in the context.
172     *
173     * @param string $key The context key
174     * @return bool
175     */
176    public function has(string $key): bool
177    {
178        return array_key_exists($key, $this->data);
179    }
180
181    /**
182     * Get all context values.
183     *
184     * @return array<string, mixed>
185     */
186    public function all(): array
187    {
188        return $this->data;
189    }
190}