diff --git a/.gitignore b/.gitignore
index d3096f4..d0c6610 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@ clover.xml
coveralls-upload.json
.phpunit.result.cache
.phpcs-cache
+config/log.global.php
+config/error-handling.global.php
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
diff --git a/README.md b/README.md
index d51f7e1..85167ad 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,12 @@
# dot-errorhandler
-Error Logging Handler for Dotkernel
+dot-errorhandler is Dotkernel's PSR-15 compliant error handler.
+
+## Documentation
+
+Documentation is available at: https://docs.dotkernel.org/dot-errorhandler/
+
+## Badges


@@ -8,9 +14,9 @@ Error Logging Handler for Dotkernel
[](https://github.com/dotkernel/dot-errorhandler/issues)
[](https://github.com/dotkernel/dot-errorhandler/network)
[](https://github.com/dotkernel/dot-errorhandler/stargazers)
-[](https://github.com/dotkernel/dot-errorhandler/blob/4.0/LICENSE)
+[](https://github.com/dotkernel/dot-errorhandler/blob/4.1/LICENSE)
-[](https://github.com/dotkernel/dot-errorhandler/actions/workflows/continuous-integration.yml)
+[](https://github.com/dotkernel/dot-errorhandler/actions/workflows/continuous-integration.yml)
[](https://codecov.io/gh/dotkernel/dot-errorhandler)
## Adding the error handler
diff --git a/composer.json b/composer.json
index c3ce551..5999689 100644
--- a/composer.json
+++ b/composer.json
@@ -6,18 +6,14 @@
"homepage": "https://github.com/dotkernel/dot-errorhandler",
"authors": [
{
- "name": "DotKernel Team",
+ "name": "Dotkernel Team",
"email": "team@dotkernel.com"
}
],
"keywords": [
"error",
"errorhandler",
- "factories",
- "container",
- "laminas",
- "mezzio",
- "service-manager"
+ "error_log"
],
"config": {
"sort-packages": true,
@@ -27,18 +23,19 @@
},
"require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
- "dotkernel/dot-log": "^4.1.0",
- "laminas/laminas-diactoros": "^3.3",
- "laminas/laminas-stratigility": "^3.11",
- "mezzio/mezzio": "^3.19",
+ "dotkernel/dot-log": "^5.0",
+ "laminas/laminas-diactoros": "^3.5",
+ "laminas/laminas-stdlib": "^3.20",
+ "laminas/laminas-stratigility": "^3.13",
+ "mezzio/mezzio": "^3.20.1",
"psr/http-message": "^1.0 || ^2.0",
- "psr/http-server-middleware": "^1.0"
+ "psr/http-server-middleware": "^1.0.2"
},
"require-dev": {
- "laminas/laminas-coding-standard": "^3.0",
- "mikey179/vfsstream": "^1.6.7",
- "phpunit/phpunit": "^10.5",
- "vimeo/psalm": "^6.0"
+ "laminas/laminas-coding-standard": "^3.0.1",
+ "mikey179/vfsstream": "^1.6.12",
+ "phpunit/phpunit": "^10.5.45",
+ "vimeo/psalm": "6.6.2"
},
"autoload": {
"psr-4": {
@@ -60,7 +57,6 @@
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
- "test-coverage": "phpunit --colors=always --coverage-clover clover.xml",
"static-analysis": "psalm --shepherd --stats"
}
}
diff --git a/config/error-handling.global.php.dist b/config/error-handling.global.php.dist
index eb07019..ac7908a 100644
--- a/config/error-handling.global.php.dist
+++ b/config/error-handling.global.php.dist
@@ -1,16 +1,84 @@
[
+ 'dependencies' => [
'aliases' => [
ErrorHandlerInterface::class => LogErrorHandler::class,
- ]
+ ],
],
'dot-errorhandler' => [
- 'loggerEnabled' => true,
- 'logger' => 'dot-log.default_logger'
- ]
+ 'loggerEnabled' => true,
+ 'logger' => 'dot-log.default_logger',
+ ExtraProvider::CONFIG_KEY => [
+ CookieProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => CookieProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ ProcessorInterface::ALL,
+ ],
+ ],
+ ],
+ HeaderProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => HeaderProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ ],
+ ],
+ RequestProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => RequestProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ 'password',
+ ],
+ ],
+ ],
+ ServerProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => ServerProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ ProcessorInterface::ALL,
+ ],
+ ],
+ ],
+ SessionProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => SessionProcessor::class,
+ ],
+ ],
+ TraceProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => TraceProcessor::class,
+ ],
+ ],
+ ],
+ ],
];
diff --git a/config/log.global.php.dist b/config/log.global.php.dist
index f8090fd..146b29b 100644
--- a/config/log.global.php.dist
+++ b/config/log.global.php.dist
@@ -1,27 +1,32 @@
[
'loggers' => [
'default_logger' => [
'writers' => [
'FileWriter' => [
- 'name' => 'stream',
- 'priority' => \Dot\Log\Logger::ALERT,
+ 'name' => 'stream',
+ 'level' => Logger::ALERT,
'options' => [
'stream' => __DIR__ . '/../../log/error-log-{Y}-{m}-{d}.log',
// explicitly log all messages
- 'filters' => [
+ 'filters' => [
'allMessages' => [
- 'name' => 'priority',
+ 'name' => 'level',
'options' => [
'operator' => '>=',
- 'priority' => \Dot\Log\Logger::EMERG,
+ 'level' => Logger::EMERG,
],
],
],
'formatter' => [
- 'name' => \Dot\Log\Formatter\Json::class,
+ 'name' => Json::class,
],
],
],
diff --git a/docs/book/v4/extra/cookie.md b/docs/book/v4/extra/cookie.md
new file mode 100644
index 0000000..494cb8d
--- /dev/null
+++ b/docs/book/v4/extra/cookie.md
@@ -0,0 +1,120 @@
+# Log cookie data
+
+Looking at `dot-errorhandler`'s config file, the array found at `CookieProvider::class` allows you to configure the behaviour of this provider:
+
+- **enabled**: enabled/disable this provider
+- **processor**: an array configuring the data processor to be used by the **CookieProvider**:
+ - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`
+ - **replacementStrategy**: whether to replace specific cookie values completely or partially
+ - **sensitiveParameters**: an array of cookies names that may contain sensitive information so their value should be masked partially/completely
+
+## Configure provider
+
+By default, **CookieProvider** is disabled.
+It can be enabled only by setting **enabled** to **true**.
+
+If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `cookie`.
+If **enabled** is set to **false**, no additional field is added under the `extra` key.
+
+## Configure processor
+
+From here, we assume that **enabled** is set to **true**.
+
+If **processor** is missing/empty, the processor is ignored the provider will log the raw data available.
+If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available.
+
+From here, we assume that **processor**.**class** is valid.
+
+### Replacement strategy
+
+This value should be an instance of `Dot\ErrorHandler\Extra\ReplacementStrategy`.
+
+If **replacementStrategy** is missing/invalid, the default **replacementStrategy** is used, which is `ReplacementStrategy::Full`.
+Else, the value used should be one of:
+
+- `ReplacementStrategy::Partial` for half-string replacements (e.g.: "abcdef" becomes "abc***")
+- `ReplacementStrategy::Full` for full-string replacements (e.g.: "abcdef" becomes "******")
+
+### Sensitive parameters
+
+If **sensitiveParameters** is missing/empty, the processor is ignored the provider will log the raw data available.
+This is because without a set of **sensitiveParameters**, the processor is unable to determine which key needs to be processed or left untouched.
+When specifying the array of **sensitiveParameters**, there are two possibilities:
+
+- use the constant `ProcessorInterface::ALL`, meaning alter all cookie values using the strategy specified by the **replacementStrategy**
+
+```php
+'sensitiveParameters' => [
+ Dot\ErrorHandler\Extra\Processor\ProcessorInterface::ALL,
+],
+```
+
+- use exact strings to list the cookies for which the values should be altered using the strategy specified by the **replacementStrategy**
+
+```php
+'sensitiveParameters' => [
+ 'rememberMe',
+],
+```
+
+> **CookieProcessor** uses EXACT cookie name lookups.
+> In order to alter the value of a cookie, you need to specify the exact cookie name.
+
+> The config `sensitiveParameters` is case-insensitive.
+
+## Why should I use a processor
+
+Consider the following request cookies:
+
+```text
+[
+ "sessionId" => "feb21b39f9c54e3a49af1f862acc8300",
+ "language" => "en",
+]
+```
+
+Without a **CookieProcessor**, the plain text session cookie identifier would end up saved in the log file:
+
+```text
+..."extra":{"file":"/path/to/some/class.php","line":314,"cookie":{"sessionId":"feb21b39f9c54e3a49af1f862acc8300","language":"en"},...
+```
+
+But, with a properly configured **CookieProcessor**:
+
+```php
+'processor' => [
+ 'class' => CookieProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ 'sessionId',
+ ],
+],
+```
+
+the logged cookie data becomes:
+
+```text
+..."extra":{"file":"/path/to/some/class.php","line":314,"cookie":{"sessionId":"********************************","language":"en"},...
+```
+
+## Custom processor
+
+If the existing processor does not offer enough features, you can create a custom processor.
+The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`.
+Once the custom processor is ready, you need to configure **CookieProvider** to use it.
+For this, open `dot-errorhandler`'s config file and - under **CookieProvider::class** - set **processor**.**class** to the class string of your custom processor:
+
+```php
+CookieProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => CustomCookieProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ ProcessorInterface::ALL,
+ ],
+ ],
+],
+```
+
+Using this, cookie data will be processed by `CustomCookieProcessor` and logged as provided by this new processor.
diff --git a/docs/book/v4/extra/header.md b/docs/book/v4/extra/header.md
new file mode 100644
index 0000000..f6bd033
--- /dev/null
+++ b/docs/book/v4/extra/header.md
@@ -0,0 +1,148 @@
+# Log header data
+
+Looking at `dot-errorhandler`'s config file, the array found at `HeaderProvider::class` allows you to configure the behaviour of this provider:
+
+- **enabled**: enabled/disable this provider
+- **processor**: an array configuring the data processor to be used by the **HeaderProvider**:
+ - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`
+ - **replacementStrategy**: whether to replace specific header values completely or partially
+ - **sensitiveParameters**: an array of headers names that may contain sensitive information so their value should be masked partially/completely
+
+## Configure provider
+
+By default, **HeaderProvider** is disabled.
+It can be enabled only by setting **enabled** to **true**.
+
+If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `header`.
+If **enabled** is set to **false**, no additional field is added under the `extra` key.
+
+## Configure processor
+
+From here, we assume that **enabled** is set to **true**.
+
+If **processor** is missing/empty, the processor is ignored the provider will log the raw data available.
+If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available.
+
+From here, we assume that **processor**.**class** is valid.
+
+### Replacement strategy
+
+This value should be an instance of `Dot\ErrorHandler\Extra\ReplacementStrategy`.
+
+If **replacementStrategy** is missing/invalid, the default **replacementStrategy** is used, which is `ReplacementStrategy::Full`.
+Else, the value used should be one of:
+
+- `ReplacementStrategy::Partial` for half-string replacements (e.g.: "abcdef" becomes "abc***")
+- `ReplacementStrategy::Full` for full-string replacements (e.g.: "abcdef" becomes "******")
+
+### Sensitive parameters
+
+If **sensitiveParameters** is missing/empty, the processor is ignored the provider will log the raw data available.
+This is because without a set of **sensitiveParameters**, the processor is unable to determine which key needs to be processed or left untouched.
+When specifying the array of **sensitiveParameters**, there are two possibilities:
+
+- use the constant `ProcessorInterface::ALL`, meaning alter all header values using the strategy specified by the **replacementStrategy**
+
+```php
+'sensitiveParameters' => [
+ Dot\ErrorHandler\Extra\Processor\ProcessorInterface::ALL,
+],
+```
+
+- use exact strings to list the headers for which the values should be altered using the strategy specified by the **replacementStrategy**
+
+```php
+'sensitiveParameters' => [
+ 'Authorization',
+],
+```
+
+> **HeaderProcessor** uses EXACT header name lookups.
+> In order to alter the value of a header, you need to specify the exact header name.
+
+> The config `sensitiveParameters` is case-insensitive.
+
+## Why should I use a processor
+
+Consider the following request headers:
+
+```text
+[
+ "Authorization" => "Bearer 63560eb4398d21024b32f2fb45dacca512db0bc725149e1371f493063a03e687",
+ "Content-Type" => "application/json",
+]
+```
+
+Without a **HeaderProcessor**, the plain text auth token would end up saved in the log file:
+
+```text
+..."extra":{"file":"/path/to/some/class.php","line":314,"header":{"Authorization":"Bearer 63560eb4398d21024b32f2fb45dacca512db0bc725149e1371f493063a03e687","Content-Type":"application/json"},...
+```
+
+But, with a properly configured **HeaderProcessor**:
+
+```php
+'processor' => [
+ 'class' => HeaderProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ 'Authorization',
+ ],
+],
+```
+
+the logged header data becomes:
+
+```text
+..."extra":{"file":"/path/to/some/class.php","line":314,"header":{"Authorization":"***********************************************************************","Content-Type":"application/json"},...
+```
+
+## Special case
+
+There is a special case, the `cookie` header, which is handled differently than the rest of the headers.
+
+Let's take an example of a cookie header:
+
+```text
+FRONTEND_SESSID=feb21b39f9c54e3a49af1f862acc8300; rememberMe=63560eb4398d21024b32f2fb45dacca512db0bc725149e1371f493063a03e687
+```
+
+If the existing **HeaderProcessor** is not used, then the log file will contain dangerous data that may compromise user security - in this case exposing the value of both cookies.
+
+> To avoid this issue, the developer should never use **HeaderProvider** without a **HeaderProcessor** in a production environment.
+
+Depending on **HeaderProvider**'s configuration, **HeaderProcessor** will partially mask the cookie values:
+
+```text
+FRONTEND_SESSID=feb21b39f9c54e3a****************; rememberMe=63560eb4398d21024b32f2******************************************
+```
+
+when using `ReplacementStrategy::Partial` or completely mask the cookie values:
+
+```text
+FRONTEND_SESSID=********************************; rememberMe=****************************************************************
+```
+
+when using `ReplacementStrategy::Full`.
+
+## Custom processor
+
+If the existing processor does not offer enough features, you can create a custom processor.
+The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`.
+Once the custom processor is ready, you need to configure **HeaderProvider** to use it.
+For this, open `dot-errorhandler`'s config file and - under **HeaderProvider::class** - set **processor**.**class** to the class string of your custom processor:
+
+```php
+HeaderProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => CustomHeaderProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ ProcessorInterface::ALL,
+ ],
+ ],
+],
+```
+
+Using this, header data will be processed by `CustomHeaderProcessor` and logged as provided by this new processor.
diff --git a/docs/book/v4/extra/introduction.md b/docs/book/v4/extra/introduction.md
new file mode 100644
index 0000000..5c8f6cc
--- /dev/null
+++ b/docs/book/v4/extra/introduction.md
@@ -0,0 +1,33 @@
+# Extra data
+
+`dot-errorhandler` provides the following data:
+
+- **cookie**: an array containing the request cookies
+- **header**: an array containing the request headers
+- **request**: an array containing the request body (**\$_PATCH**/**\$_POST**/**\$_PUT**)
+- **server**: an array containing the **\$_SERVER** values
+- **session**: an array containing the **\$_SESSION** values
+- **trace**: an array containing the full stack trace of the request
+
+## Configuring extra data
+
+At this point, you should already have `dot-errorhandler` configured.
+If not, proceed to the [Configuration](../configuration.md) page and when you're done, return to this page.
+
+In order start logging one or more of the above data, we first need to enable them from the package's config file.
+Open the config file and under `dot-errorhandler` locate the `ExtraProvider::CONFIG_KEY` key.
+There you will find 6 associative arrays, each array representing a set of data this package can provide:
+
+- **CookieProvider::class**: request cookie information - [how to use](cookie.md)
+- **HeaderProvider::class**: request header information - [how to use](header.md)
+- **RequestProvider::class**: request body contents - [how to use](request.md)
+- **ServerProvider::class**: server information - [how to use](server.md)
+- **SessionProvider::class**: session contents - [how to use](session.md)
+- **TraceProvider::class**: trace route - [how to use](trace.md)
+
+These providers and their configuration values are all optional.
+If any of them is missing from the config file, the provider simply stays disabled and code execution continues normally.
+Invalid configuration values are simply ignored, because the purpose of these providers is to log the extra data if possible, but not to interfere with the app logic.
+That's why it does not throw errors on missing/invalid config values.
+
+> By default, `*Provider` classes are used in fall-through mode; without a processor, they just return the unaltered data they were given.
diff --git a/docs/book/v4/extra/request.md b/docs/book/v4/extra/request.md
new file mode 100644
index 0000000..6b87275
--- /dev/null
+++ b/docs/book/v4/extra/request.md
@@ -0,0 +1,127 @@
+# Log request data
+
+Looking at `dot-errorhandler`'s config file, the array found at `RequestProvider::class` allows you to configure the behaviour of this provider:
+
+- **enabled**: enabled/disable this provider
+- **processor**: an array configuring the data processor to be used by the **RequestProvider**:
+ - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`
+ - **replacementStrategy**: whether to replace specific cookie values completely or partially
+ - **sensitiveParameters**: an array of cookies names that may contain sensitive information so their value should be masked partially/completely
+
+## Configure provider
+
+By default, **RequestProvider** is disabled.
+It can be enabled only by setting **enabled** to **true**.
+
+If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `request`.
+If **enabled** is set to **false**, no additional field is added under the `extra` key.
+
+## Configure processor
+
+From here, we assume that **enabled** is set to **true**.
+
+If **processor** is missing/empty, the processor is ignored the provider will log the raw data available.
+If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available.
+
+From here, we assume that **processor**.**class** is valid.
+
+### Replacement strategy
+
+This value should be an instance of `Dot\ErrorHandler\Extra\ReplacementStrategy`.
+
+If **replacementStrategy** is missing/invalid, the default **replacementStrategy** is used, which is `ReplacementStrategy::Full`.
+Else, the value used should be one of:
+
+- `ReplacementStrategy::Partial` for half-string replacements (e.g.: "abcdef" becomes "abc***")
+- `ReplacementStrategy::Full` for full-string replacements (e.g.: "abcdef" becomes "******")
+
+### Sensitive parameters
+
+If **sensitiveParameters** is missing/empty, the processor is ignored the provider will log the raw data available.
+This is because without a set of **sensitiveParameters**, the processor is unable to determine which key needs to be processed or left untouched.
+When specifying the array of **sensitiveParameters**, there are two possibilities:
+
+- use the constant `ProcessorInterface::ALL`, meaning alter all cookie values using the strategy specified by the **replacementStrategy**
+
+```php
+'sensitiveParameters' => [
+ Dot\ErrorHandler\Extra\Processor\ProcessorInterface::ALL,
+],
+```
+
+- use exact strings to list the cookies for which the values should be altered using the strategy specified by the **replacementStrategy**
+
+```php
+'sensitiveParameters' => [
+ 'password',
+],
+```
+
+> **RequestProcessor** uses recursive search to locate all array keys in a multidimensional array.
+
+> **RequestProcessor** uses PARTIAL field name lookups.
+> In order to alter the value of a request field, it is enough to specify only part of the field name.
+
+> The config `sensitiveParameters` is case-insensitive.
+
+## Why should I use a processor
+
+Consider the following request body sent via **$_POST**:
+
+```text
+[
+ "identity" => "myIdentity",
+ "password" => "p4$$w0rd",
+ "passwordConfirm" => "p4$$w0rd",
+ "details" => [
+ "secret" => "s3cr3t",
+ ]
+]
+```
+
+Without a **RequestProcessor**, the plain text passwords would end up saved in the log file:
+
+```text
+..."extra":{"file":"/path/to/some/class.php","line":314,"request":{"identity":"myIdentity","password":"p4$$w0rd","passwordConfirm":"p4$$w0rd","details":{"secret":"s3cr3t"}}},...
+```
+
+But, with a properly configured **RequestProcessor**:
+
+```php
+'processor' => [
+ 'class' => RequestProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ 'password',
+ 'secret',
+ ],
+],
+```
+
+the logged request data becomes:
+
+```text
+..."extra":{"file":"/path/to/some/class.php","line":314,"request":{"identity":"myIdentity","password":"********","passwordConfirm":"********","details":{"secret":"******"}}},...
+```
+
+## Custom processor
+
+If the existing processor does not offer enough features, you can create a custom processor.
+The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`.
+Once the custom processor is ready, you need to configure **RequestProvider** to use it.
+For this, open `dot-errorhandler`'s config file and - under **RequestProvider::class** - set **processor**.**class** to the class string of your custom processor:
+
+```php
+RequestProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => CustomRequestProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ ProcessorInterface::ALL,
+ ],
+ ],
+],
+```
+
+Using this, request data will be processed by `CustomRequestProcessor` and logged as provided by this new processor.
diff --git a/docs/book/v4/extra/server.md b/docs/book/v4/extra/server.md
new file mode 100644
index 0000000..956cb7f
--- /dev/null
+++ b/docs/book/v4/extra/server.md
@@ -0,0 +1,148 @@
+# Log server data
+
+Looking at `dot-errorhandler`'s config file, the array found at `ServerProvider::class` allows you to configure the behaviour of this provider:
+
+- **enabled**: enabled/disable this provider
+- **processor**: an array configuring the data processor to be used by the **ServerProvider**:
+ - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`
+ - **replacementStrategy**: whether to replace specific server config values completely or partially
+ - **sensitiveParameters**: an array of server config names that may contain sensitive information so their value should be masked partially/completely
+
+## Configure provider
+
+By default, **ServerProvider** is disabled.
+It can be enabled only by setting **enabled** to **true**.
+
+If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `server`.
+If **enabled** is set to **false**, no additional field is added under the `extra` key.
+
+## Configure processor
+
+From here, we assume that **enabled** is set to **true**.
+
+If **processor** is missing/empty, the processor is ignored the provider will log the raw data available.
+If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available.
+
+From here, we assume that **processor**.**class** is valid.
+
+### Replacement strategy
+
+This value should be an instance of `Dot\ErrorHandler\Extra\ReplacementStrategy`.
+
+If **replacementStrategy** is missing/invalid, the default **replacementStrategy** is used, which is `ReplacementStrategy::Full`.
+Else, the value used should be one of:
+
+- `ReplacementStrategy::Partial` for half-string replacements (e.g.: "abcdef" becomes "abc***")
+- `ReplacementStrategy::Full` for full-string replacements (e.g.: "abcdef" becomes "******")
+
+### Sensitive parameters
+
+If **sensitiveParameters** is missing/empty, the processor is ignored the provider will log the raw data available.
+This is because without a set of **sensitiveParameters**, the processor is unable to determine which key needs to be processed or left untouched.
+When specifying the array of **sensitiveParameters**, there are two possibilities:
+
+- use the constant `ProcessorInterface::ALL`, meaning alter all server config values using the strategy specified by the **replacementStrategy**
+
+```php
+'sensitiveParameters' => [
+ Dot\ErrorHandler\Extra\Processor\ProcessorInterface::ALL,
+],
+```
+
+- use exact strings to list the server configs for which the values should be altered using the strategy specified by the **replacementStrategy**
+
+```php
+'sensitiveParameters' => [
+ 'SERVER_ADMIN',
+],
+```
+
+> **ServerProcessor** uses EXACT server config name lookups.
+> In order to alter the value of a server config, you need to specify the exact server config name.
+
+> The config `sensitiveParameters` is case-insensitive.
+
+## Why should I use a processor
+
+Consider the following request server configs:
+
+```text
+[
+ "SERVER_ADMIN" => "webmaster@localhost",
+ "SERVER_SOFTWARE" => "Apache/2.4.62 (AlmaLinux)",
+]
+```
+
+Without a **ServerProcessor**, (for the purpose of this example) the key **SERVER_ADMIN** would end up saved in the log file:
+
+```text
+..."extra":{"file":"/path/to/some/class.php","line":314,"server":{"SERVER_ADMIN":"webmaster@localhost","SERVER_SOFTWARE":"Apache/2.4.62 (AlmaLinux)"},...
+```
+
+But, with a properly configured **ServerProcessor**:
+
+```php
+'processor' => [
+ 'class' => ServerProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ 'SERVER_ADMIN',
+ ],
+],
+```
+
+the logged server config data becomes:
+
+```text
+..."extra":{"file":"/path/to/some/class.php","line":314,"server":{"SERVER_ADMIN":"*******************","SERVER_SOFTWARE":"Apache/2.4.62 (AlmaLinux)"},...
+```
+
+## Special case
+
+There is a special case, the `HTTP_COOKIE` server config value, which is handled differently than the rest of the values.
+
+Let's take an example of a **HTTP_COOKIE** value:
+
+```text
+FRONTEND_SESSID=feb21b39f9c54e3a49af1f862acc8300; rememberMe=63560eb4398d21024b32f2fb45dacca512db0bc725149e1371f493063a03e687
+```
+
+If the existing **ServerProcessor** is not used, then the log file will contain dangerous data that may compromise user security - in this case exposing the value of both cookies.
+
+> To avoid this issue, the developer should never use **ServerProvider** without a **ServerProcessor** in a production environment.
+
+Depending on **ServerProvider**'s configuration, **ServerProcessor** will partially mask the cookie values:
+
+```text
+FRONTEND_SESSID=feb21b39f9c54e3a****************; rememberMe=63560eb4398d21024b32f2******************************************
+```
+
+when using `ReplacementStrategy::Partial` or completely mask the cookie values:
+
+```text
+FRONTEND_SESSID=********************************; rememberMe=****************************************************************
+```
+
+when using `ReplacementStrategy::Full`.
+
+## Custom processor
+
+If the existing processor does not offer enough features, you can create a custom processor.
+The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`.
+Once the custom processor is ready, you need to configure **ServerProvider** to use it.
+For this, open `dot-errorhandler`'s config file and - under **ServerProvider::class** - set **processor**.**class** to the class string of your custom processor:
+
+```php
+ServerProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => CustomServerProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ ProcessorInterface::ALL,
+ ],
+ ],
+],
+```
+
+Using this, server config data will be processed by `CustomServerProcessor` and logged as provided by this new processor.
diff --git a/docs/book/v4/extra/session.md b/docs/book/v4/extra/session.md
new file mode 100644
index 0000000..c18b8e5
--- /dev/null
+++ b/docs/book/v4/extra/session.md
@@ -0,0 +1,113 @@
+# Log session data
+
+Looking at `dot-errorhandler`'s config file, the array found at `SessionProvider::class` allows you to configure the behaviour of this provider:
+
+- **enabled**: enabled/disable this provider
+- **processor**: an array configuring the data processor to be used by the **SessionProvider**:
+ - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`
+
+## Configure provider
+
+By default, **SessionProvider** is disabled.
+It can be enabled only by setting **enabled** to **true**.
+
+If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `session`.
+If **enabled** is set to **false**, no additional field is added under the `extra` key.
+
+## Configure processor
+
+From here, we assume that **enabled** is set to **true**.
+
+If **processor** is missing/empty, the processor is ignored the provider will log the raw data available.
+If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available.
+
+From here, we assume that **processor**.**class** is valid.
+
+### Replacement strategy
+
+**SessionProcessor** does not use **replacementStrategy**.
+
+> If you create a custom session processor, you may add **replacementStrategy** to the config file, it will get passed to your processor.
+
+### Sensitive parameters
+
+**SessionProcessor** does not use **sensitiveParameters**.
+
+> If you create a custom session processor, you may add **sensitiveParameters** to the config file, it will get passed to your processor.
+
+## Why should I use a processor
+
+The only advantage of using **SessionProcessor** is to reduce the size of the session data that you log.
+In the below example, **$_SESSION** contains an **array** in _SessionContainer1_ and an **ArrayObject** in _SessionContainer2_.
+
+```text
+ "SessionContainer1" => array:3 [▼
+ "_REQUEST_ACCESS_TIME" => 1739973274.7284
+ "_VALID" => array:1 [▼
+ "Laminas\Session\Validator\Id" => "feb21b39f9c54e3a49af1f862acc8300"
+ ]
+ "Default" => array:1 [▼
+ "EXPIRE" => 1739969278
+ ]
+ ]
+ "SessionContainer2" => Laminas\Stdlib\ArrayObject {#795 ▼
+ #storage: array:2 [▼
+ "tokenList" => array:1 [▼
+ "b222251fb72d49ae0643bff11e11057d" => "389b5e5b415409abb61cfe718e7841bf"
+ ]
+ "hash" => "389b5e5b415409abb61cfe718e7841bf-b222251fb72d49ae0643bff11e11057d"
+ ]
+ #flag: 2
+ #iteratorClass: "ArrayIterator"
+ #protectedProperties: array:4 [▼
+ 0 => "storage"
+ 1 => "flag"
+ 2 => "iteratorClass"
+ 3 => "protectedProperties"
+ ]
+ }
+```
+
+By using **SessionProcessor**, the two items are reduced to:
+
+```text
+ "SessionContainer1" => array:3 [▼
+ "_REQUEST_ACCESS_TIME" => 1739973274.7284
+ "_VALID" => array:1 [▼
+ "Laminas\Session\Validator\Id" => "feb21b39f9c54e3a49af1f862acc8300"
+ ]
+ "Default" => array:1 [▼
+ "EXPIRE" => 1739969278
+ ]
+ ]
+ "SessionContainer2" => array:2 [▼
+ "tokenList" => array:1 [▼
+ "b222251fb72d49ae0643bff11e11057d" => "389b5e5b415409abb61cfe718e7841bf"
+ ]
+ "hash" => "389b5e5b415409abb61cfe718e7841bf-b222251fb72d49ae0643bff11e11057d"
+ ]
+```
+
+As you can see, with arrays the difference is almost ignorable, but with ArrayObjects, the difference is significant.
+
+## Custom processor
+
+If the existing processor does not offer enough features, you can create a custom processor.
+The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`.
+Once the custom processor is ready, you need to configure **SessionProvider** to use it.
+For this, open `dot-errorhandler`'s config file and - under **SessionProvider::class** - set **processor**.**class** to the class string of your custom processor:
+
+```php
+SessionProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => CustomSessionProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ ProcessorInterface::ALL,
+ ],
+ ],
+],
+```
+
+Using this, cookie data will be processed by `CustomSessionProcessor` and logged as provided by this new processor.
diff --git a/docs/book/v4/extra/trace.md b/docs/book/v4/extra/trace.md
new file mode 100644
index 0000000..abe489d
--- /dev/null
+++ b/docs/book/v4/extra/trace.md
@@ -0,0 +1,98 @@
+# Log trace data
+
+Looking at `dot-errorhandler`'s config file, the array found at `TraceProvider::class` allows you to configure the behaviour of this provider:
+
+- **enabled**: enabled/disable this provider
+- **processor**: an array configuring the data processor to be used by the **TraceProvider**:
+ - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`
+
+## Configure provider
+
+By default, **TraceProvider** is enabled.
+It can be enabled only by setting **enabled** to **true**.
+
+If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `trace`.
+If **enabled** is set to **false**, no additional field is added under the `extra` key.
+
+## Configure processor
+
+From here, we assume that **enabled** is set to **true**.
+
+If **processor** is missing/empty, the processor is ignored the provider will log the raw data available.
+If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available.
+
+From here, we assume that **processor**.**class** is valid.
+
+### Replacement strategy
+
+**TraceProcessor** does not use **replacementStrategy**.
+
+> If you create a custom session processor, you may add **replacementStrategy** to the config file, it will get passed to your processor.
+
+### Sensitive parameters
+
+**TraceProcessor** does not use **sensitiveParameters**.
+
+> If you create a custom session processor, you may add **sensitiveParameters** to the config file, it will get passed to your processor.
+
+## Why should I use a processor
+
+The only advantage of using **TraceProcessor** is to reduce the size of the trace data that you log.
+In the below example, the trace route contains an array with dozens of items, each with the same structure (except for the last one).
+
+```text
+ 0 => array:5 [▼
+ "file" => "/path/to/some/file.php"
+ "line" => 66
+ "function" => "someFunction"
+ "class" => "Path\To\Some\File"
+ "type" => "->"
+ ]
+ 1 => array:5 [▼
+ "file" => "/path/to/some/other/file.php"
+ "line" => 33
+ "function" => "someOtherFunction"
+ "class" => "Path\To\Some\Other\File"
+ "type" => "->"
+ ]
+ ...
+ 58 => array:3 [▼
+ "file" => "/path/to/index.php"
+ "line" => 36
+ "function" => "{closure}"
+ ]
+```
+
+By using **TraceProcessor**, the array are reduced to:
+
+```text
+ 0 => "Path\To\Some\File->someFunction:66"
+ 1 => "Path\To\Some\Other\File->someOtherFunction:33"
+ ...
+ 58 => "/path/to/index.php->{closure}:36"
+```
+
+Yet, it maintains the relevant information, but in a compact form.
+This works because, since using namespaced classes, it's easy to determine file paths just by looking at a FQCN.
+
+## Custom processor
+
+If the existing processor does not offer enough features, you can create a custom processor.
+The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`.
+Once the custom processor is ready, you need to configure **TraceProvider** to use it.
+For this, open `dot-errorhandler`'s config file and - under **TraceProvider::class** - set **processor**.**class** to the class string of your custom processor:
+
+```php
+TraceProvider::class => [
+ 'enabled' => false,
+ 'processor' => [
+ 'class' => CustomTraceProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [
+ ProcessorInterface::ALL,
+ ],
+ ],
+],
+```
+
+Using this, trace data will be processed by `CustomTraceProcessor` and logged as provided by this new processor.
diff --git a/docs/book/v4/log-files.md b/docs/book/v4/log-files.md
new file mode 100644
index 0000000..cd686e9
--- /dev/null
+++ b/docs/book/v4/log-files.md
@@ -0,0 +1,21 @@
+# Log files
+
+## What is a log file
+
+Log files are plain text files, containing rows of log activity, each row being a standalone activity formatted as specified in `dot-errorhandler`'s configuration file.
+By default, log activities are formatted with JSON, so each row should be a decodable JSON string.
+
+## What is in a log file
+
+Each row in a log file should contain the following values:
+
+- **timestamp**: string representation of the date and time when the error occurred
+- **priority**: numeric representation of the error level
+- **priorityName**: string representation of the error level
+- **message**: error message describing the error
+- **extra**: an array of extra information that may help the developer debug the error:
+ - **file**: the file in which the error occurred
+ - **line**: the line from **file** where the error occurred
+
+By leveraging `dot-errorhandler`'s extra providers, you can also log additional request parameters.
+Learn more about what additional parameters are available on the [extra data](extra/introduction.md) page.
diff --git a/docs/book/v4/overview.md b/docs/book/v4/overview.md
index 6fc31dd..3eb446f 100644
--- a/docs/book/v4/overview.md
+++ b/docs/book/v4/overview.md
@@ -1,6 +1,23 @@
# Overview
-`dot-errorhandler` is Dotkernel's logging error handler, providing two options:
+dot-errorhandler is Dotkernel's PSR-15 compliant error handler.
+
+## Badges
+
+
+
+
+[](https://github.com/dotkernel/dot-errorhandler/issues)
+[](https://github.com/dotkernel/dot-errorhandler/network)
+[](https://github.com/dotkernel/dot-errorhandler/stargazers)
+[](https://github.com/dotkernel/dot-errorhandler/blob/4.1/LICENSE)
+
+[](https://github.com/dotkernel/dot-errorhandler/actions/workflows/continuous-integration.yml)
+[](https://codecov.io/gh/dotkernel/dot-errorhandler)
+
+## Features
+
+This package provides two features:
- `Dot\ErrorHandler\ErrorHandler`, same as the Zend Expressive error handling class with the only difference being the removal of the `final` statement for making extension possible
- `Dot\ErrorHandler\LogErrorHandler` adds logging support to the default `ErrorHandler` class
diff --git a/mkdocs.yml b/mkdocs.yml
index b95a4df..9d2f48e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -12,12 +12,21 @@ nav:
- Overview: v4/overview.md
- Installation: v4/installation.md
- Configuration: v4/configuration.md
+ - "Log files": v4/log-files.md
+ - "Extra data":
+ - Introduction: v4/extra/introduction.md
+ - Cookie: v4/extra/cookie.md
+ - Header: v4/extra/header.md
+ - Request: v4/extra/request.md
+ - Server: v4/extra/server.md
+ - Session: v4/extra/session.md
+ - "Trace route": v4/extra/trace.md
- v3:
- Overview: v3/overview.md
- Installation: v3/installation.md
- Configuration: v3/configuration.md
site_name: dot-errorhandler
-site_description: "DotKernel's error logging handler"
+site_description: "Dotkernel's PSR-15 compliant error handler"
repo_url: "https://github.com/dotkernel/dot-errorhandler"
plugins:
- search
diff --git a/phpcs.xml b/phpcs.xml
index 1efe663..0d4c1dc 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -12,6 +12,7 @@
+ config
src
test
diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php
index 2f21f75..67a409c 100644
--- a/src/ErrorHandler.php
+++ b/src/ErrorHandler.php
@@ -8,7 +8,6 @@
use Laminas\Stratigility\Middleware\ErrorResponseGenerator;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
@@ -17,7 +16,7 @@
use function restore_error_handler;
use function set_error_handler;
-class ErrorHandler implements MiddlewareInterface, ErrorHandlerInterface
+class ErrorHandler implements ErrorHandlerInterface
{
/** @var callable[] */
private array $listeners = [];
diff --git a/src/ErrorHandlerFactory.php b/src/ErrorHandlerFactory.php
index 5f25f70..916e5a9 100644
--- a/src/ErrorHandlerFactory.php
+++ b/src/ErrorHandlerFactory.php
@@ -10,6 +10,9 @@
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface;
+use function assert;
+use function is_callable;
+
class ErrorHandlerFactory
{
/**
@@ -18,10 +21,15 @@ class ErrorHandlerFactory
*/
public function __invoke(ContainerInterface $container): ErrorHandler
{
- $generator = $container->has(ErrorResponseGenerator::class)
- ? $container->get(ErrorResponseGenerator::class)
- : null;
+ $generator = null;
+ if ($container->has(ErrorResponseGenerator::class)) {
+ $generator = $container->get(ErrorResponseGenerator::class);
+ assert($generator instanceof ErrorResponseGenerator);
+ }
+
+ $responseInterface = $container->get(ResponseInterface::class);
+ assert(is_callable($responseInterface));
- return new ErrorHandler($container->get(ResponseInterface::class), $generator);
+ return new ErrorHandler($responseInterface, $generator);
}
}
diff --git a/src/Extra/ExtraProvider.php b/src/Extra/ExtraProvider.php
new file mode 100644
index 0000000..290ce91
--- /dev/null
+++ b/src/Extra/ExtraProvider.php
@@ -0,0 +1,122 @@
+ CookieProvider::class,
+ 'header' => HeaderProvider::class,
+ 'request' => RequestProvider::class,
+ 'server' => ServerProvider::class,
+ 'session' => SessionProvider::class,
+ 'trace' => TraceProvider::class,
+ ];
+
+ foreach ($extras as $extraKey => $extraClass) {
+ if (
+ ! array_key_exists($extraClass, $options)
+ || ! array_key_exists('enabled', $options[$extraClass])
+ || $options[$extraClass]['enabled'] === false
+ ) {
+ $this->$extraKey = new $extraClass(false);
+ continue;
+ }
+
+ if (
+ ! array_key_exists('processor', $options[$extraClass])
+ || ! is_array($options[$extraClass]['processor'])
+ || ! array_key_exists('class', $options[$extraClass]['processor'])
+ || ! is_string($options[$extraClass]['processor']['class'])
+ || ! class_exists($options[$extraClass]['processor']['class'])
+ ) {
+ $this->$extraKey = new $extraClass(true);
+ continue;
+ }
+
+ $sensitiveParameters = [];
+ if (
+ array_key_exists('sensitiveParameters', $options[$extraClass]['processor'])
+ && is_array($options[$extraClass]['processor']['sensitiveParameters'])
+ && count($options[$extraClass]['processor']['sensitiveParameters']) > 0
+ ) {
+ $sensitiveParameters = $options[$extraClass]['processor']['sensitiveParameters'];
+ $sensitiveParameters = array_map('strtolower', $sensitiveParameters);
+ $sensitiveParameters = array_flip($sensitiveParameters);
+ }
+
+ $replacementStrategy = ReplacementStrategy::Full;
+ if (
+ array_key_exists('replacementStrategy', $options[$extraClass]['processor'])
+ && $options[$extraClass]['processor']['replacementStrategy'] instanceof ReplacementStrategy
+ ) {
+ $replacementStrategy = $options[$extraClass]['processor']['replacementStrategy'];
+ }
+
+ $processor = new $options[$extraClass]['processor']['class']($sensitiveParameters, $replacementStrategy);
+ assert($processor instanceof ProcessorInterface);
+
+ $this->$extraKey = new $extraClass(true, $processor);
+ }
+ }
+
+ public function getCookie(): CookieProvider
+ {
+ return $this->cookie;
+ }
+
+ public function getHeader(): HeaderProvider
+ {
+ return $this->header;
+ }
+
+ public function getRequest(): RequestProvider
+ {
+ return $this->request;
+ }
+
+ public function getServer(): ServerProvider
+ {
+ return $this->server;
+ }
+
+ public function getSession(): SessionProvider
+ {
+ return $this->session;
+ }
+
+ public function getTrace(): TraceProvider
+ {
+ return $this->trace;
+ }
+}
diff --git a/src/Extra/Processor/AbstractProcessor.php b/src/Extra/Processor/AbstractProcessor.php
new file mode 100644
index 0000000..730869c
--- /dev/null
+++ b/src/Extra/Processor/AbstractProcessor.php
@@ -0,0 +1,70 @@
+sensitiveParameters;
+ }
+
+ public function getReplacementStrategy(): ReplacementStrategy
+ {
+ return $this->replacementStrategy;
+ }
+
+ public function replace(
+ ReplacementStrategy $replacementStrategy,
+ string $subject,
+ string $replacement = ProcessorInterface::ALL
+ ): string {
+ return match ($replacementStrategy->name) {
+ ReplacementStrategy::Full->name => $this->replaceFull($subject, $replacement),
+ ReplacementStrategy::Partial->name => $this->replacePartial($subject, $replacement),
+ };
+ }
+
+ private function replaceFull(string $subject, string $replacement = ProcessorInterface::ALL): string
+ {
+ return str_repeat($replacement, strlen($subject));
+ }
+
+ private function replacePartial(string $subject, string $replacement = ProcessorInterface::ALL): string
+ {
+ return str_pad(substr($subject, 0, (int) round(strlen($subject) / 2)), strlen($subject), $replacement);
+ }
+
+ public function replaceInlineCookieValues(ReplacementStrategy $replacementStrategy, string $header): string
+ {
+ return (string) preg_replace_callback(
+ '/([^=\s;]+)=([^;]*)/',
+ fn (array $matches): string => sprintf(
+ '%s=%s',
+ $matches[1] ?? '',
+ $this->replace($replacementStrategy, $matches[2] ?? '')
+ ),
+ $header
+ );
+ }
+}
diff --git a/src/Extra/Processor/CookieProcessor.php b/src/Extra/Processor/CookieProcessor.php
new file mode 100644
index 0000000..b4d858f
--- /dev/null
+++ b/src/Extra/Processor/CookieProcessor.php
@@ -0,0 +1,34 @@
+sensitiveParameters) === 0 || count($data) === 0) {
+ return $data;
+ }
+
+ $return = [];
+
+ foreach ($data as $cookieName => $cookieValue) {
+ if (
+ ! isset($this->sensitiveParameters[ProcessorInterface::ALL])
+ && ! isset($this->sensitiveParameters[strtolower($cookieName)])
+ ) {
+ $return[$cookieName] = $cookieValue;
+ continue;
+ }
+
+ $return[$cookieName] = $this->replace($this->replacementStrategy, $cookieValue);
+ }
+
+ return $return;
+ }
+}
diff --git a/src/Extra/Processor/HeaderProcessor.php b/src/Extra/Processor/HeaderProcessor.php
new file mode 100644
index 0000000..cd65020
--- /dev/null
+++ b/src/Extra/Processor/HeaderProcessor.php
@@ -0,0 +1,42 @@
+ $headerValue) {
+ if (is_array($headerValue)) {
+ $headerValue = implode('; ', $headerValue);
+ }
+ if (
+ ! isset($this->sensitiveParameters[ProcessorInterface::ALL])
+ && ! isset($this->sensitiveParameters[strtolower($headerName)])
+ && $headerName !== 'cookie'
+ ) {
+ $return[$headerName] = $headerValue;
+ continue;
+ }
+
+ $return[$headerName] = $headerName === 'cookie'
+ ? $this->replaceInlineCookieValues($this->replacementStrategy, $headerValue)
+ : $this->replace($this->replacementStrategy, (string) $headerValue);
+ }
+
+ return $return;
+ }
+}
diff --git a/src/Extra/Processor/ProcessorInterface.php b/src/Extra/Processor/ProcessorInterface.php
new file mode 100644
index 0000000..e6e4bf7
--- /dev/null
+++ b/src/Extra/Processor/ProcessorInterface.php
@@ -0,0 +1,26 @@
+sensitiveParameters) === 0 || count($data) === 0) {
+ return $data;
+ }
+
+ $return = [];
+
+ $sensitiveParameters = array_keys($this->sensitiveParameters);
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $return[$key] = $this->process($value);
+ } else {
+ $matches = array_filter(
+ $sensitiveParameters,
+ fn (string $sensitiveParameter) => str_contains(strtolower($key), $sensitiveParameter)
+ );
+
+ if (! isset($this->sensitiveParameters[ProcessorInterface::ALL]) && count($matches) === 0) {
+ $return[$key] = $value;
+ continue;
+ }
+
+ $return[$key] = $this->replace($this->replacementStrategy, (string) $value);
+ }
+ }
+
+ return $return;
+ }
+}
diff --git a/src/Extra/Processor/ServerProcessor.php b/src/Extra/Processor/ServerProcessor.php
new file mode 100644
index 0000000..da6d708
--- /dev/null
+++ b/src/Extra/Processor/ServerProcessor.php
@@ -0,0 +1,70 @@
+ $serverValue) {
+ if ($serverKey === 'HTTP_COOKIE') {
+ $serverValue = $this->stringToAssociativeArray($serverValue);
+ $serverValue = $this->process($serverValue);
+ $serverValue = $this->associativeArrayToString($serverValue);
+
+ $return[$serverKey] = $serverValue;
+ continue;
+ }
+
+ if (
+ ! isset($this->sensitiveParameters[ProcessorInterface::ALL])
+ && ! isset($this->sensitiveParameters[strtolower($serverKey)])
+ ) {
+ $return[$serverKey] = $serverValue;
+ continue;
+ }
+
+ $return[$serverKey] = $this->replace($this->replacementStrategy, $serverValue);
+ }
+
+ return $return;
+ }
+
+ private function stringToAssociativeArray(string $subject): array
+ {
+ return array_reduce(explode('; ', $subject), function (array $result, string $keyValue): array {
+ $keyValue = explode('=', $keyValue, 2);
+ $result[$keyValue[0]] = $keyValue[1] ?? '';
+
+ return $result;
+ }, []);
+ }
+
+ private function associativeArrayToString(array $subject): string
+ {
+ $subject = array_map(
+ fn(string $key, string $value) => sprintf('%s=%s', $key, $value),
+ array_keys($subject),
+ $subject
+ );
+
+ return implode('; ', $subject);
+ }
+}
diff --git a/src/Extra/Processor/SessionProcessor.php b/src/Extra/Processor/SessionProcessor.php
new file mode 100644
index 0000000..f02a570
--- /dev/null
+++ b/src/Extra/Processor/SessionProcessor.php
@@ -0,0 +1,20 @@
+ ArrayUtils::iteratorToArray($container),
+ $data
+ );
+ }
+}
diff --git a/src/Extra/Processor/TraceProcessor.php b/src/Extra/Processor/TraceProcessor.php
new file mode 100644
index 0000000..ec632c7
--- /dev/null
+++ b/src/Extra/Processor/TraceProcessor.php
@@ -0,0 +1,28 @@
+ sprintf(
+ '%s%s%s:%d',
+ $trace['class'] ?? $trace['file'] ?? 'unknown',
+ $trace['type'] ?? '->',
+ $trace['function'] ?? 'unknown',
+ $trace['line'] ?? 0
+ ),
+ $data
+ );
+ }
+}
diff --git a/src/Extra/Provider/AbstractProvider.php b/src/Extra/Provider/AbstractProvider.php
new file mode 100644
index 0000000..dc45c56
--- /dev/null
+++ b/src/Extra/Provider/AbstractProvider.php
@@ -0,0 +1,28 @@
+enabled;
+ }
+
+ public function getProcessor(): ?ProcessorInterface
+ {
+ return $this->processor;
+ }
+
+ abstract public function provide(array $data): array;
+}
diff --git a/src/Extra/Provider/CookieProvider.php b/src/Extra/Provider/CookieProvider.php
new file mode 100644
index 0000000..9e68fb3
--- /dev/null
+++ b/src/Extra/Provider/CookieProvider.php
@@ -0,0 +1,19 @@
+processor instanceof ProcessorInterface) {
+ return $this->processor->process($data);
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Extra/Provider/HeaderProvider.php b/src/Extra/Provider/HeaderProvider.php
new file mode 100644
index 0000000..5d838aa
--- /dev/null
+++ b/src/Extra/Provider/HeaderProvider.php
@@ -0,0 +1,19 @@
+processor instanceof ProcessorInterface) {
+ return $this->processor->process($data);
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Extra/Provider/ProviderInterface.php b/src/Extra/Provider/ProviderInterface.php
new file mode 100644
index 0000000..ce3c9f5
--- /dev/null
+++ b/src/Extra/Provider/ProviderInterface.php
@@ -0,0 +1,16 @@
+processor instanceof ProcessorInterface) {
+ return $this->processor->process($data);
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Extra/Provider/ServerProvider.php b/src/Extra/Provider/ServerProvider.php
new file mode 100644
index 0000000..2b196a4
--- /dev/null
+++ b/src/Extra/Provider/ServerProvider.php
@@ -0,0 +1,19 @@
+processor instanceof ProcessorInterface) {
+ return $this->processor->process($data);
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Extra/Provider/SessionProvider.php b/src/Extra/Provider/SessionProvider.php
new file mode 100644
index 0000000..09b4449
--- /dev/null
+++ b/src/Extra/Provider/SessionProvider.php
@@ -0,0 +1,19 @@
+processor instanceof ProcessorInterface) {
+ return $this->processor->process($data);
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Extra/Provider/TraceProvider.php b/src/Extra/Provider/TraceProvider.php
new file mode 100644
index 0000000..989091f
--- /dev/null
+++ b/src/Extra/Provider/TraceProvider.php
@@ -0,0 +1,19 @@
+processor instanceof ProcessorInterface) {
+ return $this->processor->process($data);
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Extra/ReplacementStrategy.php b/src/Extra/ReplacementStrategy.php
new file mode 100644
index 0000000..99508c4
--- /dev/null
+++ b/src/Extra/ReplacementStrategy.php
@@ -0,0 +1,11 @@
+responseFactory = function () use ($responseFactory): ResponseInterface {
return $responseFactory();
};
$this->responseGenerator = $responseGenerator ?: new ErrorResponseGenerator();
$this->logger = $logger;
+ $this->extraProvider = $extraProvider;
}
public function attachListener(callable $listener): void
@@ -78,14 +82,15 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
* triggers all listeners with the same arguments (but using the response
* returned from createErrorResponse()), and then returns the response.
*
- * If a valid Logger is available, the error and it's message are logged in the
+ * If a valid Logger is available, the error, and it's message are logged in the
* configured format.
*/
public function handleThrowable(Throwable $e, ServerRequestInterface $request): ResponseInterface
{
$generator = $this->responseGenerator;
if ($this->logger instanceof LoggerInterface) {
- $this->logger->err($e->getMessage(), (array) $e);
+ $extra = $this->provideExtra($e, $request);
+ $this->logger->error($e->getMessage(), $extra);
}
$response = $generator($e, $request, ($this->responseFactory)());
@@ -126,4 +131,43 @@ public function triggerListeners(
$listener($error, $request, $response);
}
}
+
+ public function provideExtra(Throwable $throwable, ServerRequestInterface $request): array
+ {
+ $extra = [
+ 'file' => $throwable->getFile(),
+ 'line' => $throwable->getLine(),
+ ];
+
+ if ($this->extraProvider?->getCookie()->isEnabled()) {
+ $extra['cookie'] = $this->extraProvider?->getCookie()->provide($request->getCookieParams());
+ }
+
+ if ($this->extraProvider?->getHeader()->isEnabled()) {
+ $extra['header'] = $this->extraProvider?->getHeader()->provide($request->getHeaders());
+ }
+
+ if ($this->extraProvider?->getRequest()->isEnabled()) {
+ $extra['request'] = $this->extraProvider?->getRequest()->provide((array) $request->getParsedBody());
+ }
+
+ if ($this->extraProvider?->getServer()->isEnabled()) {
+ $extra['server'] = $this->extraProvider?->getServer()->provide($request->getServerParams());
+ }
+
+ if ($this->extraProvider?->getSession()->isEnabled()) {
+ $extra['session'] = $this->extraProvider?->getSession()->provide($_SESSION ?? []);
+ }
+
+ if ($this->extraProvider?->getTrace()->isEnabled()) {
+ $extra['trace'] = $this->extraProvider?->getTrace()->provide($throwable->getTrace());
+ }
+
+ return $extra;
+ }
+
+ public function getExtraProvider(): ?ExtraProvider
+ {
+ return $this->extraProvider;
+ }
}
diff --git a/src/LogErrorHandlerFactory.php b/src/LogErrorHandlerFactory.php
index 4220e4a..9f89f74 100644
--- a/src/LogErrorHandlerFactory.php
+++ b/src/LogErrorHandlerFactory.php
@@ -4,7 +4,7 @@
namespace Dot\ErrorHandler;
-use Dot\Log\LoggerInterface;
+use Dot\ErrorHandler\Extra\ExtraProvider;
use InvalidArgumentException;
use Mezzio\Middleware\ErrorResponseGenerator;
use Psr\Container\ContainerExceptionInterface;
@@ -12,7 +12,10 @@
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
+use Psr\Log\LoggerInterface;
+use function array_key_exists;
+use function count;
use function is_array;
use function sprintf;
@@ -51,6 +54,20 @@ public function __invoke(ContainerInterface $container): MiddlewareInterface
? $container->get(ErrorResponseGenerator::class)
: null;
- return new LogErrorHandler($container->get(ResponseInterface::class), $generator, $logger);
+ $extraProvider = null;
+ if (
+ array_key_exists(ExtraProvider::CONFIG_KEY, $errorHandlerConfig)
+ && is_array($errorHandlerConfig[ExtraProvider::CONFIG_KEY])
+ && count($errorHandlerConfig[ExtraProvider::CONFIG_KEY]) > 0
+ ) {
+ $extraProvider = new ExtraProvider($errorHandlerConfig[ExtraProvider::CONFIG_KEY]);
+ }
+
+ return new LogErrorHandler(
+ $container->get(ResponseInterface::class),
+ $generator,
+ $logger,
+ $extraProvider
+ );
}
}
diff --git a/test/ErrorHandlerFactoryTest.php b/test/ErrorHandlerFactoryTest.php
index 2cf7de6..48ee691 100644
--- a/test/ErrorHandlerFactoryTest.php
+++ b/test/ErrorHandlerFactoryTest.php
@@ -17,7 +17,7 @@
class ErrorHandlerFactoryTest extends TestCase
{
- private ContainerInterface|MockObject $container;
+ private MockObject|ContainerInterface $container;
/** @var callable $responseFactory */
private $responseFactory;
diff --git a/test/ErrorHandlerTest.php b/test/ErrorHandlerTest.php
index 8b9536b..9d39212 100644
--- a/test/ErrorHandlerTest.php
+++ b/test/ErrorHandlerTest.php
@@ -24,16 +24,14 @@
class ErrorHandlerTest extends TestCase
{
private Subject $subject;
- private ServerRequestInterface|MockObject $serverRequest;
- private ResponseInterface|MockObject $response;
+ private MockObject&ServerRequestInterface $serverRequest;
+ private MockObject&ResponseInterface $response;
private ErrorResponseGenerator $errorResponseGenerator;
/** @var callable():ResponseInterface $responseFactory */
private $responseFactory;
- /** @var MockObject&StreamInterface */
- private $body;
- /** @var MockObject&RequestHandlerInterface */
- private $handler;
- private Throwable|MockObject $exception;
+ private MockObject&StreamInterface $body;
+ private MockObject&RequestHandlerInterface $handler;
+ private Throwable $exception;
/**
* @throws Exception
diff --git a/test/Extra/ExtraProviderTest.php b/test/Extra/ExtraProviderTest.php
new file mode 100644
index 0000000..b4aac48
--- /dev/null
+++ b/test/Extra/ExtraProviderTest.php
@@ -0,0 +1,820 @@
+assertFalse($extraProvider->getCookie()->isEnabled());
+ $this->assertNull($extraProvider->getCookie()->getProcessor());
+
+ $this->assertFalse($extraProvider->getHeader()->isEnabled());
+ $this->assertNull($extraProvider->getHeader()->getProcessor());
+
+ $this->assertFalse($extraProvider->getRequest()->isEnabled());
+ $this->assertNull($extraProvider->getRequest()->getProcessor());
+
+ $this->assertFalse($extraProvider->getServer()->isEnabled());
+ $this->assertNull($extraProvider->getServer()->getProcessor());
+
+ $this->assertFalse($extraProvider->getSession()->isEnabled());
+ $this->assertNull($extraProvider->getSession()->getProcessor());
+
+ $this->assertFalse($extraProvider->getTrace()->isEnabled());
+ $this->assertNull($extraProvider->getTrace()->getProcessor());
+ }
+
+ public function testWillInstantiateWithInvalidOptions(): void
+ {
+ $extraProvider = new ExtraProvider(['test' => 'test']);
+
+ $this->assertFalse($extraProvider->getCookie()->isEnabled());
+ $this->assertNull($extraProvider->getCookie()->getProcessor());
+
+ $this->assertFalse($extraProvider->getHeader()->isEnabled());
+ $this->assertNull($extraProvider->getHeader()->getProcessor());
+
+ $this->assertFalse($extraProvider->getRequest()->isEnabled());
+ $this->assertNull($extraProvider->getRequest()->getProcessor());
+
+ $this->assertFalse($extraProvider->getServer()->isEnabled());
+ $this->assertNull($extraProvider->getServer()->getProcessor());
+
+ $this->assertFalse($extraProvider->getSession()->isEnabled());
+ $this->assertNull($extraProvider->getSession()->getProcessor());
+
+ $this->assertFalse($extraProvider->getTrace()->isEnabled());
+ $this->assertNull($extraProvider->getTrace()->getProcessor());
+ }
+
+ public function testWillNotEnableProvidersWhenEmptyProviderOptions(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => [],
+ HeaderProvider::class => [],
+ RequestProvider::class => [],
+ ServerProvider::class => [],
+ SessionProvider::class => [],
+ TraceProvider::class => [],
+ ]);
+
+ $this->assertFalse($extraProvider->getCookie()->isEnabled());
+ $this->assertNull($extraProvider->getCookie()->getProcessor());
+
+ $this->assertFalse($extraProvider->getHeader()->isEnabled());
+ $this->assertNull($extraProvider->getHeader()->getProcessor());
+
+ $this->assertFalse($extraProvider->getRequest()->isEnabled());
+ $this->assertNull($extraProvider->getRequest()->getProcessor());
+
+ $this->assertFalse($extraProvider->getServer()->isEnabled());
+ $this->assertNull($extraProvider->getServer()->getProcessor());
+
+ $this->assertFalse($extraProvider->getSession()->isEnabled());
+ $this->assertNull($extraProvider->getSession()->getProcessor());
+
+ $this->assertFalse($extraProvider->getTrace()->isEnabled());
+ $this->assertNull($extraProvider->getTrace()->getProcessor());
+ }
+
+ public function testWillEnableProvidersWhenEnabledIsTrueInProviderOptions(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => ['enabled' => true],
+ HeaderProvider::class => ['enabled' => true],
+ RequestProvider::class => ['enabled' => true],
+ ServerProvider::class => ['enabled' => true],
+ SessionProvider::class => ['enabled' => true],
+ TraceProvider::class => ['enabled' => true],
+ ]);
+
+ $this->assertTrue($extraProvider->getCookie()->isEnabled());
+ $this->assertNull($extraProvider->getCookie()->getProcessor());
+
+ $this->assertTrue($extraProvider->getHeader()->isEnabled());
+ $this->assertNull($extraProvider->getHeader()->getProcessor());
+
+ $this->assertTrue($extraProvider->getRequest()->isEnabled());
+ $this->assertNull($extraProvider->getRequest()->getProcessor());
+
+ $this->assertTrue($extraProvider->getServer()->isEnabled());
+ $this->assertNull($extraProvider->getServer()->getProcessor());
+
+ $this->assertTrue($extraProvider->getSession()->isEnabled());
+ $this->assertNull($extraProvider->getSession()->getProcessor());
+
+ $this->assertTrue($extraProvider->getTrace()->isEnabled());
+ $this->assertNull($extraProvider->getTrace()->getProcessor());
+ }
+
+ public function testProvidersWillNotHaveProcessorWhenEmptyProcessorOptions(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => ['enabled' => true, 'processor' => []],
+ HeaderProvider::class => ['enabled' => true, 'processor' => []],
+ RequestProvider::class => ['enabled' => true, 'processor' => []],
+ ServerProvider::class => ['enabled' => true, 'processor' => []],
+ SessionProvider::class => ['enabled' => true, 'processor' => []],
+ TraceProvider::class => ['enabled' => true, 'processor' => []],
+ ]);
+
+ $this->assertTrue($extraProvider->getCookie()->isEnabled());
+ $this->assertNull($extraProvider->getCookie()->getProcessor());
+
+ $this->assertTrue($extraProvider->getHeader()->isEnabled());
+ $this->assertNull($extraProvider->getHeader()->getProcessor());
+
+ $this->assertTrue($extraProvider->getRequest()->isEnabled());
+ $this->assertNull($extraProvider->getRequest()->getProcessor());
+
+ $this->assertTrue($extraProvider->getServer()->isEnabled());
+ $this->assertNull($extraProvider->getServer()->getProcessor());
+
+ $this->assertTrue($extraProvider->getSession()->isEnabled());
+ $this->assertNull($extraProvider->getSession()->getProcessor());
+
+ $this->assertTrue($extraProvider->getTrace()->isEnabled());
+ $this->assertNull($extraProvider->getTrace()->getProcessor());
+ }
+
+ public function testProvidersWillNotHaveProcessorWhenInvalidProcessorOptions(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']],
+ HeaderProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']],
+ RequestProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']],
+ ServerProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']],
+ SessionProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']],
+ TraceProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']],
+ ]);
+
+ $this->assertTrue($extraProvider->getCookie()->isEnabled());
+ $this->assertNull($extraProvider->getCookie()->getProcessor());
+
+ $this->assertTrue($extraProvider->getHeader()->isEnabled());
+ $this->assertNull($extraProvider->getHeader()->getProcessor());
+
+ $this->assertTrue($extraProvider->getRequest()->isEnabled());
+ $this->assertNull($extraProvider->getRequest()->getProcessor());
+
+ $this->assertTrue($extraProvider->getServer()->isEnabled());
+ $this->assertNull($extraProvider->getServer()->getProcessor());
+
+ $this->assertTrue($extraProvider->getSession()->isEnabled());
+ $this->assertNull($extraProvider->getSession()->getProcessor());
+
+ $this->assertTrue($extraProvider->getTrace()->isEnabled());
+ $this->assertNull($extraProvider->getTrace()->getProcessor());
+ }
+
+ public function testProvidersWillHaveProcessorWhenValidProcessorOptions(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => ['enabled' => true, 'processor' => ['class' => CookieProcessor::class]],
+ HeaderProvider::class => ['enabled' => true, 'processor' => ['class' => HeaderProcessor::class]],
+ RequestProvider::class => ['enabled' => true, 'processor' => ['class' => RequestProcessor::class]],
+ ServerProvider::class => ['enabled' => true, 'processor' => ['class' => ServerProcessor::class]],
+ SessionProvider::class => ['enabled' => true, 'processor' => ['class' => SessionProcessor::class]],
+ TraceProvider::class => ['enabled' => true, 'processor' => ['class' => TraceProcessor::class]],
+ ]);
+
+ $this->assertTrue($extraProvider->getCookie()->isEnabled());
+ $processor = $extraProvider->getCookie()->getProcessor();
+ $this->assertInstanceOf(CookieProcessor::class, $processor);
+ $this->assertCount(0, $processor->getSensitiveParameters());
+ $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getHeader()->isEnabled());
+ $processor = $extraProvider->getHeader()->getProcessor();
+ $this->assertInstanceOf(HeaderProcessor::class, $processor);
+ $this->assertCount(0, $processor->getSensitiveParameters());
+ $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getRequest()->isEnabled());
+ $processor = $extraProvider->getRequest()->getProcessor();
+ $this->assertInstanceOf(RequestProcessor::class, $processor);
+ $this->assertCount(0, $processor->getSensitiveParameters());
+ $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getServer()->isEnabled());
+ $processor = $extraProvider->getServer()->getProcessor();
+ $this->assertInstanceOf(ServerProcessor::class, $processor);
+ $this->assertCount(0, $processor->getSensitiveParameters());
+ $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getSession()->isEnabled());
+ $processor = $extraProvider->getSession()->getProcessor();
+ $this->assertInstanceOf(SessionProcessor::class, $processor);
+ $this->assertCount(0, $processor->getSensitiveParameters());
+ $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getTrace()->isEnabled());
+ $processor = $extraProvider->getTrace()->getProcessor();
+ $this->assertInstanceOf(TraceProcessor::class, $processor);
+ $this->assertCount(0, $processor->getSensitiveParameters());
+ $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy());
+ }
+
+ public function testProvidersWillHaveProcessorWhenAllValidProcessorOptions(): void
+ {
+ $sensitiveParameters = ['test'];
+
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => CookieProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => $sensitiveParameters,
+ ],
+ ],
+ HeaderProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => HeaderProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => $sensitiveParameters,
+ ],
+ ],
+ RequestProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => RequestProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => $sensitiveParameters,
+ ],
+ ],
+ ServerProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => ServerProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => $sensitiveParameters,
+ ],
+ ],
+ SessionProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => SessionProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => $sensitiveParameters,
+ ],
+ ],
+ TraceProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => TraceProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => $sensitiveParameters,
+ ],
+ ],
+ ]);
+
+ $this->assertTrue($extraProvider->getCookie()->isEnabled());
+ $processor = $extraProvider->getCookie()->getProcessor();
+ $this->assertInstanceOf(CookieProcessor::class, $processor);
+ $this->assertCount(1, $processor->getSensitiveParameters());
+ $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters()));
+ $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getHeader()->isEnabled());
+ $processor = $extraProvider->getHeader()->getProcessor();
+ $this->assertInstanceOf(HeaderProcessor::class, $processor);
+ $this->assertCount(1, $processor->getSensitiveParameters());
+ $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters()));
+ $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getRequest()->isEnabled());
+ $processor = $extraProvider->getRequest()->getProcessor();
+ $this->assertInstanceOf(RequestProcessor::class, $processor);
+ $this->assertCount(1, $processor->getSensitiveParameters());
+ $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters()));
+ $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getServer()->isEnabled());
+ $processor = $extraProvider->getServer()->getProcessor();
+ $this->assertInstanceOf(ServerProcessor::class, $processor);
+ $this->assertCount(1, $processor->getSensitiveParameters());
+ $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters()));
+ $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getSession()->isEnabled());
+ $processor = $extraProvider->getSession()->getProcessor();
+ $this->assertInstanceOf(SessionProcessor::class, $processor);
+ $this->assertCount(1, $processor->getSensitiveParameters());
+ $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters()));
+ $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy());
+
+ $this->assertTrue($extraProvider->getTrace()->isEnabled());
+ $processor = $extraProvider->getTrace()->getProcessor();
+ $this->assertInstanceOf(TraceProcessor::class, $processor);
+ $this->assertCount(1, $processor->getSensitiveParameters());
+ $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters()));
+ $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy());
+ }
+
+ public function testWillProvideUnmodifiedCookieDataWhenNoProcessor(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => [
+ 'enabled' => true,
+ ],
+ ]);
+
+ $input = ['test' => 'test'];
+ $output = $extraProvider->getCookie()->provide($input);
+ $this->assertSame($input, $output);
+ }
+
+ public function testWillProvideModifiedCookieDataWhenProcessorSetToPartialReplaceAllKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => CookieProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => [ProcessorInterface::ALL],
+ ],
+ ],
+ ]);
+
+ $output = $extraProvider->getCookie()->provide(['first' => 'First', 'second' => 'Second', 'third' => 'Third']);
+ $this->assertSame(['first' => 'Fir**', 'second' => 'Sec***', 'third' => 'Thi**'], $output);
+ }
+
+ public function testWillProvideModifiedCookieDataWhenProcessorSetToFullReplaceAllKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => CookieProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [ProcessorInterface::ALL],
+ ],
+ ],
+ ]);
+
+ $output = $extraProvider->getCookie()->provide(['first' => 'First', 'second' => 'Second', 'third' => 'Third']);
+ $this->assertSame(['first' => '*****', 'second' => '******', 'third' => '*****'], $output);
+ }
+
+ public function testWillProvideModifiedCookieDataWhenProcessorSetToPartialReplaceSpecificKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => CookieProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => ['second'],
+ ],
+ ],
+ ]);
+
+ $output = $extraProvider->getCookie()->provide(['first' => 'First', 'second' => 'Second', 'third' => 'Third']);
+ $this->assertSame(['first' => 'First', 'second' => 'Sec***', 'third' => 'Third'], $output);
+ }
+
+ public function testWillProvideModifiedCookieDataWhenProcessorSetToFullReplaceSpecificKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ CookieProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => CookieProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => ['second'],
+ ],
+ ],
+ ]);
+
+ $output = $extraProvider->getCookie()->provide(['first' => 'First', 'second' => 'Second', 'third' => 'Third']);
+ $this->assertSame(['first' => 'First', 'second' => '******', 'third' => 'Third'], $output);
+ }
+
+ public function testWillProvideUnmodifiedHeaderDataWhenNoProcessor(): void
+ {
+ $extraProvider = new ExtraProvider([
+ HeaderProvider::class => [
+ 'enabled' => true,
+ ],
+ ]);
+
+ $input = ['cookie' => 'Test-data'];
+ $output = $extraProvider->getHeader()->provide($input);
+ $this->assertSame($input, $output);
+ }
+
+ public function testWillProvideModifiedHeaderDataWhenProcessorSetToPartialReplaceAllKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ HeaderProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => HeaderProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => [ProcessorInterface::ALL],
+ ],
+ ],
+ ]);
+
+ $output = $extraProvider->getHeader()->provide([
+ 'test' => 'Test',
+ 'cookie' => 'rememberMe=s0me-v3ry-l0ng-h4sh',
+ ]);
+ $this->assertSame(['test' => 'Te**', 'cookie' => 'rememberMe=s0me-v3ry-*********'], $output);
+
+ $output = $extraProvider->getHeader()->provide([
+ 'test' => 'Test',
+ 'cookie' => ['rememberMe=s0me-v3ry-l0ng-h4sh'],
+ ]);
+ $this->assertSame(['test' => 'Te**', 'cookie' => 'rememberMe=s0me-v3ry-*********'], $output);
+ }
+
+ public function testWillProvideModifiedHeaderDataWhenProcessorSetToFullReplaceAllKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ HeaderProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => HeaderProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [ProcessorInterface::ALL],
+ ],
+ ],
+ ]);
+
+ $output = $extraProvider->getHeader()->provide([
+ 'test' => 'Test',
+ 'cookie' => 'rememberMe=s0me-v3ry-l0ng-h4sh',
+ ]);
+ $this->assertSame(['test' => '****', 'cookie' => 'rememberMe=*******************'], $output);
+
+ $output = $extraProvider->getHeader()->provide([
+ 'test' => 'Test',
+ 'cookie' => ['rememberMe=s0me-v3ry-l0ng-h4sh'],
+ ]);
+ $this->assertSame(['test' => '****', 'cookie' => 'rememberMe=*******************'], $output);
+ }
+
+ public function testWillProvideModifiedHeaderDataWhenProcessorSetToPartialReplaceSpecificKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ HeaderProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => HeaderProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => ['test'],
+ ],
+ ],
+ ]);
+
+ $output = $extraProvider->getHeader()->provide([
+ 'test' => 'Test',
+ 'data' => 'Data',
+ 'cookie' => 'rememberMe=s0me-v3ry-l0ng-h4sh',
+ ]);
+ $this->assertSame(['test' => 'Te**', 'data' => 'Data', 'cookie' => 'rememberMe=s0me-v3ry-*********'], $output);
+
+ $output = $extraProvider->getHeader()->provide([
+ 'test' => 'Test',
+ 'data' => 'Data',
+ 'cookie' => ['rememberMe=s0me-v3ry-l0ng-h4sh'],
+ ]);
+ $this->assertSame(['test' => 'Te**', 'data' => 'Data', 'cookie' => 'rememberMe=s0me-v3ry-*********'], $output);
+ }
+
+ public function testWillProvideModifiedHeaderDataWhenProcessorSetToFullReplaceSpecificKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ HeaderProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => HeaderProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => ['test'],
+ ],
+ ],
+ ]);
+
+ $output = $extraProvider->getHeader()->provide([
+ 'test' => 'Test',
+ 'data' => 'Data',
+ 'cookie' => 'rememberMe=s0me-v3ry-l0ng-h4sh',
+ ]);
+ $this->assertSame(['test' => '****', 'data' => 'Data', 'cookie' => 'rememberMe=*******************'], $output);
+
+ $output = $extraProvider->getHeader()->provide([
+ 'test' => 'Test',
+ 'data' => 'Data',
+ 'cookie' => ['rememberMe=s0me-v3ry-l0ng-h4sh'],
+ ]);
+ $this->assertSame(['test' => '****', 'data' => 'Data', 'cookie' => 'rememberMe=*******************'], $output);
+ }
+
+ public function testWillProvideUnmodifiedRequestDataWhenNoProcessor(): void
+ {
+ $extraProvider = new ExtraProvider([
+ RequestProvider::class => [
+ 'enabled' => true,
+ ],
+ ]);
+
+ $input = ['foo' => 'bar', 'bar' => ['baz' => 'foo']];
+ $output = $extraProvider->getRequest()->provide($input);
+ $this->assertSame($input, $output);
+ }
+
+ public function testWillProvideModifiedRequestDataWhenProcessorSetToPartialReplaceAllKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ RequestProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => RequestProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => [ProcessorInterface::ALL],
+ ],
+ ],
+ ]);
+
+ $input = ['foo' => 'bar', 'bar' => ['baz' => 'foo']];
+ $output = $extraProvider->getRequest()->provide($input);
+ $this->assertSame(['foo' => 'ba*', 'bar' => ['baz' => 'fo*']], $output);
+ }
+
+ public function testWillProvideModifiedRequestDataWhenProcessorSetToFullReplaceAllKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ RequestProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => RequestProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [ProcessorInterface::ALL],
+ ],
+ ],
+ ]);
+
+ $input = ['foo' => 'bar', 'bar' => ['baz' => 'foo']];
+ $output = $extraProvider->getRequest()->provide($input);
+ $this->assertSame(['foo' => '***', 'bar' => ['baz' => '***']], $output);
+ }
+
+ public function testWillProvideModifiedRequestDataWhenProcessorSetToPartialReplaceSpecificKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ RequestProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => RequestProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => ['ba'],
+ ],
+ ],
+ ]);
+
+ $input = ['foo' => 'Foo', 'bar' => ['bar' => 'Bar'], 'baz' => 'Baz'];
+ $output = $extraProvider->getRequest()->provide($input);
+ $this->assertSame(['foo' => 'Foo', 'bar' => ['bar' => 'Ba*'], 'baz' => 'Ba*'], $output);
+ }
+
+ public function testWillProvideModifiedRequestDataWhenProcessorSetToFullReplaceSpecificKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ RequestProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => RequestProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => ['ba'],
+ ],
+ ],
+ ]);
+
+ $input = ['foo' => 'Foo', 'bar' => ['bar' => 'Bar'], 'baz' => 'Baz'];
+ $output = $extraProvider->getRequest()->provide($input);
+ $this->assertSame(['foo' => 'Foo', 'bar' => ['bar' => '***'], 'baz' => '***'], $output);
+ }
+
+ public function testWillProvideUnmodifiedServerDataWhenNoProcessor(): void
+ {
+ $extraProvider = new ExtraProvider([
+ ServerProvider::class => [
+ 'enabled' => true,
+ ],
+ ]);
+
+ $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz'];
+ $output = $extraProvider->getServer()->provide($input);
+ $this->assertSame($input, $output);
+ }
+
+ public function testWillProvideModifiedServerDataWhenProcessorSetToPartialReplaceAllKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ ServerProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => ServerProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => [ProcessorInterface::ALL],
+ ],
+ ],
+ ]);
+
+ $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz'];
+ $output = $extraProvider->getServer()->provide($input);
+ $this->assertSame(
+ ['foo' => 'Fo*', 'bar' => 'Ba*', 'baz' => 'Ba*', 'HTTP_COOKIE' => 'foo=Fo*; bar=Ba*; baz=Ba*'],
+ $output
+ );
+ }
+
+ public function testWillProvideModifiedServerDataWhenProcessorSetToFullReplaceAllKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ ServerProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => ServerProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => [ProcessorInterface::ALL],
+ ],
+ ],
+ ]);
+
+ $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz'];
+ $output = $extraProvider->getServer()->provide($input);
+ $this->assertSame(
+ ['foo' => '***', 'bar' => '***', 'baz' => '***', 'HTTP_COOKIE' => 'foo=***; bar=***; baz=***'],
+ $output
+ );
+ }
+
+ public function testWillProvideModifiedServerDataWhenProcessorSetToPartialReplaceSpecificKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ ServerProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => ServerProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Partial,
+ 'sensitiveParameters' => ['bar'],
+ ],
+ ],
+ ]);
+
+ $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz'];
+ $output = $extraProvider->getServer()->provide($input);
+ $this->assertSame(
+ ['foo' => 'Foo', 'bar' => 'Ba*', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Ba*; baz=Baz'],
+ $output
+ );
+ }
+
+ public function testWillProvideModifiedServerDataWhenProcessorSetToFullReplaceSpecificKeys(): void
+ {
+ $extraProvider = new ExtraProvider([
+ ServerProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => ServerProcessor::class,
+ 'replacementStrategy' => ReplacementStrategy::Full,
+ 'sensitiveParameters' => ['bar'],
+ ],
+ ],
+ ]);
+
+ $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz'];
+ $output = $extraProvider->getServer()->provide($input);
+ $this->assertSame(
+ ['foo' => 'Foo', 'bar' => '***', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=***; baz=Baz'],
+ $output
+ );
+ }
+
+ public function testWillProvideUnmodifiedSessionDataWhenNoProcessor(): void
+ {
+ $extraProvider = new ExtraProvider([
+ SessionProvider::class => [
+ 'enabled' => true,
+ ],
+ ]);
+
+ $input = ['foo' => ['foo' => 'Foo'], 'bar' => new ArrayObject(['bar' => 'Bar'])];
+ $output = $extraProvider->getSession()->provide($input);
+ $this->assertSame($input, $output);
+ }
+
+ public function testWillProvideModifiedSessionDataWhenProcessorIsSpecified(): void
+ {
+ $extraProvider = new ExtraProvider([
+ SessionProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => SessionProcessor::class,
+ ],
+ ],
+ ]);
+
+ $input = ['foo' => ['foo' => 'Foo'], 'bar' => new ArrayObject(['bar' => 'Bar'])];
+ $output = $extraProvider->getSession()->provide($input);
+ $this->assertSame(['foo' => ['foo' => 'Foo'], 'bar' => ['bar' => 'Bar']], $output);
+ }
+
+ public function testWillProvideUnmodifiedTraceDataWhenNoProcessor(): void
+ {
+ $extraProvider = new ExtraProvider([
+ TraceProvider::class => [
+ 'enabled' => true,
+ ],
+ ]);
+
+ $input = [
+ ['file' => '/path/to/some/class.php', 'line' => 1, 'function' => 'foo', 'class' => 'Foo', 'type' => '->'],
+ ['file' => '/path/to/index.php', 'line' => 1, 'function' => 'bar'],
+ ];
+ $output = $extraProvider->getTrace()->provide($input);
+ $this->assertSame($input, $output);
+ }
+
+ public function testWillProvideUnmodifiedTraceDataWhenProcessorIsSpecified(): void
+ {
+ $extraProvider = new ExtraProvider([
+ TraceProvider::class => [
+ 'enabled' => true,
+ 'processor' => [
+ 'class' => TraceProcessor::class,
+ ],
+ ],
+ ]);
+
+ $input = [
+ ['file' => '/path/to/some/class.php', 'line' => 8, 'function' => 'foo', 'class' => 'Foo', 'type' => '->'],
+ ['file' => '/path/to/index.php', 'line' => 8, 'function' => 'bar'],
+ ];
+ $output = $extraProvider->getTrace()->provide($input);
+ $this->assertSame(['Foo->foo:8', '/path/to/index.php->bar:8'], $output);
+
+ $input = [
+ ['file' => '/path/to/some/class.php', 'line' => 8, 'function' => 'foo', 'class' => 'Foo'],
+ ['file' => '/path/to/index.php', 'line' => 8, 'function' => 'bar'],
+ ];
+ $output = $extraProvider->getTrace()->provide($input);
+ $this->assertSame(['Foo->foo:8', '/path/to/index.php->bar:8'], $output);
+
+ $input = [
+ ['file' => '/path/to/some/class.php', 'line' => 8, 'class' => 'Foo'],
+ ['file' => '/path/to/index.php', 'line' => 8],
+ ];
+ $output = $extraProvider->getTrace()->provide($input);
+ $this->assertSame(['Foo->unknown:8', '/path/to/index.php->unknown:8'], $output);
+
+ $input = [
+ ['file' => '/path/to/some/class.php', 'line' => 8],
+ ['file' => '/path/to/index.php', 'line' => 8],
+ ];
+ $output = $extraProvider->getTrace()->provide($input);
+ $this->assertSame(['/path/to/some/class.php->unknown:8', '/path/to/index.php->unknown:8'], $output);
+
+ $input = [
+ ['file' => '/path/to/some/class.php'],
+ ['file' => '/path/to/index.php'],
+ ];
+ $output = $extraProvider->getTrace()->provide($input);
+ $this->assertSame(['/path/to/some/class.php->unknown:0', '/path/to/index.php->unknown:0'], $output);
+
+ $input = [[], []];
+ $output = $extraProvider->getTrace()->provide($input);
+ $this->assertSame(['unknown->unknown:0', 'unknown->unknown:0'], $output);
+ }
+}
diff --git a/test/LogErrorHandlerFactoryTest.php b/test/LogErrorHandlerFactoryTest.php
index 418dc7a..d64583b 100644
--- a/test/LogErrorHandlerFactoryTest.php
+++ b/test/LogErrorHandlerFactoryTest.php
@@ -4,8 +4,10 @@
namespace DotTest\ErrorHandler;
+use Dot\ErrorHandler\Extra\ExtraProvider;
use Dot\ErrorHandler\LogErrorHandler;
use Dot\ErrorHandler\LogErrorHandlerFactory;
+use InvalidArgumentException;
use Mezzio\Middleware\ErrorResponseGenerator;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;
@@ -20,7 +22,7 @@
class LogErrorHandlerFactoryTest extends TestCase
{
- private ContainerInterface|MockObject $container;
+ private ContainerInterface&MockObject $container;
/** @var callable $responseFactory */
private $responseFactory;
@@ -43,7 +45,7 @@ public function testWillNotCreateWithoutConfig(): void
->with('config')
->willReturn(false);
- $this->expectException('InvalidArgumentException');
+ $this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
sprintf('\'[%s\'] not found in config', LogErrorHandlerFactory::ERROR_HANDLER_KEY)
);
@@ -66,7 +68,7 @@ public function testWillNotCreateWithMissingLoggerKey(): void
],
]);
- $this->expectException('InvalidArgumentException');
+ $this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
sprintf(
'Logger: \'[%s\'] is enabled, but not found in config',
@@ -108,6 +110,121 @@ public function testWillCreateWithValidConfigAndMissingLogger(): void
$this->assertInstanceOf(LogErrorHandler::class, $result);
}
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws Exception
+ * @throws NotFoundExceptionInterface
+ */
+ public function testWillCreateWithoutExtraProviderConfig(): void
+ {
+ $logger = $this->createMock(LoggerInterface::class);
+
+ $this->container->method('has')
+ ->with(ErrorResponseGenerator::class)
+ ->willReturn(true);
+
+ $this->container->method('get')
+ ->willReturnMap([
+ [
+ 'config',
+ [
+ LogErrorHandlerFactory::ERROR_HANDLER_KEY => [
+ 'loggerEnabled' => true,
+ LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY => 'test',
+ ],
+ ],
+ ],
+ [
+ 'config[' . LogErrorHandlerFactory::ERROR_HANDLER_KEY . ']['
+ . LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY . ']',
+ $logger,
+ ],
+ [ErrorResponseGenerator::class, $this->createMock(ErrorResponseGenerator::class)],
+ [ResponseInterface::class, $this->responseFactory],
+ ]);
+
+ $result = (new LogErrorHandlerFactory())($this->container);
+ $this->assertInstanceOf(LogErrorHandler::class, $result);
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws Exception
+ * @throws NotFoundExceptionInterface
+ */
+ public function testWillCreateWithEmptyExtraProviderConfig(): void
+ {
+ $logger = $this->createMock(LoggerInterface::class);
+
+ $this->container->method('has')
+ ->with(ErrorResponseGenerator::class)
+ ->willReturn(true);
+
+ $this->container->method('get')
+ ->willReturnMap([
+ [
+ 'config',
+ [
+ LogErrorHandlerFactory::ERROR_HANDLER_KEY => [
+ 'loggerEnabled' => true,
+ LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY => 'test',
+ ],
+ ExtraProvider::CONFIG_KEY => [],
+ ],
+ ],
+ [
+ 'config[' . LogErrorHandlerFactory::ERROR_HANDLER_KEY . ']['
+ . LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY . ']',
+ $logger,
+ ],
+ [ErrorResponseGenerator::class, $this->createMock(ErrorResponseGenerator::class)],
+ [ResponseInterface::class, $this->responseFactory],
+ ]);
+
+ $result = (new LogErrorHandlerFactory())($this->container);
+ $this->assertInstanceOf(LogErrorHandler::class, $result);
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ * @throws Exception
+ * @throws NotFoundExceptionInterface
+ */
+ public function testWillCreateWithInvalidExtraProviderConfig(): void
+ {
+ $logger = $this->createMock(LoggerInterface::class);
+
+ $this->container->method('has')
+ ->with(ErrorResponseGenerator::class)
+ ->willReturn(true);
+
+ $this->container->method('get')
+ ->willReturnMap([
+ [
+ 'config',
+ [
+ LogErrorHandlerFactory::ERROR_HANDLER_KEY => [
+ 'loggerEnabled' => true,
+ LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY => 'test',
+ ],
+ ExtraProvider::CONFIG_KEY => [
+ 'test' => 'test',
+ ],
+ ],
+ ],
+ [
+ 'config[' . LogErrorHandlerFactory::ERROR_HANDLER_KEY . ']['
+ . LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY . ']',
+ $logger,
+ ],
+ [ErrorResponseGenerator::class, $this->createMock(ErrorResponseGenerator::class)],
+ [ResponseInterface::class, $this->responseFactory],
+ ]);
+
+ $result = (new LogErrorHandlerFactory())($this->container);
+ $this->assertInstanceOf(LogErrorHandler::class, $result);
+ }
+
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
diff --git a/test/LogErrorHandlerTest.php b/test/LogErrorHandlerTest.php
index 2d6e4b6..ff840e0 100644
--- a/test/LogErrorHandlerTest.php
+++ b/test/LogErrorHandlerTest.php
@@ -4,11 +4,17 @@
namespace DotTest\ErrorHandler;
+use Dot\ErrorHandler\Extra\ExtraProvider;
+use Dot\ErrorHandler\Extra\Provider\CookieProvider;
+use Dot\ErrorHandler\Extra\Provider\HeaderProvider;
+use Dot\ErrorHandler\Extra\Provider\RequestProvider;
+use Dot\ErrorHandler\Extra\Provider\ServerProvider;
+use Dot\ErrorHandler\Extra\Provider\SessionProvider;
+use Dot\ErrorHandler\Extra\Provider\TraceProvider;
use Dot\ErrorHandler\LogErrorHandler;
use Dot\ErrorHandler\LogErrorHandler as Subject;
use Dot\Log\Formatter\Json;
use Dot\Log\Logger;
-use Dot\Log\LoggerInterface;
use ErrorException;
use Laminas\Stratigility\Middleware\ErrorResponseGenerator;
use org\bovigo\vfs\vfsStream;
@@ -21,6 +27,7 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use Psr\Log\LoggerInterface;
use ReflectionObject;
use RuntimeException;
use Throwable;
@@ -31,13 +38,13 @@
class LogErrorHandlerTest extends TestCase
{
private Subject $subject;
- private ServerRequestInterface|MockObject $serverRequest;
- private ResponseInterface|MockObject $response;
+ private MockObject&ServerRequestInterface $serverRequest;
+ private MockObject&ResponseInterface $response;
/** @var callable():ResponseInterface $responseFactory */
private $responseFactory;
- private StreamInterface|MockObject $body;
- private RequestHandlerInterface|MockObject $handler;
- private Throwable|MockObject $exception;
+ private MockObject&StreamInterface $body;
+ private MockObject&RequestHandlerInterface $handler;
+ private Throwable $exception;
private ErrorResponseGenerator $errorResponseGenerator;
private vfsStreamDirectory $fileSystem;
@@ -77,13 +84,115 @@ public function testCreateErrorHandlerRaisesErrorException(): void
$callableErrorHandler = $this->subject->createErrorHandler();
$this->expectException(ErrorException::class);
- $callableErrorHandler(error_reporting(), ErrorException::class, 'testErrfile', 0);
+ $callableErrorHandler(error_reporting(), ErrorException::class, 'testErrorFile', 0);
}
public function testCreateErrorHandlerSkipsErrorsOutsideErrorReportingMask(): void
{
$callableErrorHandler = $this->subject->createErrorHandler();
- $this->assertNull($callableErrorHandler(-(error_reporting() + 1), ErrorException::class, 'testErrfile', 0));
+ $this->assertNull($callableErrorHandler(-(error_reporting() + 1), ErrorException::class, 'testErrorFile', 0));
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ */
+ public function testLogErrorHandlerWillInitiateWhenExtraProviderIsMissing(): void
+ {
+ $logErrorHandler = new LogErrorHandler(
+ $this->responseFactory,
+ $this->errorResponseGenerator,
+ new Logger($this->getConfig()),
+ );
+
+ $this->assertInstanceOf(LogErrorHandler::class, $logErrorHandler);
+ $this->assertNull($logErrorHandler->getExtraProvider());
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ */
+ public function testLogErrorHandlerWillInitiateWhenExtraProviderIsNull(): void
+ {
+ $logErrorHandler = new LogErrorHandler(
+ $this->responseFactory,
+ $this->errorResponseGenerator,
+ new Logger($this->getConfig()),
+ null
+ );
+
+ $this->assertInstanceOf(LogErrorHandler::class, $logErrorHandler);
+ $this->assertNull($logErrorHandler->getExtraProvider());
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ */
+ public function testLogErrorHandlerWillInitiateWhenExtraProviderIsProvided(): void
+ {
+ $logErrorHandler = new LogErrorHandler(
+ $this->responseFactory,
+ $this->errorResponseGenerator,
+ new Logger($this->getConfig()),
+ new ExtraProvider()
+ );
+
+ $this->assertInstanceOf(LogErrorHandler::class, $logErrorHandler);
+ $this->assertInstanceOf(ExtraProvider::class, $logErrorHandler->getExtraProvider());
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ */
+ public function testLogErrorHandlerLogsWillOnlyContainDefaultKeysWhenNoProvidersAreEnabled(): void
+ {
+ $logErrorHandler = new LogErrorHandler(
+ $this->responseFactory,
+ $this->errorResponseGenerator,
+ new Logger($this->getConfig()),
+ new ExtraProvider()
+ );
+
+ $log = $logErrorHandler->provideExtra(new \Exception('test'), $this->serverRequest);
+ $this->assertCount(2, $log);
+ $this->assertArrayHasKey('file', $log);
+ $this->assertArrayHasKey('line', $log);
+ }
+
+ /**
+ * @throws ContainerExceptionInterface
+ */
+ public function testLogErrorHandlerLogsWillContainExtraKeysWhenProvidersAreEnabled(): void
+ {
+ $logErrorHandler = new LogErrorHandler(
+ $this->responseFactory,
+ $this->errorResponseGenerator,
+ new Logger($this->getConfig()),
+ new ExtraProvider([
+ CookieProvider::class => ['enabled' => true],
+ HeaderProvider::class => ['enabled' => true],
+ RequestProvider::class => ['enabled' => true],
+ ServerProvider::class => ['enabled' => true],
+ SessionProvider::class => ['enabled' => true],
+ TraceProvider::class => ['enabled' => true],
+ ])
+ );
+
+ $this->serverRequest->method('getCookieParams')->willReturn([]);
+ $this->serverRequest->method('getHeaders')->willReturn([]);
+ $this->serverRequest->method('getParsedBody')->willReturn([]);
+ $this->serverRequest->method('getServerParams')->willReturn([]);
+
+ $extra = $logErrorHandler->provideExtra(new \Exception('test'), $this->serverRequest);
+
+ $this->assertCount(8, $extra);
+ $this->assertArrayHasKey('file', $extra);
+ $this->assertArrayHasKey('line', $extra);
+ $this->assertArrayHasKey('cookie', $extra);
+ $this->assertArrayHasKey('header', $extra);
+ $this->assertArrayHasKey('request', $extra);
+ $this->assertArrayHasKey('server', $extra);
+ $this->assertArrayHasKey('session', $extra);
+ $this->assertArrayHasKey('trace', $extra);
}
public function testAttachListenerDoesNotAttachDuplicates(): void
@@ -244,16 +353,16 @@ private function getConfig(): array
return [
'writers' => [
'FileWriter' => [
- 'name' => 'stream',
- 'priority' => Logger::ALERT,
- 'options' => [
+ 'name' => 'stream',
+ 'level' => Logger::ALERT,
+ 'options' => [
'stream' => $this->fileSystem->url() . '/test-error-log.log',
'filters' => [
'allMessages' => [
- 'name' => 'priority',
+ 'name' => 'level',
'options' => [
'operator' => '>=',
- 'priority' => Logger::EMERG,
+ 'level' => Logger::EMERG,
],
],
],