PHP Classes

File: src/Arguments.php

Recommend this page to a friend!
  Classes of Rodolfo Berrios Arce   Parameter   src/Arguments.php   Download  
File: src/Arguments.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Parameter
Validate function parameters with PHP attributes
Author: By
Last change:
Date: 25 days ago
Size: 10,174 bytes
 

Contents

Class file image Download
<?php

/*
 * This file is part of Chevere.
 *
 * (c) Rodolfo Berrios <[email protected]>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace
Chevere\Parameter;

use
ArgumentCountError;
use
ArrayAccess;
use
Chevere\Parameter\Interfaces\ArgumentsInterface;
use
Chevere\Parameter\Interfaces\CastInterface;
use
Chevere\Parameter\Interfaces\ParametersInterface;
use
Chevere\Parameter\Traits\ExceptionErrorMessageTrait;
use
InvalidArgumentException;
use
OutOfBoundsException;
use
ReflectionClass;
use
Throwable;
use
TypeError;
use function
Chevere\Message\message;

final class
Arguments implements ArgumentsInterface
{
    use
ExceptionErrorMessageTrait;

    private
ParametersInterface $iterable;

   
/**
     * @var array<int|string, mixed>
     */
   
private array $arguments = [];

   
/**
     * @var array<string>
     */
   
private array $null = [];

   
/**
     * @var array<string>
     */
   
private array $reflected = [];

   
/**
     * @var string[]
     */
   
private array $errors = [];

   
// @phpstan-ignore-next-line
   
public function __construct(
        private
ParametersInterface $parameters,
        array|
ArrayAccess $arguments
   
) {
        if (
$arguments instanceof ArrayAccess) {
           
$arguments = $this->getArrayAccessArray($arguments);
        }
       
$isIterable = $parameters->keys() === ['K', 'V'];
       
$countArguments = count($arguments);
        if (
array_is_list($arguments) && ! $isIterable) {
           
$parametersMap = array_slice($this->parameters->keys(), 0, $countArguments);
           
$arguments = array_combine($parametersMap, $arguments);
        }
       
$this->setArguments($arguments);
        if (
$isIterable) {
           
$pairs = [];
            foreach (
array_keys($arguments) as $key) {
               
$key = strval($key);
               
$pairs[$key] = $parameters->get('V');
            }
           
$this->iterable = new Parameters(...$pairs);
        }
        if (!
$this->parameters->isVariadic()) {
           
$this->ignoreExtraArguments();
        }
       
$this->handleDefaults();
       
$this->assertRequired();
       
$this->assertMinimumOptional();
       
$this->handleParameters();
        if (
$this->errors !== []) {
            throw new
InvalidArgumentException(
               
implode("\n", $this->errors)
            );
        }
    }

    public function
parameters(): ParametersInterface
   
{
        return
$this->iterable ?? $this->parameters;
    }

   
// @phpstan-ignore-next-line
   
public function toArray(): array
    {
        return
$this->arguments;
    }

   
// @phpstan-ignore-next-line
   
public function toArrayFill(mixed $fill): array
    {
       
$filler = array_fill_keys($this->null, $fill);

        return
array_merge($filler, $this->arguments);
    }

   
/**
     * @throws OutOfBoundsException
     * @throws TypeError
     * @throws InvalidArgumentException
     */
   
public function withPut(string $name, mixed $value): ArgumentsInterface
   
{
       
$new = clone $this;
       
$new->assertSetArgument($name, $value);
       
$new->arguments[$name] = $value;

        return
$new;
    }

    public function
has(string ...$name): bool
   
{
        foreach (
$name as $key) {
            if (!
array_key_exists($key, $this->arguments)) {
                return
false;
            }
        }

        return
true;
    }

    public function
get(string $name): mixed
   
{
       
$this->parameters()->assertHas($name);

        return
$this->arguments[$name] ?? null;
    }

    public function
required(string $name): CastInterface
   
{
        if (
$this->parameters()->optionalKeys()->contains($name)) {
            throw new
InvalidArgumentException(
                (string)
message(
                   
'Argument `%name%` is optional',
                   
name: $name
               
)
            );
        }

        return new
Cast($this->arguments[$name]);
    }

    public function
optional(string $name): ?CastInterface
   
{
        if (!
$this->parameters()->optionalKeys()->contains($name)) {
            throw new
InvalidArgumentException(
                (string)
message(
                   
'Argument `%name%` is required',
                   
name: $name
               
)
            );
        }

        if (
$this->has($name)) {
            return new
Cast($this->arguments[$name]);
        }

        return
null;
    }

    private function
ignoreExtraArguments(): void
   
{
       
$this->arguments = array_intersect_key(
           
$this->arguments,
           
array_flip($this->parameters()->keys())
        );
    }

    private function
handleDefaults(): void
   
