Skip to content

Commit

Permalink
Merge pull request #264 from musa11971/solve-properties
Browse files Browse the repository at this point in the history
Added "Undefined Property" solution
  • Loading branch information
freekmurze committed Jul 13, 2020
2 parents 7bde629 + dfec02b commit 0f96846
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/IgnitionServiceProvider.php
Expand Up @@ -41,6 +41,7 @@
use Facade\Ignition\SolutionProviders\RunningLaravelDuskInProductionProvider;
use Facade\Ignition\SolutionProviders\SolutionProviderRepository;
use Facade\Ignition\SolutionProviders\TableNotFoundSolutionProvider;
use Facade\Ignition\SolutionProviders\UndefinedPropertySolutionProvider;
use Facade\Ignition\SolutionProviders\UndefinedVariableSolutionProvider;
use Facade\Ignition\SolutionProviders\UnknownValidationSolutionProvider;
use Facade\Ignition\SolutionProviders\ViewNotFoundSolutionProvider;
Expand Down Expand Up @@ -364,6 +365,7 @@ protected function getDefaultSolutions(): array
RunningLaravelDuskInProductionProvider::class,
MissingColumnSolutionProvider::class,
UnknownValidationSolutionProvider::class,
UndefinedPropertySolutionProvider::class,
];
}

Expand Down
98 changes: 98 additions & 0 deletions src/SolutionProviders/UndefinedPropertySolutionProvider.php
@@ -0,0 +1,98 @@
<?php

namespace Facade\Ignition\SolutionProviders;

use ErrorException;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Support\Collection;
use ReflectionClass;
use ReflectionProperty;
use Throwable;

class UndefinedPropertySolutionProvider implements HasSolutionsForThrowable
{
protected const REGEX = '/([a-zA-Z\\\\]+)::\$([a-zA-Z]+)/m';
protected const MINIMUM_SIMILARITY = 80;

public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof ErrorException) {
return false;
}

if (is_null($this->getClassAndPropertyFromExceptionMessage($throwable->getMessage()))) {
return false;
}

if (! $this->similarPropertyExists($throwable)) {
return false;
}

return true;
}

public function getSolutions(Throwable $throwable): array
{
return [
BaseSolution::create('Unknown Property')
->setSolutionDescription($this->getSolutionDescription($throwable)),
];
}

public function getSolutionDescription(Throwable $throwable): string
{
if (! $this->canSolve($throwable) || ! $this->similarPropertyExists($throwable)) {
return '';
}

extract($this->getClassAndPropertyFromExceptionMessage($throwable->getMessage()), EXTR_OVERWRITE);

$possibleProperty = $this->findPossibleProperty($class, $property);

return "Did you mean {$class}::\${$possibleProperty->name} ?";
}

protected function similarPropertyExists(Throwable $throwable)
{
extract($this->getClassAndPropertyFromExceptionMessage($throwable->getMessage()), EXTR_OVERWRITE);

$possibleProperty = $this->findPossibleProperty($class, $property);

return $possibleProperty !== null;
}

protected function getClassAndPropertyFromExceptionMessage(string $message): ?array
{
if (! preg_match(self::REGEX, $message, $matches)) {
return null;
}

return [
'class' => $matches[1],
'property' => $matches[2],
];
}

protected function findPossibleProperty(string $class, string $invalidPropertyName)
{
return $this->getAvailableProperties($class)
->sortByDesc(function (ReflectionProperty $property) use ($invalidPropertyName) {
similar_text($invalidPropertyName, $property->name, $percentage);

return $percentage;
})
->filter(function (ReflectionProperty $property) use ($invalidPropertyName) {
similar_text($invalidPropertyName, $property->name, $percentage);

return $percentage >= self::MINIMUM_SIMILARITY;
})->first();
}

protected function getAvailableProperties($class): Collection
{
$class = new ReflectionClass($class);

return Collection::make($class->getProperties());
}
}
46 changes: 46 additions & 0 deletions tests/Solutions/UndefinedPropertySolutionProviderTest.php
@@ -0,0 +1,46 @@
<?php

use Facade\Ignition\SolutionProviders\UndefinedPropertySolutionProvider;
use Facade\Ignition\Tests\TestCase;

class UndefinedPropertySolutionProviderTest extends TestCase
{
/** @test */
public function it_can_solve_an_undefined_property_exception_when_there_is_a_similar_property()
{
$canSolve = app(UndefinedPropertySolutionProvider::class)->canSolve($this->getUndefinedPropertyException());

$this->assertTrue($canSolve);
}

/** @test */
public function it_cannot_solve_an_undefined_property_exception_when_there_is_no_similar_property()
{
$canSolve = app(UndefinedPropertySolutionProvider::class)->canSolve($this->getUndefinedPropertyException('balance'));

$this->assertFalse($canSolve);
}

/** @test */
public function it_can_recommend_a_property_name_when_there_is_a_similar_property()
{
/** @var \Facade\IgnitionContracts\Solution $solution */
$solution = app(UndefinedPropertySolutionProvider::class)->getSolutions($this->getUndefinedPropertyException())[0];

$this->assertEquals('Did you mean Facade\Ignition\Tests\Support\Models\Car::$color ?', $solution->getSolutionDescription());
}

/** @test */
public function it_cannot_recommend_a_property_name_when_there_is_no_similar_property()
{
/** @var \Facade\IgnitionContracts\Solution $solution */
$solution = app(UndefinedPropertySolutionProvider::class)->getSolutions($this->getUndefinedPropertyException('balance'))[0];

$this->assertEquals('', $solution->getSolutionDescription());
}

protected function getUndefinedPropertyException(string $property = 'colro'): ErrorException
{
return new ErrorException("Undefined property: Facade\Ignition\Tests\Support\Models\Car::$$property ");
}
}
15 changes: 15 additions & 0 deletions tests/Support/Models/Car.php
@@ -0,0 +1,15 @@
<?php

namespace Facade\Ignition\Tests\Support\Models;

class Car
{
public $brand;
public $color;

public function __construct($brand, $color)
{
$this->brand = $brand;
$this->color = $color;
}
}

0 comments on commit 0f96846

Please sign in to comment.