Last active
February 28, 2025 14:01
-
-
Save Phil-Venter/24502e50abb9a93a5f057ced35c8e40a to your computer and use it in GitHub Desktop.
safe try
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Class Result | |
| * | |
| * This class encapsulates the result of a callable operation, handling both success (value) and failure (error). | |
| * It implements the ArrayAccess interface for array-like access to its properties ('val' and 'err'). | |
| */ | |
| class Result implements ArrayAccess | |
| { | |
| /** | |
| * Constructor for Result class | |
| * | |
| * @param mixed $val The value returned from the callable if successful. | |
| * @param Throwable|null $err The error encountered during the callable execution, if any. | |
| */ | |
| public function __construct( | |
| public mixed $val, | |
| public ?Throwable $err, | |
| ) {} | |
| /** | |
| * Determines whether an offset exists in the Result object. | |
| * | |
| * @param mixed $offset The offset to check. | |
| * @return bool True if the offset exists, false otherwise. | |
| */ | |
| public function offsetExists(mixed $offset): bool | |
| { | |
| return in_array($offset, [0, 1, 'val', 'err'], true); | |
| } | |
| /** | |
| * Retrieves the value at the given offset in the Result object. | |
| * | |
| * @param mixed $offset The offset to retrieve. | |
| * @return mixed The value at the specified offset. | |
| * | |
| * @throws OutOfBoundsException If the offset is invalid. | |
| */ | |
| public function offsetGet(mixed $offset): mixed | |
| { | |
| return match($offset) { | |
| 0, 'val' => $this->val, | |
| 1, 'err' => $this->err, | |
| default => throw new OutOfBoundsException("Invalid offset: $offset"), | |
| }; | |
| } | |
| /** | |
| * Sets a value at the given offset in the Result object. | |
| * | |
| * @param mixed $offset The offset to set. | |
| * @param mixed $value The value to set at the offset. | |
| * | |
| * @throws RuntimeException Always throws an exception as the Result object is read-only. | |
| */ | |
| public function offsetSet(mixed $offset, mixed $value): void | |
| { | |
| throw new RuntimeException("Result object is read-only"); | |
| } | |
| /** | |
| * Unsets the value at the given offset in the Result object. | |
| * | |
| * @param mixed $offset The offset to unset. | |
| * | |
| * @throws RuntimeException Always throws an exception as the Result object is read-only. | |
| */ | |
| public function offsetUnset(mixed $offset): void | |
| { | |
| throw new RuntimeException("Result object is read-only"); | |
| } | |
| /** | |
| * Wraps a callable into a closure that will return a Result object when called. | |
| * | |
| * @param array|object|string $callable The callable to wrap. | |
| * | |
| * @return Closure A closure that wraps the callable and returns a Result object. | |
| */ | |
| public static function wrap(array|object|string $callable): Closure | |
| { | |
| return function (mixed ...$params) use ($callable): static { | |
| return static::call($callable, $params); | |
| }; | |
| } | |
| /** | |
| * Calls the provided callable and returns a Result object containing the result or error. | |
| * | |
| * @param array|object|string $callable The callable to execute. | |
| * @param array $params The parameters to pass to the callable. | |
| * | |
| * @return static A Result object containing the value or error from the callable execution. | |
| */ | |
| public static function call(array|object|string $callable, array $params = []): static | |
| { | |
| try { | |
| if (is_object($callable)) { | |
| assert(is_callable($callable), new InvalidArgumentException("The provided object is not callable.")); | |
| if ($callable instanceof Closure) { | |
| $resolved = static::resolveFunction(new ReflectionFunction($callable), $params); | |
| } else { | |
| $resolved = static::resolveFunction(new ReflectionMethod($callable, '__invoke'), $params, $callable); | |
| } | |
| return new static($resolved, null); | |
| } | |
| if (is_array($callable)) { | |
| assert(count($callable) === 2, new InvalidArgumentException("Array callable must have exactly two elements: [class/object, method].")); | |
| assert(is_object($callable[0]) || is_string($callable[0]), new InvalidArgumentException("First element of array callable must be a class name or an object.")); | |
| assert(is_string($callable[1]), new InvalidArgumentException("Second element of array callable must be a method name string.")); | |
| assert(method_exists($callable[0], $callable[1]), new BadMethodCallException("Method '{$callable[1]}' does not exist on the provided target.")); | |
| $resolved = static::resolveFunction(new ReflectionMethod($callable[0], $callable[1]), $params, $callable[0]); | |
| return new static($resolved, null); | |
| } | |
| assert(is_string($callable), new InvalidArgumentException("Invalid callable type provided.")); | |
| if (function_exists($callable)) { | |
| $resolved = static::resolveFunction(new ReflectionFunction($callable), $params); | |
| return new static($resolved, null); | |
| } | |
| assert(str_contains($callable, '::') || str_contains($callable, '@'), new InvalidArgumentException("String callable must be a function name or a 'Class::method' or 'Class@method' format.")); | |
| if (str_contains($callable, '::')) { | |
| [$target, $method] = explode('::', $callable, 2); | |
| } else { | |
| [$target, $method] = explode('@', $callable, 2); | |
| } | |
| assert(!empty($target) && !empty($method), new InvalidArgumentException("Both target and method must be non-empty strings.")); | |
| assert(class_exists($target), new InvalidArgumentException("Class '$target' does not exist.")); | |
| assert(method_exists($target, $method), new BadMethodCallException("Method '$method' does not exist on the provided target.")); | |
| $resolved = static::resolveFunction(new ReflectionMethod($target, $method), $params, $target); | |
| return new static($resolved, null); | |
| } catch (Throwable $e) { | |
| return new static(null, $e); | |
| } | |
| } | |
| /** | |
| * Resolves and instantiates a class using reflection. | |
| * | |
| * @param ReflectionClass $ref The ReflectionClass object for the target class. | |
| * @param array $params The parameters to pass to the class constructor. | |
| * | |
| * @return object The instantiated class object. | |
| * | |
| * @throws RuntimeException If the class cannot be instantiated. | |
| */ | |
| protected static function resolveClass(ReflectionClass $ref, array $params): object | |
| { | |
| assert(!$ref->isAbstract(), new RuntimeException("Cannot instantiate abstract class.")); | |
| assert(!$ref->isInterface(), new RuntimeException("Cannot instantiate interface.")); | |
| assert(!$ref->isTrait(), new RuntimeException("Cannot instantiate trait.")); | |
| assert($ref->isInstantiable(), new RuntimeException("Cannot instantiate class.")); | |
| $constructor = $ref->getConstructor(); | |
| if (!$constructor) { | |
| return $ref->newInstance(); | |
| } | |
| $classParams = is_array($params[$ref->getName()] ?? null) ? $params[$ref->getName()] : $params; | |
| $args = static::mapArgs($constructor->getParameters() ?? [], $classParams); | |
| assert(count($args) === count($constructor->getParameters()), new RuntimeException("Insufficient arguments for constructor.")); | |
| return $ref->newInstanceArgs($args); | |
| } | |
| /** | |
| * Resolves a function or method using reflection and invokes it with the provided parameters. | |
| * | |
| * @param ReflectionFunctionAbstract $ref The reflection of the function or method to invoke. | |
| * @param array $params The parameters to pass to the function or method. | |
| * @param object|string|null $class The class or object to invoke the method on, if applicable. | |
| * | |
| * @return mixed The result of the function or method invocation. | |
| * | |
| * @throws RuntimeException If there are insufficient arguments or if the type does not match. | |
| * @throws Throwable If the callable execution fails. | |
| */ | |
| protected static function resolveFunction(ReflectionFunctionAbstract $ref, array $params, object|string|null $class = null): mixed | |
| { | |
| $args = static::mapArgs($ref->getParameters(), $params); | |
| assert(count($args) === count($ref->getParameters()), new RuntimeException("Insufficient arguments for function.")); | |
| if ($ref instanceof ReflectionFunction) { | |
| return $ref->invokeArgs($args); | |
| } | |
| assert($ref instanceof ReflectionMethod, new RuntimeException("Expected instance of ReflectionMethod, but received an invalid type.")); | |
| if ($ref->isStatic()) { | |
| return $ref->invokeArgs(null, $args); | |
| } | |
| if ($class === null) { | |
| $class = static::resolveClass($ref->getDeclaringClass(), $params); | |
| } elseif (is_string($class)) { | |
| $class = static::resolveClass(new ReflectionClass($class), $params); | |
| } | |
| assert(is_object($class), new RuntimeException("Failed to resolve class.")); | |
| return $ref->invokeArgs($class, $args); | |
| } | |
| /** | |
| * Maps the provided parameters to the function or method parameters using reflection. | |
| * | |
| * @param array $parameters The reflection parameters of the function or method. | |
| * @param array $params The values to map to the function parameters. | |
| * | |
| * @return array The mapped arguments. | |
| * | |
| * @throws RuntimeException If the parameters cannot be resolved. | |
| */ | |
| protected static function mapArgs(array $parameters, array $params): array | |
| { | |
| if (empty($parameters)) { | |
| return []; | |
| } | |
| $args = []; | |
| $positional = []; | |
| $named = []; | |
| foreach ($params as $key => $value) { | |
| if (is_int($key)) { | |
| $positional[$key] = $value; | |
| } else { | |
| $named[$key] = $value; | |
| } | |
| } | |
| foreach ($parameters as $index => $param) { | |
| assert($param instanceof ReflectionParameter, new RuntimeException("Expected instance of ReflectionParameter, but received an invalid type.")); | |
| $name = $param->getName(); | |
| if (isset($named[$name]) && static::isType($param->getType(), $named[$name])) { | |
| $args[] = $named[$name]; | |
| } elseif (isset($positional[$index]) && static::isType($param->getType(), $positional[$index])) { | |
| $args[] = $positional[$index]; | |
| } elseif ($param->isDefaultValueAvailable()) { | |
| $args[] = $param->getDefaultValue(); | |
| } elseif ($param->isOptional()) { | |
| $args[] = null; | |
| } else { | |
| throw new RuntimeException("Could not resolve parameter '$name'."); | |
| } | |
| } | |
| assert(count($args) === count($parameters), new RuntimeException("Insufficient arguments resolved.")); | |
| return $args; | |
| } | |
| /** | |
| * Determines if the provided value matches the expected type. | |
| * | |
| * @param ReflectionType|null $ref The expected type. | |
| * @param mixed $value The value to check. | |
| * | |
| * @return bool True if the value matches the type, false otherwise. | |
| */ | |
| protected static function isType(?ReflectionType $ref, mixed $value): bool | |
| { | |
| if (is_null($ref)) { | |
| return true; | |
| } | |
| if ($ref->allowsNull() && is_null($value)) { | |
| return true; | |
| } | |
| if ($ref instanceof ReflectionUnionType) { | |
| foreach ($ref->getTypes() as $subType) { | |
| if (static::isType($subType, $value)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| if ($ref instanceof ReflectionIntersectionType) { | |
| foreach ($ref->getTypes() as $subType) { | |
| if (!static::isType($subType, $value)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| if (!$ref instanceof ReflectionNamedType) { | |
| return false; | |
| } | |
| $typeName = $ref->getName(); | |
| if ($ref->isBuiltin()) { | |
| return match ($typeName) { | |
| 'int' => is_int($value), | |
| 'float' => is_float($value), | |
| 'bool' => is_bool($value), | |
| 'string' => is_string($value), | |
| 'array' => is_array($value), | |
| 'object' => is_object($value), | |
| 'callable' => is_callable($value), | |
| 'iterable' => is_iterable($value), | |
| 'resource' => is_resource($value), | |
| 'mixed' => true, | |
| 'void' => false, | |
| default => false, | |
| }; | |
| } | |
| if (is_object($value)) { | |
| return $value instanceof $typeName; | |
| } | |
| return false; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment