Skip to content

Commit 9353cdc

Browse files
authored
Merge pull request #1 from lcobucci/provide-initial-implementation
Provide initial implementation
2 parents 97dbe42 + f785305 commit 9353cdc

34 files changed

+1213
-1
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/vendor/
22
/composer.lock
33
/.phpcs.cache
4-
/infection-log.txt
4+
/infection.log
55
/.phpunit.result.cache

.scrutinizer.yml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
build:
2+
environment:
3+
mysql: false
4+
postgresql: false
5+
redis: false
6+
rabbitmq: false
7+
mongodb: false
8+
php:
9+
version: 7.3
10+
11+
cache:
12+
disabled: false
13+
directories:
14+
- ~/.composer/cache
15+
16+
dependencies:
17+
override:
18+
- composer install --no-interaction --prefer-dist
19+
20+
nodes:
21+
analysis:
22+
project_setup:
23+
override: true
24+
tests:
25+
override:
26+
- php-scrutinizer-run
27+
- phpcs-run
28+
29+
checks:
30+
php : true
31+
32+
tools:
33+
external_code_coverage: true
34+
35+
build_failure_conditions:
36+
- 'elements.rating(<= C).new.exists'
37+
- 'issues.severity(>= MAJOR).new.exists'
38+
- 'project.metric_change("scrutinizer.test_coverage", < -0.01)'

.travis.yml

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
dist: trusty
2+
sudo: false
3+
language: php
4+
5+
php:
6+
- 7.3
7+
- 7.4snapshot
8+
- nightly
9+
10+
cache:
11+
directories:
12+
- $HOME/.composer/cache
13+
14+
before_install:
15+
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available"
16+
- composer self-update
17+
18+
install: travis_retry composer install
19+
20+
script:
21+
- ./vendor/bin/phpunit
22+
23+
jobs:
24+
allow_failures:
25+
- php: 7.4snapshot
26+
- php: nightly
27+
28+
include:
29+
- stage: Code Quality
30+
env: TEST_COVERAGE=1
31+
before_script:
32+
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
33+
- if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi
34+
script:
35+
- ./vendor/bin/phpunit --coverage-clover ./clover.xml
36+
after_script:
37+
- wget https://scrutinizer-ci.com/ocular.phar
38+
- php ocular.phar code-coverage:upload --format=php-clover ./clover.xml
39+
40+
- stage: Code Quality
41+
env: CODE_STANDARD=1
42+
script:
43+
- ./vendor/bin/phpcs
44+
45+
- stage: Code Quality
46+
env: STATIC_ANALYSIS=1
47+
script:
48+
- ./vendor/bin/phpstan analyse
49+
50+
- stage: Code Quality
51+
env: MUTATION_TESTS=1
52+
before_script:
53+
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
54+
- if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for mutation tests"; exit 1; fi
55+
script:
56+
- ./vendor/bin/infection --threads=$(nproc) --min-msi=100 --min-covered-msi=100

infection.json.dist

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"timeout": 1,
3+
"source": {
4+
"directories": ["src"]
5+
},
6+
"logs": {
7+
"text": "infection.log"
8+
}
9+
}

phpcs.xml.dist

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0"?>
2+
<ruleset>
3+
<arg name="basepath" value="." />
4+
<arg name="extensions" value="php" />
5+
<arg name="parallel" value="80" />
6+
<arg name="colors" />
7+
<arg name="cache" value=".phpcs.cache" />
8+
<arg value="p" />
9+
10+
<file>src</file>
11+
<file>tests</file>
12+
13+
<rule ref="Lcobucci" />
14+
</ruleset>

phpstan.neon.dist

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
includes:
2+
- vendor/phpstan/phpstan-phpunit/extension.neon
3+
- vendor/phpstan/phpstan-phpunit/rules.neon
4+
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
5+
- vendor/phpstan/phpstan-strict-rules/rules.neon
6+
7+
parameters:
8+
level: 7
9+
paths:
10+
- src
11+
- tests

