mirror of
https://github.com/pestphp/pest.git
synced 2026-03-07 00:07:22 +01:00
first
This commit is contained in:
35
src/Support/Backtrace.php
Normal file
35
src/Support/Backtrace.php
Normal 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'];
|
||||
}
|
||||
}
|
||||
24
src/Support/ChainableClosure.php
Normal file
24
src/Support/ChainableClosure.php
Normal 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());
|
||||
};
|
||||
}
|
||||
}
|
||||
35
src/Support/ExceptionTrace.php
Normal file
35
src/Support/ExceptionTrace.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/Support/HigherOrderMessage.php
Normal file
60
src/Support/HigherOrderMessage.php
Normal 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;
|
||||
}
|
||||
}
|
||||
74
src/Support/HigherOrderMessageCollection.php
Normal file
74
src/Support/HigherOrderMessageCollection.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Support/NullClosure.php
Normal file
22
src/Support/NullClosure.php
Normal 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
112
src/Support/Reflection.php
Normal 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
32
src/Support/Str.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user