Skip to content

Instantly share code, notes, and snippets.

@Phil-Venter
Last active February 28, 2025 14:01
Show Gist options
  • Select an option

  • Save Phil-Venter/24502e50abb9a93a5f057ced35c8e40a to your computer and use it in GitHub Desktop.

Select an option

Save Phil-Venter/24502e50abb9a93a5f057ced35c8e40a to your computer and use it in GitHub Desktop.
safe try
<?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