phpunit.xml.dist

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
4+
colors="true"
5+
verbose="true"
6+
beStrictAboutOutputDuringTests="true"
7+
beStrictAboutTodoAnnotatedTests="true"
8+
beStrictAboutChangesToGlobalState="true"
9+
beStrictAboutCoversAnnotation="true"
10+
beStrictAboutResourceUsageDuringSmallTests="true"
11+
forceCoversAnnotation="true"
12+
>
13+
<testsuites>
14+
<testsuite name="unit">
15+
<directory>tests</directory>
16+
</testsuite>
17+
</testsuites>
18+
19+
<filter>
20+
<whitelist>
21+
<directory>src</directory>
22+
</whitelist>
23+
</filter>
24+
</phpunit>

src/DebugInfoStrategy.php

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Lcobucci\ErrorHandling;
5+
6+
use Throwable;
7+
8+
interface DebugInfoStrategy
9+
{
10+
/**
11+
* @return array<string, mixed>|null
12+
*/
13+
public function extractDebugInfo(Throwable $error): ?array;
14+
}

src/DebugInfoStrategy/NoDebugInfo.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Lcobucci\ErrorHandling\DebugInfoStrategy;
5+
6+
use Lcobucci\ErrorHandling\DebugInfoStrategy;
7+
use Throwable;
8+
9+
final class NoDebugInfo implements DebugInfoStrategy
10+
{
11+
/**
12+
* {@inheritDoc}
13+
*/
14+
public function extractDebugInfo(Throwable $error): ?array
15+
{
16+
return null;
17+
}
18+
}

src/DebugInfoStrategy/NoTrace.php

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Lcobucci\ErrorHandling\DebugInfoStrategy;
5+
6+
use Generator;
7+
use Lcobucci\ErrorHandling\DebugInfoStrategy;
8+
use Throwable;
9+
use function get_class;
10+
use function iterator_to_array;
11+
12+
final class NoTrace implements DebugInfoStrategy
13+
{
14+
/**
15+
* {@inheritDoc}
16+
*/
17+
public function extractDebugInfo(Throwable $error): ?array
18+
{
19+
$debugInfo = $this->format($error);
20+
$stack = iterator_to_array($this->streamStack($error->getPrevious()), false);
21+
22+
if ($stack !== []) {
23+
$debugInfo['stack'] = $stack;
24+
}
25+
26+
return $debugInfo;
27+
}
28+
29+
private function streamStack(?Throwable $previous): Generator
30+
{
31+
if ($previous === null) {
32+
return;
33+
}
34+
35+
yield $this->format($previous);
36+
yield from $this->streamStack($previous->getPrevious());
37+
}
38+
39+
/**
40+
* @return array<string, string|int>
41+
*/
42+
private function format(Throwable $error): array
43+
{
44+
return [
45+
'class' => get_class($error),
46+
'code' => $error->getCode(),
47+
'message' => $error->getMessage(),
48+
'file' => $error->getFile(),
49+
'line' => $error->getLine(),
50+
];
51+
}
52+
}