{
        foreach (
$this->parameters() as $name => $parameter) {
            if (
$this->has($name)) {
                continue;
            }
            if (
$parameter->default() === null) {
               
$this->null[] = $name;

                continue;
            }
           
$this->arguments[$name] = $parameter->default();
        }
    }

    private function
assertRequired(): void
   
{
       
$values = array_keys($this->arguments);
       
$missing = array_diff(
           
$this->parameters()->requiredKeys()->toArray(),
           
$values,
        );
        if (
$missing !== []) {
            throw new
ArgumentCountError(
                (string)
message(
                   
'Missing required argument(s): `%missing%`',
                   
missing: implode(', ', $missing)
                )
            );
        }
    }

    private function
assertMinimumOptional(): void
   
{
       
$optional = $this->parameters()->optionalKeys()->toArray();
       
$providedOptionals = array_intersect(
           
$optional,
           
array_keys($this->arguments)
        );
       
$countProvided = count($providedOptionals);
        if (
$countProvided < $this->parameters()->optionalMinimum()) {
            throw new
ArgumentCountError(
                (string)
message(
                   
'Requires minimum **%minimum%** optional argument(s), **%provided%** provided',
                   
minimum: strval($this->parameters()->optionalMinimum()),
                   
provided: strval($countProvided)
                )
            );
        }
    }

   
/**
     * @infection-ignore-all
     */
   
private function assertSetArgument(string $name, mixed $argument, ?string $key = null): void
   
{
       
$parameter = $this->parameters()->get($name);
       
$property = $name;
        if (
$key !== null) {
           
$property = $key . '...' . $name;
        }
        if (
           
version_compare(PHP_VERSION, '8.2.0', '>=')
            &&
class_exists('SensitiveParameterValue')
            &&
$argument instanceof \SensitiveParameterValue
       
) {
           
$argument = $argument->getValue();
        }

        try {
           
$this->arguments[$key ?? $name] = $parameter->__invoke($argument);
        } catch (
TypeError $e) {
            throw new
$e(
               
$this->getExceptionPropertyMessage($property, $e)
            );
        } catch (
Throwable $e) {
            throw new
InvalidArgumentException(
               
$this->getExceptionPropertyMessage($property, $e)
            );
        }
    }

    private function
getExceptionPropertyMessage(string $name, Throwable $e): string
   
{
       
$message = $this->getExceptionMessage($e);

        return
"[{$name}]: {$message}";
    }

    private function
handleParameters(): void
   
{
       
$lastPos = array_key_last($this->parameters()->keys());
        foreach (
$this->parameters()->keys() as $pos => $name) {
            if (
$pos === $lastPos && $this->parameters->isVariadic()) {
               
$variadicKeys = array_diff_key(
                   
$this->arguments,
                   
array_flip($this->parameters->keys())
                );
                foreach (
$variadicKeys as $key => $value) {
                   
$key = strval($key);

                    try {
                       
$this->assertSetArgument($name, $value, $key);
                    } catch (
Throwable $e) {
                       
$this->errors[] = $e->getMessage();
                    }
                }

                break;
            }
            if (
$this->isSkipOptional($name)) {
                continue;
            }

            try {
               
$this->assertSetArgument($name, $this->get($name));
            } catch (
Throwable $e) {
               
$this->errors[] = $e->getMessage();
            }
        }
    }

    private function
isSkipOptional(string $name): bool
   
{
        return
$this->parameters()->optionalKeys()->contains($name)
            && !
$this->has($name);
    }

   
/**
     * @param array<int|string, mixed> $arguments
     */
   
private function setArguments(array $arguments): void
   
{
       
$keys = array_keys($arguments);
        foreach (
$keys as $name) {
           
$name = strval($name);
           
$this->arguments[$name] = $arguments[$name];
        }
    }

   
/**
     * @param ArrayAccess<int|string, mixed> $arguments
     * @return array<int|string, mixed>
     */
   
private function getArrayAccessArray(ArrayAccess $arguments): array
    {
       
$array = [];
       
$cast = (array) $arguments;
       
$reflector = new ReflectionClass($arguments);
       
$properties = $reflector->getProperties();
        foreach (
$properties as $property) {
           
$name = $property->getName();
           
$array[$name] = $property->getValue($arguments);
           
$this->reflected[] = $name;
        }
       
$this->fixScopeArrayCast($array, $cast);

        return
$array;
    }

   
/**
     * @param array<int|string, mixed> $array
     * @param array<int|string, mixed> $cast
     */
   
private function fixScopeArrayCast(array &$array, array $cast): void
   
{
        foreach (
$cast as $key => $value) {
           
$key = strval($key);
            if (
str_contains($key, "\x00")
                ||
in_array($key, $this->reflected, true)) {
                continue;
            }
           
$array[$key] = $value;
        }
    }
}