This commit is contained in:
Nuno Maduro
2020-05-11 18:38:30 +02:00
commit de2929077b
112 changed files with 6211 additions and 0 deletions

35
src/Support/Backtrace.php Normal file
View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
/**
* @internal
*/
final class Backtrace
{
/**
* Returns the filename that called the current function/method.
*/
public static function file(): string
{
return debug_backtrace()[1]['file'];
}
/**
* Returns the dirname that called the current function/method.
*/
public static function dirname(): string
{
return dirname(debug_backtrace()[1]['file']);
}
/**
* Returns the line that called the current function/method.
*/
public static function line(): int
{
return debug_backtrace()[1]['line'];
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
use Closure;
/**
* @internal
*/
final class ChainableClosure
{
/**
* Calls the given `$closure` and chains the the `$next` closure.
*/
public static function from(Closure $closure, Closure $next): Closure
{
return function () use ($closure, $next): void {
call_user_func_array(Closure::bind($closure, $this, get_class($this)), func_get_args());
call_user_func_array(Closure::bind($next, $this, get_class($this)), func_get_args());
};
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
use Closure;
use Throwable;
/**
* @internal
*/
final class ExceptionTrace
{
private const UNDEFINED_METHOD = 'Call to undefined method P\\';
/**
* Ensures the given closure reports
* the good execution context.
*/
public static function ensure(Closure $closure): void
{
try {
$closure();
} catch (Throwable $throwable) {
if (Str::startsWith($message = $throwable->getMessage(), self::UNDEFINED_METHOD)) {
$message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message);
Reflection::setPropertyValue($throwable, 'message', $message);
}
throw $throwable;
}
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
/**
* @internal
*/
final class HigherOrderMessage
{
/**
* The filename where the function was originally called.
*
* @readonly
*
* @var string
*/
public $filename;
/**
* The line where the function was originally called.
*
* @readonly
*
* @var int
*/
public $line;
/**
* The method name.
*
* @readonly
*
* @var string
*/
public $methodName;
/**
* The arguments.
*
* @var array<int, mixed>
*
* @readonly
*/
public $arguments;
/**
* Creates a new higher order message.
*
* @param array<int, mixed> $arguments
*/
public function __construct(string $filename, int $line, string $methodName, array $arguments)
{
$this->filename = $filename;
$this->line = $line;
$this->methodName = $methodName;
$this->arguments = $arguments;
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
use ReflectionClass;
use Throwable;
/**
* @internal
*/
final class HigherOrderMessageCollection
{
public const UNDEFINED_METHOD = 'Method %s does not exist';
/**
* @var array<int, HigherOrderMessage>
*/
private $messages = [];
/**
* Adds a new higher order message to the collection.
*
* @param array<int, mixed> $arguments
*/
public function add(string $filename, int $line, string $methodName, array $arguments): void
{
$this->messages[] = new HigherOrderMessage($filename, $line, $methodName, $arguments);
}
/**
* Proxy all the messages starting from the target.
*/
public function chain(object $target): void
{
foreach ($this->messages as $message) {
$target = $this->attempt($target, $message);
}
}
/**
* Proxy all the messages to the target.
*/
public function proxy(object $target): void
{
foreach ($this->messages as $message) {
$this->attempt($target, $message);
}
}
/**
* Re-throws the given `$throwable` with the good line and filename.
*
* @return mixed
*/
private function attempt(object $target, HigherOrderMessage $message)
{
try {
return Reflection::call($target, $message->methodName, $message->arguments);
} catch (Throwable $throwable) {
Reflection::setPropertyValue($throwable, 'file', $message->filename);
Reflection::setPropertyValue($throwable, 'line', $message->line);
if ($throwable->getMessage() === sprintf(self::UNDEFINED_METHOD, $message->methodName)) {
/** @var \ReflectionClass $reflection */
$reflection = (new ReflectionClass($target))->getParentClass();
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $message->methodName));
}
throw $throwable;
}
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
use Closure;
/**
* @internal
*/
final class NullClosure
{
/**
* Creates a nullable closure.
*/
public static function create(): Closure
{
return Closure::fromCallable(function (): void {
});
}
}

112
src/Support/Reflection.php Normal file
View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
use Closure;
use Pest\Exceptions\ShouldNotHappen;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionProperty;
/**
* @internal
*/
final class Reflection
{
/**
* Calls the given method with args on the given object.
*
* @param array<int, mixed> $args
*
* @return mixed
*/
public static function call(object $object, string $method, array $args = [])
{
$reflectionClass = new ReflectionClass($object);
$reflectionMethod = $reflectionClass->getMethod($method);
$reflectionMethod->setAccessible(true);
return $reflectionMethod->invoke($object, ...$args);
}
/**
* Infers the file name from the given closure.
*/
public static function getFileNameFromClosure(Closure $closure): string
{
$reflectionClosure = new ReflectionFunction($closure);
return (string) $reflectionClosure->getFileName();
}
/**
* Gets the property value from of the given object.
*
* @return mixed
*/
public static function getPropertyValue(object $object, string $property)
{
$reflectionClass = new ReflectionClass($object);
$reflectionProperty = null;
while ($reflectionProperty === null) {
try {
/* @var ReflectionProperty $reflectionProperty */
$reflectionProperty = $reflectionClass->getProperty($property);
} catch (ReflectionException $reflectionException) {
$reflectionClass = $reflectionClass->getParentClass();
if (!$reflectionClass instanceof ReflectionClass) {
throw new ShouldNotHappen($reflectionException);
}
}
}
if ($reflectionProperty === null) {
throw ShouldNotHappen::fromMessage('Reflection property not found.');
}
$reflectionProperty->setAccessible(true);
return $reflectionProperty->getValue($object);
}
/**
* Sets the property value of the given object.
*
* @param mixed $value
*/
public static function setPropertyValue(object $object, string $property, $value): void
{
/** @var ReflectionClass $reflectionClass */
$reflectionClass = new ReflectionClass($object);
$reflectionProperty = null;
while ($reflectionProperty === null) {
try {
/* @var ReflectionProperty $reflectionProperty */
$reflectionProperty = $reflectionClass->getProperty($property);
} catch (ReflectionException $reflectionException) {
$reflectionClass = $reflectionClass->getParentClass();
if (!$reflectionClass instanceof ReflectionClass) {
throw new ShouldNotHappen($reflectionException);
}
}
}
if ($reflectionProperty === null) {
throw ShouldNotHappen::fromMessage('Reflection property not found.');
}
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($object, $value);
}
}

32
src/Support/Str.php Normal file
View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
/**
* @internal
*/
final class Str
{
/**
* Checks if the given `$target` starts with the given `$search`.
*/
public static function startsWith(string $target, string $search): bool
{
return substr($target, 0, strlen($search)) === $search;
}
/**
* Checks if the given `$target` ends with the given `$search`.
*/
public static function endsWith(string $target, string $search): bool
{
$length = strlen($search);
if ($length === 0) {
return true;
}
return substr($target, -$length) === $search;
}
}