diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1cc9797b..6dde2330 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,12 +5,13 @@ cache: - .composer services: - - mysql:5.6 + - mariadb:10.2 variables: MYSQL_DATABASE: engelsystem MYSQL_USER: engel MYSQL_PASSWORD: engelsystem + MYSQL_HOST: mariadb COMPOSER_HOME: .composer MYSQL_RANDOM_ROOT_PASSWORD: "yes" @@ -20,14 +21,14 @@ before_script: - find . -type d -exec chmod 755 {} \; # Install required Packages - apt update -yqq - - apt install -yqq git unzip mysql-client + - apt install -yqq git unzip mariadb-client - docker-php-ext-install pdo pdo_mysql gettext # Install xdebug - pecl install xdebug - docker-php-ext-enable xdebug # MySQL DB - - mysql -h mysql -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" < db/install.sql - - mysql -h mysql -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" < db/update.sql + - mysql -h "$MYSQL_HOST" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" < db/install.sql + - mysql -h "$MYSQL_HOST" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" < db/update.sql # Install Composer - curl -sS https://getcomposer.org/installer | php -- --no-ansi --install-dir /usr/local/bin/ --filename composer - /usr/local/bin/composer --no-ansi install diff --git a/composer.json b/composer.json index a8f0b0d6..ed34ba03 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,9 @@ "twbs/bootstrap": "^3.3" }, "require-dev": { - "phpunit/phpunit": "^6.3" + "filp/whoops": "^2.1", + "phpunit/phpunit": "^6.3", + "symfony/var-dumper": "^3.3" }, "autoload": { "psr-4": { diff --git a/config/config.default.php b/config/config.default.php index 1bad9668..7594346a 100644 --- a/config/config.default.php +++ b/config/config.default.php @@ -5,7 +5,7 @@ return [ // MySQL-Connection Settings 'database' => [ - 'host' => env('MYSQL_HOST', (env('CI', false) ? 'mysql' : 'localhost')), + 'host' => env('MYSQL_HOST', (env('CI', false) ? 'mariadb' : 'localhost')), 'user' => env('MYSQL_USER', 'root'), 'pw' => env('MYSQL_PASSWORD', ''), 'db' => env('MYSQL_DATABASE', 'engelsystem'), diff --git a/includes/engelsystem.php b/includes/engelsystem.php index 97076895..07abbb42 100644 --- a/includes/engelsystem.php +++ b/includes/engelsystem.php @@ -2,7 +2,8 @@ use Engelsystem\Application; use Engelsystem\Config\Config; -use Engelsystem\Exceptions\Handler as ExceptionHandler; +use Engelsystem\Exceptions\Handler; +use Engelsystem\Exceptions\Handlers\HandlerInterface; /** * This file includes all needed functions, connects to the db etc. @@ -32,7 +33,8 @@ date_default_timezone_set($app->get('config')->get('timezone')); if (config('environment') == 'development') { $errorHandler = $app->get('error.handler'); - $errorHandler->setEnvironment(ExceptionHandler::ENV_DEVELOPMENT); + $errorHandler->setEnvironment(Handler::ENV_DEVELOPMENT); + $app->bind(HandlerInterface::class, 'error.handler.development'); ini_set('display_errors', true); error_reporting(E_ALL); } else { diff --git a/src/Exceptions/ExceptionsServiceProvider.php b/src/Exceptions/ExceptionsServiceProvider.php index 7755e1e7..a9bc2b17 100644 --- a/src/Exceptions/ExceptionsServiceProvider.php +++ b/src/Exceptions/ExceptionsServiceProvider.php @@ -3,13 +3,56 @@ namespace Engelsystem\Exceptions; use Engelsystem\Container\ServiceProvider; -use Engelsystem\Exceptions\Handler as ExceptionHandler; +use Engelsystem\Exceptions\Handlers\HandlerInterface; +use Engelsystem\Exceptions\Handlers\Legacy; +use Engelsystem\Exceptions\Handlers\LegacyDevelopment; +use Engelsystem\Exceptions\Handlers\Whoops; +use Whoops\Run as WhoopsRunner; class ExceptionsServiceProvider extends ServiceProvider { public function register() { - $errorHandler = $this->app->make(ExceptionHandler::class); + $errorHandler = $this->app->make(Handler::class); + $this->addProductionHandler($errorHandler); + $this->addDevelopmentHandler($errorHandler); $this->app->instance('error.handler', $errorHandler); + $this->app->bind(Handler::class, 'error.handler'); + $errorHandler->register(); + } + + public function boot() + { + /** @var Handler $handler */ + $handler = $this->app->get('error.handler'); + $request = $this->app->get('request'); + + $handler->setRequest($request); + } + + /** + * @param Handler $errorHandler + */ + protected function addProductionHandler($errorHandler) + { + $handler = $this->app->make(Legacy::class); + $this->app->instance('error.handler.production', $handler); + $errorHandler->setHandler(Handler::ENV_PRODUCTION, $handler); + $this->app->bind(HandlerInterface::class, 'error.handler.production'); + } + + /** + * @param Handler $errorHandler + */ + protected function addDevelopmentHandler($errorHandler) + { + $handler = $this->app->make(LegacyDevelopment::class); + + if (class_exists(WhoopsRunner::class)) { + $handler = $this->app->make(Whoops::class); + } + + $this->app->instance('error.handler.development', $handler); + $errorHandler->setHandler(Handler::ENV_DEVELOPMENT, $handler); } } diff --git a/src/Exceptions/Handler.php b/src/Exceptions/Handler.php index 95bcd132..ee15717a 100644 --- a/src/Exceptions/Handler.php +++ b/src/Exceptions/Handler.php @@ -2,6 +2,9 @@ namespace Engelsystem\Exceptions; +use Engelsystem\Exceptions\Handlers\HandlerInterface; +use Engelsystem\Http\Request; +use ErrorException; use Throwable; class Handler @@ -9,34 +12,44 @@ class Handler /** @var string */ protected $environment; + /** @var HandlerInterface[] */ + protected $handler = []; + + /** @var Request */ + protected $request; + const ENV_PRODUCTION = 'prod'; const ENV_DEVELOPMENT = 'dev'; /** * Handler constructor. * - * @param string $environment production|development + * @param string $environment prod|dev */ public function __construct($environment = self::ENV_PRODUCTION) { $this->environment = $environment; + } + /** + * Activate the error handler + */ + public function register() + { set_error_handler([$this, 'errorHandler']); set_exception_handler([$this, 'exceptionHandler']); } /** * @param int $number - * @param string $string + * @param string $message * @param string $file * @param int $line - * @param array $context */ - public function errorHandler($number, $string, $file, $line, $context) + public function errorHandler($number, $message, $file, $line) { - $trace = array_reverse(debug_backtrace()); - - $this->handle('error', $number, $string, $file, $line, $context, $trace); + $exception = new ErrorException($message, 0, $number, $file, $line); + $this->exceptionHandler($exception); } /** @@ -44,91 +57,34 @@ class Handler */ public function exceptionHandler($e) { - $this->handle( - 'exception', - $e->getCode(), - get_class($e) . ': ' . $e->getMessage(), - $e->getFile(), - $e->getLine(), - ['exception' => $e] - ); + if (!$this->request instanceof Request) { + $this->request = new Request(); + } + + $handler = $this->handler[$this->environment]; + $handler->report($e); + $handler->render($this->request, $e); + $this->die(); } /** - * @param string $type - * @param int $number - * @param string $string - * @param string $file - * @param int $line - * @param array $context - * @param array $trace + * Exit the application + * + * @codeCoverageIgnore + * @param string $message */ - protected function handle($type, $number, $string, $file, $line, $context = [], $trace = []) + protected function die($message = '') { - error_log(sprintf('%s: Number: %s, String: %s, File: %s:%u, Context: %s', - $type, - $number, - $string, - $file, - $line, - json_encode($context) - )); - - $file = $this->stripBasePath($file); - - if ($this->environment == self::ENV_DEVELOPMENT) { - echo '
'; - echo sprintf('%s: (%s)' . PHP_EOL, ucfirst($type), $number); - var_export([ - 'string' => $string, - 'file' => $file . ':' . $line, - 'context' => $context, - 'stacktrace' => $this->formatStackTrace($trace), - ]); - echo ''; - die(); - } - - echo 'An
'; + echo sprintf('%s: (%s)' . PHP_EOL, get_class($e), $e->getCode()); + $data = [ + 'string' => $e->getMessage(), + 'file' => $file . ':' . $e->getLine(), + 'stacktrace' => $this->formatStackTrace($e->getTrace()), + ]; + var_dump($data); + echo ''; + } + + /** + * @param array $stackTrace + * @return array + */ + protected function formatStackTrace($stackTrace) + { + $return = []; + $stackTrace = array_reverse($stackTrace); + + foreach ($stackTrace as $trace) { + $path = ''; + $line = ''; + + if (isset($trace['file']) && isset($trace['line'])) { + $path = $this->stripBasePath($trace['file']); + $line = $trace['line']; + } + + $functionName = $trace['function']; + + $return[] = [ + 'file' => $path . ':' . $line, + $functionName => isset($trace['args']) ? $trace['args'] : null, + ]; + } + + return $return; + } +} diff --git a/src/Exceptions/Handlers/Whoops.php b/src/Exceptions/Handlers/Whoops.php new file mode 100644 index 00000000..807f5eb0 --- /dev/null +++ b/src/Exceptions/Handlers/Whoops.php @@ -0,0 +1,85 @@ +app = $app; + } + + /** + * @param Request $request + * @param Throwable $e + */ + public function render($request, Throwable $e) + { + $whoops = $this->app->make(WhoopsRunner::class); + $handler = $this->getPrettyPageHandler($e); + $whoops->pushHandler($handler); + + if ($request->isXmlHttpRequest()) { + $handler = $this->getJsonResponseHandler(); + $whoops->pushHandler($handler); + } + + echo $whoops->handleException($e); + } + + /** + * @param Throwable $e + * @return PrettyPageHandler + */ + protected function getPrettyPageHandler(Throwable $e) + { + $handler = $this->app->make(PrettyPageHandler::class); + + $handler->setPageTitle('Just another ' . get_class($e) . ' to fix :('); + $handler->setApplicationPaths([realpath(__DIR__ . '/../..')]); + + $data = $this->getData(); + $handler->addDataTable('Application', $data); + + return $handler; + } + + /** + * @return JsonResponseHandler + */ + protected function getJsonResponseHandler() + { + $handler = $this->app->make(JsonResponseHandler::class); + $handler->setJsonApi(true); + $handler->addTraceToOutput(true); + + return $handler; + } + + /** + * Aggregate application data + * + * @return array + */ + protected function getData() + { + global $user; + + $data = []; + $data['user'] = $user; + $data['Booted'] = $this->app->isBooted(); + + return $data; + } +} diff --git a/tests/Unit/Exceptions/ExceptionsServiceProviderTest.php b/tests/Unit/Exceptions/ExceptionsServiceProviderTest.php index 9c943d52..4f2ae654 100644 --- a/tests/Unit/Exceptions/ExceptionsServiceProviderTest.php +++ b/tests/Unit/Exceptions/ExceptionsServiceProviderTest.php @@ -3,27 +3,107 @@ namespace Engelsystem\Test\Unit\Exceptions; use Engelsystem\Exceptions\ExceptionsServiceProvider; -use Engelsystem\Exceptions\Handler as ExceptionHandler; +use Engelsystem\Exceptions\Handler; +use Engelsystem\Exceptions\Handlers\HandlerInterface; +use Engelsystem\Exceptions\Handlers\Legacy; +use Engelsystem\Exceptions\Handlers\LegacyDevelopment; +use Engelsystem\Exceptions\Handlers\Whoops; +use Engelsystem\Http\Request; use Engelsystem\Test\Unit\ServiceProviderTest; -use PHPUnit_Framework_MockObject_MockObject; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class ExceptionsServiceProviderTest extends ServiceProviderTest { /** * @covers \Engelsystem\Exceptions\ExceptionsServiceProvider::register() + * @covers \Engelsystem\Exceptions\ExceptionsServiceProvider::addProductionHandler() + * @covers \Engelsystem\Exceptions\ExceptionsServiceProvider::addDevelopmentHandler() */ public function testRegister() { - /** @var PHPUnit_Framework_MockObject_MockObject|ExceptionHandler $exceptionHandler */ - $exceptionHandler = $this->getMockBuilder(ExceptionHandler::class) + $app = $this->getApp(['make', 'instance', 'bind']); + + /** @var MockObject|Handler $handler */ + $handler = $this->createMock(Handler::class); + $this->setExpects($handler, 'register'); + /** @var Legacy|MockObject $legacyHandler */ + $legacyHandler = $this->createMock(Legacy::class); + /** @var LegacyDevelopment|MockObject $developmentHandler */ + $developmentHandler = $this->createMock(LegacyDevelopment::class); + + $whoopsHandler = $this->getMockBuilder(Whoops::class) + ->setConstructorArgs([$app]) ->getMock(); - $app = $this->getApp(); + $app->expects($this->exactly(3)) + ->method('instance') + ->withConsecutive( + ['error.handler.production', $legacyHandler], + ['error.handler.development', $whoopsHandler], + ['error.handler', $handler] + ); - $this->setExpects($app, 'make', [ExceptionHandler::class], $exceptionHandler); - $this->setExpects($app, 'instance', ['error.handler', $exceptionHandler]); + $app->expects($this->exactly(4)) + ->method('make') + ->withConsecutive( + [Handler::class], + [Legacy::class], + [LegacyDevelopment::class], + [Whoops::class] + ) + ->willReturnOnConsecutiveCalls( + $handler, + $legacyHandler, + $developmentHandler, + $whoopsHandler + ); + + $app->expects($this->exactly(2)) + ->method('bind') + ->withConsecutive( + [HandlerInterface::class, 'error.handler.production'], + [Handler::class, 'error.handler'] + ); + + $handler->expects($this->exactly(2)) + ->method('setHandler') + ->withConsecutive( + [Handler::ENV_PRODUCTION, $legacyHandler], + [Handler::ENV_DEVELOPMENT, $whoopsHandler] + ); $serviceProvider = new ExceptionsServiceProvider($app); $serviceProvider->register(); } + + /** + * @covers \Engelsystem\Exceptions\ExceptionsServiceProvider::boot() + */ + public function testBoot() + { + /** @var MockObject|Handler $handler */ + $handler = $this->createMock(Handler::class); + + /** @var MockObject|Request $request */ + $request = $this->createMock(Request::class); + + $handler->expects($this->once()) + ->method('setRequest') + ->with($request); + + $app = $this->getApp(['get']); + $app->expects($this->exactly(2)) + ->method('get') + ->withConsecutive( + ['error.handler'], + ['request'] + ) + ->willReturnOnConsecutiveCalls( + $handler, + $request + ); + + $provider = new ExceptionsServiceProvider($app); + $provider->boot(); + } } diff --git a/tests/Unit/Exceptions/HandlerTest.php b/tests/Unit/Exceptions/HandlerTest.php new file mode 100644 index 00000000..40202be8 --- /dev/null +++ b/tests/Unit/Exceptions/HandlerTest.php @@ -0,0 +1,140 @@ +assertInstanceOf(Handler::class, $handler); + $this->assertEquals(Handler::ENV_PRODUCTION, $handler->getEnvironment()); + + $anotherHandler = new Handler(Handler::ENV_DEVELOPMENT); + $this->assertEquals(Handler::ENV_DEVELOPMENT, $anotherHandler->getEnvironment()); + } + + /** + * @covers \Engelsystem\Exceptions\Handler::errorHandler() + */ + public function testErrorHandler() + { + /** @var Handler|Mock $handler */ + $handler = $this->getMockBuilder(Handler::class) + ->setMethods(['exceptionHandler']) + ->getMock(); + + $handler->expects($this->once()) + ->method('exceptionHandler') + ->with($this->isInstanceOf(ErrorException::class)); + + $handler->errorHandler(1, 'Foo and bar!', '/lo/rem.php', 123); + } + + /** + * @covers \Engelsystem\Exceptions\Handler::exceptionHandler() + */ + public function testExceptionHandler() + { + $exception = new Exception(); + + /** @var HandlerInterface|Mock $handlerMock */ + $handlerMock = $this->getMockForAbstractClass(HandlerInterface::class); + $handlerMock->expects($this->once()) + ->method('report') + ->with($exception); + $handlerMock->expects($this->once()) + ->method('render') + ->with($this->isInstanceOf(Request::class), $exception); + + /** @var Handler|Mock $handler */ + $handler = $this->getMockBuilder(Handler::class) + ->setMethods(['die']) + ->getMock(); + $handler->expects($this->once()) + ->method('die'); + + $handler->setHandler(Handler::ENV_PRODUCTION, $handlerMock); + + $handler->exceptionHandler($exception); + } + + /** + * @covers \Engelsystem\Exceptions\Handler::register() + */ + public function testRegister() + { + /** @var Handler|Mock $handler */ + $handler = $this->getMockForAbstractClass(Handler::class); + $handler->register(); + + set_error_handler($errorHandler = set_error_handler('var_dump')); + $this->assertEquals($handler, array_shift($errorHandler)); + + set_exception_handler($exceptionHandler = set_error_handler('var_dump')); + $this->assertEquals($handler, array_shift($exceptionHandler)); + + restore_error_handler(); + restore_exception_handler(); + } + + /** + * @covers \Engelsystem\Exceptions\Handler::setEnvironment() + * @covers \Engelsystem\Exceptions\Handler::getEnvironment() + */ + public function testEnvironment() + { + $handler = new Handler(); + + $handler->setEnvironment(Handler::ENV_DEVELOPMENT); + $this->assertEquals(Handler::ENV_DEVELOPMENT, $handler->getEnvironment()); + + $handler->setEnvironment(Handler::ENV_PRODUCTION); + $this->assertEquals(Handler::ENV_PRODUCTION, $handler->getEnvironment()); + } + + /** + * @covers \Engelsystem\Exceptions\Handler::setHandler() + * @covers \Engelsystem\Exceptions\Handler::getHandler() + */ + public function testHandler() + { + $handler = new Handler(); + /** @var HandlerInterface|Mock $devHandler */ + $devHandler = $this->getMockForAbstractClass(HandlerInterface::class); + /** @var HandlerInterface|Mock $prodHandler */ + $prodHandler = $this->getMockForAbstractClass(HandlerInterface::class); + + $handler->setHandler(Handler::ENV_DEVELOPMENT, $devHandler); + $handler->setHandler(Handler::ENV_PRODUCTION, $prodHandler); + $this->assertEquals($devHandler, $handler->getHandler(Handler::ENV_DEVELOPMENT)); + $this->assertEquals($prodHandler, $handler->getHandler(Handler::ENV_PRODUCTION)); + $this->assertCount(2, $handler->getHandler()); + } + + /** + * @covers \Engelsystem\Exceptions\Handler::setRequest() + * @covers \Engelsystem\Exceptions\Handler::getRequest() + */ + public function testRequest() + { + $handler = new Handler(); + /** @var Request|Mock $request */ + $request = $this->createMock(Request::class); + + $handler->setRequest($request); + $this->assertEquals($request, $handler->getRequest()); + } +} diff --git a/tests/Unit/Exceptions/Handlers/LegacyDevelopmentTest.php b/tests/Unit/Exceptions/Handlers/LegacyDevelopmentTest.php new file mode 100644 index 00000000..d5390c9e --- /dev/null +++ b/tests/Unit/Exceptions/Handlers/LegacyDevelopmentTest.php @@ -0,0 +1,35 @@ +createMock(Request::class); + $exception = new ErrorException('Lorem Ipsum', 4242, 1, 'foo.php', 9999); + + $regex = sprintf( + '%%