src/ErrorConversionMiddleware.php

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Lcobucci\ErrorHandling;
5+
6+
use Lcobucci\ContentNegotiation\UnformattedResponse;
7+
use Lcobucci\ErrorHandling\Problem\Detailed;
8+
use Lcobucci\ErrorHandling\Problem\Titled;
9+
use Lcobucci\ErrorHandling\Problem\Typed;
10+
use Psr\Http\Message\ResponseFactoryInterface;
11+
use Psr\Http\Message\ResponseInterface;
12+
use Psr\Http\Message\ServerRequestInterface;
13+
use Psr\Http\Server\MiddlewareInterface;
14+
use Psr\Http\Server\RequestHandlerInterface;
15+
use Throwable;
16+
use function array_key_exists;
17+
18+
final class ErrorConversionMiddleware implements MiddlewareInterface
19+
{
20+
private const CONTENT_TYPE_CONVERSION = [
21+
'application/json' => 'application/problem+json',
22+
'application/xml' => 'application/problem+xml',
23+
];
24+
25+
private const STATUS_URL = 'https://httpstatuses.com/';
26+
27+
/**
28+
* @var ResponseFactoryInterface
29+
*/
30+
private $responseFactory;
31+
32+
/**
33+
* @var DebugInfoStrategy
34+
*/
35+
private $debugInfoStrategy;
36+
37+
/**
38+
* @var StatusCodeExtractionStrategy
39+
*/
40+
private $statusCodeExtractor;
41+
42+
public function __construct(
43+
ResponseFactoryInterface $responseFactory,
44+
DebugInfoStrategy $debugInfoStrategy,
45+
StatusCodeExtractionStrategy $statusCodeExtractor
46+
) {
47+
$this->responseFactory = $responseFactory;
48+
$this->debugInfoStrategy = $debugInfoStrategy;
49+
$this->statusCodeExtractor = $statusCodeExtractor;
50+
}
51+
52+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
53+
{
54+
try {
55+
return $handler->handle($request);
56+
} catch (Throwable $error) {
57+
$response = $this->generateResponse($request, $error);
58+
59+
return new UnformattedResponse(
60+
$response,
61+
$this->extractData($error, $response),
62+
['error' => $error]
63+
);
64+
}
65+
}
66+
67+
private function generateResponse(ServerRequestInterface $request, Throwable $error): ResponseInterface
68+
{
69+
$response = $this->responseFactory->createResponse($this->statusCodeExtractor->extractStatusCode($error));
70+
71+
$accept = $request->getHeaderLine('Accept');
72+
73+
if (! array_key_exists($accept, self::CONTENT_TYPE_CONVERSION)) {
74+
return $response;
75+
}
76+
77+
return $response->withAddedHeader(
78+
'Content-Type',
79+
self::CONTENT_TYPE_CONVERSION[$accept] . '; charset=' . $request->getHeaderLine('Accept-Charset')
80+
);
81+
}
82+
83+
/**
84+
* @return array<string, mixed>
85+
*/
86+
private function extractData(Throwable $error, ResponseInterface $response): array
87+
{
88+
$data = [
89+
'type' => $error instanceof Typed ? $error->getTypeUri() : self::STATUS_URL . $response->getStatusCode(),
90+
'title' => $error instanceof Titled ? $error->getTitle() : $response->getReasonPhrase(),
91+
'details' => $error->getMessage(),
92+
];
93+
94+
if ($error instanceof Detailed) {
95+
$data += $error->getExtraDetails();
96+
}
97+
98+
$debugInfo = $this->debugInfoStrategy->extractDebugInfo($error);
99+
100+
if ($debugInfo !== null) {
101+
$data['_debug'] = $debugInfo;
102+
}
103+
104+
return $data;
105+
}
106+
}

src/ErrorLoggingMiddleware.php

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Lcobucci\ErrorHandling;
5+
6+
use Psr\Http\Message\ResponseInterface;
7+
use Psr\Http\Message\ServerRequestInterface;
8+
use Psr\Http\Server\MiddlewareInterface;
9+
use Psr\Http\Server\RequestHandlerInterface;
10+
use Psr\Log\LoggerInterface;
11+
use Throwable;
12+
13+
final class ErrorLoggingMiddleware implements MiddlewareInterface
14+
{
15+
/**
16+
* @var LoggerInterface
17+
*/
18+
private $logger;
19+
20+
public function __construct(LoggerInterface $logger)
21+
{
22+
$this->logger = $logger;
23+
}
24+
25+
/**
26+
* {@inheritDoc}
27+
*
28+
* @throws Throwable
29+
*/
30+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
31+
{
32+
try {
33+
return $handler->handle($request);
34+
} catch (Throwable $error) {
35+
$this->logger->debug('Error happened while processing request', ['exception' => $error]);
36+
37+
throw $error;
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)