upgraded dependencies

This commit is contained in:
RafficMohammed
2023-01-08 01:59:16 +05:30
parent 51056e3aad
commit f9ae387337
6895 changed files with 133617 additions and 178680 deletions

View File

@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: nunomaduro
patreon: nunomaduro
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L

View File

@@ -1,38 +0,0 @@
name: Tests
on:
push:
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php: [7.2, 7.3, 7.4, 8.0]
dependency-version: [prefer-stable]
name: PHP ${{ matrix.php }} ${{ matrix.dependency-version }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer
coverage: none
- name: Setup Problem Matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP dependencies
run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist --no-progress --ansi
- name: Run Unit Tests
run: vendor/bin/phpunit --colors=always

29
vendor/nunomaduro/collision/.php_cs vendored Normal file
View File

@@ -0,0 +1,29 @@
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests')
->notPath(__DIR__ . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'laravel')
->in(__DIR__ . DIRECTORY_SEPARATOR . 'src')
->append(['.php_cs']);
$rules = [
'@Symfony' => true,
'phpdoc_no_empty_return' => false,
'array_syntax' => ['syntax' => 'short'],
'yoda_style' => false,
'binary_operator_spaces' => [
'operators' => [
'=>' => 'align',
'=' => 'align',
],
],
'concat_space' => ['spacing' => 'one'],
'not_operator_with_space' => false,
];
$rules['increment_style'] = ['style' => 'post'];
return PhpCsFixer\Config::create()
->setUsingCache(true)
->setRules($rules)
->setFinder($finder);

View File

@@ -7,23 +7,22 @@
<p align="center">
<a href="https://travis-ci.org/nunomaduro/collision"><img src="https://img.shields.io/travis/nunomaduro/collision/stable.svg" alt="Build Status"></img></a>
<a href="https://scrutinizer-ci.com/g/nunomaduro/collision"><img src="https://img.shields.io/scrutinizer/g/nunomaduro/collision.svg" alt="Quality Score"></img></a>
<a href="https://scrutinizer-ci.com/g/nunomaduro/collision"><img src="https://img.shields.io/scrutinizer/coverage/g/nunomaduro/collision.svg" alt="Coverage"></img></a>
<a href="https://packagist.org/packages/nunomaduro/collision"><img src="https://poser.pugx.org/nunomaduro/collision/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/nunomaduro/collision"><img src="https://poser.pugx.org/nunomaduro/collision/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/nunomaduro/collision"><img src="https://poser.pugx.org/nunomaduro/collision/license.svg" alt="License"></a>
</p>
## About Collision
---
Collision was created by, and is maintained by [Nuno Maduro](https://github.com/nunomaduro), and is an error handler framework for console/command-line PHP applications.
Collision was created by, and is maintained by **[Nuno Maduro](https://github.com/nunomaduro)**, and is a package designed to give you beautiful error reporting when interacting with your app through the command line.
- Build on top of [Whoops](https://github.com/filp/whoops).
- Supports [Laravel](https://github.com/laravel/laravel) Artisan & [PHPUnit](https://github.com/sebastianbergmann/phpunit).
- Built with [PHP 7](https://php.net) using modern coding standards.
* It's included on **[Laravel](https://laravel.com)**, the most popular free, open-source PHP framework in the world.
* Built on top of the **[Whoops](https://github.com/filp/whoops)** error handler.
* Supports [Laravel](https://github.com/laravel/laravel), [Symfony](https://symfony.com), [PHPUnit](https://github.com/sebastianbergmann/phpunit), and many other frameworks.
## Installation & Usage
> **Requires [PHP 7.1+](https://php.net/releases/)**
> **Requires [PHP 7.2.5+](https://php.net/releases/)**
Require Collision using [Composer](https://getcomposer.org):
@@ -31,12 +30,6 @@ Require Collision using [Composer](https://getcomposer.org):
composer require nunomaduro/collision --dev
```
If you are not using Laravel, you need to register the handler in your code:
```php
(new \NunoMaduro\Collision\Provider)->register();
```
## Lumen adapter
Configure the Collision service provider:
@@ -47,14 +40,21 @@ $app->register(\NunoMaduro\Collision\Adapters\Laravel\CollisionServiceProvider::
## Phpunit adapter
Phpunit must be 7.0 or higher.
Phpunit must be 8.5.1 or higher.
Add the following configuration to your `phpunit.xml`:
Add the Collision `printerClass` to your `phpunit.xml` in the `phpunit` section:
```xml
<listeners>
<listener class="NunoMaduro\Collision\Adapters\Phpunit\Listener" />
</listeners>
<phpunit
printerClass="NunoMaduro\Collision\Adapters\Phpunit\Printer">
```
## No adapter
You need to register the handler in your code:
```php
(new \NunoMaduro\Collision\Provider)->register();
```
## Contributing

View File

@@ -15,17 +15,28 @@
],
"require": {
"php": "^7.2.5 || ^8.0",
"filp/whoops": "^2.1.4",
"symfony/console": "~2.8|~3.3|~4.0",
"php-parallel-lint/php-console-highlighter": "0.5.*"
"facade/ignition-contracts": "^1.0",
"filp/whoops": "^2.4",
"symfony/console": "^5.0"
},
"require-dev": {
"laravel/framework": "^6.0",
"phpunit/phpunit": "^8.0 || ^9.0"
"friendsofphp/php-cs-fixer": "^2.16",
"facade/ignition": "^2.0",
"fideloper/proxy": "^4.2",
"fruitcake/laravel-cors": "^1.0",
"laravel/framework": "^7.0",
"laravel/tinker": "^2.0",
"nunomaduro/larastan": "^0.6",
"orchestra/testbench": "^5.0",
"phpstan/phpstan": "^0.12.3",
"phpunit/phpunit": "^8.5.1 || ^9.0"
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
"Tests\\Unit\\": "tests/Unit",
"Tests\\FakeProgram\\": "tests/FakeProgram",
"Tests\\": "tests/LaravelApp/tests",
"App\\": "tests/LaravelApp/app/"
}
},
"minimum-stability": "dev",
@@ -45,5 +56,16 @@
"NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider"
]
}
},
"scripts": {
"lint": "php-cs-fixer fix -v",
"test:types": "phpstan analyse --ansi",
"test:unit": "phpunit --colors=always",
"test:lint": "php-cs-fixer fix -v --dry-run",
"test": [
"@test:lint",
"@test:unit",
"@test:types"
]
}
}

View File

@@ -0,0 +1,19 @@
includes:
- ./vendor/nunomaduro/larastan/extension.neon
parameters:
level: max
paths:
- src
checkMissingIterableValueType: false
reportUnmatchedIgnoredErrors: false
excludes_analyse:
- src/Adapters/Phpunit/Printer
ignoreErrors:
- '#Parameter \#1 \$input of function str_pad expects string, int given.#'
- '#Cannot call method addTheme\(\) on array|JakubOnderka\\PhpConsoleColor\\ConsoleColor#'
- '#Method NunoMaduro\\Collision\\Adapters\\Laravel\\IgnitionSolutionsRepository::getFromThrowable\(\) should return array<int,#'
- '#Result of static method Dotenv\\Repository\\RepositoryBuilder::create\(\) \(void\) is used.#'
- message: '#Cannot call method make\(\) on void.#'
paths:
- src/Adapters/Laravel/Commands/*

View File

@@ -17,7 +17,7 @@
>
<testsuites>
<testsuite name="Collision Test Suite">
<directory suffix="Test.php">./tests</directory>
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>

View File

@@ -11,12 +11,14 @@
namespace NunoMaduro\Collision\Adapters\Laravel;
use NunoMaduro\Collision\Provider;
use Illuminate\Support\ServiceProvider;
use NunoMaduro\Collision\Adapters\Phpunit\Listener;
use NunoMaduro\Collision\Contracts\Provider as ProviderContract;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
use NunoMaduro\Collision\Contracts\Adapters\Phpunit\Listener as ListenerContract;
use Illuminate\Support\ServiceProvider;
use NunoMaduro\Collision\Adapters\Laravel\Commands\TestCommand;
use NunoMaduro\Collision\Contracts\Provider as ProviderContract;
use NunoMaduro\Collision\Handler;
use NunoMaduro\Collision\Provider;
use NunoMaduro\Collision\SolutionsRepositories\NullSolutionsRepository;
use NunoMaduro\Collision\Writer;
/**
* This is an Collision Laravel Adapter Service Provider implementation.
@@ -29,17 +31,43 @@ class CollisionServiceProvider extends ServiceProvider
{
/**
* {@inheritdoc}
*
* @var bool
*/
protected $defer = true;
/**
* Boots application services.
*
* @return void
*/
public function boot()
{
$this->commands([
TestCommand::class,
]);
}
/**
* {@inheritdoc}
*/
public function register()
{
if ($this->app->runningInConsole() && ! $this->app->runningUnitTests()) {
$this->app->singleton(ListenerContract::class, Listener::class);
$this->app->bind(ProviderContract::class, Provider::class);
if ($this->app->runningInConsole() && !$this->app->runningUnitTests()) {
$this->app->bind(ProviderContract::class, function () {
if ($this->app->has(\Facade\IgnitionContracts\SolutionProviderRepository::class)) {
$solutionsRepository = new IgnitionSolutionsRepository(
$this->app->get(\Facade\IgnitionContracts\SolutionProviderRepository::class)
);
} else {
$solutionsRepository = new NullSolutionsRepository();
}
$writer = new Writer($solutionsRepository);
$handler = new Handler($writer);
return new Provider(null, $handler);
});
$appExceptionHandler = $this->app->make(ExceptionHandlerContract::class);

View File

@@ -0,0 +1,143 @@
<?php
namespace NunoMaduro\Collision\Adapters\Laravel\Commands;
use Dotenv\Dotenv;
use Dotenv\Repository\RepositoryBuilder;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Process;
class TestCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test {--without-tty : Disable output to TTY}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the application tests';
/**
* The arguments to be used while calling phpunit.
*
* @var array
*/
protected $arguments = [
'--printer',
'NunoMaduro\Collision\Adapters\Phpunit\Printer',
];
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->ignoreValidationErrors();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$options = array_slice($_SERVER['argv'], $this->option('without-tty') ? 3 : 2);
$this->clearEnv();
$process = (new Process(array_merge(
$this->binary(),
array_merge(
$this->arguments,
$this->phpunitArguments($options)
)
)))->setTimeout(null);
try {
$process->setTty(!$this->option('without-tty'));
} catch (RuntimeException $e) {
$this->output->writeln('Warning: ' . $e->getMessage());
}
try {
return $process->run(function ($type, $line) {
$this->output->write($line);
});
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}
}
}
/**
* Get the PHP binary to execute.
*
* @return array
*/
protected function binary()
{
if ('phpdbg' === PHP_SAPI) {
return [PHP_BINARY, '-qrr', 'vendor/phpunit/phpunit/phpunit'];
}
return [PHP_BINARY, 'vendor/phpunit/phpunit/phpunit'];
}
/**
* Get the array of arguments for running PHPUnit.
*
* @param array $options
*
* @return array
*/
protected function phpunitArguments($options)
{
$options = array_values(array_filter($options, function ($option) {
return !Str::startsWith($option, '--env=');
}));
if (!file_exists($file = base_path('phpunit.xml'))) {
$file = base_path('phpunit.xml.dist');
}
return array_merge(['-c', $file], $options);
}
/**
* Clears any set Environment variables set by Laravel if the --env option is empty.
*
* @return void
*/
protected function clearEnv()
{
if (!$this->option('env')) {
$repositories = RepositoryBuilder::create()
->make();
$envs = Dotenv::create(
$repositories,
$this->laravel->environmentPath(),
$this->laravel->environmentFile()
)->safeLoad();
foreach ($envs as $name => $value) {
$repositories->clear($name);
}
}
}
}

View File

@@ -11,11 +11,11 @@
namespace NunoMaduro\Collision\Adapters\Laravel;
use Exception;
use Illuminate\Contracts\Container\Container;
use NunoMaduro\Collision\Contracts\Provider as ProviderContract;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
use NunoMaduro\Collision\Contracts\Provider as ProviderContract;
use Symfony\Component\Console\Exception\ExceptionInterface as SymfonyConsoleExceptionInterface;
use Throwable;
/**
* This is an Collision Laravel Adapter ExceptionHandler implementation.
@@ -42,20 +42,17 @@ class ExceptionHandler implements ExceptionHandlerContract
/**
* Creates a new instance of the ExceptionHandler.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param \Illuminate\Contracts\Debug\ExceptionHandler $appExceptionHandler
*/
public function __construct(Container $container, ExceptionHandlerContract $appExceptionHandler)
{
$this->container = $container;
$this->container = $container;
$this->appExceptionHandler = $appExceptionHandler;
}
/**
* {@inheritdoc}
*/
public function report(Exception $e)
public function report(Throwable $e)
{
$this->appExceptionHandler->report($e);
}
@@ -63,7 +60,7 @@ class ExceptionHandler implements ExceptionHandlerContract
/**
* {@inheritdoc}
*/
public function render($request, Exception $e)
public function render($request, Throwable $e)
{
return $this->appExceptionHandler->render($request, $e);
}
@@ -71,7 +68,7 @@ class ExceptionHandler implements ExceptionHandlerContract
/**
* {@inheritdoc}
*/
public function renderForConsole($output, Exception $e)
public function renderForConsole($output, Throwable $e)
{
if ($e instanceof SymfonyConsoleExceptionInterface) {
$this->appExceptionHandler->renderForConsole($output, $e);
@@ -90,10 +87,9 @@ class ExceptionHandler implements ExceptionHandlerContract
/**
* Determine if the exception should be reported.
*
* @param \Exception $e
* @return bool
*/
public function shouldReport(Exception $e)
public function shouldReport(Throwable $e)
{
return $this->appExceptionHandler->shouldReport($e);
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Adapters\Laravel;
use Facade\IgnitionContracts\SolutionProviderRepository;
use NunoMaduro\Collision\Contracts\SolutionsRepository;
use Throwable;
/**
* This is an Collision Laravel Adapter Solutions Provider implementation.
*
* Registers the Error Handler on Laravel.
*
* @author Nuno Maduro <enunomaduro@gmail.com>
*/
class IgnitionSolutionsRepository implements SolutionsRepository
{
/**
* Holds an instance of ignition solutions provider repository.
*
* @var \Facade\IgnitionContracts\SolutionProviderRepository
*/
protected $solutionProviderRepository;
/**
* IgnitionSolutionsRepository constructor.
*/
public function __construct(SolutionProviderRepository $solutionProviderRepository)
{
$this->solutionProviderRepository = $solutionProviderRepository;
}
/**
* {@inheritdoc}
*/
public function getFromThrowable(Throwable $throwable): array
{
return $this->solutionProviderRepository->getSolutionsForThrowable($throwable);
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Adapters\Phpunit;
use ReflectionObject;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\Output;
/**
* @internal
*/
final class ConfigureIO
{
/**
* Configures both given input and output with
* options from the enviroment.
*
* @throws \ReflectionException
*/
public static function of(InputInterface $input, Output $output): void
{
$application = new Application();
$reflector = new ReflectionObject($application);
$method = $reflector->getMethod('configureIO');
$method->setAccessible(true);
$method->invoke($application, $input, $output);
}
}

View File

@@ -1,177 +0,0 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Adapters\Phpunit;
use ReflectionObject;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\Warning;
use Whoops\Exception\Inspector;
use NunoMaduro\Collision\Writer;
use PHPUnit\Framework\TestSuite;
use Symfony\Component\Console\Application;
use PHPUnit\Framework\AssertionFailedError;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use NunoMaduro\Collision\Contracts\Writer as WriterContract;
use NunoMaduro\Collision\Contracts\Adapters\Phpunit\Listener as ListenerContract;
if (class_exists(\PHPUnit\Runner\Version::class) && intval(substr(\PHPUnit\Runner\Version::id(), 0, 1)) >= 7) {
/**
* This is an Collision Phpunit Adapter implementation.
*
* @author Nuno Maduro <enunomaduro@gmail.com>
*/
class Listener implements ListenerContract
{
/**
* Holds an instance of the writer.
*
* @var \NunoMaduro\Collision\Contracts\Writer
*/
protected $writer;
/**
* Holds the exception found, if any.
*
* @var \Throwable|null
*/
protected $exceptionFound;
/**
* Creates a new instance of the class.
*
* @param \NunoMaduro\Collision\Contracts\Writer|null $writer
*/
public function __construct(WriterContract $writer = null)
{
$this->writer = $writer ?: $this->buildWriter();
}
/**
* {@inheritdoc}
*/
public function render(\Throwable $t)
{
$inspector = new Inspector($t);
$this->writer->write($inspector);
}
/**
* {@inheritdoc}
*/
public function addError(Test $test, \Throwable $t, float $time): void
{
if ($this->exceptionFound === null) {
$this->exceptionFound = $t;
}
}
/**
* {@inheritdoc}
*/
public function addWarning(Test $test, Warning $t, float $time): void
{
}
/**
* {@inheritdoc}
*/
public function addFailure(Test $test, AssertionFailedError $t, float $time): void
{
$this->writer->ignoreFilesIn(['/vendor/'])
->showTrace(false);
if ($this->exceptionFound === null) {
$this->exceptionFound = $t;
}
}
/**
* {@inheritdoc}
*/
public function addIncompleteTest(Test $test, \Throwable $t, float $time): void
{
}
/**
* {@inheritdoc}
*/
public function addRiskyTest(Test $test, \Throwable $t, float $time): void
{
}
/**
* {@inheritdoc}
*/
public function addSkippedTest(Test $test, \Throwable $t, float $time): void
{
}
/**
* {@inheritdoc}
*/
public function startTestSuite(TestSuite $suite): void
{
}
/**
* {@inheritdoc}
*/
public function endTestSuite(TestSuite $suite): void
{
}
/**
* {@inheritdoc}
*/
public function startTest(Test $test): void
{
}
/**
* {@inheritdoc}
*/
public function endTest(Test $test, float $time): void
{
}
/**
* {@inheritdoc}
*/
public function __destruct()
{
if ($this->exceptionFound !== null) {
$this->render($this->exceptionFound);
}
}
/**
* Builds an Writer.
*
* @return \NunoMaduro\Collision\Contracts\Writer
*/
protected function buildWriter(): WriterContract
{
$writer = new Writer;
$application = new Application();
$reflector = new ReflectionObject($application);
$method = $reflector->getMethod('configureIO');
$method->setAccessible(true);
$method->invoke($application, new ArgvInput, $output = new ConsoleOutput);
return $writer->setOutput($output);
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Adapters\Phpunit;
/*
* This `if` condition exists because phpunit
* is not a direct dependency of Collision.
*
* This code bellow it's for phpunit@8
*/
if (class_exists(\PHPUnit\Runner\Version::class) && intval(substr(\PHPUnit\Runner\Version::id(), 0, 1)) === 8) {
/**
* This is an Collision Phpunit Adapter implementation.
*
* @internal
*/
final class Printer extends \PHPUnit\Util\Printer implements \PHPUnit\Framework\TestListener
{
use PrinterContents;
}
}
/*
* This `if` condition exists because phpunit
* is not a direct dependency of Collision.
*
* This code bellow it's for phpunit@9
*/
if (class_exists(\PHPUnit\Runner\Version::class) && intval(substr(\PHPUnit\Runner\Version::id(), 0, 1)) === 9) {
/**
* This is an Collision Phpunit Adapter implementation.
*
* @internal
*/
final class Printer implements \PHPUnit\TextUI\ResultPrinter
{
use PrinterContents;
/**
* Intentionally left blank as we output things on events of the listener.
*/
public function printResult(\PHPUnit\Framework\TestResult $result): void
{
// ..
}
}
}

View File

@@ -0,0 +1,224 @@
<?php
namespace NunoMaduro\Collision\Adapters\Phpunit;
use NunoMaduro\Collision\Exceptions\ShouldNotHappen;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use ReflectionObject;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Throwable;
trait PrinterContents
{
/**
* Holds an instance of the style.
*
* Style is a class we use to interact with output.
*
* @var Style
*/
private $style;
/**
* Holds the duration time of the test suite.
*
* @var Timer
*/
private $timer;
/**
* Holds the state of the test
* suite. The number of tests, etc.
*
* @var State
*/
private $state;
/**
* If the test suite has ended before.
*
* @var bool
*/
private $ended = false;
/**
* Creates a new instance of the listener.
*
* @param ConsoleOutput $output
*
* @throws \ReflectionException
*/
public function __construct(ConsoleOutput $output = null)
{
if (intval(substr(\PHPUnit\Runner\Version::id(), 0, 1)) === 8) {
parent::__construct();
}
$this->timer = Timer::start();
$output = $output ?? new ConsoleOutput();
ConfigureIO::of(new ArgvInput(), $output);
$this->style = new Style($output);
$dummyTest = new class() extends TestCase {
};
$this->state = State::from($dummyTest);
}
/**
* {@inheritdoc}
*/
public function addError(Test $testCase, Throwable $throwable, float $time): void
{
$testCase = $this->testCaseFromTest($testCase);
$this->state->add(TestResult::fromTestCase($testCase, TestResult::FAIL));
$this->style->writeError($this->state, $throwable);
}
/**
* {@inheritdoc}
*/
public function addWarning(Test $testCase, Warning $warning, float $time): void
{
$testCase = $this->testCaseFromTest($testCase);
$this->state->add(TestResult::fromTestCase($testCase, TestResult::WARN, $warning->getMessage()));
}
/**
* {@inheritdoc}
*/
public function addFailure(Test $testCase, AssertionFailedError $error, float $time): void
{
$testCase = $this->testCaseFromTest($testCase);
$this->state->add(TestResult::fromTestCase($testCase, TestResult::FAIL));
$reflector = new ReflectionObject($error);
if ($reflector->hasProperty('message')) {
$message = trim((string) preg_replace("/\r|\n/", ' ', $error->getMessage()));
$property = $reflector->getProperty('message');
$property->setAccessible(true);
$property->setValue($error, $message);
}
$this->style->writeError($this->state, $error);
}
/**
* {@inheritdoc}
*/
public function addIncompleteTest(Test $testCase, Throwable $t, float $time): void
{
$testCase = $this->testCaseFromTest($testCase);
$this->state->add(TestResult::fromTestCase($testCase, TestResult::INCOMPLETE));
}
/**
* {@inheritdoc}
*/
public function addRiskyTest(Test $testCase, Throwable $t, float $time): void
{
$testCase = $this->testCaseFromTest($testCase);
$this->state->add(TestResult::fromTestCase($testCase, TestResult::RISKY, $t->getMessage()));
}
/**
* {@inheritdoc}
*/
public function addSkippedTest(Test $testCase, Throwable $t, float $time): void
{
$testCase = $this->testCaseFromTest($testCase);
$this->state->add(TestResult::fromTestCase($testCase, TestResult::SKIPPED, $t->getMessage()));
}
/**
* {@inheritdoc}
*/
public function startTestSuite(TestSuite $suite): void
{
if ($this->state->suiteTotalTests === null) {
$this->state->suiteTotalTests = $suite->count();
}
}
/**
* {@inheritdoc}
*/
public function endTestSuite(TestSuite $suite): void
{
if (!$this->ended && $this->state->suiteTotalTests === $this->state->testSuiteTestsCount()) {
$this->ended = true;
$this->style->writeCurrentRecap($this->state);
$this->style->updateFooter($this->state);
$this->style->writeRecap($this->timer);
}
}
/**
* {@inheritdoc}
*/
public function startTest(Test $testCase): void
{
$testCase = $this->testCaseFromTest($testCase);
// Let's check first if the testCase is over.
if ($this->state->testCaseHasChanged($testCase)) {
$this->style->writeCurrentRecap($this->state);
$this->state->moveTo($testCase);
}
$this->style->updateFooter($this->state, $testCase);
}
/**
* {@inheritdoc}
*/
public function endTest(Test $testCase, float $time): void
{
$testCase = $this->testCaseFromTest($testCase);
if (!$this->state->existsInTestCase($testCase)) {
$this->state->add(TestResult::fromTestCase($testCase, TestResult::PASS));
}
}
/**
* Intentionally left blank as we output things on events of the listener.
*/
public function write(string $content): void
{
// ..
}
/**
* Returns a test case from the given test.
*
* Note: This printer is do not work with normal Test classes - only
* with Test Case classes. Please report an issue if you think
* this should work any other way.
*/
private function testCaseFromTest(Test $test): TestCase
{
if (!$test instanceof TestCase) {
throw new ShouldNotHappen();
}
return $test;
}
}

View File

@@ -0,0 +1,194 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Adapters\Phpunit;
use NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName;
use PHPUnit\Framework\TestCase;
/**
* @internal
*/
final class State
{
/**
* The complete test suite number of tests.
*
* @var int|null
*/
public $suiteTotalTests;
/**
* The complete test suite tests.
*
* @var array<int, TestResult>
*/
public $suiteTests = [];
/**
* The current test case class.
*
* @var string
*/
public $testCaseName;
/**
* The current test case tests.
*
* @var array<int, TestResult>
*/
public $testCaseTests = [];
/**
* The state constructor.
*/
private function __construct(string $testCaseName)
{
$this->testCaseName = $testCaseName;
}
/**
* Creates a new State starting from the given test case.
*/
public static function from(TestCase $test): self
{
return new self(self::getPrintableTestCaseName($test));
}
/**
* Adds the given test to the State.
*/
public function add(TestResult $test): void
{
$this->testCaseTests[] = $test;
$this->suiteTests[] = $test;
}
/**
* Gets the test case title.
*/
public function getTestCaseTitle(): string
{
foreach ($this->testCaseTests as $test) {
if ($test->type === TestResult::FAIL) {
return 'FAIL';
}
}
foreach ($this->testCaseTests as $test) {
if ($test->type !== TestResult::PASS) {
return 'WARN';
}
}
return 'PASS';
}
/**
* Gets the test case title color.
*/
public function getTestCaseTitleColor(): string
{
foreach ($this->testCaseTests as $test) {
if ($test->type === TestResult::FAIL) {
return 'red';
}
}
foreach ($this->testCaseTests as $test) {
if ($test->type !== TestResult::PASS) {
return 'yellow';
}
}
return 'green';
}
/**
* Returns the number of tests on the current test case.
*/
public function testCaseTestsCount(): int
{
return count($this->testCaseTests);
}
/**
* Returns the number of tests on the complete test suite.
*/
public function testSuiteTestsCount(): int
{
return count($this->suiteTests);
}
/**
* Checks if the given test case is different from the current one.
*/
public function testCaseHasChanged(TestCase $testCase): bool
{
return self::getPrintableTestCaseName($testCase) !== $this->testCaseName;
}
/**
* Moves the a new test case.
*/
public function moveTo(TestCase $testCase): void
{
$this->testCaseName = self::getPrintableTestCaseName($testCase);
$this->testCaseTests = [];
}
/**
* Foreach test in the test case.
*/
public function eachTestCaseTests(callable $callback): void
{
foreach ($this->testCaseTests as $test) {
$callback($test);
}
}
public function countTestsInTestSuiteBy(string $type): int
{
return count(array_filter($this->suiteTests, function (TestResult $testResult) use ($type) {
return $testResult->type === $type;
}));
}
/**
* Checks if the given test already contains a result.
*/
public function existsInTestCase(TestCase $test): bool
{
foreach ($this->testCaseTests as $testResult) {
if (TestResult::makeDescription($test) === $testResult->description) {
return true;
}
}
return false;
}
/**
* Returns the printable test case name from the given `TestCase`.
*/
private static function getPrintableTestCaseName(TestCase $test): string
{
if ($test instanceof HasPrintableTestCaseName) {
$name = $test->getPrintableTestCaseName();
} else {
$name = get_class($test);
}
return $name;
}
}

View File

@@ -0,0 +1,239 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Adapters\Phpunit;
use NunoMaduro\Collision\Writer;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExceptionWrapper;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Throwable;
use Whoops\Exception\Inspector;
/**
* @internal
*/
final class Style
{
/**
* @var ConsoleOutput
*/
private $output;
/**
* @var ConsoleSectionOutput
*/
private $footer;
/**
* Style constructor.
*/
public function __construct(ConsoleOutput $output)
{
$this->output = $output;
$this->footer = $output->section();
}
/**
* Prints the content similar too:.
*
* ```
* PASS Unit\ExampleTest
* ✓ basic test
* ```
*/
public function writeCurrentRecap(State $state): void
{
if (!$state->testCaseTestsCount()) {
return;
}
$this->footer->clear();
$this->output->writeln($this->titleLineFrom(
$state->getTestCaseTitle() === 'FAIL' ? 'white' : 'black',
$state->getTestCaseTitleColor(),
$state->getTestCaseTitle(),
$state->testCaseName
));
$state->eachTestCaseTests(function (TestResult $testResult) {
$this->output->writeln($this->testLineFrom(
$testResult->color,
$testResult->icon,
$testResult->description,
$testResult->warning
));
});
}
/**
* Prints the content similar too on the footer. Where
* we are updating the current test.
*
* ```
* Runs Unit\ExampleTest
* • basic test
* ```
*/
public function updateFooter(State $state, TestCase $testCase = null): void
{
$runs = [];
if ($testCase) {
$runs[] = $this->titleLineFrom(
'black',
'yellow',
'RUNS',
get_class($testCase)
);
$testResult = TestResult::fromTestCase($testCase, TestResult::RUNS);
$runs[] = $this->testLineFrom(
$testResult->color,
$testResult->icon,
$testResult->description
);
}
$types = [TestResult::FAIL, TestResult::WARN, TestResult::RISKY, TestResult::INCOMPLETE, TestResult::SKIPPED, TestResult::PASS];
foreach ($types as $type) {
if ($countTests = $state->countTestsInTestSuiteBy($type)) {
$color = TestResult::makeColor($type);
$tests[] = "<fg=$color;options=bold>$countTests $type</>";
}
}
$pending = $state->suiteTotalTests - $state->testSuiteTestsCount();
if ($pending) {
$tests[] = "\e[2m$pending pending\e[22m";
}
if (!empty($tests)) {
$this->footer->overwrite(array_merge($runs, [
'',
sprintf(
' <fg=white;options=bold>Tests: </><fg=default>%s</>',
implode(', ', $tests)
),
]));
}
}
/**
* Writes the final recap.
*/
public function writeRecap(Timer $timer): void
{
$timeElapsed = number_format($timer->result(), 2, '.', '');
$this->footer->writeln(
sprintf(
' <fg=white;options=bold>Time: </><fg=default>%ss</>',
$timeElapsed
)
);
}
/**
* Displays the error using Collision's writer
* and terminates with exit code === 1.
*
* @return void
*/
public function writeError(State $state, Throwable $throwable)
{
$this->writeCurrentRecap($state);
$this->updateFooter($state);
$writer = (new Writer())->setOutput($this->output);
if ($throwable instanceof AssertionFailedError) {
$writer->showTitle(false);
$this->output->write('', true);
}
$writer->ignoreFilesIn([
'/vendor\/phpunit\/phpunit\/src/',
'/vendor\/mockery\/mockery/',
'/vendor\/laravel\/framework\/src\/Illuminate\/Testing/',
'/vendor\/laravel\/framework\/src\/Illuminate\/Foundation\/Testing/',
]);
if ($throwable instanceof ExceptionWrapper && $throwable->getOriginalException() !== null) {
$throwable = $throwable->getOriginalException();
}
$inspector = new Inspector($throwable);
$writer->write($inspector);
if ($throwable instanceof ExpectationFailedException && $comparisionFailure = $throwable->getComparisonFailure()) {
$this->output->write($comparisionFailure->getDiff());
}
exit(1);
}
/**
* Returns the title contents.
*/
private function titleLineFrom(string $fg, string $bg, string $title, string $testCaseName): string
{
if (class_exists($testCaseName)) {
$nameParts = explode('\\', $testCaseName);
$highlightedPart = array_pop($nameParts);
$nonHighlightedPart = implode('\\', $nameParts);
$testCaseName = sprintf("\e[2m%s\e[22m<fg=white;options=bold>%s</>", "$nonHighlightedPart\\", $highlightedPart);
} elseif (file_exists($testCaseName)) {
$testCaseName = substr($testCaseName, strlen((string) getcwd()) + 1);
$nameParts = explode(DIRECTORY_SEPARATOR, $testCaseName);
$highlightedPart = (string) array_pop($nameParts);
$highlightedPart = substr($highlightedPart, 0, (int) strrpos($highlightedPart, '.'));
$nonHighlightedPart = implode('\\', $nameParts);
$testCaseName = sprintf("\e[2m%s\e[22m<fg=white;options=bold>%s</>", "$nonHighlightedPart\\", $highlightedPart);
}
return sprintf(
"\n <fg=%s;bg=%s;options=bold> %s </><fg=default> %s</>",
$fg,
$bg,
$title,
$testCaseName
);
}
/**
* Returns the test contents.
*/
private function testLineFrom(string $fg, string $icon, string $description, string $warning = null): string
{
if (!empty($warning)) {
$warning = sprintf(
' → %s',
$warning
);
}
return sprintf(
" <fg=%s;options=bold>%s</><fg=default> \e[2m%s\e[22m</><fg=yellow>%s</>",
$fg,
$icon,
$description,
$warning
);
}
}

View File

@@ -0,0 +1,156 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Adapters\Phpunit;
use PHPUnit\Framework\TestCase;
/**
* @internal
*/
final class TestResult
{
public const FAIL = 'failed';
public const SKIPPED = 'skipped';
public const INCOMPLETE = 'incompleted';
public const RISKY = 'risked';
public const WARN = 'warnings';
public const RUNS = 'pending';
public const PASS = 'passed';
/**
* @readonly
*
* @var string
*/
public $description;
/**
* @readonly
*
* @var string
*/
public $type;
/**
* @readonly
*
* @var string
*/
public $icon;
/**
* @readonly
*
* @var string
*/
public $color;
/**
* @readonly
*
* @var string|null
*/
public $warning;
/**
* Test constructor.
*
* @param string $warning
*/
private function __construct(string $description, string $type, string $icon, string $color, string $warning = null)
{
$this->description = $description;
$this->type = $type;
$this->icon = $icon;
$this->color = $color;
$this->warning = trim((string) preg_replace("/\r|\n/", ' ', (string) $warning));
}
/**
* Creates a new test from the given test case.
*/
public static function fromTestCase(TestCase $testCase, string $type, string $warning = null): self
{
$description = self::makeDescription($testCase);
$icon = self::makeIcon($type);
$color = self::makeColor($type);
return new self($description, $type, $icon, $color, $warning);
}
/**
* Get the test case description.
*/
public static function makeDescription(TestCase $testCase): string
{
$name = $testCase->getName(true);
// First, lets replace underscore by spaces.
$name = str_replace('_', ' ', $name);
// Then, replace upper cases by spaces.
$name = (string) preg_replace('/([A-Z])/', ' $1', $name);
// Finally, if it starts with `test`, we remove it.
$name = (string) preg_replace('/^test/', '', $name);
// Removes spaces
$name = (string) trim($name);
// Finally, lower case everything
return (string) mb_strtolower($name);
}
/**
* Get the test case icon.
*/
public static function makeIcon(string $type): string
{
switch ($type) {
case self::FAIL:
return '✕';
case self::SKIPPED:
return 's';
case self::RISKY:
return 'r';
case self::INCOMPLETE:
return 'i';
case self::WARN:
return 'w';
case self::RUNS:
return '•';
default:
return '✓';
}
}
/**
* Get the test case color.
*/
public static function makeColor(string $type): string
{
switch ($type) {
case self::FAIL:
return 'red';
case self::SKIPPED:
case self::INCOMPLETE:
case self::RISKY:
case self::WARN:
case self::RUNS:
return 'yellow';
default:
return 'green';
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Adapters\Phpunit;
/**
* @internal
*/
final class Timer
{
/**
* @var float
*/
private $start;
/**
* Timer constructor.
*/
private function __construct(float $start)
{
$this->start = $start;
}
/**
* Starts the timer.
*/
public static function start(): Timer
{
return new self(microtime(true));
}
/**
* Returns the elapsed time in microseconds.
*/
public function result(): float
{
return microtime(true) - $this->start;
}
}

View File

@@ -30,16 +30,16 @@ class ArgumentFormatter implements ArgumentFormatterContract
foreach ($arguments as $argument) {
switch (true) {
case is_string($argument):
$result[] = '"'.$argument.'"';
$result[] = '"' . $argument . '"';
break;
case is_array($argument):
$associative = array_keys($argument) !== range(0, count($argument) - 1);
if ($recursive && $associative && count($argument) <= 5) {
$result[] = '['.$this->format($argument, false).']';
$result[] = '[' . $this->format($argument, false) . ']';
}
break;
case is_object($argument):
$class = get_class($argument);
$class = get_class($argument);
$result[] = "Object($class)";
break;
}

View File

@@ -0,0 +1,310 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision;
use NunoMaduro\Collision\Exceptions\ShouldNotHappen;
/**
* This is an Collision Console Color implementation.
*
* Code originally from { JakubOnderka\\PhpConsoleColor }. But the package got deprecated.
*
* @internal
*
* @final
*/
class ConsoleColor
{
const FOREGROUND = 38;
const BACKGROUND = 48;
const COLOR256_REGEXP = '~^(bg_)?color_([0-9]{1,3})$~';
const RESET_STYLE = 0;
/** @var bool */
private $isSupported;
/** @var bool */
private $forceStyle = false;
/** @var array */
private $styles = [
'none' => null,
'bold' => '1',
'dark' => '2',
'italic' => '3',
'underline' => '4',
'blink' => '5',
'reverse' => '7',
'concealed' => '8',
'default' => '39',
'black' => '30',
'red' => '31',
'green' => '32',
'yellow' => '33',
'blue' => '34',
'magenta' => '35',
'cyan' => '36',
'light_gray' => '37',
'dark_gray' => '90',
'light_red' => '91',
'light_green' => '92',
'light_yellow' => '93',
'light_blue' => '94',
'light_magenta' => '95',
'light_cyan' => '96',
'white' => '97',
'bg_default' => '49',
'bg_black' => '40',
'bg_red' => '41',
'bg_green' => '42',
'bg_yellow' => '43',
'bg_blue' => '44',
'bg_magenta' => '45',
'bg_cyan' => '46',
'bg_light_gray' => '47',
'bg_dark_gray' => '100',
'bg_light_red' => '101',
'bg_light_green' => '102',
'bg_light_yellow' => '103',
'bg_light_blue' => '104',
'bg_light_magenta' => '105',
'bg_light_cyan' => '106',
'bg_white' => '107',
];
/** @var array */
private $themes = [];
public function __construct()
{
$this->isSupported = $this->isSupported();
}
/**
* @param string|array $style
* @param string $text
*
* @return string
*
* @throws InvalidStyleException
* @throws \InvalidArgumentException
*/
public function apply($style, $text)
{
if (!$this->isStyleForced() && !$this->isSupported()) {
return $text;
}
if (is_string($style)) {
$style = [$style];
}
if (!is_array($style)) {
throw new \InvalidArgumentException('Style must be string or array.');
}
$sequences = [];
foreach ($style as $s) {
if (isset($this->themes[$s])) {
$sequences = array_merge($sequences, $this->themeSequence($s));
} elseif ($this->isValidStyle($s)) {
$sequences[] = $this->styleSequence($s);
} else {
throw new ShouldNotHappen();
}
}
$sequences = array_filter($sequences, function ($val) {
return $val !== null;
});
if (empty($sequences)) {
return $text;
}
return $this->escSequence(implode(';', $sequences)) . $text . $this->escSequence(self::RESET_STYLE);
}
/**
* @param bool $forceStyle
*/
public function setForceStyle($forceStyle)
{
$this->forceStyle = (bool) $forceStyle;
}
/**
* @return bool
*/
public function isStyleForced()
{
return $this->forceStyle;
}
public function setThemes(array $themes)
{
$this->themes = [];
foreach ($themes as $name => $styles) {
$this->addTheme($name, $styles);
}
}
/**
* @param string $name
* @param array|string $styles
*/
public function addTheme($name, $styles)
{
if (is_string($styles)) {
$styles = [$styles];
}
if (!is_array($styles)) {
throw new \InvalidArgumentException('Style must be string or array.');
}
foreach ($styles as $style) {
if (!$this->isValidStyle($style)) {
throw new InvalidStyleException($style);
}
}
$this->themes[$name] = $styles;
}
/**
* @return array
*/
public function getThemes()
{
return $this->themes;
}
/**
* @param string $name
*
* @return bool
*/
public function hasTheme($name)
{
return isset($this->themes[$name]);
}
/**
* @param string $name
*/
public function removeTheme($name)
{
unset($this->themes[$name]);
}
/**
* @return bool
*/
public function isSupported()
{
if (DIRECTORY_SEPARATOR === '\\') {
if (function_exists('sapi_windows_vt100_support') && @sapi_windows_vt100_support(STDOUT)) {
return true;
} elseif (getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON') {
return true;
}
return false;
} else {
return function_exists('posix_isatty') && @posix_isatty(STDOUT);
}
}
/**
* @return bool
*/
public function are256ColorsSupported()
{
if (DIRECTORY_SEPARATOR === '\\') {
return function_exists('sapi_windows_vt100_support') && @sapi_windows_vt100_support(STDOUT);
} else {
return strpos(getenv('TERM'), '256color') !== false;
}
}
/**
* @return array
*/
public function getPossibleStyles()
{
return array_keys($this->styles);
}
/**
* @param string $name
*
* @return string[]
*/
private function themeSequence($name)
{
$sequences = [];
foreach ($this->themes[$name] as $style) {
$sequences[] = $this->styleSequence($style);
}
return $sequences;
}
/**
* @param string $style
*
* @return string
*/
private function styleSequence($style)
{
if (array_key_exists($style, $this->styles)) {
return $this->styles[$style];
}
if (!$this->are256ColorsSupported()) {
return null;
}
preg_match(self::COLOR256_REGEXP, $style, $matches);
$type = $matches[1] === 'bg_' ? self::BACKGROUND : self::FOREGROUND;
$value = $matches[2];
return "$type;5;$value";
}
/**
* @param string $style
*
* @return bool
*/
private function isValidStyle($style)
{
return array_key_exists($style, $this->styles) || preg_match(self::COLOR256_REGEXP, $style);
}
/**
* @param string|int $value
*
* @return string
*/
private function escSequence($value)
{
return "\033[{$value}m";
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Contracts\Adapters\Phpunit;
/**
* @internal
*/
interface HasPrintableTestCaseName
{
/**
* Returns the test case name that should be used by the printer.
*/
public function getPrintableTestCaseName(): string;
}

View File

@@ -11,6 +11,7 @@
namespace NunoMaduro\Collision\Contracts\Adapters\Phpunit;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestListener;
/**
@@ -24,9 +25,7 @@ interface Listener extends TestListener
* Renders the provided error
* on the console.
*
* @param \Throwable $t
*
* @return void
*/
public function render(\Throwable $t);
public function render(Test $test, \Throwable $t);
}

View File

@@ -21,11 +21,6 @@ interface ArgumentFormatter
/**
* Formats the provided array of arguments into
* an understandable description.
*
* @param array $arguments
* @param bool $recursive
*
* @return string
*/
public function format(array $arguments, bool $recursive = true): string;
}

View File

@@ -11,8 +11,8 @@
namespace NunoMaduro\Collision\Contracts;
use Whoops\Handler\HandlerInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Whoops\Handler\HandlerInterface;
/**
* This is an Collision Handler contract.
@@ -24,8 +24,6 @@ interface Handler extends HandlerInterface
/**
* Sets the output.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return \NunoMaduro\Collision\Contracts\Handler
*/
public function setOutput(OutputInterface $output): Handler;

View File

@@ -20,11 +20,6 @@ interface Highlighter
{
/**
* Highlights the provided content.
*
* @param string $content
* @param int $line
*
* @return string
*/
public function highlight(string $content, int $line): string;
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Contracts;
use Facade\IgnitionContracts\Solution;
use Throwable;
/**
* This is an Collision Solutions Repository contract.
*
* @author Nuno Maduro <enunomaduro@gmail.com>
*/
interface SolutionsRepository
{
/**
* Gets the solutions from the given `$throwable`.
*
* @return array<int, Solution>
*/
public function getFromThrowable(Throwable $throwable): array;
}

View File

@@ -11,8 +11,8 @@
namespace NunoMaduro\Collision\Contracts;
use Whoops\Exception\Inspector;
use Symfony\Component\Console\Output\OutputInterface;
use Whoops\Exception\Inspector;
/**
* This is the Collision Writer contract.
@@ -25,7 +25,7 @@ interface Writer
* Ignores traces where the file string matches one
* of the provided regex expressions.
*
* @param string[] $ignore The regex expressions.
* @param string[] $ignore the regex expressions
*
* @return \NunoMaduro\Collision\Contracts\Writer
*/
@@ -34,16 +34,19 @@ interface Writer
/**
* Declares whether or not the Writer should show the trace.
*
* @param bool $show
*
* @return \NunoMaduro\Collision\Contracts\Writer
*/
public function showTrace(bool $show): Writer;
/**
* Declares whether or not the Writer should show the editor.
* Declares whether or not the Writer should show the title.
*
* @param bool $show
* @return \NunoMaduro\Collision\Contracts\Writer
*/
public function showTitle(bool $show): Writer;
/**
* Declares whether or not the Writer should show the editor.
*
* @return \NunoMaduro\Collision\Contracts\Writer
*/
@@ -51,24 +54,18 @@ interface Writer
/**
* Writes the details of the exception on the console.
*
* @param \Whoops\Exception\Inspector $inspector
*/
public function write(Inspector $inspector): void;
/**
* Sets the output.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return \NunoMaduro\Collision\Contracts\Writer
*/
public function setOutput(OutputInterface $output): Writer;
/**
* Gets the output.
*
* @return \Symfony\Component\Console\Output\OutputInterface
*/
public function getOutput(): OutputInterface;
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\Exceptions;
use RuntimeException;
/**
* @internal
*/
final class ShouldNotHappen extends RuntimeException
{
public function __construct()
{
$message = 'This should not happen, please open an issue on collision repository: %s';
parent::__construct(sprintf($message, 'https://github.com/nunomaduro/collision/issues/new'));
}
}

View File

@@ -11,10 +11,10 @@
namespace NunoMaduro\Collision;
use Whoops\Handler\Handler as AbstractHandler;
use Symfony\Component\Console\Output\OutputInterface;
use NunoMaduro\Collision\Contracts\Writer as WriterContract;
use NunoMaduro\Collision\Contracts\Handler as HandlerContract;
use NunoMaduro\Collision\Contracts\Writer as WriterContract;
use Symfony\Component\Console\Output\OutputInterface;
use Whoops\Handler\Handler as AbstractHandler;
/**
* This is an Collision Handler implementation.
@@ -32,12 +32,10 @@ class Handler extends AbstractHandler implements HandlerContract
/**
* Creates an instance of the Handler.
*
* @param \NunoMaduro\Collision\Contracts\Writer|null $writer
*/
public function __construct(WriterContract $writer = null)
{
$this->writer = $writer ?: new Writer;
$this->writer = $writer ?: new Writer();
}
/**

View File

@@ -11,43 +11,73 @@
namespace NunoMaduro\Collision;
use JakubOnderka\PhpConsoleColor\ConsoleColor;
use JakubOnderka\PhpConsoleHighlighter\Highlighter as BaseHighlighter;
use NunoMaduro\Collision\Contracts\Highlighter as HighlighterContract;
/**
* This is an Collision Highlighter implementation.
*
* @author Nuno Maduro <enunomaduro@gmail.com>
* Code originally from { JakubOnderka\\PhpConsoleColor }. But the package got deprecated.
*
* @internal
*
* @final
*/
class Highlighter extends BaseHighlighter implements HighlighterContract
class Highlighter implements HighlighterContract
{
/**
* Holds the theme.
*
* @var array
*/
protected $theme = [
BaseHighlighter::TOKEN_STRING => ['light_gray'],
BaseHighlighter::TOKEN_COMMENT => ['dark_gray', 'italic'],
BaseHighlighter::TOKEN_KEYWORD => ['yellow'],
BaseHighlighter::TOKEN_DEFAULT => ['default', 'bold'],
BaseHighlighter::TOKEN_HTML => ['blue', 'bold'],
BaseHighlighter::ACTUAL_LINE_MARK => ['bg_red', 'bold'],
BaseHighlighter::LINE_NUMBER => ['dark_gray', 'italic'],
private $theme = [
self::TOKEN_STRING => ['light_gray'],
self::TOKEN_COMMENT => ['dark_gray', 'italic'],
self::TOKEN_KEYWORD => ['magenta', 'bold'],
self::TOKEN_DEFAULT => ['default', 'bold'],
self::TOKEN_HTML => ['blue', 'bold'],
self::ACTUAL_LINE_MARK => ['red', 'bold'],
self::LINE_NUMBER => ['dark_gray'],
];
const TOKEN_DEFAULT = 'token_default';
const TOKEN_COMMENT = 'token_comment';
const TOKEN_STRING = 'token_string';
const TOKEN_HTML = 'token_html';
const TOKEN_KEYWORD = 'token_keyword';
const ACTUAL_LINE_MARK = 'actual_line_mark';
const LINE_NUMBER = 'line_number';
/** @var ConsoleColor */
private $color;
/** @var array */
private $defaultTheme = [
self::TOKEN_STRING => 'red',
self::TOKEN_COMMENT => 'yellow',
self::TOKEN_KEYWORD => 'green',
self::TOKEN_DEFAULT => 'default',
self::TOKEN_HTML => 'cyan',
self::ACTUAL_LINE_MARK => 'red',
self::LINE_NUMBER => 'dark_gray',
];
/**
* Creates an instance of the Highlighter.
*
* @param \JakubOnderka\PhpConsoleColor\ConsoleColor|null $color
*/
public function __construct(ConsoleColor $color = null)
{
parent::__construct($color = $color ?: new ConsoleColor);
$this->color = $color ?: new ConsoleColor();
foreach ($this->defaultTheme as $name => $styles) {
if (!$this->color->hasTheme($name)) {
$this->color->addTheme($name, $styles);
}
}
foreach ($this->theme as $name => $styles) {
$color->addTheme((string) $name, $styles);
$this->color->addTheme((string) $name, $styles);
}
}
@@ -58,4 +88,192 @@ class Highlighter extends BaseHighlighter implements HighlighterContract
{
return rtrim($this->getCodeSnippet($content, $line, 4, 4));
}
/**
* @param string $source
* @param int $lineNumber
* @param int $linesBefore
* @param int $linesAfter
*
* @return string
*/
public function getCodeSnippet($source, $lineNumber, $linesBefore = 2, $linesAfter = 2)
{
$tokenLines = $this->getHighlightedLines($source);
$offset = $lineNumber - $linesBefore - 1;
$offset = max($offset, 0);
$length = $linesAfter + $linesBefore + 1;
$tokenLines = array_slice($tokenLines, $offset, $length, $preserveKeys = true);
$lines = $this->colorLines($tokenLines);
return $this->lineNumbers($lines, $lineNumber);
}
/**
* @param string $source
*
* @return array
*/
private function getHighlightedLines($source)
{
$source = str_replace(["\r\n", "\r"], "\n", $source);
$tokens = $this->tokenize($source);
return $this->splitToLines($tokens);
}
/**
* @param string $source
*
* @return array
*/
private function tokenize($source)
{
$tokens = token_get_all($source);
$output = [];
$currentType = null;
$buffer = '';
foreach ($tokens as $token) {
if (is_array($token)) {
switch ($token[0]) {
case T_WHITESPACE:
break;
case T_OPEN_TAG:
case T_OPEN_TAG_WITH_ECHO:
case T_CLOSE_TAG:
case T_STRING:
case T_VARIABLE:
// Constants
case T_DIR:
case T_FILE:
case T_METHOD_C:
case T_DNUMBER:
case T_LNUMBER:
case T_NS_C:
case T_LINE:
case T_CLASS_C:
case T_FUNC_C:
case T_TRAIT_C:
$newType = self::TOKEN_DEFAULT;
break;
case T_COMMENT:
case T_DOC_COMMENT:
$newType = self::TOKEN_COMMENT;
break;
case T_ENCAPSED_AND_WHITESPACE:
case T_CONSTANT_ENCAPSED_STRING:
$newType = self::TOKEN_STRING;
break;
case T_INLINE_HTML:
$newType = self::TOKEN_HTML;
break;
default:
$newType = self::TOKEN_KEYWORD;
}
} else {
$newType = $token === '"' ? self::TOKEN_STRING : self::TOKEN_KEYWORD;
}
if ($currentType === null) {
$currentType = $newType;
}
if ($currentType !== $newType) {
$output[] = [$currentType, $buffer];
$buffer = '';
$currentType = $newType;
}
$buffer .= is_array($token) ? $token[1] : $token;
}
if (isset($newType)) {
$output[] = [$newType, $buffer];
}
return $output;
}
/**
* @return array
*/
private function splitToLines(array $tokens)
{
$lines = [];
$line = [];
foreach ($tokens as $token) {
foreach (explode("\n", $token[1]) as $count => $tokenLine) {
if ($count > 0) {
$lines[] = $line;
$line = [];
}
if ($tokenLine === '') {
continue;
}
$line[] = [$token[0], $tokenLine];
}
}
$lines[] = $line;
return $lines;
}
/**
* @return array
*/
private function colorLines(array $tokenLines)
{
$lines = [];
foreach ($tokenLines as $lineCount => $tokenLine) {
$line = '';
foreach ($tokenLine as $token) {
[$tokenType, $tokenValue] = $token;
if ($this->color->hasTheme($tokenType)) {
$line .= $this->color->apply($tokenType, $tokenValue);
} else {
$line .= $tokenValue;
}
}
$lines[$lineCount] = $line;
}
return $lines;
}
/**
* @param int|null $markLine
*
* @return string
*/
private function lineNumbers(array $lines, $markLine = null)
{
end($lines);
$lineStrlen = strlen(key($lines) + 1);
$snippet = '';
foreach ($lines as $i => $line) {
if ($markLine !== null) {
$snippet .= ($markLine === $i + 1 ? $this->color->apply(self::ACTUAL_LINE_MARK, ' > ') : ' ');
}
$snippet .= $this->color->apply(self::LINE_NUMBER, str_pad($i + 1, $lineStrlen, ' ', STR_PAD_LEFT) . '| ');
$snippet .= $line . PHP_EOL;
}
return $snippet;
}
}

View File

@@ -11,10 +11,10 @@
namespace NunoMaduro\Collision;
use Whoops\Run;
use Whoops\RunInterface;
use NunoMaduro\Collision\Contracts\Handler as HandlerContract;
use NunoMaduro\Collision\Contracts\Provider as ProviderContract;
use Whoops\Run;
use Whoops\RunInterface;
/**
* This is an Collision Provider implementation.
@@ -39,14 +39,11 @@ class Provider implements ProviderContract
/**
* Creates a new instance of the Provider.
*
* @param \Whoops\RunInterface|null $run
* @param \NunoMaduro\Collision\Contracts\Handler|null $handler
*/
public function __construct(RunInterface $run = null, HandlerContract $handler = null)
{
$this->run = $run ?: new Run;
$this->handler = $handler ?: new Handler;
$this->run = $run ?: new Run();
$this->handler = $handler ?: new Handler();
}
/**

View File

@@ -0,0 +1,31 @@
<?php
/**
* This file is part of Collision.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NunoMaduro\Collision\SolutionsRepositories;
use NunoMaduro\Collision\Contracts\SolutionsRepository;
use Throwable;
/**
* This is an Collision Null Solutions Provider implementation.
*
* @author Nuno Maduro <enunomaduro@gmail.com>
*/
class NullSolutionsRepository implements SolutionsRepository
{
/**
* {@inheritdoc}
*/
public function getFromThrowable(Throwable $throwable): array
{
return [];
}
}

View File

@@ -11,13 +11,15 @@
namespace NunoMaduro\Collision;
use Whoops\Exception\Frame;
use Whoops\Exception\Inspector;
use NunoMaduro\Collision\Contracts\ArgumentFormatter as ArgumentFormatterContract;
use NunoMaduro\Collision\Contracts\Highlighter as HighlighterContract;
use NunoMaduro\Collision\Contracts\SolutionsRepository;
use NunoMaduro\Collision\Contracts\Writer as WriterContract;
use NunoMaduro\Collision\SolutionsRepositories\NullSolutionsRepository;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use NunoMaduro\Collision\Contracts\Writer as WriterContract;
use NunoMaduro\Collision\Contracts\Highlighter as HighlighterContract;
use NunoMaduro\Collision\Contracts\ArgumentFormatter as ArgumentFormatterContract;
use Whoops\Exception\Frame;
use Whoops\Exception\Inspector;
/**
* This is an Collision Writer implementation.
@@ -31,6 +33,13 @@ class Writer implements WriterContract
*/
const VERBOSITY_NORMAL_FRAMES = 1;
/**
* Holds an instance of the solutions repository.
*
* @var \NunoMaduro\Collision\Contracts\SolutionsRepository
*/
private $solutionsRepository;
/**
* Holds an instance of the Output.
*
@@ -67,6 +76,13 @@ class Writer implements WriterContract
*/
protected $showTrace = true;
/**
* Declares whether or not the title should appear.
*
* @var bool
*/
protected $showTitle = true;
/**
* Declares whether or not the editor should appear.
*
@@ -76,19 +92,17 @@ class Writer implements WriterContract
/**
* Creates an instance of the writer.
*
* @param \Symfony\Component\Console\Output\OutputInterface|null $output
* @param \NunoMaduro\Collision\Contracts\ArgumentFormatter|null $argumentFormatter
* @param \NunoMaduro\Collision\Contracts\Highlighter|null $highlighter
*/
public function __construct(
SolutionsRepository $solutionsRepository = null,
OutputInterface $output = null,
ArgumentFormatterContract $argumentFormatter = null,
HighlighterContract $highlighter = null
) {
$this->output = $output ?: new ConsoleOutput;
$this->argumentFormatter = $argumentFormatter ?: new ArgumentFormatter;
$this->highlighter = $highlighter ?: new Highlighter;
$this->solutionsRepository = $solutionsRepository ?: new NullSolutionsRepository();
$this->output = $output ?: new ConsoleOutput();
$this->argumentFormatter = $argumentFormatter ?: new ArgumentFormatter();
$this->highlighter = $highlighter ?: new Highlighter();
}
/**
@@ -96,16 +110,19 @@ class Writer implements WriterContract
*/
public function write(Inspector $inspector): void
{
$this->renderTitle($inspector);
$this->renderTitleAndDescription($inspector);
$frames = $this->getFrames($inspector);
$editorFrame = array_shift($frames);
if ($this->showEditor && $editorFrame !== null) {
$this->renderEditor($editorFrame);
}
if ($this->showTrace && ! empty($frames)) {
$this->renderSolution($inspector);
if ($this->showTrace && !empty($frames)) {
$this->renderTrace($frames);
} else {
$this->output->writeln('');
@@ -132,6 +149,16 @@ class Writer implements WriterContract
return $this;
}
/**
* {@inheritdoc}
*/
public function showTitle(bool $show): WriterContract
{
$this->showTitle = $show;
return $this;
}
/**
* {@inheritdoc}
*/
@@ -162,16 +189,18 @@ class Writer implements WriterContract
/**
* Returns pertinent frames.
*
* @param \Whoops\Exception\Inspector $inspector
*
* @return array
*/
protected function getFrames(Inspector $inspector): array
{
return $inspector->getFrames()
->filter(
function ($frame) {
// If we are in verbose mode, we always
// display the full stack trace.
if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
return true;
}
foreach ($this->ignore as $ignore) {
if (preg_match($ignore, $frame->getFile())) {
return false;
@@ -186,18 +215,48 @@ class Writer implements WriterContract
/**
* Renders the title of the exception.
*
* @param \Whoops\Exception\Inspector $inspector
*
* @return \NunoMaduro\Collision\Contracts\Writer
*/
protected function renderTitle(Inspector $inspector): WriterContract
protected function renderTitleAndDescription(Inspector $inspector): WriterContract
{
$exception = $inspector->getException();
$message = $exception->getMessage();
$class = $inspector->getExceptionName();
$message = rtrim($exception->getMessage());
$class = $inspector->getExceptionName();
$this->render("<bg=red;options=bold> $class </> : <comment>$message</>");
if ($this->showTitle) {
$this->render("<bg=red;options=bold> $class </>");
$this->output->writeln('');
}
$this->output->writeln("<fg=default;options=bold> $message</>");
return $this;
}
/**
* Renders the solution of the exception, if any.
*/
protected function renderSolution(Inspector $inspector): WriterContract
{
$throwable = $inspector->getException();
$solutions = $this->solutionsRepository->getFromThrowable($throwable);
foreach ($solutions as $solution) {
/** @var \Facade\IgnitionContracts\Solution $solution */
$title = $solution->getSolutionTitle();
$description = $solution->getSolutionDescription();
$links = $solution->getDocumentationLinks();
$description = trim((string) preg_replace("/\n/", "\n ", $description));
$this->render(sprintf(
'<fg=blue;options=bold>• </><fg=default;options=bold>%s</>: %s %s',
rtrim($title, '.'),
$description,
implode(', ', array_map(function (string $link) {
return sprintf("\n <fg=blue>%s</>", $link);
}, $links))
));
}
return $this;
}
@@ -205,14 +264,14 @@ class Writer implements WriterContract
/**
* Renders the editor containing the code that was the
* origin of the exception.
*
* @param \Whoops\Exception\Frame $frame
*
* @return \NunoMaduro\Collision\Contracts\Writer
*/
protected function renderEditor(Frame $frame): WriterContract
{
$this->render('at <fg=green>'.$frame->getFile().'</>'.':<fg=green>'.$frame->getLine().'</>');
$file = $this->getFileRelativePath((string) $frame->getFile());
// getLine() might return null so cast to int to get 0 instead
$line = (int) $frame->getLine();
$this->render('at <fg=green>' . $file . '</>' . ':<fg=green>' . $line . '</>');
$content = $this->highlighter->highlight((string) $frame->getFileContents(), (int) $frame->getLine());
@@ -223,41 +282,56 @@ class Writer implements WriterContract
/**
* Renders the trace of the exception.
*
* @param array $frames
*
* @return \NunoMaduro\Collision\Contracts\Writer
*/
protected function renderTrace(array $frames): WriterContract
{
$this->render('<comment>Exception trace:</comment>');
$vendorFrames = 0;
$userFrames = 0;
foreach ($frames as $i => $frame) {
if ($i > static::VERBOSITY_NORMAL_FRAMES && $this->output->getVerbosity(
) < OutputInterface::VERBOSITY_VERBOSE) {
$this->render('<info>Please use the argument <fg=red>-v</> to see more details.</info>');
if ($this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE && strpos($frame->getFile(), '/vendor/') !== false) {
$vendorFrames++;
continue;
}
if ($userFrames > static::VERBOSITY_NORMAL_FRAMES && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
break;
}
$file = $frame->getFile();
$line = $frame->getLine();
$class = empty($frame->getClass()) ? '' : $frame->getClass().'::';
$function = $frame->getFunction();
$args = $this->argumentFormatter->format($frame->getArgs());
$pos = str_pad((int) $i + 1, 4, ' ');
$userFrames++;
$this->render("<comment><fg=cyan>$pos</>$class$function($args)</comment>");
$this->render(" <fg=green>$file</>:<fg=green>$line</>", false);
$file = $this->getFileRelativePath($frame->getFile());
$line = $frame->getLine();
$class = empty($frame->getClass()) ? '' : $frame->getClass() . '::';
$function = $frame->getFunction();
$args = $this->argumentFormatter->format($frame->getArgs());
$pos = str_pad((string) ((int) $i + 1), 4, ' ');
if ($vendorFrames > 0) {
$this->output->write(
sprintf("\n \e[2m+%s vendor frames \e[22m", $vendorFrames)
);
$vendorFrames = 0;
}
$this->render("<fg=yellow>$pos</><fg=default;options=bold>$file</>:<fg=default;options=bold>$line</>");
$this->render("<fg=white> $class$function($args)</>", false);
}
/* Let's consider add this later...
* if ($vendorFrames > 0) {
* $this->output->write(
* sprintf("\n \e[2m+%s vendor frames \e[22m\n", $vendorFrames)
* );
* $vendorFrames = 0;
* }.
*/
return $this;
}
/**
* Renders an message into the console.
*
* @param string $message
* @param bool $break
*
* @return $this
*/
protected function render(string $message, bool $break = true): WriterContract
@@ -270,4 +344,18 @@ class Writer implements WriterContract
return $this;
}
/**
* Returns the relative path of the given file path.
*/
protected function getFileRelativePath(string $filePath): string
{
$cwd = (string) getcwd();
if (!empty($cwd)) {
return str_replace("$cwd/", '', $filePath);
}
return $filePath;
}
}