Skip to content

Commit

Permalink
Merge pull request #1 from lcobucci/provide-initial-implementation
Browse files Browse the repository at this point in the history
Provide initial implementation
  • Loading branch information
lcobucci authored Jun 27, 2019
2 parents 97dbe42 + f785305 commit 9353cdc
Show file tree
Hide file tree
Showing 34 changed files with 1,213 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/vendor/
/composer.lock
/.phpcs.cache
/infection-log.txt
/infection.log
/.phpunit.result.cache
38 changes: 38 additions & 0 deletions .scrutinizer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
build:
environment:
mysql: false
postgresql: false
redis: false
rabbitmq: false
mongodb: false
php:
version: 7.3

cache:
disabled: false
directories:
- ~/.composer/cache

dependencies:
override:
- composer install --no-interaction --prefer-dist

nodes:
analysis:
project_setup:
override: true
tests:
override:
- php-scrutinizer-run
- phpcs-run

checks:
php : true

tools:
external_code_coverage: true

build_failure_conditions:
- 'elements.rating(<= C).new.exists'
- 'issues.severity(>= MAJOR).new.exists'
- 'project.metric_change("scrutinizer.test_coverage", < -0.01)'
56 changes: 56 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
dist: trusty
sudo: false
language: php

php:
- 7.3
- 7.4snapshot
- nightly

cache:
directories:
- $HOME/.composer/cache

before_install:
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available"
- composer self-update

install: travis_retry composer install

script:
- ./vendor/bin/phpunit

jobs:
allow_failures:
- php: 7.4snapshot
- php: nightly

include:
- stage: Code Quality
env: TEST_COVERAGE=1
before_script:
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
- if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi
script:
- ./vendor/bin/phpunit --coverage-clover ./clover.xml
after_script:
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover ./clover.xml

- stage: Code Quality
env: CODE_STANDARD=1
script:
- ./vendor/bin/phpcs

- stage: Code Quality
env: STATIC_ANALYSIS=1
script:
- ./vendor/bin/phpstan analyse

- stage: Code Quality
env: MUTATION_TESTS=1
before_script:
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
- if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for mutation tests"; exit 1; fi
script:
- ./vendor/bin/infection --threads=$(nproc) --min-msi=100 --min-covered-msi=100
9 changes: 9 additions & 0 deletions infection.json.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"timeout": 1,
"source": {
"directories": ["src"]
},
"logs": {
"text": "infection.log"
}
}
14 changes: 14 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<ruleset>
<arg name="basepath" value="." />
<arg name="extensions" value="php" />
<arg name="parallel" value="80" />
<arg name="colors" />
<arg name="cache" value=".phpcs.cache" />
<arg value="p" />

<file>src</file>
<file>tests</file>

<rule ref="Lcobucci" />
</ruleset>
11 changes: 11 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon

parameters:
level: 7
paths:
- src
- tests
24 changes: 24 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
verbose="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
beStrictAboutChangesToGlobalState="true"
beStrictAboutCoversAnnotation="true"
beStrictAboutResourceUsageDuringSmallTests="true"
forceCoversAnnotation="true"
>
<testsuites>
<testsuite name="unit">
<directory>tests</directory>
</testsuite>
</testsuites>

<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>
</phpunit>
14 changes: 14 additions & 0 deletions src/DebugInfoStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);

namespace Lcobucci\ErrorHandling;

use Throwable;

interface DebugInfoStrategy
{
/**
* @return array<string, mixed>|null
*/
public function extractDebugInfo(Throwable $error): ?array;
}
18 changes: 18 additions & 0 deletions src/DebugInfoStrategy/NoDebugInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

namespace Lcobucci\ErrorHandling\DebugInfoStrategy;

use Lcobucci\ErrorHandling\DebugInfoStrategy;
use Throwable;

final class NoDebugInfo implements DebugInfoStrategy
{
/**
* {@inheritDoc}
*/
public function extractDebugInfo(Throwable $error): ?array
{
return null;
}
}
52 changes: 52 additions & 0 deletions src/DebugInfoStrategy/NoTrace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);

namespace Lcobucci\ErrorHandling\DebugInfoStrategy;

