├── .phpunit-watcher.yml
├── .styleci.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer-require-checker.json
├── composer.json
├── config
└── di-web.php
├── infection.json.dist
├── psalm.xml
├── rector.php
├── src
├── CompositeException.php
├── ErrorData.php
├── ErrorHandler.php
├── Event
│ └── ApplicationError.php
├── Exception
│ ├── ErrorException.php
│ └── UserException.php
├── Factory
│ └── ThrowableResponseFactory.php
├── HeadersProvider.php
├── Middleware
│ ├── ErrorCatcher.php
│ └── ExceptionResponder.php
├── Renderer
│ ├── HeaderRenderer.php
│ ├── HtmlRenderer.php
│ ├── JsonRenderer.php
│ ├── PlainTextRenderer.php
│ └── XmlRenderer.php
├── RendererProvider
│ ├── ClosureRendererProvider.php
│ ├── CompositeRendererProvider.php
│ ├── ContentTypeRendererProvider.php
│ ├── HeadRendererProvider.php
│ └── RendererProviderInterface.php
├── ThrowableRendererInterface.php
├── ThrowableResponseFactory.php
└── ThrowableResponseFactoryInterface.php
├── templates
├── _call-stack-item.php
├── _call-stack-items.php
├── _previous-exception.php
├── development.css
├── development.php
├── highlight.min.js
└── production.php
└── tools
├── .gitignore
└── composer-require-checker
└── composer.json
/.phpunit-watcher.yml:
--------------------------------------------------------------------------------
1 | watch:
2 | directories:
3 | - src
4 | - tests
5 | fileMask: '*.php'
6 | notifications:
7 | passingTests: false
8 | failingTests: false
9 | phpunit:
10 | binaryPath: vendor/bin/phpunit
11 | timeout: 180
12 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: psr12
2 | risky: true
3 |
4 | version: 8.1
5 |
6 | finder:
7 | exclude:
8 | - docs
9 | - vendor
10 |
11 | enabled:
12 | - alpha_ordered_traits
13 | - array_indentation
14 | - array_push
15 | - combine_consecutive_issets
16 | - combine_consecutive_unsets
17 | - combine_nested_dirname
18 | - declare_strict_types
19 | - dir_constant
20 | - fully_qualified_strict_types
21 | - function_to_constant
22 | - hash_to_slash_comment
23 | - is_null
24 | - logical_operators
25 | - magic_constant_casing
26 | - magic_method_casing
27 | - method_separation
28 | - modernize_types_casting
29 | - native_function_casing
30 | - native_function_type_declaration_casing
31 | - no_alias_functions
32 | - no_empty_comment
33 | - no_empty_phpdoc
34 | - no_empty_statement
35 | - no_extra_block_blank_lines
36 | - no_short_bool_cast
37 | - no_superfluous_elseif
38 | - no_unneeded_control_parentheses
39 | - no_unneeded_curly_braces
40 | - no_unneeded_final_method
41 | - no_unset_cast
42 | - no_unused_imports
43 | - no_unused_lambda_imports
44 | - no_useless_else
45 | - no_useless_return
46 | - normalize_index_brace
47 | - php_unit_dedicate_assert
48 | - php_unit_dedicate_assert_internal_type
49 | - php_unit_expectation
50 | - php_unit_mock
51 | - php_unit_mock_short_will_return
52 | - php_unit_namespaced
53 | - php_unit_no_expectation_annotation
54 | - phpdoc_no_empty_return
55 | - phpdoc_no_useless_inheritdoc
56 | - phpdoc_order
57 | - phpdoc_property
58 | - phpdoc_scalar
59 | - phpdoc_singular_inheritdoc
60 | - phpdoc_trim
61 | - phpdoc_trim_consecutive_blank_line_separation
62 | - phpdoc_type_to_var
63 | - phpdoc_types
64 | - phpdoc_types_order
65 | - print_to_echo
66 | - regular_callable_call
67 | - return_assignment
68 | - self_accessor
69 | - self_static_accessor
70 | - set_type_to_cast
71 | - short_array_syntax
72 | - short_list_syntax
73 | - simplified_if_return
74 | - single_quote
75 | - standardize_not_equals
76 | - ternary_to_null_coalescing
77 | - trailing_comma_in_multiline_array
78 | - unalign_double_arrow
79 | - unalign_equals
80 | - empty_loop_body_braces
81 | - integer_literal_case
82 | - union_type_without_spaces
83 |
84 | disabled:
85 | - function_declaration
86 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Error Handler Change Log
2 |
3 | ## 4.1.1 under development
4 |
5 | - no changes in this release.
6 |
7 | ## 4.1.0 April 18, 2025
8 |
9 | - New #145: Add `Yiisoft\ErrorHandler\ThrowableResponseFactory` that provides a response for `Throwable` object with
10 | renderer provider usage (@vjik)
11 | - Chg #145: Mark `Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` as deprecated (@vjik)
12 | - Enh #145: Set content type header in renderers (@vjik)
13 | - Bug #142: Fix dark mode argument display issues (@pamparam83)
14 |
15 | ## 4.0.0 February 05, 2025
16 |
17 | - Chg #137: Add separate parameters for each of `HtmlRenderer` settings in constructor. Mark `$settings` parameter as
18 | deprecated (@vjik)
19 | - Chg #139: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik)
20 | - Enh #125: Add error code & show function arguments (@xepozz)
21 | - Enh #130: Pass exception message instead of rendered exception to logger in `ErrorHandler` (@olegbaturin)
22 | - Enh #133: Extract response generator from `ErrorCatcher` middleware into separate `ThrowableResponseFactory`
23 | class (@olegbaturin)
24 | - Enh #138, #139: Raise the minimum PHP version to 8.1 and minor refactoring (@vjik)
25 | - Bug #139: Explicitly mark nullable parameters (@vjik)
26 |
27 | ## 3.3.0 July 11, 2024
28 |
29 | - Enh #112: Add copy cURL button, sort request headers, fix UI (@xepozz)
30 | - Enh #113: Simplify error log (@xepozz)
31 | - Enh #114: Show full argument by click (@xepozz)
32 | - Enh #116: Remove @anonymous postfix (@xepozz)
33 | - Enh #117, #120: Show arguments table by click (@xepozz, @vjik)
34 | - Bug #114: Stop `click` event on text selection (@xepozz)
35 | - Bug #122: Do `exit(1)` after all shutdown functions, even postponed ones (@samdark)
36 |
37 | ## 3.2.1 March 07, 2024
38 |
39 | - Enh #102: Add support for `psr/http-message` of `^2.0` version (@vjik)
40 |
41 | ## 3.2.0 January 30, 2024
42 |
43 | - New #98: Add ability to execute `getBody()` on response when `ExceptionResponder` middleware is processing (@vjik)
44 | - Enh #96: Trace PHP errors (@xepozz, @vjik)
45 |
46 | ## 3.1.0 January 07, 2024
47 |
48 | - New #87: Add `CompositeException` to be able to render multiple exceptions (@xepozz)
49 | - Chg #75: Dispatch `ApplicationError` in `ErrorCatcher` (@xepozz)
50 | - Enh #82: Add `HeadersProvider` (@xepozz)
51 | - Enh #86: Add color scheme definition based on system settings (@dood-)
52 | - Bug #87: Fix a bug with try/finally from #75 (@xepozz)
53 |
54 | ## 3.0.0 February 14, 2023
55 |
56 | - Chg #64: Raise PHP version to `^8.0` (@vjik, @xepozz)
57 | - Chg #72: Adapt configuration group names to Yii conventions (@vjik)
58 | - Enh #65: Explicitly add transitive dependencies `ext-mbstring`, `psr/http-factory` and
59 | `psr/http-server-handler` (@vjik)
60 |
61 | ## 2.1.1 January 26, 2023
62 |
63 | - Bug #70: Prevent duplication of throwable rendering (@vjik)
64 |
65 | ## 2.1.0 June 15, 2022
66 |
67 | - Enh #54: Add shutdown event, fix cwd (@rustamwin)
68 | - Enh #55: Defer exit on terminate (@rustamwin)
69 | - Enh #57: Add markdown support for friendly exception solutions (@vjik)
70 | - Enh #58: Add support for `2.0`, `3.0` versions of `psr/log` (@rustamwin)
71 |
72 | ## 2.0.2 February 04, 2022
73 |
74 | - Bug #50: Fix JSON rendering on JSON recursion exception (@thenotsoft)
75 |
76 | ## 2.0.1 January 26, 2022
77 |
78 | - Bug #49: Fix JSON rendering of non-UTF-8 encoded string (@devanych)
79 |
80 | ## 2.0.0 November 09, 2021
81 |
82 | - Chg #48: Transfer `HeaderHelper` to `yiisoft/http` package (@devanych)
83 | - Enh #45: Improve appearance of solution from friendly exceptions (@vjik)
84 |
85 | ## 1.0.0 May 13, 2021
86 |
87 | Initial release.
88 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software ( )
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii Error Handler
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/error-handler)
10 | [](https://packagist.org/packages/yiisoft/error-handler)
11 | [](https://github.com/yiisoft/error-handler/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/error-handler)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/error-handler/master)
14 | [](https://github.com/yiisoft/error-handler/actions?query=workflow%3A%22static+analysis%22)
15 |
16 | The package provides advanced error handling. The features are:
17 |
18 | - PSR-15 middleware for catching unhandled errors.
19 | - PSR-15 middleware for mapping certain exceptions to custom responses.
20 | - Production and debug modes.
21 | - Debug mode displays details, stacktrace, has dark and light themes and handy buttons to search for error without typing.
22 | - Takes PHP settings into account.
23 | - Handles out of memory errors, fatals, warnings, notices and exceptions.
24 | - Can use any PSR-3 compatible logger for error logging.
25 | - Detects response format based on mime type of the request.
26 | - Supports responding with HTML, plain text, JSON, XML and headers out of the box.
27 | - Has ability to implement your own error rendering for additional types.
28 | - [Friendly exceptions](https://github.com/yiisoft/friendly-exception/) support.
29 |
30 | ## Requirements
31 |
32 | - PHP 8.1 or higher.
33 | - `DOM` PHP extension.
34 | - `mbstring` PHP extension.
35 |
36 | ## Installation
37 |
38 | The package could be installed with [Composer](https://getcomposer.org):
39 |
40 | ```shell
41 | composer require yiisoft/error-handler
42 | ```
43 |
44 | ## General usage
45 |
46 | Creating an error handler:
47 |
48 | ```php
49 | use Yiisoft\ErrorHandler\ErrorHandler;
50 | use Yiisoft\ErrorHandler\Renderer\HtmlRenderer;
51 |
52 | /**
53 | * @var \Psr\Log\LoggerInterface $logger
54 | */
55 |
56 | $errorHandler = new ErrorHandler($logger, new HtmlRenderer());
57 | ```
58 |
59 | The error handler logs information about the error using any [PSR-3](https://www.php-fig.org/psr/psr-3/)
60 | compatible logger. If for some reason you do not want to log error information,
61 | specify an instance of the `\Psr\Log\NullLogger`.
62 |
63 | By default, the error handler is set to production mode and displays no detailed information.
64 | You can enable and disable debug mode as follows:
65 |
66 | ```php
67 | // Enable debug mode:
68 | $errorHandler->debug();
69 |
70 | // Disable debug mode:
71 | $errorHandler->debug(false);
72 |
73 | // Or define the environment dynamically:
74 | $errorHandler->debug($_ENV['debug'] ?? false);
75 | ```
76 |
77 | The error handler handles out-of-memory errors. To achieve it, memory is pre-allocated so that if a problem occurs with
78 | a lack of memory, the error handler can handle the error using this reserved memory. You can specify your own reserve
79 | size using the `memoryReserveSize()` method. If you set this value to 0, no memory will be reserved.
80 |
81 | ```php
82 | // Allocate 512KB. Defaults to 256KB.
83 | $errorHandler->memoryReserveSize(524_288);
84 | ```
85 |
86 | The `register()` method registers the PHP error and exception handlers.
87 | To unregister these and restore the PHP error and exception handlers, use the `unregister()` method.
88 |
89 | ```php
90 | $errorHandler->register();
91 | // Errors are being handled.
92 | $errorHandler->unregister();
93 | // Errors are not handled.
94 | ```
95 |
96 | ### Rendering error data
97 |
98 | The following renderers are available out of the box:
99 |
100 | - `Yiisoft\ErrorHandler\Renderer\HeaderRenderer` - Renders error into HTTP headers. It is used for HEAD requests.
101 | - `Yiisoft\ErrorHandler\Renderer\HtmlRenderer` - Renders error into HTML.
102 | - `Yiisoft\ErrorHandler\Renderer\JsonRenderer` - Renders error into JSON.
103 | - `Yiisoft\ErrorHandler\Renderer\PlainTextRenderer` - Renders error into plain text.
104 | - `Yiisoft\ErrorHandler\Renderer\XmlRenderer` - Renders error into XML.
105 |
106 | If the existing renderers are not enough, you can create your own. To do this, you must implement the
107 | `Yiisoft\ErrorHandler\ThrowableRendererInterface` and specify it when creating an instance of the error handler.
108 |
109 | ```php
110 | use Yiisoft\ErrorHandler\ErrorHandler;
111 |
112 | /**
113 | * @var \Psr\Log\LoggerInterface $logger
114 | * @var \Yiisoft\ErrorHandler\ThrowableRendererInterface $renderer
115 | */
116 |
117 | $errorHandler = new ErrorHandler($logger, $renderer);
118 | ```
119 |
120 | For more information about creating your own renders and examples of rendering error data,
121 | [see here](https://github.com/yiisoft/docs/blob/master/guide/en/runtime/handling-errors.md#rendering-error-data).
122 |
123 | ### Using a factory to create a response
124 |
125 | `Yiisoft\ErrorHandler\ThrowableResponseFactory` renders `Throwable` object and produces a response according to the content type provided by the client.
126 |
127 | ```php
128 | use Yiisoft\ErrorHandler\RendererProvider;
129 | use Yiisoft\ErrorHandler\ThrowableResponseFactory;
130 |
131 | /**
132 | * @var \Throwable $throwable
133 | * @var \Psr\Container\ContainerInterface $container
134 | * @var \Psr\Http\Message\ResponseFactoryInterface $responseFactory
135 | * @var \Psr\Http\Message\ServerRequestInterface $request
136 | * @var \Yiisoft\ErrorHandler\ErrorHandler $errorHandler
137 | */
138 |
139 | $throwableResponseFactory = new ThrowableResponseFactory(
140 | $responseFactory,
141 | $errorHandler,
142 | new RendererProvider\CompositeRendererProvider(
143 | new RendererProvider\HeadRendererProvider(),
144 | new RendererProvider\ContentTypeRendererProvider($container),
145 | ),
146 | );
147 |
148 | // Creating an instance of the `Psr\Http\Message\ResponseInterface` with error information.
149 | $response = $throwableResponseFactory->create($throwable, $request);
150 | ```
151 |
152 | `Yiisoft\ErrorHandler\ThrowableResponseFactory` chooses how to render an exception by renderer provider. Providers
153 | available out of the box:
154 |
155 | - `HeadRendererProvider` - renders error into HTTP headers. It is used for HEAD requests.
156 | - `ContentTypeRendererProvider` - renders error based on accept HTTP header. By default, JSON, XML and plain text are
157 | supported.
158 | - `ClosureRendererProvider` - allows you to create your own renderer provider using closures.
159 | - `CompositeRendererProvider` - allows you to combine several renderer providers.
160 |
161 | ### Using a middleware for catching unhandled errors
162 |
163 | `Yiisoft\ErrorHandler\Middleware\ErrorCatcher` is a [PSR-15](https://www.php-fig.org/psr/psr-15/) middleware that
164 | catches exceptions raised during middleware stack execution and passes them to the instance of `Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface` to create a response.
165 |
166 | ```php
167 | use Yiisoft\ErrorHandler\Middleware\ErrorCatcher;
168 |
169 | /**
170 | * @var \Psr\EventDispatcher\EventDispatcherInterface $eventDispatcher
171 | * @var \Psr\Http\Message\ServerRequestInterface $request
172 | * @var \Psr\Http\Server\RequestHandlerInterface $handler
173 | * @var \Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface $throwableResponseFactory
174 | */
175 |
176 | $errorCatcher = new ErrorCatcher($throwableResponseFactory);
177 |
178 | // In any case, it will return an instance of the `Psr\Http\Message\ResponseInterface`.
179 | // Either the expected response, or a response with error information.
180 | $response = $errorCatcher->process($request, $handler);
181 | ```
182 |
183 | `Yiisoft\ErrorHandler\Middleware\ErrorCatcher` can be instantiated with [PSR-14](https://www.php-fig.org/psr/psr-14/) event dispatcher as an optional dependency.
184 | In this case `\Yiisoft\ErrorHandler\Event\ApplicationError` will be dispatched when `ErrorCatcher` catches an error.
185 |
186 | ```php
187 | $errorCatcher = new ErrorCatcher($throwableResponseFactory, $eventDispatcher);
188 | ```
189 |
190 | ### Using a middleware for mapping certain exceptions to custom responses
191 |
192 | `Yiisoft\ErrorHandler\Middleware\ExceptionResponder` is a [PSR-15](https://www.php-fig.org/psr/psr-15/)
193 | middleware that maps certain exceptions to custom responses.
194 |
195 | ```php
196 | use Yiisoft\ErrorHandler\Middleware\ExceptionResponder;
197 |
198 | /**
199 | * @var \Psr\Http\Message\ResponseFactoryInterface $responseFactory
200 | * @var \Psr\Http\Message\ServerRequestInterface $request
201 | * @var \Psr\Http\Server\RequestHandlerInterface $handler
202 | * @var \Yiisoft\Injector\Injector $injector
203 | */
204 |
205 | $exceptionMap = [
206 | // Status code with which the response will be created by the factory.
207 | MyNotFoundException::class => 404,
208 | // PHP callable that must return a `Psr\Http\Message\ResponseInterface`.
209 | MyHttpException::class => static fn (MyHttpException $exception) => new MyResponse($exception),
210 | // ...
211 | ];
212 |
213 | $exceptionResponder = new ExceptionResponder($exceptionMap, $responseFactory, $injector);
214 |
215 | // Returns the expected response, or the response associated with the thrown exception,
216 | // or throws an exception if it does not present in the exception map.
217 | $response = $exceptionResponder->process($request, $handler);
218 | ```
219 |
220 | In the application middleware stack `Yiisoft\ErrorHandler\Middleware\ExceptionResponder` must be placed before
221 | `Yiisoft\ErrorHandler\Middleware\ErrorCatcher`.
222 |
223 | ## Events
224 |
225 | - When `ErrorCatcher` catches an error it optionally dispatches `\Yiisoft\ErrorHandler\Event\ApplicationError` event. Instance of `Psr\EventDispatcher\EventDispatcherInterface` must be provided to the `ErrorCatcher`.
226 |
227 | ## Friendly Exceptions
228 |
229 | `HtmlRenderer` supports [friendly exceptions](https://github.com/yiisoft/friendly-exception/).
230 |
231 | Code blocks in solution markdown support language syntax highlight:
232 |
233 | | Language | Aliases |
234 | |------------|--------------------------------------------------------|
235 | | Bash | bash, sh, zsh |
236 | | CSS | css |
237 | | HTML, XML | xml, html, xhtml, rss, atom, xjb, xsd, xsl, plist, svg |
238 | | JavaScript | javascript, js, jsx |
239 | | JSON | json |
240 | | PHP | php |
241 | | Plaintext | plaintext, txt, text |
242 | | SQL | sql |
243 |
244 | For example:
245 |
246 | ```html
247 |
248 |
249 | This text is normal.
250 | This text is bold.
251 |
252 |
253 | ```
254 |
255 | ## Documentation
256 |
257 | - [Yii guide to handling errors](https://github.com/yiisoft/docs/blob/master/guide/en/runtime/handling-errors.md)
258 | - [Internals](docs/internals.md)
259 |
260 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that.
261 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
262 |
263 | ## License
264 |
265 | The Yii Error Handler is free software. It is released under the terms of the BSD License.
266 | Please see [`LICENSE`](./LICENSE.md) for more information.
267 |
268 | Maintained by [Yii Software](https://www.yiiframework.com/).
269 |
270 | ## Credits
271 |
272 | The Yii Error Handler use code of [Highlight.js](https://highlightjs.org/) by Ivan Sagalaev and other contributors.
273 |
274 | ## Support the project
275 |
276 | [](https://opencollective.com/yiisoft)
277 |
278 | ## Follow updates
279 |
280 | [](https://www.yiiframework.com/)
281 | [](https://twitter.com/yiiframework)
282 | [](https://t.me/yii3en)
283 | [](https://www.facebook.com/groups/yiitalk)
284 | [](https://yiiframework.com/go/slack)
285 |
--------------------------------------------------------------------------------
/composer-require-checker.json:
--------------------------------------------------------------------------------
1 | {
2 | "symbol-whitelist" : [
3 | "xdebug_get_function_stack"
4 | ],
5 | "php-core-extensions" : [
6 | "Core",
7 | "date",
8 | "json",
9 | "pcre",
10 | "Phar",
11 | "Reflection",
12 | "SPL",
13 | "random",
14 | "standard"
15 | ],
16 | "scan-files" : []
17 | }
18 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/error-handler",
3 | "type": "library",
4 | "description": "Yii Error Handling Library",
5 | "keywords": [
6 | "yiisoft",
7 | "error-handler",
8 | "psr-3",
9 | "psr-7",
10 | "psr-11",
11 | "psr-15"
12 | ],
13 | "homepage": "https://www.yiiframework.com/",
14 | "license": "BSD-3-Clause",
15 | "support": {
16 | "issues": "https://github.com/yiisoft/error-handler/issues?state=open",
17 | "source": "https://github.com/yiisoft/error-handler",
18 | "forum": "https://www.yiiframework.com/forum/",
19 | "wiki": "https://www.yiiframework.com/wiki/",
20 | "irc": "ircs://irc.libera.chat:6697/yii",
21 | "chat": "https://t.me/yii3en"
22 | },
23 | "funding": [
24 | {
25 | "type": "opencollective",
26 | "url": "https://opencollective.com/yiisoft"
27 | },
28 | {
29 | "type": "github",
30 | "url": "https://github.com/sponsors/yiisoft"
31 | }
32 | ],
33 | "require": {
34 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
35 | "ext-dom": "*",
36 | "ext-mbstring": "*",
37 | "alexkart/curl-builder": "^1.0",
38 | "cebe/markdown": "^1.2",
39 | "psr/container": "^1.0|^2.0",
40 | "psr/http-factory": "^1.0",
41 | "psr/http-message": "^1.0|^2.0",
42 | "psr/http-server-handler": "^1.0",
43 | "psr/http-server-middleware": "^1.0",
44 | "psr/log": "^1.1|^2.0|^3.0",
45 | "yiisoft/friendly-exception": "^1.0",
46 | "yiisoft/http": "^1.2",
47 | "yiisoft/injector": "^1.0"
48 | },
49 | "require-dev": {
50 | "bamarni/composer-bin-plugin": "^1.8.2",
51 | "httpsoft/http-message": "^1.1.6",
52 | "phpunit/phpunit": "^10.5.45",
53 | "psr/event-dispatcher": "^1.0",
54 | "rector/rector": "^2.0.11",
55 | "roave/infection-static-analysis-plugin": "^1.35",
56 | "spatie/phpunit-watcher": "^1.24",
57 | "vimeo/psalm": "^5.26.1 || ^6.9.1",
58 | "yiisoft/di": "^1.3",
59 | "yiisoft/test-support": "^3.0.2"
60 | },
61 | "autoload": {
62 | "psr-4": {
63 | "Yiisoft\\ErrorHandler\\": "src"
64 | }
65 | },
66 | "autoload-dev": {
67 | "psr-4": {
68 | "Yiisoft\\ErrorHandler\\Tests\\": "tests"
69 | }
70 | },
71 | "extra": {
72 | "bamarni-bin": {
73 | "bin-links": true,
74 | "target-directory": "tools",
75 | "forward-command": true
76 | },
77 | "config-plugin-options": {
78 | "source-directory": "config"
79 | },
80 | "config-plugin": {
81 | "di-web": "di-web.php"
82 | }
83 | },
84 | "config": {
85 | "sort-packages": true,
86 | "allow-plugins": {
87 | "bamarni/composer-bin-plugin": true,
88 | "composer/package-versions-deprecated": true,
89 | "infection/extension-installer": true
90 | }
91 | },
92 | "scripts": {
93 | "test": "phpunit --testdox --no-interaction",
94 | "test-watch": "phpunit-watcher watch"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/config/di-web.php:
--------------------------------------------------------------------------------
1 | HtmlRenderer::class,
16 | ThrowableResponseFactoryInterface::class => ThrowableResponseFactory::class,
17 | ];
18 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src"
5 | ]
6 | },
7 | "logs": {
8 | "text": "php:\/\/stderr",
9 | "stryker": {
10 | "report": "master"
11 | }
12 | },
13 | "mutators": {
14 | "@default": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | withPaths([
14 | __DIR__ . '/src',
15 | __DIR__ . '/tests',
16 | ])
17 | ->withPhpSets(php81: true)
18 | ->withRules([
19 | InlineConstructorDefaultToPropertyRector::class,
20 | ])
21 | ->withSkip([
22 | ClosureToArrowFunctionRector::class,
23 | NullToStrictStringFuncCallArgRector::class,
24 | RemoveExtraParametersRector::class,
25 | NewInInitializerRector::class,
26 | ]);
27 |
--------------------------------------------------------------------------------
/src/CompositeException.php:
--------------------------------------------------------------------------------
1 | rest = $rest;
25 | parent::__construct($first->getMessage(), (int) $first->getCode(), $first);
26 | }
27 |
28 | public function getFirstException(): Throwable
29 | {
30 | return $this->first;
31 | }
32 |
33 | /**
34 | * @return Throwable[]
35 | */
36 | public function getPreviousExceptions(): array
37 | {
38 | return $this->rest;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/ErrorData.php:
--------------------------------------------------------------------------------
1 | $headers The headers to add to the response.
18 | */
19 | public function __construct(
20 | private readonly string $content,
21 | private readonly array $headers = [],
22 | ) {
23 | }
24 |
25 | /**
26 | * Returns a content to use as response body.
27 | *
28 | * @return string The content to use as response body.
29 | */
30 | public function __toString(): string
31 | {
32 | return $this->content;
33 | }
34 |
35 | /**
36 | * Returns a response with error data.
37 | *
38 | * @param ResponseInterface $response The response for setting error data.
39 | *
40 | * @return ResponseInterface The response with error data.
41 | */
42 | public function addToResponse(ResponseInterface $response): ResponseInterface
43 | {
44 | foreach ($this->headers as $name => $value) {
45 | $response = $response->withHeader($name, $value);
46 | }
47 |
48 | $response
49 | ->getBody()
50 | ->write($this->content);
51 | return $response;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/ErrorHandler.php:
--------------------------------------------------------------------------------
1 | defaultRenderer;
69 |
70 | try {
71 | $this->logger->error($t->getMessage(), ['throwable' => $t]);
72 | return $this->debug ? $renderer->renderVerbose($t, $request) : $renderer->render($t, $request);
73 | } catch (Throwable $t) {
74 | return new ErrorData((string) $t);
75 | }
76 | }
77 |
78 | /**
79 | * Enables and disables debug mode.
80 | *
81 | * Ensure that is is disabled in production environment since debug mode exposes sensitive details.
82 | *
83 | * @param bool $enable Enable/disable debugging mode.
84 | */
85 | public function debug(bool $enable = true): void
86 | {
87 | $this->debug = $enable;
88 | }
89 |
90 | /**
91 | * Sets the size of the reserved memory.
92 | *
93 | * @param int $size The size of the reserved memory.
94 | *
95 | * @see $memoryReserveSize
96 | */
97 | public function memoryReserveSize(int $size): void
98 | {
99 | $this->memoryReserveSize = $size;
100 | }
101 |
102 | /**
103 | * Register PHP exception and error handlers and enable this error handler.
104 | */
105 | public function register(): void
106 | {
107 | if ($this->enabled) {
108 | return;
109 | }
110 |
111 | if ($this->memoryReserveSize > 0) {
112 | $this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
113 | }
114 |
115 | $this->initializeOnce();
116 |
117 | // Handles throwable that isn't caught otherwise, echo output and exit.
118 | set_exception_handler(function (Throwable $t): void {
119 | if (!$this->enabled) {
120 | return;
121 | }
122 |
123 | $this->renderThrowableAndTerminate($t);
124 | });
125 |
126 | // Handles PHP execution errors such as warnings and notices.
127 | set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
128 | if (!$this->enabled) {
129 | return false;
130 | }
131 |
132 | if (!(error_reporting() & $severity)) {
133 | // This error code is not included in error_reporting.
134 | return true;
135 | }
136 |
137 | $backtrace = debug_backtrace(0);
138 | array_shift($backtrace);
139 | throw new ErrorException($message, $severity, $severity, $file, $line, null, $backtrace);
140 | });
141 |
142 | $this->enabled = true;
143 | }
144 |
145 | /**
146 | * Disable this error handler.
147 | */
148 | public function unregister(): void
149 | {
150 | if (!$this->enabled) {
151 | return;
152 | }
153 |
154 | $this->memoryReserve = '';
155 |
156 | $this->enabled = false;
157 | }
158 |
159 | private function initializeOnce(): void
160 | {
161 | if ($this->initialized) {
162 | return;
163 | }
164 |
165 | // Disables the display of error.
166 | if (function_exists('ini_set')) {
167 | ini_set('display_errors', '0');
168 | }
169 |
170 | // Handles fatal error.
171 | register_shutdown_function(function (): void {
172 | if (!$this->enabled) {
173 | return;
174 | }
175 |
176 | $this->memoryReserve = '';
177 | $e = error_get_last();
178 |
179 | if ($e !== null && ErrorException::isFatalError($e)) {
180 | $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
181 | $error = new ErrorException(
182 | $e['message'],
183 | $e['type'],
184 | $e['type'],
185 | $e['file'],
186 | $e['line'],
187 | null,
188 | $backtrace
189 | );
190 | $this->renderThrowableAndTerminate($error);
191 | }
192 | });
193 |
194 | if (!(PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg')) {
195 | /**
196 | * @var string
197 | */
198 | $this->workingDirectory = getcwd();
199 | }
200 |
201 | $this->initialized = true;
202 | }
203 |
204 | /**
205 | * Renders the throwable and terminates the script.
206 | */
207 | private function renderThrowableAndTerminate(Throwable $t): void
208 | {
209 | if (!empty($this->workingDirectory)) {
210 | chdir($this->workingDirectory);
211 | }
212 | // Disable error capturing to avoid recursive errors while handling exceptions.
213 | $this->unregister();
214 | // Set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent.
215 | http_response_code(Status::INTERNAL_SERVER_ERROR);
216 |
217 | echo $this->handle($t);
218 | $this->eventDispatcher?->dispatch(new ApplicationError($t));
219 |
220 | $handler = $this->wrapShutdownHandler(
221 | static function (): void {
222 | exit(1);
223 | },
224 | $this->exitShutdownHandlerDepth
225 | );
226 |
227 | register_shutdown_function($handler);
228 | }
229 |
230 | /**
231 | * Wraps shutdown handler into another shutdown handler to ensure it is called last after all other shutdown
232 | * functions, even those added to the end.
233 | *
234 | * @param callable $handler Shutdown handler to wrap.
235 | * @param int $depth Wrapping depth.
236 | * @return callable Wrapped handler.
237 | */
238 | private function wrapShutdownHandler(callable $handler, int $depth): callable
239 | {
240 | $currentDepth = 0;
241 | while ($currentDepth < $depth) {
242 | $handler = static function() use ($handler): void {
243 | register_shutdown_function($handler);
244 | };
245 | $currentDepth++;
246 | }
247 | return $handler;
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/src/Event/ApplicationError.php:
--------------------------------------------------------------------------------
1 | throwable;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/ErrorException.php:
--------------------------------------------------------------------------------
1 | ,class?:class-string,file?:string,function:string,line?:int,object?:object,type?:string}>
18 | *
19 | * @final
20 | */
21 | class ErrorException extends \ErrorException implements FriendlyExceptionInterface
22 | {
23 | /** @psalm-suppress MissingClassConstType Private constants never change. */
24 | private const ERROR_NAMES = [
25 | E_ERROR => 'PHP Fatal Error',
26 | E_WARNING => 'PHP Warning',
27 | E_PARSE => 'PHP Parse Error',
28 | E_NOTICE => 'PHP Notice',
29 | E_CORE_ERROR => 'PHP Core Error',
30 | E_CORE_WARNING => 'PHP Core Warning',
31 | E_COMPILE_ERROR => 'PHP Compile Error',
32 | E_COMPILE_WARNING => 'PHP Compile Warning',
33 | E_USER_ERROR => 'PHP User Error',
34 | E_USER_WARNING => 'PHP User Warning',
35 | E_USER_NOTICE => 'PHP User Notice',
36 | 2048 => 'PHP Strict Warning', // E_STRICT
37 | E_RECOVERABLE_ERROR => 'PHP Recoverable Error',
38 | E_DEPRECATED => 'PHP Deprecated Warning',
39 | E_USER_DEPRECATED => 'PHP User Deprecated Warning',
40 | ];
41 |
42 | /** @psalm-param DebugBacktraceType $backtrace */
43 | public function __construct(string $message = '', int $code = 0, int $severity = 1, string $filename = __FILE__, int $line = __LINE__, ?Exception $previous = null, private readonly array $backtrace = [])
44 | {
45 | parent::__construct($message, $code, $severity, $filename, $line, $previous);
46 | $this->addXDebugTraceToFatalIfAvailable();
47 | }
48 |
49 | /**
50 | * Returns if error is one of fatal type.
51 | *
52 | * @param array $error error got from error_get_last()
53 | *
54 | * @return bool If error is one of fatal type.
55 | */
56 | public static function isFatalError(array $error): bool
57 | {
58 | return isset($error['type']) && in_array(
59 | $error['type'],
60 | [E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING],
61 | true,
62 | );
63 | }
64 |
65 | /**
66 | * @return string The user-friendly name of this exception.
67 | */
68 | public function getName(): string
69 | {
70 | return self::ERROR_NAMES[$this->getCode()] ?? 'Error';
71 | }
72 |
73 | public function getSolution(): ?string
74 | {
75 | return null;
76 | }
77 |
78 | /**
79 | * @psalm-return DebugBacktraceType
80 | */
81 | public function getBacktrace(): array
82 | {
83 | return $this->backtrace;
84 | }
85 |
86 | /**
87 | * Fatal errors normally do not provide any trace making it harder to debug. In case XDebug is installed, we
88 | * can get a trace using `xdebug_get_function_stack()`.
89 | */
90 | private function addXDebugTraceToFatalIfAvailable(): void
91 | {
92 | if ($this->isXdebugStackAvailable()) {
93 | /**
94 | * XDebug trace can't be modified and used directly with PHP 7
95 | *
96 | * @see https://github.com/yiisoft/yii2/pull/11723
97 | *
98 | * @psalm-var array
99 | */
100 | $xDebugTrace = array_slice(array_reverse(xdebug_get_function_stack()), 1, -1);
101 | $trace = [];
102 |
103 | foreach ($xDebugTrace as $frame) {
104 | if (!isset($frame['function'])) {
105 | $frame['function'] = 'unknown';
106 | }
107 |
108 | // XDebug < 2.1.1: https://bugs.xdebug.org/view.php?id=695
109 | if (!isset($frame['type']) || $frame['type'] === 'static') {
110 | $frame['type'] = '::';
111 | } elseif ($frame['type'] === 'dynamic') {
112 | $frame['type'] = '->';
113 | }
114 |
115 | // XDebug has a different key name
116 | if (isset($frame['params']) && !isset($frame['args'])) {
117 | /** @var mixed */
118 | $frame['args'] = $frame['params'];
119 | }
120 | $trace[] = $frame;
121 | }
122 |
123 | $ref = new ReflectionProperty(Exception::class, 'trace');
124 | $ref->setAccessible(true);
125 | $ref->setValue($this, $trace);
126 | }
127 | }
128 |
129 | /**
130 | * Ensures that Xdebug stack trace is available based on Xdebug version.
131 | * Idea taken from developer bishopb at https://github.com/rollbar/rollbar-php
132 | */
133 | private function isXdebugStackAvailable(): bool
134 | {
135 | if (!function_exists('\xdebug_get_function_stack')) {
136 | return false;
137 | }
138 |
139 | // check for Xdebug being installed to ensure origin of xdebug_get_function_stack()
140 | $version = phpversion('xdebug');
141 |
142 | if ($version === false) {
143 | return false;
144 | }
145 |
146 | // Xdebug 2 and prior
147 | if (version_compare($version, '3.0.0', '<')) {
148 | return true;
149 | }
150 |
151 | // Xdebug 3 and later, proper mode is required
152 | return str_contains((string) \ini_get('xdebug.mode'), 'develop');
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/Exception/UserException.php:
--------------------------------------------------------------------------------
1 | >
46 | */
47 | private array $renderers = [
48 | 'application/json' => JsonRenderer::class,
49 | 'application/xml' => XmlRenderer::class,
50 | 'text/xml' => XmlRenderer::class,
51 | 'text/plain' => PlainTextRenderer::class,
52 | 'text/html' => HtmlRenderer::class,
53 | '*/*' => HtmlRenderer::class,
54 | ];
55 | private ?string $contentType = null;
56 |
57 | public function __construct(
58 | private readonly ResponseFactoryInterface $responseFactory,
59 | private readonly ErrorHandler $errorHandler,
60 | private readonly ContainerInterface $container,
61 | ?HeadersProvider $headersProvider = null,
62 | ) {
63 | $this->headersProvider = $headersProvider ?? new HeadersProvider();
64 | }
65 |
66 | public function create(Throwable $throwable, ServerRequestInterface $request): ResponseInterface
67 | {
68 | $contentType = $this->contentType ?? $this->getContentType($request);
69 | $renderer = $request->getMethod() === Method::HEAD ? new HeaderRenderer() : $this->getRenderer($contentType);
70 |
71 | $data = $this->errorHandler->handle($throwable, $renderer, $request);
72 | $response = $this->responseFactory->createResponse(Status::INTERNAL_SERVER_ERROR);
73 | foreach ($this->headersProvider->getAll() as $name => $value) {
74 | $response = $response->withHeader($name, $value);
75 | }
76 | return $data->addToResponse($response->withHeader(Header::CONTENT_TYPE, $contentType));
77 | }
78 |
79 | /**
80 | * Returns a new instance with the specified content type and renderer class.
81 | *
82 | * @param string $contentType The content type to add associated renderers for.
83 | * @param string $rendererClass The classname implementing the {@see ThrowableRendererInterface}.
84 | */
85 | public function withRenderer(string $contentType, string $rendererClass): self
86 | {
87 | if (!is_subclass_of($rendererClass, ThrowableRendererInterface::class)) {
88 | throw new InvalidArgumentException(sprintf(
89 | 'Class "%s" does not implement "%s".',
90 | $rendererClass,
91 | ThrowableRendererInterface::class,
92 | ));
93 | }
94 |
95 | $new = clone $this;
96 | $new->renderers[$this->normalizeContentType($contentType)] = $rendererClass;
97 | return $new;
98 | }
99 |
100 | /**
101 | * Returns a new instance without renderers by the specified content types.
102 | *
103 | * @param string[] $contentTypes The content types to remove associated renderers for.
104 | * If not specified, all renderers will be removed.
105 | */
106 | public function withoutRenderers(string ...$contentTypes): self
107 | {
108 | $new = clone $this;
109 |
110 | if (count($contentTypes) === 0) {
111 | $new->renderers = [];
112 | return $new;
113 | }
114 |
115 | foreach ($contentTypes as $contentType) {
116 | unset($new->renderers[$this->normalizeContentType($contentType)]);
117 | }
118 |
119 | return $new;
120 | }
121 |
122 | /**
123 | * Force content type to respond with regardless of request.
124 | *
125 | * @param string $contentType The content type to respond with regardless of request.
126 | */
127 | public function forceContentType(string $contentType): self
128 | {
129 | $contentType = $this->normalizeContentType($contentType);
130 |
131 | if (!isset($this->renderers[$contentType])) {
132 | throw new InvalidArgumentException(sprintf('The renderer for %s is not set.', $contentType));
133 | }
134 |
135 | $new = clone $this;
136 | $new->contentType = $contentType;
137 | return $new;
138 | }
139 |
140 | /**
141 | * Returns the renderer by the specified content type, or null if the renderer was not set.
142 | *
143 | * @param string $contentType The content type associated with the renderer.
144 | */
145 | private function getRenderer(string $contentType): ?ThrowableRendererInterface
146 | {
147 | if (isset($this->renderers[$contentType])) {
148 | /** @var ThrowableRendererInterface */
149 | return $this->container->get($this->renderers[$contentType]);
150 | }
151 |
152 | return null;
153 | }
154 |
155 | /**
156 | * Returns the priority content type from the accept request header.
157 | *
158 | * @return string The priority content type.
159 | */
160 | private function getContentType(ServerRequestInterface $request): string
161 | {
162 | try {
163 | foreach (HeaderValueHelper::getSortedAcceptTypes($request->getHeader(Header::ACCEPT)) as $header) {
164 | if (array_key_exists($header, $this->renderers)) {
165 | return $header;
166 | }
167 | }
168 | } catch (InvalidArgumentException) {
169 | // The Accept header contains an invalid q factor.
170 | }
171 |
172 | return '*/*';
173 | }
174 |
175 | /**
176 | * Normalizes the content type.
177 | *
178 | * @param string $contentType The raw content type.
179 | *
180 | * @return string Normalized content type.
181 | */
182 | private function normalizeContentType(string $contentType): string
183 | {
184 | if (!str_contains($contentType, '/')) {
185 | throw new InvalidArgumentException('Invalid content type.');
186 | }
187 |
188 | return strtolower(trim($contentType));
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/HeadersProvider.php:
--------------------------------------------------------------------------------
1 | $headers Default headers list.
17 | */
18 | public function __construct(
19 | private array $headers = [],
20 | ) {
21 | }
22 |
23 | /**
24 | * Adds a header to the list of headers.
25 | *
26 | * @param string $name The header name.
27 | * @param string|string[] $values The header value.
28 | */
29 | public function add(string $name, string|array $values): void
30 | {
31 | $this->headers[$name] = (array)$values;
32 | }
33 |
34 | /**
35 | * Returns all headers.
36 | *
37 | * @return array The headers list.
38 | */
39 | public function getAll(): array
40 | {
41 | return $this->headers;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Middleware/ErrorCatcher.php:
--------------------------------------------------------------------------------
1 | handle($request);
33 | } catch (Throwable $t) {
34 | try {
35 | $this->eventDispatcher?->dispatch(new ApplicationError($t));
36 | } catch (Throwable $e) {
37 | $t = new CompositeException($e, $t);
38 | }
39 |
40 | return $this->throwableResponseFactory->create($t, $request);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Middleware/ExceptionResponder.php:
--------------------------------------------------------------------------------
1 | callable, \DomainException::class => callable, ...]`
27 | * - int format: `[\Exception::class => 404, \DomainException::class => 500, ...]`
28 | *
29 | * When an exception is thrown, the map in callable format allows to take control of the response.
30 | * Сallable must return `Psr\Http\Message\ResponseInterface`. If specified exception classes are equal,
31 | * then the first one will be processed. Below are some examples:
32 | *
33 | * ```php
34 | * $exceptionMap = [
35 | * DomainException::class => function (\Psr\Http\Message\ResponseFactoryInterface $responseFactory) {
36 | * return $responseFactory->createResponse(\Yiisoft\Http\Status::CREATED);
37 | * },
38 | * MyHttpException::class => static fn (MyHttpException $exception) => new MyResponse($exception),
39 | * ]
40 | * ```
41 | *
42 | * When an exception is thrown, the map in int format allows to send the response with set http code.
43 | * If specified exception classes are equal, then the first one will be processed. Below are some examples:
44 | *
45 | * ```php
46 | * $exceptionMap = [
47 | * \DomainException::class => \Yiisoft\Http\Status::BAD_REQUEST,
48 | * \InvalidArgumentException::class => \Yiisoft\Http\Status::BAD_REQUEST,
49 | * MyNotFoundException::class => \Yiisoft\Http\Status::NOT_FOUND,
50 | * ]
51 | * ```
52 | *
53 | * @param callable[]|int[] $exceptionMap A callable that must return a `ResponseInterface` or response status code.
54 | * @param bool $checkResponseBody Whether executing `getBody()` on response needs to be done. It's useful for
55 | * catching exceptions that can be thrown in the process of body generation.
56 | */
57 | public function __construct(
58 | private readonly array $exceptionMap,
59 | private readonly ResponseFactoryInterface $responseFactory,
60 | private readonly Injector $injector,
61 | private readonly bool $checkResponseBody = false,
62 | ) {
63 | }
64 |
65 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
66 | {
67 | try {
68 | $response = $handler->handle($request);
69 | if ($this->checkResponseBody) {
70 | $response->getBody();
71 | }
72 | } catch (Throwable $t) {
73 | foreach ($this->exceptionMap as $exceptionType => $responseHandler) {
74 | if ($t instanceof $exceptionType) {
75 | if (is_int($responseHandler)) {
76 | return $this->responseFactory->createResponse($responseHandler);
77 | }
78 |
79 | if (is_callable($responseHandler)) {
80 | /** @var ResponseInterface */
81 | return $this->injector->invoke($responseHandler, ['exception' => $t]);
82 | }
83 | }
84 | }
85 | throw $t;
86 | }
87 |
88 | return $response;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Renderer/HeaderRenderer.php:
--------------------------------------------------------------------------------
1 | addContentTypeHeader([
31 | 'X-Error-Message' => self::DEFAULT_ERROR_MESSAGE,
32 | ]),
33 | );
34 | }
35 |
36 | public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
37 | {
38 | return new ErrorData(
39 | '',
40 | $this->addContentTypeHeader([
41 | 'X-Error-Type' => $t::class,
42 | 'X-Error-Message' => $t->getMessage(),
43 | 'X-Error-Code' => (string) $t->getCode(),
44 | 'X-Error-File' => $t->getFile(),
45 | 'X-Error-Line' => (string) $t->getLine(),
46 | ]),
47 | );
48 | }
49 |
50 | /**
51 | * @param array $headers
52 | * @return array
53 | */
54 | private function addContentTypeHeader(array $headers): array
55 | {
56 | if ($this->contentType !== null) {
57 | $headers[Header::CONTENT_TYPE] = $this->contentType;
58 | }
59 | return $headers;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Renderer/HtmlRenderer.php:
--------------------------------------------------------------------------------
1 | {icon}
98 | * ```
99 | */
100 | private readonly ?string $traceHeaderLine;
101 |
102 | /**
103 | * @var string[]|null The list of vendor paths is determined automatically.
104 | *
105 | * One path if the error handler is installed as a vendor package, or a list of package vendor paths
106 | * if the error handler is installed for development in {@link https://github.com/yiisoft/yii-dev-tool}.
107 | */
108 | private ?array $vendorPaths = null;
109 |
110 | /**
111 | * @param array $settings (deprecated) Settings can have the following keys:
112 | * - template: string, full path of the template file for rendering exceptions without call stack information.
113 | * - verboseTemplate: string, full path of the template file for rendering exceptions with call stack information.
114 | * - maxSourceLines: int, maximum number of source code lines to be displayed. Defaults to 19.
115 | * - maxTraceLines: int, maximum number of trace source code lines to be displayed. Defaults to 13.
116 | * - traceHeaderLine: string, trace header line with placeholders to be substituted. Defaults to null.
117 | * @param string|null $template The full path of the template file for rendering exceptions without call stack
118 | * information.
119 | * @param string|null $verboseTemplate The full path of the template file for rendering exceptions with call stack
120 | * information.
121 | * @param int|null $maxSourceLines The maximum number of source code lines to be displayed. Defaults to 19.
122 | * @param int|null $maxTraceLines The maximum number of trace source code lines to be displayed. Defaults to 13.
123 | * @param string|null $traceHeaderLine The trace header line with placeholders to be substituted. Defaults to null.
124 | *
125 | * @psalm-param array{
126 | * template?: string,
127 | * verboseTemplate?: string,
128 | * maxSourceLines?: int,
129 | * maxTraceLines?: int,
130 | * traceHeaderLine?: string,
131 | * } $settings
132 | */
133 | public function __construct(
134 | array $settings = [],
135 | ?string $template = null,
136 | ?string $verboseTemplate = null,
137 | ?int $maxSourceLines = null,
138 | ?int $maxTraceLines = null,
139 | ?string $traceHeaderLine = null,
140 | ) {
141 | $this->markdownParser = new GithubMarkdown();
142 | $this->markdownParser->html5 = true;
143 |
144 | $this->defaultTemplatePath = dirname(__DIR__, 2) . '/templates';
145 | $this->template = $template
146 | ?? $settings['template']
147 | ?? $this->defaultTemplatePath . '/production.php';
148 | $this->verboseTemplate = $verboseTemplate
149 | ?? $settings['verboseTemplate']
150 | ?? $this->defaultTemplatePath . '/development.php';
151 | $this->maxSourceLines = $maxSourceLines
152 | ?? $settings['maxSourceLines']
153 | ?? 19;
154 | $this->maxTraceLines = $maxTraceLines
155 | ?? $settings['maxTraceLines']
156 | ?? 13;
157 | $this->traceHeaderLine = $traceHeaderLine
158 | ?? $settings['traceHeaderLine']
159 | ?? null;
160 | }
161 |
162 | public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
163 | {
164 | return new ErrorData(
165 | $this->renderTemplate($this->template, [
166 | 'request' => $request,
167 | 'throwable' => $t,
168 | ]),
169 | [Header::CONTENT_TYPE => self::CONTENT_TYPE],
170 | );
171 | }
172 |
173 | public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
174 | {
175 | return new ErrorData(
176 | $this->renderTemplate($this->verboseTemplate, [
177 | 'request' => $request,
178 | 'throwable' => $t,
179 | ]),
180 | [Header::CONTENT_TYPE => self::CONTENT_TYPE],
181 | );
182 | }
183 |
184 | /**
185 | * Encodes special characters into HTML entities for use as a content.
186 | *
187 | * @param string $content The content to be encoded.
188 | *
189 | * @return string Encoded content.
190 | */
191 | public function htmlEncode(string $content): string
192 | {
193 | return htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
194 | }
195 |
196 | public function parseMarkdown(string $content): string
197 | {
198 | $html = $this->markdownParser->parse($content);
199 | /**
200 | * @psalm-suppress InvalidArgument
201 | *
202 | * @link https://github.com/vimeo/psalm/issues/4317
203 | */
204 | return strip_tags($html, [
205 | 'h1',
206 | 'h2',
207 | 'h3',
208 | 'h4',
209 | 'h5',
210 | 'h6',
211 | 'hr',
212 | 'pre',
213 | 'code',
214 | 'blockquote',
215 | 'table',
216 | 'tr',
217 | 'td',
218 | 'th',
219 | 'thead',
220 | 'tbody',
221 | 'strong',
222 | 'em',
223 | 'b',
224 | 'i',
225 | 'u',
226 | 's',
227 | 'span',
228 | 'a',
229 | 'p',
230 | 'br',
231 | 'nobr',
232 | 'ul',
233 | 'ol',
234 | 'li',
235 | 'img',
236 | ]);
237 | }
238 |
239 | /**
240 | * Renders the previous exception stack for a given Exception.
241 | *
242 | * @param Throwable $t The exception whose precursors should be rendered.
243 | *
244 | * @throws Throwable
245 | *
246 | * @return string HTML content of the rendered previous exceptions. Empty string if there are none.
247 | */
248 | public function renderPreviousExceptions(Throwable $t): string
249 | {
250 | $templatePath = $this->defaultTemplatePath . '/_previous-exception.php';
251 |
252 | if ($t instanceof CompositeException) {
253 | $result = [];
254 | foreach ($t->getPreviousExceptions() as $exception) {
255 | $result[] = $this->renderTemplate($templatePath, ['throwable' => $exception]);
256 | }
257 | return implode('', $result);
258 | }
259 | if ($t->getPrevious() !== null) {
260 | return $this->renderTemplate($templatePath, ['throwable' => $t->getPrevious()]);
261 | }
262 |
263 | return '';
264 | }
265 |
266 | /**
267 | * Renders call stack.
268 | *
269 | * @param Throwable $t The exception to get call stack from.
270 | *
271 | * @throws Throwable
272 | *
273 | * @return string HTML content of the rendered call stack.
274 | *
275 | * @psalm-param DebugBacktraceType $trace
276 | */
277 | public function renderCallStack(Throwable $t, array $trace = []): string
278 | {
279 | $application = $vendor = [];
280 | $application[1] = $this->renderCallStackItem(
281 | $t->getFile(),
282 | $t->getLine(),
283 | null,
284 | null,
285 | [],
286 | 1,
287 | false,
288 | [],
289 | );
290 |
291 | $index = 1;
292 | if ($t instanceof ErrorException) {
293 | $index = 0;
294 | }
295 |
296 | foreach ($trace as $traceItem) {
297 | $file = !empty($traceItem['file']) ? $traceItem['file'] : null;
298 | $line = !empty($traceItem['line']) ? $traceItem['line'] : null;
299 | $class = !empty($traceItem['class']) ? $traceItem['class'] : null;
300 | $args = !empty($traceItem['args']) ? $traceItem['args'] : [];
301 |
302 | $parameters = [];
303 | $function = null;
304 | if (!empty($traceItem['function']) && $traceItem['function'] !== 'unknown') {
305 | $function = $traceItem['function'];
306 | if (!str_contains($function, '{closure}')) {
307 | try {
308 | if ($class !== null && class_exists($class)) {
309 | $parameters = (new \ReflectionMethod($class, $function))->getParameters();
310 | } elseif (function_exists($function)) {
311 | $parameters = (new \ReflectionFunction($function))->getParameters();
312 | }
313 | } catch (\ReflectionException) {
314 | // pass
315 | }
316 | }
317 | }
318 | $index++;
319 |
320 | if ($this->isVendorFile($file)) {
321 | $vendor[$index] = $this->renderCallStackItem(
322 | $file,
323 | $line,
324 | $class,
325 | $function,
326 | $args,
327 | $index,
328 | true,
329 | $parameters,
330 | );
331 | } else {
332 | $application[$index] = $this->renderCallStackItem(
333 | $file,
334 | $line,
335 | $class,
336 | $function,
337 | $args,
338 | $index,
339 | false,
340 | $parameters,
341 | );
342 | }
343 | }
344 |
345 | return $this->renderTemplate($this->defaultTemplatePath . '/_call-stack-items.php', [
346 | 'applicationItems' => $application,
347 | 'vendorItemGroups' => $this->groupVendorCallStackItems($vendor),
348 | ]);
349 | }
350 |
351 | /**
352 | * Converts arguments array to its string representation.
353 | *
354 | * @param array $args arguments array to be converted
355 | *
356 | * @return string The string representation of the arguments array.
357 | */
358 | public function argumentsToString(array $args, bool $truncate = true): string
359 | {
360 | $count = 0;
361 | $isAssoc = $args !== array_values($args);
362 |
363 | /**
364 | * @var mixed $value
365 | */
366 | foreach ($args as $key => $value) {
367 | $count++;
368 |
369 | if ($truncate && $count >= 5) {
370 | if ($count > 5) {
371 | unset($args[$key]);
372 | } else {
373 | $args[$key] = '...';
374 | }
375 | continue;
376 | }
377 |
378 | if (is_object($value)) {
379 | $args[$key] = '' . $this->htmlEncode($this->removeAnonymous($value::class) . '#' . spl_object_id($value)) . ' ';
380 | } elseif (is_bool($value)) {
381 | $args[$key] = '' . ($value ? 'true' : 'false') . ' ';
382 | } elseif (is_string($value)) {
383 | $fullValue = $this->htmlEncode($value);
384 | if ($truncate && mb_strlen($value, 'UTF-8') > 32) {
385 | $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
386 | $args[$key] = "'$displayValue' ";
387 | } else {
388 | $args[$key] = "'$fullValue' ";
389 | }
390 | } elseif (is_array($value)) {
391 | unset($args[$key]);
392 | $args[$key] = '[' . $this->argumentsToString($value, $truncate) . ']';
393 | } elseif ($value === null) {
394 | $args[$key] = 'null ';
395 | } elseif (is_resource($value)) {
396 | $args[$key] = 'resource ';
397 | } else {
398 | $args[$key] = '' . (string) $value . ' ';
399 | }
400 |
401 | if (is_string($key)) {
402 | $args[$key] = '\'' . $this->htmlEncode($key) . "' => $args[$key]";
403 | } elseif ($isAssoc) {
404 | $args[$key] = "$key => $args[$key]";
405 | }
406 | }
407 |
408 | /** @var string[] $args */
409 |
410 | ksort($args);
411 | return implode(', ', $args);
412 | }
413 |
414 | /**
415 | * Renders the information about request.
416 | *
417 | * @return string The rendering result.
418 | */
419 | public function renderRequest(ServerRequestInterface $request): string
420 | {
421 | $output = $request->getMethod() . ' ' . $request->getUri() . "\n";
422 |
423 | $headers = $request->getHeaders();
424 | unset($headers['Host']);
425 | ksort($headers);
426 |
427 | foreach ($headers as $name => $values) {
428 | foreach ($values as $value) {
429 | $output .= "$name: $value\n";
430 | }
431 | }
432 |
433 | $body = (string)$request->getBody();
434 | if (!empty($body)) {
435 | $output .= "\n" . $body . "\n\n";
436 | }
437 |
438 | return $output;
439 | }
440 |
441 | /**
442 | * Renders the information about curl request.
443 | *
444 | * @return string The rendering result.
445 | */
446 | public function renderCurl(ServerRequestInterface $request): string
447 | {
448 | try {
449 | $output = (new Command())
450 | ->setRequest($request)
451 | ->build();
452 | } catch (Throwable $e) {
453 | return 'Error generating curl command: ' . $e->getMessage();
454 | }
455 |
456 | return $output;
457 | }
458 |
459 | /**
460 | * Creates string containing HTML link which refers to the home page
461 | * of determined web-server software and its full name.
462 | *
463 | * @return string The server software information hyperlink.
464 | */
465 | public function createServerInformationLink(ServerRequestInterface $request): string
466 | {
467 | $serverSoftware = (string) ($request->getServerParams()['SERVER_SOFTWARE'] ?? '');
468 |
469 | if ($serverSoftware === '') {
470 | return '';
471 | }
472 |
473 | $serverUrls = [
474 | 'https://httpd.apache.org/' => ['apache'],
475 | 'https://nginx.org/' => ['nginx'],
476 | 'https://lighttpd.net/' => ['lighttpd'],
477 | 'https://iis.net/' => ['iis', 'services'],
478 | 'https://www.php.net/manual/en/features.commandline.webserver.php' => ['development'],
479 | ];
480 |
481 | foreach ($serverUrls as $url => $keywords) {
482 | foreach ($keywords as $keyword) {
483 | if (stripos($serverSoftware, $keyword) !== false) {
484 | return ''
485 | . $this->htmlEncode($serverSoftware) . ' ';
486 | }
487 | }
488 | }
489 |
490 | return '';
491 | }
492 |
493 | /**
494 | * Returns the name of the throwable instance.
495 | *
496 | * @return string The name of the throwable instance.
497 | */
498 | public function getThrowableName(Throwable $throwable): string
499 | {
500 | $name = $throwable::class;
501 |
502 | if ($throwable instanceof FriendlyExceptionInterface) {
503 | $name = $throwable->getName() . ' (' . $name . ')';
504 | }
505 |
506 | return $name;
507 | }
508 |
509 | /**
510 | * Renders a template.
511 | *
512 | * @param string $path The full path of the template file for rendering.
513 | * @param array $parameters The name-value pairs that will be extracted and made available in the template file.
514 | *
515 | * @throws Throwable
516 | *
517 | * @return string The rendering result.
518 | *
519 | * @psalm-suppress PossiblyInvalidFunctionCall
520 | * @psalm-suppress PossiblyFalseArgument
521 | * @psalm-suppress UnresolvableInclude
522 | */
523 | private function renderTemplate(string $path, array $parameters): string
524 | {
525 | if (!file_exists($path)) {
526 | throw new RuntimeException("Template not found at $path");
527 | }
528 |
529 | $renderer = function (): void {
530 | /** @psalm-suppress MixedArgument */
531 | extract(func_get_arg(1), EXTR_OVERWRITE);
532 | require func_get_arg(0);
533 | };
534 |
535 | $obInitialLevel = ob_get_level();
536 | ob_start();
537 | ob_implicit_flush(false);
538 |
539 | try {
540 | /** @psalm-suppress PossiblyNullFunctionCall */
541 | $renderer->bindTo($this)($path, $parameters);
542 | return (string) ob_get_clean();
543 | } catch (Throwable $e) {
544 | while (ob_get_level() > $obInitialLevel) {
545 | if (!@ob_end_clean()) {
546 | ob_clean();
547 | }
548 | }
549 | throw $e;
550 | }
551 | }
552 |
553 | /**
554 | * Renders a single call stack element.
555 | *
556 | * @param string|null $file The name where call has happened.
557 | * @param int|null $line The number on which call has happened.
558 | * @param string|null $class The called class name.
559 | * @param string|null $function The called function/method name.
560 | * @param array $args The array of method arguments.
561 | * @param int $index The number of the call stack element.
562 | * @param bool $isVendorFile Whether given name of the file belongs to the vendor package.
563 | *
564 | * @throws Throwable
565 | *
566 | * @return string HTML content of the rendered call stack element.
567 | */
568 | private function renderCallStackItem(
569 | ?string $file,
570 | ?int $line,
571 | ?string $class,
572 | ?string $function,
573 | array $args,
574 | int $index,
575 | bool $isVendorFile,
576 | array $reflectionParameters,
577 | ): string {
578 | $lines = [];
579 | $begin = $end = 0;
580 |
581 | if ($file !== null && $line !== null) {
582 | $line--; // adjust line number from one-based to zero-based
583 | $lines = @file($file);
584 | if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
585 | return '';
586 | }
587 | $half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
588 | $begin = $line - $half > 0 ? $line - $half : 0;
589 | $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
590 | }
591 |
592 | return $this->renderTemplate($this->defaultTemplatePath . '/_call-stack-item.php', [
593 | 'file' => $file,
594 | 'line' => $line,
595 | 'class' => $class,
596 | 'function' => $function,
597 | 'index' => $index,
598 | 'lines' => $lines,
599 | 'begin' => $begin,
600 | 'end' => $end,
601 | 'args' => $args,
602 | 'isVendorFile' => $isVendorFile,
603 | 'reflectionParameters' => $reflectionParameters,
604 | ]);
605 | }
606 |
607 | /**
608 | * Groups a vendor call stack items to render.
609 | *
610 | * @param array $items The list of the vendor call stack items.
611 | *
612 | * @return array> The grouped items of the vendor call stack.
613 | */
614 | private function groupVendorCallStackItems(array $items): array
615 | {
616 | $groupIndex = null;
617 | $groupedItems = [];
618 |
619 | foreach ($items as $index => $item) {
620 | if ($groupIndex === null) {
621 | $groupIndex = $index;
622 | $groupedItems[$groupIndex][$index] = $item;
623 | continue;
624 | }
625 |
626 | if (isset($items[$index - 1])) {
627 | $groupedItems[$groupIndex][$index] = $item;
628 | continue;
629 | }
630 |
631 | $groupIndex = $index;
632 | $groupedItems[$groupIndex][$index] = $item;
633 | }
634 |
635 | /** @psalm-var array> $groupedItems It's needed for Psalm <=4.30 only. */
636 |
637 | return $groupedItems;
638 | }
639 |
640 | /**
641 | * Determines whether given name of the file belongs to the vendor package.
642 | *
643 | * @param string|null $file The name to be checked.
644 | *
645 | * @return bool Whether given name of the file belongs to the vendor package.
646 | */
647 | private function isVendorFile(?string $file): bool
648 | {
649 | if ($file === null) {
650 | return false;
651 | }
652 |
653 | $file = realpath($file);
654 |
655 | if ($file === false) {
656 | return false;
657 | }
658 |
659 | foreach ($this->getVendorPaths() as $vendorPath) {
660 | if (str_starts_with($file, $vendorPath)) {
661 | return true;
662 | }
663 | }
664 |
665 | return false;
666 | }
667 |
668 | /**
669 | * Returns a list of vendor paths.
670 | *
671 | * @return string[] The list of vendor paths.
672 | *
673 | * @see $vendorPaths
674 | */
675 | private function getVendorPaths(): array
676 | {
677 | if ($this->vendorPaths !== null) {
678 | return $this->vendorPaths;
679 | }
680 |
681 | $rootPath = dirname(__DIR__, 4);
682 |
683 | // If the error handler is installed as a vendor package.
684 | /** @psalm-suppress InvalidLiteralArgument It is Psalm bug, {@see https://github.com/vimeo/psalm/issues/9196} */
685 | if (strlen($rootPath) > 6 && str_contains($rootPath, 'vendor')) {
686 | $this->vendorPaths = [$rootPath];
687 | return $this->vendorPaths;
688 | }
689 |
690 | // If the error handler is installed for development in `yiisoft/yii-dev-tool`.
691 | if (is_file("{$rootPath}/yii-dev") || is_file("{$rootPath}/yii-dev.bat")) {
692 | $vendorPaths = glob("{$rootPath}/dev/*/vendor");
693 | /** @var string[] */
694 | $this->vendorPaths = empty($vendorPaths) ? [] : str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $vendorPaths);
695 | return $this->vendorPaths;
696 | }
697 |
698 | $this->vendorPaths = [];
699 | return $this->vendorPaths;
700 | }
701 |
702 | public function removeAnonymous(string $value): string
703 | {
704 | $anonymousPosition = strpos($value, '@anonymous');
705 |
706 | return $anonymousPosition !== false ? substr($value, 0, $anonymousPosition) : $value;
707 | }
708 | }
709 |
--------------------------------------------------------------------------------
/src/Renderer/JsonRenderer.php:
--------------------------------------------------------------------------------
1 | self::DEFAULT_ERROR_MESSAGE,
28 | ],
29 | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
30 | ),
31 | [Header::CONTENT_TYPE => self::CONTENT_TYPE],
32 | );
33 | }
34 |
35 | public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
36 | {
37 | return new ErrorData(
38 | json_encode(
39 | [
40 | 'type' => $t::class,
41 | 'message' => $t->getMessage(),
42 | 'code' => $t->getCode(),
43 | 'file' => $t->getFile(),
44 | 'line' => $t->getLine(),
45 | 'trace' => $t->getTrace(),
46 | ],
47 | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR
48 | ),
49 | [Header::CONTENT_TYPE => self::CONTENT_TYPE],
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Renderer/PlainTextRenderer.php:
--------------------------------------------------------------------------------
1 | $this->contentType],
30 | );
31 | }
32 |
33 | public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
34 | {
35 | return new ErrorData(
36 | self::throwableToString($t),
37 | [Header::CONTENT_TYPE => $this->contentType],
38 | );
39 | }
40 |
41 | public static function throwableToString(Throwable $t): string
42 | {
43 | return sprintf(
44 | <<getMessage(),
54 | $t->getFile(),
55 | $t->getLine(),
56 | $t->getTraceAsString()
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Renderer/XmlRenderer.php:
--------------------------------------------------------------------------------
1 | ';
25 | $content .= "\n\n";
26 | $content .= $this->tag('message', self::DEFAULT_ERROR_MESSAGE);
27 | $content .= ' ';
28 | return new ErrorData(
29 | $content,
30 | [Header::CONTENT_TYPE => self::CONTENT_TYPE],
31 | );
32 | }
33 |
34 | public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
35 | {
36 | $content = '';
37 | $content .= "\n\n";
38 | $content .= $this->tag('type', $t::class);
39 | $content .= $this->tag('message', $this->cdata($t->getMessage()));
40 | $content .= $this->tag('code', $this->cdata((string) $t->getCode()));
41 | $content .= $this->tag('file', $t->getFile());
42 | $content .= $this->tag('line', (string) $t->getLine());
43 | $content .= $this->tag('trace', $t->getTraceAsString());
44 | $content .= ' ';
45 | return new ErrorData(
46 | $content,
47 | [Header::CONTENT_TYPE => self::CONTENT_TYPE],
48 | );
49 | }
50 |
51 | private function tag(string $name, string $value): string
52 | {
53 | return "<$name>" . $value . "$name>\n";
54 | }
55 |
56 | private function cdata(string $value): string
57 | {
58 | return '', ']]]]>', $value) . ']]>';
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/RendererProvider/ClosureRendererProvider.php:
--------------------------------------------------------------------------------
1 | |ThrowableRendererInterface|null)
18 | */
19 | final class ClosureRendererProvider implements RendererProviderInterface
20 | {
21 | /**
22 | * @psalm-param TClosure $closure
23 | */
24 | public function __construct(
25 | private readonly Closure $closure,
26 | private readonly ContainerInterface $container,
27 | ) {
28 | }
29 |
30 | public function get(ServerRequestInterface $request): ?ThrowableRendererInterface
31 | {
32 | $result = ($this->closure)($request);
33 |
34 | if (is_string($result)) {
35 | /** @var ThrowableRendererInterface */
36 | return $this->container->get($result);
37 | }
38 |
39 | return $result;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/RendererProvider/CompositeRendererProvider.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | private readonly array $providers;
19 |
20 | /**
21 | * @no-named-arguments
22 | */
23 | public function __construct(RendererProviderInterface ...$providers)
24 | {
25 | $this->providers = $providers;
26 | }
27 |
28 | public function get(ServerRequestInterface $request): ?ThrowableRendererInterface
29 | {
30 | foreach ($this->providers as $provider) {
31 | $renderer = $provider->get($request);
32 | if ($renderer !== null) {
33 | return $renderer;
34 | }
35 | }
36 |
37 | return null;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/RendererProvider/ContentTypeRendererProvider.php:
--------------------------------------------------------------------------------
1 | >
27 | */
28 | private readonly array $renderers;
29 |
30 | /**
31 | * @psalm-param array>|null $renderers
32 | */
33 | public function __construct(
34 | private readonly ContainerInterface $container,
35 | ?array $renderers = null,
36 | ) {
37 | $this->renderers = $renderers ?? [
38 | 'application/json' => JsonRenderer::class,
39 | 'application/xml' => XmlRenderer::class,
40 | 'text/xml' => XmlRenderer::class,
41 | 'text/plain' => PlainTextRenderer::class,
42 | 'text/html' => HtmlRenderer::class,
43 | '*/*' => HtmlRenderer::class,
44 | ];
45 | }
46 |
47 | public function get(ServerRequestInterface $request): ?ThrowableRendererInterface
48 | {
49 | $rendererClass = $this->selectRendererClass($request);
50 | if ($rendererClass === null) {
51 | return null;
52 | }
53 |
54 | /** @var ThrowableRendererInterface */
55 | return $this->container->get($rendererClass);
56 | }
57 |
58 | /**
59 | * @psalm-return class-string|null
60 | */
61 | private function selectRendererClass(ServerRequestInterface $request): ?string
62 | {
63 | $acceptHeader = $request->getHeader(Header::ACCEPT);
64 |
65 | try {
66 | $contentTypes = HeaderValueHelper::getSortedAcceptTypes($acceptHeader);
67 | } catch (InvalidArgumentException) {
68 | // The "Accept" header contains an invalid "q" factor.
69 | return null;
70 | }
71 |
72 | foreach ($contentTypes as $contentType) {
73 | if (array_key_exists($contentType, $this->renderers)) {
74 | return $this->renderers[$contentType];
75 | }
76 | }
77 |
78 | return null;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/RendererProvider/HeadRendererProvider.php:
--------------------------------------------------------------------------------
1 | getMethod() === Method::HEAD) {
23 | return new HeaderRenderer(
24 | $this->getAcceptContentType($request),
25 | );
26 | }
27 |
28 | return null;
29 | }
30 |
31 | private function getAcceptContentType(ServerRequestInterface $request): ?string
32 | {
33 | $acceptHeader = $request->getHeader(Header::ACCEPT);
34 |
35 | try {
36 | $contentTypes = HeaderValueHelper::getSortedAcceptTypes($acceptHeader);
37 | } catch (InvalidArgumentException) {
38 | // The "Accept" header contains an invalid "q" factor.
39 | return null;
40 | }
41 |
42 | return empty($contentTypes) ? null : reset($contentTypes);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/RendererProvider/RendererProviderInterface.php:
--------------------------------------------------------------------------------
1 | headersProvider = $headersProvider ?? new HeadersProvider();
28 | }
29 |
30 | public function create(Throwable $throwable, ServerRequestInterface $request): ResponseInterface
31 | {
32 | $renderer = $this->rendererProvider->get($request);
33 |
34 | $response = $this->responseFactory->createResponse(Status::INTERNAL_SERVER_ERROR);
35 | foreach ($this->headersProvider->getAll() as $name => $value) {
36 | $response = $response->withHeader($name, $value);
37 | }
38 |
39 | return $this->errorHandler
40 | ->handle($throwable, $renderer, $request)
41 | ->addToResponse($response);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/ThrowableResponseFactoryInterface.php:
--------------------------------------------------------------------------------
1 |
22 | Open the target page
23 |
24 |
25 |
26 | HTML;
27 | ?>
28 |
30 |
31 |
32 |
33 |
34 | = "{$index}. in {$this->htmlEncode($file)}" ?>
35 | traceHeaderLine !== null): ?>
36 | = strtr($this->traceHeaderLine, ['{file}' => $file, '{line}' => $line + 1, '{icon}' => $icon]) ?>
37 |
38 |
39 |
40 |
41 |
42 |
43 | removeAnonymous($class)}::$function";
46 |
47 | echo '' . $this->htmlEncode($function) . ' ';
48 | echo '(';
49 | echo '' . $this->argumentsToString($args, true) . ' ';
50 | echo '' . $this->argumentsToString($args, false) . ' ';
51 | echo ') ';
52 | ?>
53 |
54 |
55 |
56 |
57 |
58 |
59 | = sprintf('at line %d', $line + 1) ?>
60 |
61 | arguments
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | = $this->htmlEncode($function) ?> arguments:
70 |
71 |
72 | $argument) { ?>
73 |
74 |
75 | $= $this->htmlEncode(
76 | is_int($key) && isset($reflectionParameters[$key])
77 | ? $reflectionParameters[$key]->getName()
78 | : $key
79 | ) ?>
80 |
81 |
82 | = gettype($argument) ?>
83 |
84 |
85 | = $this->argumentsToString(is_array($argument) ? $argument : [$argument]) ?>
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
= (int) ($i + 1) ?>
99 |
htmlEncode($lines[$i]);
103 | }
104 | ?>
105 |
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/templates/_call-stack-items.php:
--------------------------------------------------------------------------------
1 | */
3 | /* @var $vendorItemGroups array> */
4 | /* @var $this \Yiisoft\ErrorHandler\Renderer\HtmlRenderer */
5 | $insertItem = static function (array &$items, array $item, int $offset = 0): void {
6 | $itemIndex = array_key_first($item);
7 | foreach (array_keys($items) as $index) {
8 | $offset++;
9 | if ($index === ($itemIndex - 1)) {
10 | break;
11 | }
12 | }
13 | $items = array_slice($items, 0, $offset, true) + $item + array_slice($items, $offset, null, true);
14 | };
15 | foreach ($vendorItemGroups as $key => $vendorItemGroup) {
16 | $count = count($vendorItemGroup);
17 | if ($count === 0) {
18 | continue;
19 | }
20 | if ($count === 1) {
21 | $insertItem($applicationItems, $vendorItemGroup);
22 | continue;
23 | }
24 | $firstIndex = array_key_first($vendorItemGroup);
25 | $lastIndex = array_key_last($vendorItemGroup);
26 | $itemsContent = implode('', $vendorItemGroup);
27 | $itemGroupContent = <<
29 |
30 |
31 | +
32 | {$firstIndex} - {$lastIndex} Vendor package files ({$count})
33 |
34 |
35 |
36 | {$itemsContent}
37 |
38 |
39 | HTML;
40 | $insertItem($applicationItems, [$firstIndex => $itemGroupContent]);
41 | }
42 | ?>
43 |
44 |
45 | = $applicationItem ?>
46 |
47 |
48 |
--------------------------------------------------------------------------------
/templates/_previous-exception.php:
--------------------------------------------------------------------------------
1 |
5 |
6 |
↵
7 |
8 |
9 | Caused by:
10 | = $this->htmlEncode(get_class($throwable)) ?>
11 |
12 |
= nl2br($this->htmlEncode($throwable->getMessage())) ?>
13 |
in = $this->htmlEncode($throwable->getFile()) ?> at line = $throwable->getLine() ?>
14 | = $this->renderPreviousExceptions($throwable) ?>
15 |
16 |
17 |
--------------------------------------------------------------------------------
/templates/development.css:
--------------------------------------------------------------------------------
1 | /* reset */
2 | html,
3 | body,
4 | div,
5 | span,
6 | p,
7 | pre,
8 | a,
9 | code,
10 | em,
11 | img,
12 | strong,
13 | b,
14 | i,
15 | ul,
16 | li {
17 | margin: 0;
18 | padding: 0;
19 | border: 0;
20 | font: inherit;
21 | vertical-align: baseline;
22 | }
23 |
24 | body {
25 | font-family: 'Roboto', sans-serif;
26 | min-width: 800px;
27 | color: var(--page-text-color);
28 | background: var(--page-bg-color);
29 | line-height: 1;
30 | }
31 |
32 | ul {
33 | list-style: none;
34 | }
35 | /* end reset */
36 |
37 |
38 | /* light theme */
39 | :root {
40 | --page-bg-color: #fff;
41 | --page-text-color: #505050;
42 | --page-text-muted-color: #888;
43 | --icon-color: #505050;
44 | --icon-hover-color: #000;
45 | --table-line-even-bg: #fff;
46 | --table-line-even-color: #141414;
47 | --table-line-odd-bg: #eee;
48 | --table-line-odd-color: #141414;
49 | --table-line-hover: #ccc;
50 | --button-bg: #e3e5e7;
51 | --button-color: #333;
52 | --button-bg-hover: #c4c7cb;
53 | --button-color-hover: #333;
54 | }
55 |
56 | .table {
57 | border-collapse: collapse;
58 | width: 100%;
59 | }
60 |
61 | .table td, .table th {
62 | border: 1px solid #ddd;
63 | padding: 8px;
64 | }
65 |
66 | .table tr:nth-child(odd) {
67 | border-color: var(--table-line-odd-bg);
68 | background-color: var(--table-line-odd-bg);
69 | color: var(--table-line-odd-color);
70 | }
71 |
72 | .table tr:nth-child(even) {
73 | border-color: var(--table-line-even-bg);
74 | background-color: var(--table-line-even-bg);
75 | color: var(--table-line-even-color);
76 | }
77 |
78 | .table tr:hover {
79 | background-color: var(--table-line-hover);
80 | }
81 |
82 |
83 | .button {
84 | padding: 4px 8px;
85 | font-size: 14px;
86 | box-sizing: border-box;
87 | border: 0;
88 | background: var(--button-bg);
89 | color: var(--button-color);
90 | border-radius: 6px;
91 | cursor: pointer;
92 | }
93 | .button:hover {
94 | background: var(--button-bg-hover);
95 | color: var(--button-color-hover);
96 | }
97 |
98 | .functionArguments {
99 | padding: 20px 0;
100 | border-top: 1px solid var(--element-wrap-border-color);
101 | --function-arguments-bg: #f8f8f8;
102 | --function-arguments-border-color: #e3e3e3;
103 | }
104 | .dark-theme .functionArguments {
105 | --function-arguments-bg: #303030;
106 | --function-arguments-border-color: #272727;
107 | }
108 | .functionArguments_title {
109 | padding: 0 30px 12px;
110 | color: var(--page-text-muted-color);
111 | }
112 | .functionArguments table {
113 | padding: 4px 0;
114 | width: 100%;
115 | background: var(--function-arguments-bg);
116 | border-collapse: collapse;
117 | }
118 | .functionArguments td {
119 | padding: 12px 8px;
120 | border-top: 1px solid var(--function-arguments-border-color);
121 | }
122 | .functionArguments tr:first-child td {
123 | border-top: 0;
124 | }
125 | .functionArguments_key {
126 | padding-left: 30px !important;
127 | white-space: nowrap;
128 | }
129 | .functionArguments_type {
130 | color: var(--page-text-muted-color);
131 | white-space: nowrap;
132 | }
133 | .functionArguments_value {
134 | width: 100%;
135 | padding-right: 30px !important;
136 | }
137 | .toggleFunctionArguments {
138 | margin-top: 4px;
139 | }
140 |
141 | header {
142 | --header-bg-color: #ededed;
143 | --previous-text-color: inherit;
144 | --previous-arrow-color: #e51717;
145 | }
146 |
147 | header .solution {
148 | --text-color: var(--page-text-color);
149 | --link-color: #00617b;
150 | --link-hover-color: #1191b3;
151 | --blockquote-text-color: #707070;
152 | --blockquote-border-color: #e4e4e4;
153 | --code-bg-color: #f3f3f3;
154 | --pre-bg-color: #f3f3f3;
155 | --table-border-color: #e4e4e4;
156 | --separator-color: #ddd;
157 | }
158 |
159 | .exception-card {
160 | --exception-card-bg-color: #fafafa;
161 | --exception-card-border-color: #d83c24;
162 | --exception-class-text-color: #e51717;
163 | --exception-class-friendly-text-color: #E79185;
164 | --exception-class-friendly-link-color: #E57373;
165 | --exception-message-text-color: #4b4b4b;
166 | }
167 |
168 | .call-stack {
169 | --bg-color: #fff;
170 | --border-color: #d0d0d0;
171 | --box-shadow: 0 15px 20px rgba(0, 0, 0, 0.05);
172 | --link-color: #505050;
173 | --error-line-bg-color: #ffebeb;
174 | --hover-line-bg-color: #edf9ff;
175 | --element-wrap-border-color: #d0d0d0;
176 | --element-wrap-text-color: #4b4b4b;
177 | --element-wrap-hover-text-color: #086eb6;
178 | --vendor-bg-color: #ededed;
179 | --vendor-border-color: var(--border-color);
180 | --vendor-state-bg-color: #999;
181 | --vendor-content-bg-color: #ededed;
182 | }
183 |
184 | .hljs {
185 | --hljs-text-color: #2f3337;
186 | --hljs-comment-text-color: #656e77;
187 | --hljs-keyword-text-color: #015692;
188 | --hljs-attribute-text-color: #803378;
189 | --hljs-name-text-color: #b75501;
190 | --hljs-string-text-color: #54790d;
191 | --hljs-code-text-color: #535a60;
192 | --hljs-delition-text-color: #c02d2e;
193 | --hljs-addition-text-color: #2f6f44;
194 | }
195 |
196 | /* base */
197 | a {
198 | text-decoration: none;
199 | }
200 |
201 | a:hover {
202 | text-decoration: underline;
203 | }
204 | /* end base */
205 |
206 | /* header */
207 | header {
208 | padding: 65px 100px 270px 100px;
209 | background: var(--header-bg-color);
210 | }
211 |
212 | header .tools {
213 | margin-bottom: 50px;
214 | text-align: right;
215 | }
216 |
217 | header .tools a {
218 | margin-left: 45px;
219 | text-decoration: none;
220 | }
221 |
222 | header .tools a:hover svg path {
223 | fill: var(--icon-hover-color);
224 | }
225 |
226 | header .exception-card {
227 | position: relative;
228 | background: var(--exception-card-bg-color);
229 | border: 2px solid var(--exception-card-border-color);
230 | box-sizing: border-box;
231 | border-radius: 3px;
232 | padding: 40px 30px;
233 | word-break: break-word;
234 | }
235 |
236 | header .exception-class {
237 | padding-right: 114px;
238 | margin-bottom: 30px;
239 | font-weight: 500;
240 | font-size: 36px;
241 | line-height: 42px;
242 | color: var(--exception-class-friendly-text-color);
243 | }
244 |
245 | header .exception-class a {
246 | color: var(--exception-class-friendly-link-color);
247 | }
248 |
249 | header .exception-class span,
250 | header .exception-class span a {
251 | color: var(--exception-class-text-color);
252 | }
253 |
254 | header .exception-message {
255 | font-size: 24px;
256 | line-height: 28px;
257 | color: var(--exception-message-text-color);
258 | }
259 |
260 | header .solution {
261 | margin-top: 24px;
262 | font-size: 16px;
263 | line-height: 22px;
264 | color: var(--text-color);
265 | }
266 |
267 | header .solution h1 {
268 | margin-top: 24px;
269 | font-size: 26px;
270 | line-height: 32px;
271 | font-weight: bold;
272 | }
273 |
274 | header .solution h2 {
275 | margin-top: 24px;
276 | font-size: 22px;
277 | line-height: 28px;
278 | font-weight: bold;
279 | }
280 |
281 | header .solution h3 {
282 | margin-top: 24px;
283 | font-size: 20px;
284 | line-height: 26px;
285 | font-weight: bold;
286 | }
287 |
288 | header .solution h4 {
289 | margin-top: 24px;
290 | font-size: 18px;
291 | line-height: 24px;
292 | font-weight: bold;
293 | }
294 |
295 | header .solution h5 {
296 | margin-top: 24px;
297 | font-size: 16px;
298 | line-height: 22px;
299 | font-weight: bold;
300 | }
301 |
302 | header .solution h6 {
303 | margin-top: 24px;
304 | font-size: 14px;
305 | line-height: 20px;
306 | font-weight: bold;
307 | }
308 |
309 | header .solution p {
310 | margin-top: 16px;
311 | }
312 |
313 | header .solution a {
314 | color: var(--link-color);
315 | text-decoration: underline;
316 | }
317 | header .solution a:hover {
318 | color: var(--link-hover-color);
319 | }
320 |
321 | header .solution h1:first-child,
322 | header .solution h2:first-child,
323 | header .solution h3:first-child,
324 | header .solution h4:first-child,
325 | header .solution h5:first-child,
326 | header .solution h6:first-child,
327 | header .solution p:first-child {
328 | margin-top: 0;
329 | }
330 |
331 | header .solution blockquote {
332 | margin: 18px 0 18px 4px;
333 | padding: 3px 0 2px 16px;
334 | border-left: 4px solid var(--blockquote-border-color);
335 | color: var(--blockquote-text-color);
336 | }
337 |
338 | header .solution ul,
339 | header .solution ol {
340 | padding: 0;
341 | margin: 16px 0 0 32px;
342 | }
343 | header .solution li ul,
344 | header .solution li ol {
345 | margin: 0 0 0 24px;
346 | }
347 |
348 | header .solution li {
349 | margin: 8px 0 0 0;
350 | }
351 |
352 | header .solution ul {
353 | list-style: outside;
354 | }
355 |
356 | header .solution pre,
357 | header .solution code {
358 | font-family: monospace;
359 | }
360 |
361 | header .solution code {
362 | padding: 2px 6px;
363 | font-size: 90%;
364 | background-color: var(--code-bg-color);
365 | border-radius: 6px;
366 | }
367 |
368 | header .solution pre {
369 | margin: 24px 0;
370 | width: 100%;
371 | box-sizing: border-box;
372 | overflow: auto;
373 | padding: 14px;
374 | border-radius: 8px;
375 | background: var(--pre-bg-color);
376 | }
377 | header .solution pre code {
378 | font-size: 100%;
379 | padding: 0;
380 | width: max-content;
381 | }
382 |
383 | header .solution table {
384 | margin: 16px 0 0 0;
385 | border-collapse: collapse;
386 | }
387 | header .solution td,
388 | header .solution th {
389 | padding: 6px 12px;
390 | border: 1px solid var(--table-border-color);
391 | }
392 |
393 | header .solution HR {
394 | margin: 24px 0;
395 | border: 1px solid var(--separator-color);
396 | border-width: 1px 0 0 0;
397 | }
398 |
399 | header .previous {
400 | display: flex;
401 | margin-top: 20px;
402 | color: var(--previous-text-color);
403 | }
404 |
405 | header .previous .arrow {
406 | display: inline-block;
407 | transform: scale(-1, 1);
408 | font-size: 26px;
409 | color: var(--previous-arrow-color);
410 | margin-top: -5px;
411 | margin-right: 10px;
412 | }
413 |
414 | header .previous h2 {
415 | font-size: 20px;
416 | color: #e57373;
417 | margin-bottom: 10px;
418 | }
419 |
420 | header .previous h2 span {
421 | color: var(--previous-arrow-color);
422 | }
423 |
424 | header .previous h3 {
425 | font-size: 14px;
426 | margin: 10px 0;
427 | }
428 |
429 | #clipboard {
430 | position: absolute;
431 | top: -500px;
432 | right: 300px;
433 | width: 750px;
434 | height: 150px;
435 | }
436 |
437 | .copy-clipboard {
438 | position: absolute;
439 | right: 40px;
440 | top: 44px;
441 | }
442 |
443 | .copy-clipboard:hover svg path {
444 | fill: var(--icon-hover-color);
445 | }
446 |
447 | #copied {
448 | display: none;
449 | position: absolute;
450 | right: 76px;
451 | top: 51px;
452 | }
453 |
454 | #light-mode {
455 | display: none;
456 | }
457 | /* end header */
458 |
459 | main {
460 | margin-left: 100px;
461 | margin-right: 100px;
462 | }
463 |
464 | @media screen and (max-width: 1200px) {
465 | header {
466 | padding-left: 50px;
467 | padding-right: 50px;
468 | }
469 |
470 | main {
471 | margin-left: 50px;
472 | margin-right: 50px;
473 | }
474 | }
475 |
476 | .hidden {
477 | display: none;
478 | }
479 |
480 | .flex {
481 | display: flex;
482 | }
483 |
484 | .flex-column {
485 | flex-direction: column;
486 | }
487 |
488 | .flex-1 {
489 | flex: 1;
490 | }
491 |
492 | .mw-100 {
493 | max-width: 100%;
494 | }
495 |
496 | .w-100 {
497 | width: 100%;
498 | }
499 |
500 | .bold {
501 | font-weight: bold;
502 | }
503 |
504 | .word-break {
505 | overflow-wrap: break-word;
506 | word-break: break-word;
507 | }
508 |
509 | /* call stack */
510 | .call-stack ul li,
511 | .request {
512 | border: 2px solid var(--border-color);
513 | box-shadow: var(--box-shadow);
514 | background: var(--bg-color);
515 | margin-bottom: 30px;
516 | border-radius: 3px;
517 | }
518 |
519 | .call-stack > ul > li:first-child {
520 | margin-top: -200px;
521 | }
522 |
523 | .call-stack > ul > li:last-child {
524 | margin-bottom: 50px;
525 | }
526 |
527 | .call-stack > ul > li.call-stack-vendor-group {
528 | border: 2px solid var(--vendor-border-color);
529 | background: var(--vendor-bg-color);
530 | }
531 |
532 | .call-stack > ul > li.call-stack-vendor-group .call-stack-vendor-state {
533 | display: inline-block;
534 | height: 22px;
535 | width: 22px;
536 | font-size: 20px;
537 | color: #fff;
538 | background: var(--vendor-state-bg-color);
539 | border-radius: 3px;
540 | text-align: center;
541 | margin-right: 15px;
542 | }
543 |
544 | .call-stack > ul > li.call-stack-vendor-group > ul {
545 | display: none;
546 | background: var(--vendor-content-bg-color);
547 | }
548 |
549 | .call-stack > ul > li.call-stack-vendor-group > ul > li {
550 | border-left: 0;
551 | border-right: 0;
552 | box-shadow: none;
553 | border-color: var(--vendor-border-color);
554 | }
555 |
556 | .call-stack > ul > li.call-stack-vendor-group > ul > li:last-child {
557 | border-bottom: 2px solid transparent;
558 | margin-bottom: 0;
559 | }
560 |
561 | .call-stack ul li .element-wrap {
562 | display: flex;
563 | cursor: pointer;
564 | padding: 20px 30px;
565 | font-weight: 500;
566 | font-size: 18px;
567 | line-height: 21px;
568 | color: var(--element-wrap-text-color);
569 | }
570 |
571 | .call-stack ul li .element-wrap .file-name {
572 | color: var(--element-wrap-text-color);
573 | }
574 |
575 | .call-stack ul li .element-wrap .file-name:hover,
576 | .call-stack ul li .element-code-wrap .code-wrap .lines-item:hover {
577 | color: var(--element-wrap-hover-text-color);
578 | }
579 |
580 | .call-stack ul li .arguments:hover {
581 | color: var(--element-wrap-hover-text-color);
582 | }
583 |
584 | .call-stack ul li .element-wrap .function-info {
585 | display: inline-block;
586 | line-break: normal;
587 | }
588 |
589 | .call-stack ul li a {
590 | color: var(--link-color);
591 | }
592 |
593 | .call-stack ul li a:hover {
594 | color: #000;
595 | }
596 |
597 | .call-stack ul li a .external-link {
598 | vertical-align: middle;
599 | }
600 |
601 | .call-stack ul li a .external-link:hover path {
602 | fill: var(--icon-hover-color);
603 | }
604 |
605 | .call-stack ul li a .external-link path {
606 | fill: var(--icon-color);
607 | }
608 |
609 | .call-stack ul li .element-code-wrap {
610 | border-top: 1px solid var(--element-wrap-border-color);
611 | overflow-x: auto;
612 | }
613 |
614 | .call-stack ul li .element-code-wrap .code-wrap {
615 | display: none;
616 | position: relative;
617 | }
618 |
619 | .call-stack ul li.application .element-code-wrap .code-wrap {
620 | display: block;
621 | }
622 |
623 | .call-stack ul li .error-line,
624 | .call-stack ul li .hover-line {
625 | background-color: var(--error-line-bg-color);
626 | position: absolute;
627 | width: 100%;
628 | z-index: 100;
629 | margin-top: 0;
630 | }
631 |
632 | .call-stack ul li .hover-line {
633 | background: none;
634 | }
635 |
636 | .call-stack ul li .hover-line.hover,
637 | .call-stack ul li .hover-line:hover {
638 | background: var(--hover-line-bg-color) !important;
639 | }
640 |
641 | .call-stack ul li .code {
642 | min-width: 700px;
643 | /* 800px - 50px * 2 */
644 | margin: 15px auto;
645 | padding: 0 50px;
646 | position: relative;
647 | }
648 |
649 | .call-stack ul li .code .lines-item {
650 | position: absolute;
651 | z-index: 200;
652 | display: block;
653 | width: 25px;
654 | text-align: right;
655 | color: #aaa;
656 | line-height: 20px;
657 | font-size: 12px;
658 | margin-top: 1px;
659 | font-family: JetBrains Mono, Consolas, monospace;
660 | }
661 |
662 | .call-stack ul li .code pre {
663 | position: relative;
664 | z-index: 200;
665 | left: 50px;
666 | line-height: 20px;
667 | font-size: 12px;
668 | font-family: JetBrains Mono, Consolas, monospace;
669 | display: inline;
670 | }
671 |
672 | @-moz-document url-prefix() {
673 | .call-stack ul li .code pre {
674 | line-height: 20px;
675 | }
676 | }
677 | /* end call stack */
678 |
679 | /* request */
680 | .request {
681 | position: relative;
682 | font-size: 14px;
683 | line-height: 18px;
684 | overflow-x: auto;
685 | font-family: JetBrains Mono, Consolas, monospace;
686 | }
687 | .request .body {
688 | overflow: auto;
689 | }
690 | /* end request */
691 |
692 | /* footer */
693 | .footer {
694 | display: flex;
695 | }
696 |
697 | .footer div {
698 | align-self: center;
699 | }
700 |
701 | .footer .timestamp,
702 | .footer .server {
703 | margin-bottom: 20px;
704 | }
705 |
706 | .footer p,
707 | .footer p a {
708 | font-size: 24px;
709 | line-height: 28px;
710 | color: #9c9c9c;
711 | }
712 |
713 | .footer p a:hover {
714 | color: #000;
715 | }
716 |
717 | .footer svg {
718 | margin-right: -50px;
719 | }
720 | /* end footer */
721 |
722 | /* highlight.js */
723 | .hljs {
724 | display: block;
725 | color: var(--hljs-text-color);
726 | }
727 |
728 | .hljs-comment {
729 | color: var(--hljs-comment-text-color);
730 | }
731 |
732 | .hljs-keyword,
733 | .hljs-selector-tag,
734 | .hljs-meta-keyword,
735 | .hljs-doctag,
736 | .hljs-section,
737 | .hljs-selector-class,
738 | .hljs-meta,
739 | .hljs-selector-pseudo,
740 | .hljs-attr {
741 | color: var(--hljs-keyword-text-color);
742 | }
743 |
744 | .hljs-attribute {
745 | color: var(--hljs-attribute-text-color);
746 | }
747 |
748 | .hljs-name,
749 | .hljs-type,
750 | .hljs-number,
751 | .hljs-selector-id,
752 | .hljs-quote,
753 | .hljs-template-tag,
754 | .hljs-built_in,
755 | .hljs-title,
756 | .hljs-literal {
757 | color: var(--hljs-name-text-color);
758 | }
759 |
760 | .hljs-string,
761 | .hljs-regexp,
762 | .hljs-symbol,
763 | .hljs-variable,
764 | .hljs-template-variable,
765 | .hljs-link,
766 | .hljs-selector-attr,
767 | .hljs-meta-string {
768 | color: var(--hljs-string-text-color);
769 | }
770 |
771 | .hljs-bullet,
772 | .hljs-code {
773 | color: var(--hljs-code-text-color);
774 | }
775 |
776 | .hljs-deletion {
777 | color: var(--hljs-delition-text-color);
778 | }
779 |
780 | .hljs-addition {
781 | color: var(--hljs-addition-text-color);
782 | }
783 |
784 | .hljs-emphasis {
785 | font-style: italic;
786 | }
787 |
788 | .hljs-strong {
789 | font-weight: bold;
790 | }
791 | /* end highlight.js */
792 |
793 | /* start dark-theme */
794 |
795 | .dark-theme {
796 | --page-bg-color: rgba(46, 46, 46, 0.9);
797 | --page-text-color: #fff;
798 | --page-text-muted-color: #aaa;
799 | --icon-color: #989898;
800 | --icon-hover-color: #fff;
801 |
802 | --table-line-even-bg: #555;
803 | --table-line-even-color: #eee;
804 | --table-line-odd-bg: #999;
805 | --table-line-odd-color: #eee;
806 | --table-line-hover: #141414;
807 |
808 | --button-bg: #6c757d;
809 | --button-color: #fff;
810 | --button-bg-hover: #5c636a;
811 | --button-color-hover: #fff;
812 | }
813 |
814 | .dark-theme header {
815 | --header-bg-color: #2e2e2e;
816 | --previous-text-color: rgba(255, 255, 255, 0.8);
817 | --previous-arrow-color: #fff;
818 | }
819 |
820 | .dark-theme .exception-card {
821 | --exception-card-bg-color: #222;
822 | --exception-card-border-color: #591e15;
823 | --exception-class-text-color: #fff;
824 | --exception-class-friendly-text-color: rgba(255, 255, 255, 0.5);
825 | --exception-class-friendly-link-color: #E57373;
826 | --exception-message-text-color: rgba(255, 255, 255, 0.8);
827 | }
828 |
829 | .dark-theme header .solution {
830 | --text-color: rgba(255, 255, 255, 0.8);
831 | --link-color: #03a9f4;
832 | --link-hover-color: #39b9f3;
833 | --blockquote-text-color: #999;
834 | --blockquote-border-color: #484c50;
835 | --code-bg-color: #2d333b;
836 | --pre-bg-color: #2d333b;
837 | --table-border-color: #484c50;
838 | --separator-color: #484c50;
839 | }
840 |
841 | .dark-theme .call-stack {
842 | --bg-color: #1e1e1e;
843 | --border-color: transparent;
844 | --box-shadow: 0 13px 20px rgba(0, 0, 0, 0.25);
845 | --link-color: rgba(255, 255, 255, 0.5);
846 | --error-line-bg-color: #422c2c;
847 | --hover-line-bg-color: #292929;
848 | --element-wrap-border-color: #141414;
849 | --element-wrap-text-color: #fff;
850 | --element-wrap-hover-text-color: #9cdcfe;
851 | --vendor-bg-color: rgba(46,46,46, 0.9);
852 | --vendor-border-color: #666;
853 | --vendor-state-bg-color: #666;
854 | --vendor-content-bg-color: rgba(46,46,46, 0.9);
855 | }
856 |
857 | .dark-theme .hljs {
858 | --hljs-text-color: #fff;
859 | --hljs-comment-text-color: #999;
860 | --hljs-keyword-text-color: #88aece;
861 | --hljs-attribute-text-color: #c59bc1;
862 | --hljs-name-text-color: #f08d49;
863 | --hljs-string-text-color: #b5bd68;
864 | --hljs-code-text-color: #cccccc;
865 | --hljs-delition-text-color: #de7176;
866 | --hljs-addition-text-color: #76c490;
867 | }
868 |
869 | .dark-theme #dark-mode {
870 | display: none;
871 | }
872 | .dark-theme #light-mode {
873 | display: inline;
874 | }
875 |
876 | @media (prefers-color-scheme: dark) {
877 | body:not(.light-theme) {
878 | --page-bg-color: rgba(46, 46, 46, 0.9);
879 | --page-text-color: #fff;
880 | --page-text-muted-color: #aaa;
881 | --icon-color: #989898;
882 | --icon-hover-color: #fff;
883 |
884 | --table-line-even-bg: #555;
885 | --table-line-even-color: #eee;
886 | --table-line-odd-bg: #999;
887 | --table-line-odd-color: #eee;
888 | --table-line-hover: #141414;
889 |
890 | --button-bg: #6c757d;
891 | --button-color: #fff;
892 | --button-bg-hover: #5c636a;
893 | --button-color-hover: #fff;
894 | }
895 |
896 | body:not(.light-theme) header {
897 | --header-bg-color: #2e2e2e;
898 | --previous-text-color: rgba(255, 255, 255, 0.8);
899 | --previous-arrow-color: #fff;
900 | }
901 |
902 | body:not(.light-theme) .exception-card {
903 | --exception-card-bg-color: #222;
904 | --exception-card-border-color: #591e15;
905 | --exception-class-text-color: #fff;
906 | --exception-class-friendly-text-color: rgba(255, 255, 255, 0.5);
907 | --exception-class-friendly-link-color: #E57373;
908 | --exception-message-text-color: rgba(255, 255, 255, 0.8);
909 | }
910 |
911 | body:not(.light-theme) header .solution {
912 | --text-color: rgba(255, 255, 255, 0.8);
913 | --link-color: #03a9f4;
914 | --link-hover-color: #39b9f3;
915 | --blockquote-text-color: #999;
916 | --blockquote-border-color: #484c50;
917 | --code-bg-color: #2d333b;
918 | --pre-bg-color: #2d333b;
919 | --table-border-color: #484c50;
920 | --separator-color: #484c50;
921 | }
922 |
923 | body:not(.light-theme) .call-stack {
924 | --bg-color: #1e1e1e;
925 | --border-color: transparent;
926 | --box-shadow: 0 13px 20px rgba(0, 0, 0, 0.25);
927 | --link-color: rgba(255, 255, 255, 0.5);
928 | --error-line-bg-color: #422c2c;
929 | --hover-line-bg-color: #292929;
930 | --element-wrap-border-color: #141414;
931 | --element-wrap-text-color: #fff;
932 | --element-wrap-hover-text-color: #9cdcfe;
933 | --vendor-bg-color: rgba(46,46,46, 0.9);
934 | --vendor-border-color: #666;
935 | --vendor-state-bg-color: #666;
936 | --vendor-content-bg-color: rgba(46,46,46, 0.9);
937 | }
938 |
939 | body:not(.light-theme) .functionArguments {
940 | --function-arguments-bg: #303030;
941 | --function-arguments-border-color: #272727;
942 | }
943 | body:not(.light-theme) .hljs {
944 | --hljs-text-color: #fff;
945 | --hljs-comment-text-color: #999;
946 | --hljs-keyword-text-color: #88aece;
947 | --hljs-attribute-text-color: #c59bc1;
948 | --hljs-name-text-color: #f08d49;
949 | --hljs-string-text-color: #b5bd68;
950 | --hljs-code-text-color: #cccccc;
951 | --hljs-delition-text-color: #de7176;
952 | --hljs-addition-text-color: #76c490;
953 | }
954 |
955 | body:not(.light-theme) #dark-mode {
956 | display: none;
957 | }
958 |
959 | body:not(.light-theme) #light-mode {
960 | display: inline;
961 | }
962 | }
963 |
964 | /* end dark-theme */
965 |
--------------------------------------------------------------------------------
/templates/development.php:
--------------------------------------------------------------------------------
1 | getFirstException();
20 | }
21 | $isFriendlyException = $throwable instanceof FriendlyExceptionInterface;
22 | $solution = $isFriendlyException ? $throwable->getSolution() : null;
23 | $exceptionClass = get_class($throwable);
24 | $exceptionMessage = $throwable->getMessage();
25 |
26 | ?>
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | = $this->htmlEncode($this->getThrowableName($throwable)) ?>
35 |
36 |
39 |
40 |
41 |
49 |
50 |
51 | htmlEncode($theme)}\"" : '' ?>>
52 |
53 |
77 |
78 |
79 |
80 |
82 | = $this->htmlEncode($throwable->getName())?>
83 | —
84 | = $exceptionClass ?>
85 |
86 | = $exceptionClass ?>
87 |
88 | (Code #= $throwable->getCode() ?>)
89 |
90 |
91 |
92 | = nl2br($this->htmlEncode($exceptionMessage)) ?>
93 |
94 |
95 |
96 |
= $this->parseMarkdown($solution) ?>
97 |
98 |
99 | = $this->renderPreviousExceptions($originalException) ?>
100 |
101 |
102 |
Copied!
103 |
104 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | = $this->renderCallStack(
119 | $throwable,
120 | $originalException === $throwable && $originalException instanceof ErrorException
121 | ? $originalException->getBacktrace()
122 | : $throwable->getTrace()
123 | ) ?>
124 |
125 | renderRequest($request)) !== ''): ?>
126 |
127 |
Request info
128 |
129 |
= $this->htmlEncode(rtrim($requestInfo, "\n")) ?>
130 |
131 |
132 |
133 | renderCurl($request)) !== 'curl'): ?>
134 |
135 |
136 |
Copied!
137 |
cURL
138 |
144 |
145 |
146 |
147 |
148 |
149 |
= $this->htmlEncode($curlInfo) ?>
150 |
151 |
152 |
153 |
198 |
199 |
200 |
203 |
414 |
415 |
416 |
417 |
--------------------------------------------------------------------------------
/templates/highlight.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | Highlight.js v11.5.1 (git: b8f233c8e2)
3 | (c) 2006-2022 Ivan Sagalaev and other contributors
4 | License: BSD-3-Clause
5 | */
6 | var hljs=function(){"use strict";var e={exports:{}};function t(e){
7 | return e instanceof Map?e.clear=e.delete=e.set=()=>{
8 | throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{
9 | throw Error("set is read-only")
10 | }),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((n=>{var i=e[n]
11 | ;"object"!=typeof i||Object.isFrozen(i)||t(i)})),e}
12 | e.exports=t,e.exports.default=t;var n=e.exports;class i{constructor(e){
13 | void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
14 | ignoreMatch(){this.isMatchIgnored=!0}}function r(e){
15 | return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")
16 | }function s(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t]
17 | ;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const o=e=>!!e.kind
18 | ;class a{constructor(e,t){
19 | this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){
20 | this.buffer+=r(e)}openNode(e){if(!o(e))return;let t=e.kind
21 | ;t=e.sublanguage?"language-"+t:((e,{prefix:t})=>{if(e.includes(".")){
22 | const n=e.split(".")
23 | ;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ")
24 | }return`${t}${e}`})(t,{prefix:this.classPrefix}),this.span(t)}closeNode(e){
25 | o(e)&&(this.buffer+="")}value(){return this.buffer}span(e){
26 | this.buffer+=``}}class c{constructor(){this.rootNode={
27 | children:[]},this.stack=[this.rootNode]}get top(){
28 | return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
29 | this.top.children.push(e)}openNode(e){const t={kind:e,children:[]}
30 | ;this.add(t),this.stack.push(t)}closeNode(){
31 | if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
32 | for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
33 | walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){
34 | return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),
35 | t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){
36 | "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
37 | c._collapse(e)})))}}class l extends c{constructor(e){super(),this.options=e}
38 | addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())}
39 | addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root
40 | ;n.kind=t,n.sublanguage=!0,this.add(n)}toHTML(){
41 | return new a(this,this.options).value()}finalize(){return!0}}function g(e){
42 | return e?"string"==typeof e?e:e.source:null}function d(e){return f("(?=",e,")")}
43 | function u(e){return f("(?:",e,")*")}function h(e){return f("(?:",e,")?")}
44 | function f(...e){return e.map((e=>g(e))).join("")}function p(...e){const t=(e=>{
45 | const t=e[e.length-1]
46 | ;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}
47 | })(e);return"("+(t.capture?"":"?:")+e.map((e=>g(e))).join("|")+")"}
48 | function b(e){return RegExp(e.toString()+"|").exec("").length-1}
49 | const m=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
50 | ;function E(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n
51 | ;let i=g(e),r="";for(;i.length>0;){const e=m.exec(i);if(!e){r+=i;break}
52 | r+=i.substring(0,e.index),
53 | i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?r+="\\"+(Number(e[1])+t):(r+=e[0],
54 | "("===e[0]&&n++)}return r})).map((e=>`(${e})`)).join(t)}
55 | const x="[a-zA-Z]\\w*",w="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",_="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",k="\\b(0b[01]+)",v={
56 | begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'",
57 | illegal:"\\n",contains:[v]},N={scope:"string",begin:'"',end:'"',illegal:"\\n",
58 | contains:[v]},M=(e,t,n={})=>{const i=s({scope:"comment",begin:e,end:t,
59 | contains:[]},n);i.contains.push({scope:"doctag",
60 | begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
61 | end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
62 | ;const r=p("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
63 | ;return i.contains.push({begin:f(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i
64 | },S=M("//","$"),R=M("/\\*","\\*/"),j=M("#","$");var A=Object.freeze({
65 | __proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:x,UNDERSCORE_IDENT_RE:w,
66 | NUMBER_RE:y,C_NUMBER_RE:_,BINARY_NUMBER_RE:k,
67 | RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
68 | SHEBANG:(e={})=>{const t=/^#![ ]*\//
69 | ;return e.binary&&(e.begin=f(t,/.*\b/,e.binary,/\b.*/)),s({scope:"meta",begin:t,
70 | end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)},
71 | BACKSLASH_ESCAPE:v,APOS_STRING_MODE:O,QUOTE_STRING_MODE:N,PHRASAL_WORDS_MODE:{
72 | begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
73 | },COMMENT:M,C_LINE_COMMENT_MODE:S,C_BLOCK_COMMENT_MODE:R,HASH_COMMENT_MODE:j,
74 | NUMBER_MODE:{scope:"number",begin:y,relevance:0},C_NUMBER_MODE:{scope:"number",
75 | begin:_,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:k,relevance:0},
76 | REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//,
77 | end:/\/[gimuy]*/,illegal:/\n/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,
78 | contains:[v]}]}]},TITLE_MODE:{scope:"title",begin:x,relevance:0},
79 | UNDERSCORE_TITLE_MODE:{scope:"title",begin:w,relevance:0},METHOD_GUARD:{
80 | begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{
81 | "on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{
82 | t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function I(e,t){
83 | "."===e.input[e.index-1]&&t.ignoreMatch()}function T(e,t){
84 | void 0!==e.className&&(e.scope=e.className,delete e.className)}function L(e,t){
85 | t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
86 | e.__beforeBegin=I,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
87 | void 0===e.relevance&&(e.relevance=0))}function B(e,t){
88 | Array.isArray(e.illegal)&&(e.illegal=p(...e.illegal))}function D(e,t){
89 | if(e.match){
90 | if(e.begin||e.end)throw Error("begin & end are not supported with match")
91 | ;e.begin=e.match,delete e.match}}function H(e,t){
92 | void 0===e.relevance&&(e.relevance=1)}const P=(e,t)=>{if(!e.beforeMatch)return
93 | ;if(e.starts)throw Error("beforeMatch cannot be used with starts")
94 | ;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t]
95 | })),e.keywords=n.keywords,e.begin=f(n.beforeMatch,d(n.begin)),e.starts={
96 | relevance:0,contains:[Object.assign(n,{endsParent:!0})]
97 | },e.relevance=0,delete n.beforeMatch
98 | },C=["of","and","for","in","not","or","if","then","parent","list","value"]
99 | ;function $(e,t,n="keyword"){const i=Object.create(null)
100 | ;return"string"==typeof e?r(n,e.split(" ")):Array.isArray(e)?r(n,e):Object.keys(e).forEach((n=>{
101 | Object.assign(i,$(e[n],t,n))})),i;function r(e,n){
102 | t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|")
103 | ;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){
104 | return t?Number(t):(e=>C.includes(e.toLowerCase()))(e)?0:1}const z={},K=e=>{
105 | console.error(e)},W=(e,...t)=>{console.log("WARN: "+e,...t)},X=(e,t)=>{
106 | z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0)
107 | },G=Error();function Z(e,t,{key:n}){let i=0;const r=e[n],s={},o={}
108 | ;for(let e=1;e<=t.length;e++)o[e+i]=r[e],s[e+i]=!0,i+=b(t[e-1])
109 | ;e[n]=o,e[n]._emit=s,e[n]._multi=!0}function F(e){(e=>{
110 | e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
111 | delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={
112 | _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope
113 | }),(e=>{if(Array.isArray(e.begin)){
114 | if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),
115 | G
116 | ;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"),
117 | G;Z(e,e.begin,{key:"beginScope"}),e.begin=E(e.begin,{joinWith:""})}})(e),(e=>{
118 | if(Array.isArray(e.end)){
119 | if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"),
120 | G
121 | ;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"),
122 | G;Z(e,e.end,{key:"endScope"}),e.end=E(e.end,{joinWith:""})}})(e)}function V(e){
123 | function t(t,n){
124 | return RegExp(g(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":""))
125 | }class n{constructor(){
126 | this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
127 | addRule(e,t){
128 | t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),
129 | this.matchAt+=b(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null)
130 | ;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(E(e,{joinWith:"|"
131 | }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex
132 | ;const t=this.matcherRe.exec(e);if(!t)return null
133 | ;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n]
134 | ;return t.splice(0,n),Object.assign(t,i)}}class i{constructor(){
135 | this.rules=[],this.multiRegexes=[],
136 | this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
137 | if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n
138 | ;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),
139 | t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){
140 | return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){
141 | this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){
142 | const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex
143 | ;let n=t.exec(e)
144 | ;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{
145 | const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}
146 | return n&&(this.regexIndex+=n.position+1,
147 | this.regexIndex===this.count&&this.considerAll()),n}}
148 | if(e.compilerExtensions||(e.compilerExtensions=[]),
149 | e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.")
150 | ;return e.classNameAliases=s(e.classNameAliases||{}),function n(r,o){const a=r
151 | ;if(r.isCompiled)return a
152 | ;[T,D,F,P].forEach((e=>e(r,o))),e.compilerExtensions.forEach((e=>e(r,o))),
153 | r.__beforeBegin=null,[L,B,H].forEach((e=>e(r,o))),r.isCompiled=!0;let c=null
154 | ;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords),
155 | c=r.keywords.$pattern,
156 | delete r.keywords.$pattern),c=c||/\w+/,r.keywords&&(r.keywords=$(r.keywords,e.case_insensitive)),
157 | a.keywordPatternRe=t(c,!0),
158 | o&&(r.begin||(r.begin=/\B|\b/),a.beginRe=t(a.begin),r.end||r.endsWithParent||(r.end=/\B|\b/),
159 | r.end&&(a.endRe=t(a.end)),
160 | a.terminatorEnd=g(a.end)||"",r.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(r.end?"|":"")+o.terminatorEnd)),
161 | r.illegal&&(a.illegalRe=t(r.illegal)),
162 | r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>s(e,{
163 | variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?s(e,{
164 | starts:e.starts?s(e.starts):null
165 | }):Object.isFrozen(e)?s(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{n(e,a)
166 | })),r.starts&&n(r.starts,o),a.matcher=(e=>{const t=new i
167 | ;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"
168 | }))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"
169 | }),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){
170 | return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{
171 | constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}}
172 | const Y=r,Q=s,ee=Symbol("nomatch");var te=(e=>{
173 | const t=Object.create(null),r=Object.create(null),s=[];let o=!0
174 | ;const a="Could not find the language '{}', did you forget to load/include a language module?",c={
175 | disableAutodetect:!0,name:"Plain text",contains:[]};let g={
176 | ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
177 | languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
178 | cssSelector:"pre code",languages:null,__emitter:l};function b(e){
179 | return g.noHighlightRe.test(e)}function m(e,t,n){let i="",r=""
180 | ;"object"==typeof t?(i=e,
181 | n=t.ignoreIllegals,r=t.language):(X("10.7.0","highlight(lang, code, ...args) has been deprecated."),
182 | X("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
183 | r=e,i=t),void 0===n&&(n=!0);const s={code:i,language:r};N("before:highlight",s)
184 | ;const o=s.result?s.result:E(s.language,s.code,n)
185 | ;return o.code=s.code,N("after:highlight",o),o}function E(e,n,r,s){
186 | const c=Object.create(null);function l(){if(!O.keywords)return void M.addText(S)
187 | ;let e=0;O.keywordPatternRe.lastIndex=0;let t=O.keywordPatternRe.exec(S),n=""
188 | ;for(;t;){n+=S.substring(e,t.index)
189 | ;const r=y.case_insensitive?t[0].toLowerCase():t[0],s=(i=r,O.keywords[i]);if(s){
190 | const[e,i]=s
191 | ;if(M.addText(n),n="",c[r]=(c[r]||0)+1,c[r]<=7&&(R+=i),e.startsWith("_"))n+=t[0];else{
192 | const n=y.classNameAliases[e]||e;M.addKeyword(t[0],n)}}else n+=t[0]
193 | ;e=O.keywordPatternRe.lastIndex,t=O.keywordPatternRe.exec(S)}var i
194 | ;n+=S.substr(e),M.addText(n)}function d(){null!=O.subLanguage?(()=>{
195 | if(""===S)return;let e=null;if("string"==typeof O.subLanguage){
196 | if(!t[O.subLanguage])return void M.addText(S)
197 | ;e=E(O.subLanguage,S,!0,N[O.subLanguage]),N[O.subLanguage]=e._top
198 | }else e=x(S,O.subLanguage.length?O.subLanguage:null)
199 | ;O.relevance>0&&(R+=e.relevance),M.addSublanguage(e._emitter,e.language)
200 | })():l(),S=""}function u(e,t){let n=1;const i=t.length-1;for(;n<=i;){
201 | if(!e._emit[n]){n++;continue}const i=y.classNameAliases[e[n]]||e[n],r=t[n]
202 | ;i?M.addKeyword(r,i):(S=r,l(),S=""),n++}}function h(e,t){
203 | return e.scope&&"string"==typeof e.scope&&M.openNode(y.classNameAliases[e.scope]||e.scope),
204 | e.beginScope&&(e.beginScope._wrap?(M.addKeyword(S,y.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
205 | S=""):e.beginScope._multi&&(u(e.beginScope,t),S="")),O=Object.create(e,{parent:{
206 | value:O}}),O}function f(e,t,n){let r=((e,t)=>{const n=e&&e.exec(t)
207 | ;return n&&0===n.index})(e.endRe,n);if(r){if(e["on:end"]){const n=new i(e)
208 | ;e["on:end"](t,n),n.isMatchIgnored&&(r=!1)}if(r){
209 | for(;e.endsParent&&e.parent;)e=e.parent;return e}}
210 | if(e.endsWithParent)return f(e.parent,t,n)}function p(e){
211 | return 0===O.matcher.regexIndex?(S+=e[0],1):(I=!0,0)}function b(e){
212 | const t=e[0],i=n.substr(e.index),r=f(O,e,i);if(!r)return ee;const s=O
213 | ;O.endScope&&O.endScope._wrap?(d(),
214 | M.addKeyword(t,O.endScope._wrap)):O.endScope&&O.endScope._multi?(d(),
215 | u(O.endScope,e)):s.skip?S+=t:(s.returnEnd||s.excludeEnd||(S+=t),
216 | d(),s.excludeEnd&&(S=t));do{
217 | O.scope&&M.closeNode(),O.skip||O.subLanguage||(R+=O.relevance),O=O.parent
218 | }while(O!==r.parent);return r.starts&&h(r.starts,e),s.returnEnd?0:t.length}
219 | let m={};function w(t,s){const a=s&&s[0];if(S+=t,null==a)return d(),0
220 | ;if("begin"===m.type&&"end"===s.type&&m.index===s.index&&""===a){
221 | if(S+=n.slice(s.index,s.index+1),!o){const t=Error(`0 width match regex (${e})`)
222 | ;throw t.languageName=e,t.badRule=m.rule,t}return 1}
223 | if(m=s,"begin"===s.type)return(e=>{
224 | const t=e[0],n=e.rule,r=new i(n),s=[n.__beforeBegin,n["on:begin"]]
225 | ;for(const n of s)if(n&&(n(e,r),r.isMatchIgnored))return p(t)
226 | ;return n.skip?S+=t:(n.excludeBegin&&(S+=t),
227 | d(),n.returnBegin||n.excludeBegin||(S=t)),h(n,e),n.returnBegin?0:t.length})(s)
228 | ;if("illegal"===s.type&&!r){
229 | const e=Error('Illegal lexeme "'+a+'" for mode "'+(O.scope||"")+'"')
230 | ;throw e.mode=O,e}if("end"===s.type){const e=b(s);if(e!==ee)return e}
231 | if("illegal"===s.type&&""===a)return 1
232 | ;if(A>1e5&&A>3*s.index)throw Error("potential infinite loop, way more iterations than matches")
233 | ;return S+=a,a.length}const y=k(e)
234 | ;if(!y)throw K(a.replace("{}",e)),Error('Unknown language: "'+e+'"')
235 | ;const _=V(y);let v="",O=s||_;const N={},M=new g.__emitter(g);(()=>{const e=[]
236 | ;for(let t=O;t!==y;t=t.parent)t.scope&&e.unshift(t.scope)
237 | ;e.forEach((e=>M.openNode(e)))})();let S="",R=0,j=0,A=0,I=!1;try{
238 | for(O.matcher.considerAll();;){
239 | A++,I?I=!1:O.matcher.considerAll(),O.matcher.lastIndex=j
240 | ;const e=O.matcher.exec(n);if(!e)break;const t=w(n.substring(j,e.index),e)
241 | ;j=e.index+t}return w(n.substr(j)),M.closeAllNodes(),M.finalize(),v=M.toHTML(),{
242 | language:e,value:v,relevance:R,illegal:!1,_emitter:M,_top:O}}catch(t){
243 | if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n),
244 | illegal:!0,relevance:0,_illegalBy:{message:t.message,index:j,
245 | context:n.slice(j-100,j+100),mode:t.mode,resultSoFar:v},_emitter:M};if(o)return{
246 | language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:O}
247 | ;throw t}}function x(e,n){n=n||g.languages||Object.keys(t);const i=(e=>{
248 | const t={value:Y(e),illegal:!1,relevance:0,_top:c,_emitter:new g.__emitter(g)}
249 | ;return t._emitter.addText(e),t})(e),r=n.filter(k).filter(O).map((t=>E(t,e,!1)))
250 | ;r.unshift(i);const s=r.sort(((e,t)=>{
251 | if(e.relevance!==t.relevance)return t.relevance-e.relevance
252 | ;if(e.language&&t.language){if(k(e.language).supersetOf===t.language)return 1
253 | ;if(k(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=s,l=o
254 | ;return l.secondBest=a,l}function w(e){let t=null;const n=(e=>{
255 | let t=e.className+" ";t+=e.parentNode?e.parentNode.className:""
256 | ;const n=g.languageDetectRe.exec(t);if(n){const t=k(n[1])
257 | ;return t||(W(a.replace("{}",n[1])),
258 | W("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}
259 | return t.split(/\s+/).find((e=>b(e)||k(e)))})(e);if(b(n))return
260 | ;if(N("before:highlightElement",{el:e,language:n
261 | }),e.children.length>0&&(g.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),
262 | console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),
263 | console.warn("The element with unescaped HTML:"),
264 | console.warn(e)),g.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML)
265 | ;t=e;const i=t.textContent,s=n?m(i,{language:n,ignoreIllegals:!0}):x(i)
266 | ;e.innerHTML=s.value,((e,t,n)=>{const i=t&&r[t]||n
267 | ;e.classList.add("hljs"),e.classList.add("language-"+i)
268 | })(e,n,s.language),e.result={language:s.language,re:s.relevance,
269 | relevance:s.relevance},s.secondBest&&(e.secondBest={
270 | language:s.secondBest.language,relevance:s.secondBest.relevance
271 | }),N("after:highlightElement",{el:e,result:s,text:i})}let y=!1;function _(){
272 | "loading"!==document.readyState?document.querySelectorAll(g.cssSelector).forEach(w):y=!0
273 | }function k(e){return e=(e||"").toLowerCase(),t[e]||t[r[e]]}
274 | function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
275 | r[e.toLowerCase()]=t}))}function O(e){const t=k(e)
276 | ;return t&&!t.disableAutodetect}function N(e,t){const n=e;s.forEach((e=>{
277 | e[n]&&e[n](t)}))}
278 | "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
279 | y&&_()}),!1),Object.assign(e,{highlight:m,highlightAuto:x,highlightAll:_,
280 | highlightElement:w,
281 | highlightBlock:e=>(X("10.7.0","highlightBlock will be removed entirely in v12.0"),
282 | X("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{g=Q(g,e)},
283 | initHighlighting:()=>{
284 | _(),X("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},
285 | initHighlightingOnLoad:()=>{
286 | _(),X("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")
287 | },registerLanguage:(n,i)=>{let r=null;try{r=i(e)}catch(e){
288 | if(K("Language definition for '{}' could not be registered.".replace("{}",n)),
289 | !o)throw e;K(e),r=c}
290 | r.name||(r.name=n),t[n]=r,r.rawDefinition=i.bind(null,e),r.aliases&&v(r.aliases,{
291 | languageName:n})},unregisterLanguage:e=>{delete t[e]
292 | ;for(const t of Object.keys(r))r[t]===e&&delete r[t]},
293 | listLanguages:()=>Object.keys(t),getLanguage:k,registerAliases:v,
294 | autoDetection:O,inherit:Q,addPlugin:e=>{(e=>{
295 | e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{
296 | e["before:highlightBlock"](Object.assign({block:t.el},t))
297 | }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{
298 | e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),s.push(e)}
299 | }),e.debugMode=()=>{o=!1},e.safeMode=()=>{o=!0
300 | },e.versionString="11.5.1",e.regex={concat:f,lookahead:d,either:p,optional:h,
301 | anyNumberOfTimes:u};for(const e in A)"object"==typeof A[e]&&n(A[e])
302 | ;return Object.assign(e,A),e})({});return te}()
303 | ;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `css` grammar compiled for Highlight.js 11.5.1 */
304 | (()=>{var e=(()=>{"use strict"
305 | ;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],i=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],r=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse()
306 | ;return n=>{const a=n.regex,l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"},
307 | BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number",
308 | begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{
309 | className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{
310 | scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",
311 | contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{
312 | scope:"number",
313 | begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",
314 | relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/}
315 | }))(n),s=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS",
316 | case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"},
317 | classNameAliases:{keyframePosition:"selector-tag"},contains:[l.BLOCK_COMMENT,{
318 | begin:/-(webkit|moz|ms|o)-(?=[a-z])/},l.CSS_NUMBER_MODE,{
319 | className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{
320 | className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0
321 | },l.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{
322 | begin:":("+r.join("|")+")"},{begin:":(:)?("+t.join("|")+")"}]},l.CSS_VARIABLE,{
323 | className:"attribute",begin:"\\b("+o.join("|")+")\\b"},{begin:/:/,end:/[;}{]/,
324 | contains:[l.BLOCK_COMMENT,l.HEXCOLOR,l.IMPORTANT,l.CSS_NUMBER_MODE,...s,{
325 | begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri"
326 | },contains:[{className:"string",begin:/[^)]/,endsWithParent:!0,excludeEnd:!0}]
327 | },l.FUNCTION_DISPATCH]},{begin:a.lookahead(/@/),end:"[{;]",relevance:0,
328 | illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/},{
329 | begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{
330 | $pattern:/[a-z-]+/,keyword:"and or not only",attribute:i.join(" ")},contains:[{
331 | begin:/[a-z-]+(?=:)/,className:"attribute"},...s,l.CSS_NUMBER_MODE]}]},{
332 | className:"selector-tag",begin:"\\b("+e.join("|")+")\\b"}]}}})()
333 | ;hljs.registerLanguage("css",e)})();/*! `json` grammar compiled for Highlight.js 11.5.1 */
334 | (()=>{var e=(()=>{"use strict";return e=>({name:"JSON",contains:[{
335 | className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{
336 | match:/[{}[\],:]/,className:"punctuation",relevance:0},e.QUOTE_STRING_MODE,{
337 | beginKeywords:"true false null"
338 | },e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],illegal:"\\S"})
339 | })();hljs.registerLanguage("json",e)})();/*! `bash` grammar compiled for Highlight.js 11.5.1 */
340 | (()=>{var e=(()=>{"use strict";return e=>{const s=e.regex,t={},n={begin:/\$\{/,
341 | end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]};Object.assign(t,{
342 | className:"variable",variants:[{
343 | begin:s.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},n]});const a={
344 | className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},i={
345 | begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,
346 | end:/(\w+)/,className:"string"})]}},c={className:"string",begin:/"/,end:/"/,
347 | contains:[e.BACKSLASH_ESCAPE,t,a]};a.contains.push(c);const o={begin:/\$\(\(/,
348 | end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t]
349 | },r=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10
350 | }),l={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,
351 | contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{
352 | name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/,
353 | keyword:["if","then","else","elif","fi","for","while","in","do","done","case","esac","function"],
354 | literal:["true","false"],
355 | built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"]
356 | },contains:[r,e.SHEBANG(),l,o,e.HASH_COMMENT_MODE,i,{match:/(\/[a-z._-]+)+/},c,{
357 | className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},t]}}})()
358 | ;hljs.registerLanguage("bash",e)})();/*! `php` grammar compiled for Highlight.js 11.5.1 */
359 | (()=>{var e=(()=>{"use strict";return e=>{
360 | const t=e.regex,a=/(?![A-Za-z0-9])(?![$])/,r=t.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,a),n=t.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,a),o={
361 | scope:"variable",match:"\\$+"+r},c={scope:"subst",variants:[{begin:/\$\w+/},{
362 | begin:/\{\$/,end:/\}/}]},i=e.inherit(e.APOS_STRING_MODE,{illegal:null
363 | }),s="[ \t\n]",l={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{
364 | illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(c)
365 | }),i,e.END_SAME_AS_BEGIN({begin:/<<<[ \t]*(\w+)\n/,end:/[ \t]*(\w+)\b/,
366 | contains:e.QUOTE_STRING_MODE.contains.concat(c)})]},_={scope:"number",
367 | variants:[{begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{
368 | begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{
369 | begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{
370 | begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?"
371 | }],relevance:0
372 | },d=["false","null","true"],p=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],E={
373 | keyword:p,literal:(e=>{const t=[];return e.forEach((e=>{
374 | t.push(e),e.toLowerCase()===e?t.push(e.toUpperCase()):t.push(e.toLowerCase())
375 | })),t})(d),built_in:b},u=e=>e.map((e=>e.replace(/\|\d+$/,""))),g={variants:[{
376 | match:[/new/,t.concat(s,"+"),t.concat("(?!",u(b).join("\\b|"),"\\b)"),n],scope:{
377 | 1:"keyword",4:"title.class"}}]},h=t.concat(r,"\\b(?!\\()"),m={variants:[{
378 | match:[t.concat(/::/,t.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant"
379 | }},{match:[/::/,/class/],scope:{2:"variable.language"}},{
380 | match:[n,t.concat(/::/,t.lookahead(/(?!class\b)/)),h],scope:{1:"title.class",
381 | 3:"variable.constant"}},{match:[n,t.concat("::",t.lookahead(/(?!class\b)/))],
382 | scope:{1:"title.class"}},{match:[n,/::/,/class/],scope:{1:"title.class",
383 | 3:"variable.language"}}]},I={scope:"attr",
384 | match:t.concat(r,t.lookahead(":"),t.lookahead(/(?!::)/))},f={relevance:0,
385 | begin:/\(/,end:/\)/,keywords:E,contains:[I,o,m,e.C_BLOCK_COMMENT_MODE,l,_,g]
386 | },O={relevance:0,
387 | match:[/\b/,t.concat("(?!fn\\b|function\\b|",u(p).join("\\b|"),"|",u(b).join("\\b|"),"\\b)"),r,t.concat(s,"*"),t.lookahead(/(?=\()/)],
388 | scope:{3:"title.function.invoke"},contains:[f]};f.contains.push(O)
389 | ;const v=[I,m,e.C_BLOCK_COMMENT_MODE,l,_,g];return{case_insensitive:!1,
390 | keywords:E,contains:[{begin:t.concat(/#\[\s*/,n),beginScope:"meta",end:/]/,
391 | endScope:"meta",keywords:{literal:d,keyword:["new","array"]},contains:[{
392 | begin:/\[/,end:/]/,keywords:{literal:d,keyword:["new","array"]},
393 | contains:["self",...v]},...v,{scope:"meta",match:n}]
394 | },e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{
395 | scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/,
396 | keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE,
397 | contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{
398 | begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{
399 | begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},o,O,m,{
400 | match:[/const/,/\s/,r],scope:{1:"keyword",3:"variable.constant"}},g,{
401 | scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/,
402 | excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use"
403 | },e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params",
404 | begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:E,
405 | contains:["self",o,m,e.C_BLOCK_COMMENT_MODE,l,_]}]},{scope:"class",variants:[{
406 | beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait",
407 | illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{
408 | beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{
409 | beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/,
410 | contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{
411 | beginKeywords:"use",relevance:0,end:";",contains:[{
412 | match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},l,_]}
413 | }})();hljs.registerLanguage("php",e)})();/*! `xml` grammar compiled for Highlight.js 11.5.1 */
414 | (()=>{var e=(()=>{"use strict";return e=>{
415 | const a=e.regex,n=a.concat(/[A-Z_]/,a.optional(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),s={
416 | className:"symbol",begin:/&[a-z]+;|[0-9]+;|[a-f0-9]+;/},t={begin:/\s/,
417 | contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]
418 | },i=e.inherit(t,{begin:/\(/,end:/\)/}),c=e.inherit(e.APOS_STRING_MODE,{
419 | className:"string"}),l=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),r={
420 | endsWithParent:!0,illegal:/,relevance:0,contains:[{className:"attr",
421 | begin:/[A-Za-z0-9._:-]+/,relevance:0},{begin:/=\s*/,relevance:0,contains:[{
422 | className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[s]},{
423 | begin:/'/,end:/'/,contains:[s]},{begin:/[^\s"'=<>`]+/}]}]}]};return{
424 | name:"HTML, XML",
425 | aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],
426 | case_insensitive:!0,contains:[{className:"meta",begin://,
427 | relevance:10,contains:[t,l,c,i,{begin:/\[/,end:/\]/,contains:[{className:"meta",
428 | begin://,contains:[t,i,l,c]}]}]},e.COMMENT(//,{
429 | relevance:10}),{begin://,relevance:10},s,{
430 | className:"meta",end:/\?>/,variants:[{begin:/<\?xml/,relevance:10,contains:[l]
431 | },{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag",begin:/
59 |
60 |
61 |
62 | = $this->htmlEncode($name) ?>
63 | = nl2br($this->htmlEncode($message)) ?>
64 |
65 | The above error occurred while the Web server was processing your request.
66 |
67 |
68 | Please contact us if you think this is a server error. Thank you.
69 |
70 |
71 | = date('Y-m-d H:i:s') ?>
72 |
73 |
74 |