diff --git a/CHANGELOG.md b/CHANGELOG.md index a13f187..dcb2cd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.2.0 (2022-09-08) +* Reorganised to contain main Lucid interfaces +* Updated key handling in MultiContextProvider +* Added Sanitizer loader interfaces + ## v0.1.1 (2022-09-08) * Removed force methods diff --git a/README.md b/README.md index 0797529..e487179 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,12 @@ For example: ```php namespace My\Library; -use DecodeLabs\Lucid\Sanitizer\SingleContextProvider; -use DecodeLabs\Lucid\Sanitizer\SingleContextProviderTrait; +use DecodeLabs\Lucid\Provider\SingleContext; +use DecodeLabs\Lucid\Provider\SingleContextTrait; -class MyClass implements SingleContextProvider { +class MyClass implements SingleContext { - use SingleContextProviderTrait; + use SingleContextTrait; protected mixed $value; diff --git a/composer.json b/composer.json index f96518d..5118113 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "require": { "php": "^8.0", + "decodelabs/coercion": "^0.2.2", "decodelabs/exceptional": "^0.4" }, "require-dev": { @@ -20,8 +21,7 @@ "php-parallel-lint/php-parallel-lint": "^1.3", "symplify/easy-coding-standard": "^11", - "decodelabs/phpstan-decodelabs": "^0.6", - "decodelabs/lucid": "^0.1|^0.2" + "decodelabs/phpstan-decodelabs": "^0.6" }, "suggest": {}, "autoload": { @@ -31,7 +31,7 @@ }, "extra": { "branch-alias": { - "dev-develop": "0.1.x-dev" + "dev-develop": "0.2.x-dev" } }, "config": { diff --git a/src/Constraint.php b/src/Constraint.php new file mode 100644 index 0000000..5d7e085 --- /dev/null +++ b/src/Constraint.php @@ -0,0 +1,59 @@ + $processor + */ + public function __construct(Processor $processor); + + /** + * @return array|null + */ + public static function getProcessorOutputTypes(): ?array; + + public function getName(): string; + public function getWeight(): int; + + /** + * @phpstan-return Processor + */ + public function getProcessor(): Processor; + + /** + * @phpstan-param TParam $param + * @return $this + */ + public function setParameter(mixed $param): static; + public function getParameter(): mixed; + + public function prepareValue(mixed $value): mixed; + + /** + * @phpstan-param TValue $value + * @phpstan-return TValue|null + */ + public function alterValue(mixed $value): mixed; + + /** + * @phpstan-param TValue|null $value + * @phpstan-return Generator + */ + public function validate(mixed $value): Generator; +} diff --git a/src/Constraint/Processor.php b/src/Constraint/Processor.php new file mode 100644 index 0000000..619e1a2 --- /dev/null +++ b/src/Constraint/Processor.php @@ -0,0 +1,40 @@ + + */ +class Processor implements Constraint +{ + /** + * @phpstan-use ConstraintTrait + */ + use ConstraintTrait; + + public const OUTPUT_TYPES = []; + + public function getName(): string + { + return $this->processor->getName(); + } + + public function getWeight(): int + { + return 1; + } +} diff --git a/src/ConstraintTrait.php b/src/ConstraintTrait.php new file mode 100644 index 0000000..252daac --- /dev/null +++ b/src/ConstraintTrait.php @@ -0,0 +1,96 @@ + + */ + protected Processor $processor; + + /** + * @phpstan-param Processor $processor + */ + public function __construct(Processor $processor) + { + $this->processor = $processor; + } + + public static function getProcessorOutputTypes(): ?array + { + if ( + defined('static::OUTPUT_TYPES') && + /** @phpstan-ignore-next-line */ + is_array(static::OUTPUT_TYPES) + ) { + /** @phpstan-ignore-next-line */ + return static::OUTPUT_TYPES; + } + + return null; + } + + public function getName(): string + { + return (new ReflectionClass($this)) + ->getShortName(); + } + + public function getWeight(): int + { + return 10; + } + + public function getProcessor(): Processor + { + return $this->processor; + } + + /** + * @phpstan-param TParam $param + * @return $this + */ + public function setParameter(mixed $param): static + { + return $this; + } + + public function getParameter(): mixed + { + return null; + } + + public function prepareValue(mixed $value): mixed + { + return $value; + } + + public function alterValue(mixed $value): mixed + { + return $value; + } + + /** + * @phpstan-param TValue $value + */ + public function validate(mixed $value): Generator + { + yield null; + return true; + } +} diff --git a/src/Processor.php b/src/Processor.php new file mode 100644 index 0000000..0963f39 --- /dev/null +++ b/src/Processor.php @@ -0,0 +1,94 @@ + + */ + public function getOutputTypes(): array; + + public function getName(): string; + public function getSanitizer(): Sanitizer; + + /** + * Does this processor require a list of values? + */ + public function isMultiValue(): bool; + + /** + * Prepare value before coercion + */ + public function prepareValue(mixed $value): mixed; + + /** + * Apply value processing before validation + * + * @phpstan-param TOutput $value + * @phpstan-return TOutput|null + */ + public function alterValue(mixed $value): mixed; + + /** + * Coerce input to output type or null + * + * @phpstan-return TOutput|null + */ + public function coerce(mixed $value): mixed; + + + /** + * Test validity of constraint + * + * @return $this + */ + public function test( + string $constraint, + mixed $param + ): static; + + + /** + * @return array + */ + public function getDefaultConstraints(): array; + + + /** + * @phpstan-return array> + */ + public function prepareConstraints(): array; + + + /** + * Test constraints and yield errors in sequence + * + * @phpstan-param TOutput|null $value + * @return Generator + */ + public function validate(mixed $value): Generator; + + /** + * Test type and yield errors in sequence + * + * @phpstan-param TOutput|null $value + * @return Generator + */ + public function validateType(mixed $value): Generator; +} diff --git a/src/Provider.php b/src/Provider.php new file mode 100644 index 0000000..3a62d79 --- /dev/null +++ b/src/Provider.php @@ -0,0 +1,14 @@ + - */ public function sanitize(mixed $value): Sanitizer; } diff --git a/src/Sanitizer/DirectContextProviderTrait.php b/src/Provider/DirectContextTrait.php similarity index 75% rename from src/Sanitizer/DirectContextProviderTrait.php rename to src/Provider/DirectContextTrait.php index 013cc7b..b98a38d 100644 --- a/src/Sanitizer/DirectContextProviderTrait.php +++ b/src/Provider/DirectContextTrait.php @@ -7,20 +7,19 @@ declare(strict_types=1); -namespace DecodeLabs\Lucid\Sanitizer; +namespace DecodeLabs\Lucid\Provider; use Closure; -use DecodeLabs\Exceptional; use DecodeLabs\Lucid\Constraint\NotFoundException as ConstraintNotFoundException; +use DecodeLabs\Lucid\ProviderTrait; use DecodeLabs\Lucid\Sanitizer; use DecodeLabs\Lucid\Validate\Result; use Exception; -/** - * @template TValue - */ -trait DirectContextProviderTrait +trait DirectContextTrait { + use ProviderTrait; + public function make( mixed $value, string $type, @@ -53,12 +52,6 @@ public function is( public function sanitize(mixed $value): Sanitizer { - if (!class_exists(Sanitizer::class)) { - throw Exceptional::ComponentUnavailable( - 'DecodeLabs/Lucid package is required for sanitisation' - ); - } - - return new Sanitizer($value); + return $this->newSanitizer($value); } } diff --git a/src/Provider/MixedContext.php b/src/Provider/MixedContext.php new file mode 100644 index 0000000..7ba02eb --- /dev/null +++ b/src/Provider/MixedContext.php @@ -0,0 +1,24 @@ + + */ +interface MixedContext extends SingleContext +{ + /** + * Get list of interal values + * + * @return array + */ + public function getChildValues(): array; +} diff --git a/src/Provider/MixedContextTrait.php b/src/Provider/MixedContextTrait.php new file mode 100644 index 0000000..1c45ae3 --- /dev/null +++ b/src/Provider/MixedContextTrait.php @@ -0,0 +1,35 @@ + + */ + use SingleContextTrait; + + public function sanitize(): Sanitizer + { + return $this->newSanitizer(function (Processor $processor): mixed { + if ($processor->isMultiValue()) { + return $this->getChildValues(); + } else { + return $this->getValue(); + } + }); + } +} diff --git a/src/Sanitizer/MultiContextProvider.php b/src/Provider/MultiContext.php similarity index 75% rename from src/Sanitizer/MultiContextProvider.php rename to src/Provider/MultiContext.php index f0b6123..c98a33a 100644 --- a/src/Sanitizer/MultiContextProvider.php +++ b/src/Provider/MultiContext.php @@ -7,22 +7,23 @@ declare(strict_types=1); -namespace DecodeLabs\Lucid\Sanitizer; +namespace DecodeLabs\Lucid\Provider; use Closure; +use DecodeLabs\Lucid\Provider; use DecodeLabs\Lucid\Sanitizer; use DecodeLabs\Lucid\Validate\Result; /** * @template TValue */ -interface MultiContextProvider +interface MultiContext extends Provider { /** * @param array|Closure|null $setup */ public function make( - string $key, + int|string $key, string $type, array|Closure|null $setup = null ): mixed; @@ -32,7 +33,7 @@ public function make( * @return Result */ public function validate( - string $key, + int|string $key, string $type, array|Closure|null $setup = null ): Result; @@ -41,13 +42,12 @@ public function validate( * @param array|Closure|null $setup */ public function is( - string $key, + int|string $key, string $type, array|Closure|null $setup = null ): bool; - /** - * @phpstan-return Sanitizer - */ - public function sanitize(string $key): Sanitizer; + public function sanitize( + int|string $key + ): Sanitizer; } diff --git a/src/Sanitizer/MultiContextProviderTrait.php b/src/Provider/MultiContextTrait.php similarity index 66% rename from src/Sanitizer/MultiContextProviderTrait.php rename to src/Provider/MultiContextTrait.php index 387d149..cddc65a 100644 --- a/src/Sanitizer/MultiContextProviderTrait.php +++ b/src/Provider/MultiContextTrait.php @@ -7,11 +7,11 @@ declare(strict_types=1); -namespace DecodeLabs\Lucid\Sanitizer; +namespace DecodeLabs\Lucid\Provider; use Closure; -use DecodeLabs\Exceptional; use DecodeLabs\Lucid\Constraint\NotFoundException as ConstraintNotFoundException; +use DecodeLabs\Lucid\ProviderTrait; use DecodeLabs\Lucid\Sanitizer; use DecodeLabs\Lucid\Validate\Result; use Exception; @@ -19,10 +19,12 @@ /** * @template TValue */ -trait MultiContextProviderTrait +trait MultiContextTrait { + use ProviderTrait; + public function make( - string $key, + int|string $key, string $type, array|Closure|null $setup = null ): mixed { @@ -30,7 +32,7 @@ public function make( } public function validate( - string $key, + int|string $key, string $type, array|Closure|null $setup = null ): Result { @@ -38,7 +40,7 @@ public function validate( } public function is( - string $key, + int|string $key, string $type, array|Closure|null $setup = null ): bool { @@ -51,19 +53,16 @@ public function is( } } - public function sanitize(string $key): Sanitizer - { - if (!class_exists(Sanitizer::class)) { - throw Exceptional::ComponentUnavailable( - 'DecodeLabs/Lucid package is required for sanitisation' - ); - } - - return new Sanitizer($this->getValue($key)); + public function sanitize( + int|string $key + ): Sanitizer { + return $this->newSanitizer($this->getValue($key)); } /** * @phpstan-return TValue|null */ - abstract protected function getValue(string $key): mixed; + abstract protected function getValue( + int|string $key + ): mixed; } diff --git a/src/Sanitizer/SingleContextProvider.php b/src/Provider/SingleContext.php similarity index 86% rename from src/Sanitizer/SingleContextProvider.php rename to src/Provider/SingleContext.php index b249254..a48fd8c 100644 --- a/src/Sanitizer/SingleContextProvider.php +++ b/src/Provider/SingleContext.php @@ -7,16 +7,17 @@ declare(strict_types=1); -namespace DecodeLabs\Lucid\Sanitizer; +namespace DecodeLabs\Lucid\Provider; use Closure; +use DecodeLabs\Lucid\Provider; use DecodeLabs\Lucid\Sanitizer; use DecodeLabs\Lucid\Validate\Result; /** * @template TValue */ -interface SingleContextProvider +interface SingleContext extends Provider { /** * @param array|Closure|null $setup @@ -44,8 +45,5 @@ public function is( ): bool; - /** - * @phpstan-return Sanitizer - */ public function sanitize(): Sanitizer; } diff --git a/src/Sanitizer/SingleContextProviderTrait.php b/src/Provider/SingleContextTrait.php similarity index 76% rename from src/Sanitizer/SingleContextProviderTrait.php rename to src/Provider/SingleContextTrait.php index ee7db72..472e37c 100644 --- a/src/Sanitizer/SingleContextProviderTrait.php +++ b/src/Provider/SingleContextTrait.php @@ -7,11 +7,11 @@ declare(strict_types=1); -namespace DecodeLabs\Lucid\Sanitizer; +namespace DecodeLabs\Lucid\Provider; use Closure; -use DecodeLabs\Exceptional; use DecodeLabs\Lucid\Constraint\NotFoundException as ConstraintNotFoundException; +use DecodeLabs\Lucid\ProviderTrait; use DecodeLabs\Lucid\Sanitizer; use DecodeLabs\Lucid\Validate\Result; use Exception; @@ -19,8 +19,10 @@ /** * @template TValue */ -trait SingleContextProviderTrait +trait SingleContextTrait { + use ProviderTrait; + public function as( string $type, array|Closure|null $setup = null @@ -50,13 +52,7 @@ public function is( public function sanitize(): Sanitizer { - if (!class_exists(Sanitizer::class)) { - throw Exceptional::ComponentUnavailable( - 'DecodeLabs/Lucid package is required for sanitisation' - ); - } - - return new Sanitizer($this->getValue()); + return $this->newSanitizer($this->getValue()); } /** diff --git a/src/ProviderTrait.php b/src/ProviderTrait.php new file mode 100644 index 0000000..0a4dbc6 --- /dev/null +++ b/src/ProviderTrait.php @@ -0,0 +1,27 @@ +|Closure|null $setup + */ + public function as( + string $type, + array|Closure|null $setup = null + ): mixed; + + + /** + * Validate value as type + * + * @param array|Closure|null $setup + * @return Result + */ + public function validate( + string $type, + array|Closure|null $setup = null + ): Result; + + + /** + * Load processor for value + * + * @param array|Closure|null $setup + * @phpstan-return Processor + */ + public function loadProcessor( + string $type, + array|Closure|null $setup = null + ): Processor; +} diff --git a/src/Validate/Error.php b/src/Validate/Error.php new file mode 100644 index 0000000..238f847 --- /dev/null +++ b/src/Validate/Error.php @@ -0,0 +1,137 @@ + + */ + protected array $params = []; + + /** + * @phpstan-var Constraint + */ + protected Constraint $constraint; + protected string $constraintKey; + + /** + * @phpstan-param Constraint|Processor $constraint + * @param array $params + */ + public function __construct( + Constraint|Processor $constraint, + mixed $value, + string $message, + array $params = [] + ) { + if ($constraint instanceof Processor) { + $constraint = new ProcessorConstraint($constraint); + } + + $this->constraint = $constraint; + $this->value = $value; + $this->message = $message; + $this->params = $params; + + $this->constraintKey = lcfirst( + $this->constraint->getName() + ); + } + + + public function getId(): string + { + return md5($this->constraintKey . ':' . $this->message); + } + + /** + * @phpstan-return Constraint + */ + public function getConstraint(): Constraint + { + return $this->constraint; + } + + public function setConstraintKey(string $key): void + { + $this->constraintKey = $key; + } + + public function getConstraintKey(): string + { + return $this->constraintKey; + } + + public function getProcessorName(): string + { + return $this->constraint->getProcessor()->getName(); + } + + public function getValue(): mixed + { + return $this->value; + } + + public function getMessageTemplate(): string + { + return $this->message; + } + + public function getMessage(): string + { + $output = $this->message; + $params = $this->params; + + // Type + if (false !== strstr($output, '%type%')) { + $type = $this->getProcessorName(); + + if (substr($type, -6) === 'Native') { + $type = substr($type, 0, -6); + } + + $params['type'] = $type; + } + + // Param + $key = $this->getConstraintKey(); + $params[$key] = $this->constraint->getParameter(); + + + // Replace + foreach ($params as $key => $param) { + $output = str_replace( + '%' . $key . '%', + Coercion::forceString($param), + $output + ); + } + + return $output; + } + + + /** + * @return array + */ + public function getParams(): array + { + return $this->params; + } +} diff --git a/src/Validate/Result.php b/src/Validate/Result.php new file mode 100644 index 0000000..2321a39 --- /dev/null +++ b/src/Validate/Result.php @@ -0,0 +1,132 @@ + + */ + protected Processor $processor; + + /** + * @var array + */ + protected array $errors = []; + + /** + * Init with processor + * + * @phpstan-param Processor $processor + */ + public function __construct(Processor $processor) + { + $this->processor = $processor; + } + + + /** + * Set value + * + * @phpstan-param TValue $value + * @return $this + */ + public function setValue(mixed $value): static + { + $this->value = $value; + return $this; + } + + + /** + * Get value + * + * @phpstan-return TValue|null + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * Get processor + * + * @phpstan-return Processor + */ + public function getProcessor(): Processor + { + return $this->processor; + } + + + /** + * Get type name + */ + public function getType(): string + { + return $this->processor->getName(); + } + + + /** + * Is valid + */ + public function isValid(): bool + { + return empty($this->errors); + } + + /** + * Add error + * + * @return $this; + */ + public function addError(Error $error): static + { + $this->errors[$error->getId()] = $error; + return $this; + } + + /** + * Get errors + * + * @return array + */ + public function getErrors(): array + { + return array_values($this->errors); + } + + /** + * Has errors + */ + public function hasErrors(): bool + { + return !empty($this->errors); + } + + /** + * Count errors + */ + public function countErrors(): int + { + return count($this->errors); + } +} diff --git a/tests/AnalyzeDirectContextProvider.php b/tests/AnalyzeDirectContextProvider.php new file mode 100644 index 0000000..f8d2769 --- /dev/null +++ b/tests/AnalyzeDirectContextProvider.php @@ -0,0 +1,29 @@ +sanitize(new \Exception('test')); diff --git a/tests/AnalyzeMixedContextProvider.php b/tests/AnalyzeMixedContextProvider.php new file mode 100644 index 0000000..9eb23ee --- /dev/null +++ b/tests/AnalyzeMixedContextProvider.php @@ -0,0 +1,62 @@ + + */ +class AnalyzeMixedContextProvider implements MixedContext +{ + /** + * @use MixedContextTrait + */ + use MixedContextTrait; + + /** + * @phpstan-var TValue + */ + protected mixed $value; + + public function __construct(mixed $value) + { + $this->value = $value; + } + + /** + * @phpstan-return TValue + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * @phpstan-return array + */ + public function getChildValues(): array + { + return [$this->value]; + } + + protected function newSanitizer(mixed $value): Sanitizer + { + return new SanitizerImplementation($value); + } +} + + +// Test passing an Exception through +$test = new AnalyzeSingleContextProvider(new \Exception('test')); +$test->sanitize(); diff --git a/tests/AnalyzeMultiContextProvider.php b/tests/AnalyzeMultiContextProvider.php index 0f7d5ca..05382ec 100644 --- a/tests/AnalyzeMultiContextProvider.php +++ b/tests/AnalyzeMultiContextProvider.php @@ -9,19 +9,20 @@ namespace DecodeLabs\Lucid\Tests; -use DecodeLabs\Lucid\Sanitizer\MultiContextProvider; -use DecodeLabs\Lucid\Sanitizer\MultiContextProviderTrait; +use DecodeLabs\Lucid\Provider\MultiContext; +use DecodeLabs\Lucid\Provider\MultiContextTrait; +use DecodeLabs\Lucid\Sanitizer; /** * @template TValue - * @implements MultiContextProvider + * @implements MultiContext */ -class AnalyzeMultiContextProvider implements MultiContextProvider +class AnalyzeMultiContextProvider implements MultiContext { /** - * @use MultiContextProviderTrait + * @use MultiContextTrait */ - use MultiContextProviderTrait; + use MultiContextTrait; /** * @phpstan-var array @@ -38,13 +39,19 @@ public function __construct( /** * @phpstan-return TValue|null */ - public function getValue(string $key): mixed - { + public function getValue( + int|string $key + ): mixed { return $this->values[$key] ?? null; } + + protected function newSanitizer(mixed $value): Sanitizer + { + return new SanitizerImplementation($value); + } } // Test passing an Exception through $test = new AnalyzeMultiContextProvider('value', new \Exception('test')); -$test->sanitize('value')->getValue()->getCode(); +$test->sanitize('value'); diff --git a/tests/AnalyzeSingleContextProvider.php b/tests/AnalyzeSingleContextProvider.php index 8060d45..88adcf1 100644 --- a/tests/AnalyzeSingleContextProvider.php +++ b/tests/AnalyzeSingleContextProvider.php @@ -9,19 +9,20 @@ namespace DecodeLabs\Lucid\Tests; -use DecodeLabs\Lucid\Sanitizer\SingleContextProvider; -use DecodeLabs\Lucid\Sanitizer\SingleContextProviderTrait; +use DecodeLabs\Lucid\Provider\SingleContext; +use DecodeLabs\Lucid\Provider\SingleContextTrait; +use DecodeLabs\Lucid\Sanitizer; /** * @template TValue - * @implements SingleContextProvider + * @implements SingleContext */ -class AnalyzeSingleContextProvider implements SingleContextProvider +class AnalyzeSingleContextProvider implements SingleContext { /** - * @use SingleContextProviderTrait + * @use SingleContextTrait */ - use SingleContextProviderTrait; + use SingleContextTrait; /** * @phpstan-var TValue @@ -40,9 +41,14 @@ public function getValue(): mixed { return $this->value; } + + protected function newSanitizer(mixed $value): Sanitizer + { + return new SanitizerImplementation($value); + } } // Test passing an Exception through $test = new AnalyzeSingleContextProvider(new \Exception('test')); -$test->sanitize()->getValue()->getCode(); +$test->sanitize(); diff --git a/tests/SanitizerImplementation.php b/tests/SanitizerImplementation.php new file mode 100644 index 0000000..a093689 --- /dev/null +++ b/tests/SanitizerImplementation.php @@ -0,0 +1,54 @@ +value = $value; + } + + public function as( + string $type, + array|Closure|null $setup = null + ): mixed { + throw Exceptional::NotImplemented('This is not implemented'); + } + + public function validate( + string $type, + array|Closure|null $setup = null + ): Result { + throw Exceptional::NotImplemented('This is not implemented'); + } + + + /** + * Load processor for value + * + * @param array|Closure|null $setup + * @phpstan-return Processor + */ + public function loadProcessor( + string $type, + array|Closure|null $setup = null + ): Processor { + throw Exceptional::NotImplemented('This is not implemented'); + } +}