use Generator;
use Lcobucci\ErrorHandling\DebugInfoStrategy;
use Throwable;
use function get_class;
use function iterator_to_array;

final class NoTrace implements DebugInfoStrategy
{
/**
* {@inheritDoc}
*/
public function extractDebugInfo(Throwable $error): ?array
{
$debugInfo = $this->format($error);
$stack = iterator_to_array($this->streamStack($error->getPrevious()), false);

if ($stack !== []) {
$debugInfo['stack'] = $stack;
}

return $debugInfo;
}

private function streamStack(?Throwable $previous): Generator
{
if ($previous === null) {
return;
}

yield $this->format($previous);
yield from $this->streamStack($previous->getPrevious());
}

/**
* @return array<string, string|int>
*/
private function format(Throwable $error): array
{
return [
'class' => get_class($error),
'code' => $error->getCode(),
'message' => $error->getMessage(),
'file' => $error->getFile(),
'line' => $error->getLine(),
];
}
}
106 changes: 106 additions & 0 deletions src/ErrorConversionMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);

namespace Lcobucci\ErrorHandling;

use Lcobucci\ContentNegotiation\UnformattedResponse;
use Lcobucci\ErrorHandling\Problem\Detailed;
use Lcobucci\ErrorHandling\Problem\Titled;
use Lcobucci\ErrorHandling\Problem\Typed;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use function array_key_exists;

final class ErrorConversionMiddleware implements MiddlewareInterface
{
private const CONTENT_TYPE_CONVERSION = [
'application/json' => 'application/problem+json',
'application/xml' => 'application/problem+xml',
];

private const STATUS_URL = 'https://httpstatuses.com/';

/**
* @var ResponseFactoryInterface
*/
private $responseFactory;

/**
* @var DebugInfoStrategy
*/
private $debugInfoStrategy;

/**
* @var StatusCodeExtractionStrategy
*/
private $statusCodeExtractor;

public function __construct(
ResponseFactoryInterface $responseFactory,
DebugInfoStrategy $debugInfoStrategy,
StatusCodeExtractionStrategy $statusCodeExtractor
) {
$this->responseFactory = $responseFactory;
$this->debugInfoStrategy = $debugInfoStrategy;
$this->statusCodeExtractor = $statusCodeExtractor;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} catch (Throwable $error) {
$response = $this->generateResponse($request, $error);

return new UnformattedResponse(
$response,
$this->extractData($error, $response),
['error' => $error]
);
}
}

private function generateResponse(ServerRequestInterface $request, Throwable $error): ResponseInterface
{
$response = $this->responseFactory->createResponse($this->statusCodeExtractor->extractStatusCode($error));

$accept = $request->getHeaderLine('Accept');

if (! array_key_exists($accept, self::CONTENT_TYPE_CONVERSION)) {
return $response;
}

return $response->withAddedHeader(
'Content-Type',
self::CONTENT_TYPE_CONVERSION[$accept] . '; charset=' . $request->getHeaderLine('Accept-Charset')
);
}

/**
* @return array<string, mixed>
*/
private function extractData(Throwable $error, ResponseInterface $response): array
{
$data = [
'type' => $error instanceof Typed ? $error->getTypeUri() : self::STATUS_URL . $response->getStatusCode(),
'title' => $error instanceof Titled ? $error->getTitle() : $response->getReasonPhrase(),
'details' => $error->getMessage(),
];

if ($error instanceof Detailed) {
$data += $error->getExtraDetails();
}

$debugInfo = $this->debugInfoStrategy->extractDebugInfo($error);

if ($debugInfo !== null) {
$data['_debug'] = $debugInfo;
}

return $data;
}
}
40 changes: 40 additions & 0 deletions src/ErrorLoggingMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);

namespace Lcobucci\ErrorHandling;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Throwable;

final class ErrorLoggingMiddleware implements MiddlewareInterface
{
/**
* @var LoggerInterface
*/
private $logger;

public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}

/**
* {@inheritDoc}
*
* @throws Throwable
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} catch (Throwable $error) {
$this->logger->debug('Error happened while processing request', ['exception' => $error]);

throw $error;
}
}
}
Loading

0 comments on commit 9353cdc

Please sign in to comment.