PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.8.1
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.8.1
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / vendor / wpfluent / framework / src / WPFluent / Foundation / Exceptions / ExceptionHandler.php

ExceptionHandler.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.8.1, at vendor/wpfluent/framework/src/WPFluent/Foundation/Exceptions/ExceptionHandler.php

238 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\Framework\Foundation\Exceptions;
4
5 use Throwable;
6 use WP_REST_Response;
7 use InvalidArgumentException;
8
9 /**
10 * Pluggable exception renderer registry.
11 *
12 * Plugins extend this class, override `register()`, and map third-party /
13 * domain exceptions to `HttpException` instances (or directly to a
14 * `WP_REST_Response`). `Route::callback()` consults the bound handler
15 * AFTER its own `HttpException` catch and BEFORE the production sanitizer.
16 * Unmapped exceptions still fall through to `handleUnknownException` and
17 * get their messages scrubbed — the registry is opt-in for "I've
18 * authored a user-safe response for this exception class".
19 *
20 * Plugin example:
21 *
22 * namespace Acme\App\Hooks\Handlers;
23 *
24 * use FluentCommunity\Framework\Foundation\Exceptions\ExceptionHandler as BaseHandler;
25 * use FluentCommunity\Framework\Foundation\Exceptions\ServiceUnavailableHttpException;
26 * use FluentCommunity\Framework\Foundation\Exceptions\TooManyRequestsHttpException;
27 *
28 * class ExceptionHandler extends BaseHandler
29 * {
30 * public function register()
31 * {
32 * $this->renderable(\PDOException::class, function ($e, $app) {
33 * return new ServiceUnavailableHttpException(
34 * 'Database temporarily unavailable.',
35 * 'db_unavailable'
36 * );
37 * });
38 *
39 * $this->renderable(\Acme\Domain\RateLimitException::class,
40 * function ($e, $app) {
41 * return new TooManyRequestsHttpException(
42 * $e->getMessage(),
43 * 'rate_limited',
44 * [],
45 * ['Retry-After' => (string) $e->retryAfter()]
46 * );
47 * });
48 * }
49 * }
50 *
51 * Plugins bind their subclass over the default during boot in
52 * `boot/bindings.php`:
53 *
54 * $app->singleton(
55 * \FluentCommunity\Framework\Foundation\Exceptions\ExceptionHandler::class,
56 * \Acme\App\Hooks\Handlers\ExceptionHandler::class
57 * );
58 *
59 * Renderer return contract — closures may return any of:
60 * - `HttpException` (rendered via `Route::renderHttpException()`)
61 * - `WP_REST_Response` (returned to the client as-is)
62 * - `null` (skip this entry; try the next match)
63 * - any other value (treated as `null` — invalid return ignored)
64 *
65 * A renderer that throws is caught internally and treated as `null` —
66 * a buggy renderer never crashes the request.
67 *
68 * Matching — `render()` walks the registry in **registration order** and
69 * returns the first `instanceof` match. The conventional registration
70 * order is therefore "specific classes first, broader classes last". The
71 * framework default registers nothing; plugins fully control the
72 * registry. Re-registering an existing class name overwrites the closure
73 * but keeps its registration position.
74 */
75 class ExceptionHandler
76 {
77 /**
78 * Registered renderers, keyed by exception class name. Walked in
79 * insertion order; PHP associative arrays preserve insertion order
80 * and `$arr[$key] = $value` to an existing key updates in place.
81 *
82 * @var array<string, callable>
83 */
84 protected $renderables = [];
85
86 /**
87 * Construct the handler. Calls `register()` so subclasses can declare
88 * their renderables in one place without remembering to call it.
89 */
90 public function __construct()
91 {
92 $this->register();
93 }
94
95 /**
96 * Override in a subclass to declare renderables. The base
97 * implementation is a no-op so the framework default acts as a
98 * bare registry plugins can add to.
99 *
100 * @return void
101 */
102 public function register()
103 {
104 //
105 }
106
107 /**
108 * Map an exception class to a renderer closure.
109 *
110 * @param string $class
111 * @param callable $renderer signature: function ($exception, $app)
112 * @return $this
113 *
114 * @throws \InvalidArgumentException when `$class` is not a non-empty string
115 */
116 public function renderable($class, callable $renderer)
117 {
118 if (!is_string($class) || $class === '') {
119 throw new InvalidArgumentException(
120 'ExceptionHandler::renderable() expects a non-empty class name string '
121 . 'as the first argument.'
122 );
123 }
124
125 $this->renderables[$class] = $renderer;
126
127 return $this;
128 }
129
130 /**
131 * Remove a previously-registered renderer. No-op if the class
132 * wasn't registered. Useful for plugins that want to opt out of a
133 * default registered by a parent handler.
134 *
135 * @param string $class
136 * @return $this
137 */
138 public function forget($class)
139 {
140 unset($this->renderables[$class]);
141
142 return $this;
143 }
144
145 /**
146 * Consult the registry for a thrown exception.
147 *
148 * Walks `$renderables` in registration order. The first entry whose
149 * key the exception is `instanceof` invokes its renderer. The
150 * renderer may return an `HttpException`, a `WP_REST_Response`, or
151 * `null` (to fall through to the next match). Any thrown exception
152 * from the renderer is caught and treated as `null` so a buggy
153 * renderer never crashes the request.
154 *
155 * @param \Throwable $e
156 * @param mixed $app the framework App instance (or null in tests)
157 * @return \FluentCommunity\Framework\Foundation\Exceptions\HttpException|\WP_REST_Response|null
158 */
159 public function render(Throwable $e, $app = null)
160 {
161 foreach ($this->renderables as $class => $renderer) {
162 if (!($e instanceof $class)) {
163 continue;
164 }
165
166 try {
167 $result = $renderer($e, $app);
168 } catch (Throwable $inner) {
169 // Renderer crashed. Surface to error_log in debug so the
170 // developer sees it, but never let it kill the request.
171 if (function_exists('error_log')) {
172 error_log(sprintf(
173 '[WPFluent] ExceptionHandler renderer for %s threw: %s in %s:%d',
174 $class,
175 $inner->getMessage(),
176 $inner->getFile(),
177 $inner->getLine()
178 ));
179 }
180 return null;
181 }
182
183 if ($result instanceof HttpException) {
184 return $result;
185 }
186
187 if ($result instanceof WP_REST_Response) {
188 return $result;
189 }
190
191 if ($result === null) {
192 continue;
193 }
194
195 // Invalid return type (string, int, array, etc.) — log in
196 // debug and fall through to the sanitizer. Strict by design:
197 // the contract is HttpException | WP_REST_Response | null.
198 if (function_exists('error_log')) {
199 error_log(sprintf(
200 '[WPFluent] ExceptionHandler renderer for %s returned %s; '
201 . 'expected HttpException | WP_REST_Response | null. Ignoring.',
202 $class,
203 is_object($result) ? get_class($result) : gettype($result)
204 ));
205 }
206 return null;
207 }
208
209 return null;
210 }
211
212 /**
213 * List the class names this handler has renderables registered for.
214 * Useful for introspection, debugging, and AI tooling that wants to
215 * know which exception types are mapped to safe responses.
216 *
217 * @return string[]
218 */
219 public function registeredFor()
220 {
221 return array_keys($this->renderables);
222 }
223
224 /**
225 * Whether the handler has a renderer registered for `$class`. Does
226 * NOT walk the inheritance chain — exact key match only. Use this
227 * for "is THIS specific class registered" checks; use `render()`
228 * for "does any registration handle this exception".
229 *
230 * @param string $class
231 * @return bool
232 */
233 public function hasRenderable($class)
234 {
235 return is_string($class) && array_key_exists($class, $this->renderables);
236 }
237 }
238