From 5b563eddfb7ef72d989ccc291ae06728d2d8dbd8 Mon Sep 17 00:00:00 2001 From: Unay Santisteban Date: Wed, 3 Jan 2024 11:41:46 +1000 Subject: [PATCH] Feature/Add PHPStan Extensions. * PHPStan level 8 meet. * PHPStan level 7 meet. * PHPStan level 6 meet. --- composer.json | 9 +++-- src/IsModel.php | 29 +++++++++----- src/Traits/HasAttributes.php | 24 +++-------- src/Traits/HasImmutability.php | 2 +- src/Traits/HasInvariants.php | 71 +++++++++++++++++++-------------- src/TypedCollection.php | 15 ++++--- src/ValueObjects/ArrayValue.php | 17 +++++--- src/ValueObjects/EnumValue.php | 6 +-- 8 files changed, 97 insertions(+), 76 deletions(-) diff --git a/composer.json b/composer.json index d44aa04..d6f5d2c 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,9 @@ "pestphp/pest": "^2.0", "pestphp/pest-plugin-mock": "^2.0", "pestphp/pest-plugin-faker": "^2.0", - "phpstan/phpstan": "^1.0" + "phpstan/phpstan": "^1.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan-mockery": "^1.1" }, "autoload": { "psr-4": { @@ -39,7 +41,7 @@ "scripts": { "test": "vendor/bin/pest --configuration=phpunit.xml --coverage-clover=coverage.xml --log-junit=test.xml", "test-cov": "vendor/bin/pest --configuration=phpunit.xml --coverage-html=coverage", - "analyse": "vendor/bin/phpstan analyse src --no-progress --level=5", + "analyse": "vendor/bin/phpstan analyse src --no-progress --level=8", "check": [ "@analyse", "@test" @@ -47,7 +49,8 @@ }, "config": { "allow-plugins": { - "pestphp/pest-plugin": true + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true } } } diff --git a/src/IsModel.php b/src/IsModel.php index 1746f47..a8ad029 100644 --- a/src/IsModel.php +++ b/src/IsModel.php @@ -26,26 +26,37 @@ trait IsModel * Initialize the Model. Just as the constructor will do. * * @param array $source - * @param callable|null $onFail + * @param string|callable $onFail * * @return static */ - protected function initialize(array $source, callable $onFail = null): static + protected function initialize(array $source, string|callable $onFail = 'invariantHandler'): static + { + $this->hydrate($this->prepareAttributes($source)); + $this->check($onFail); + + return $this; + } + + /** + * Transform an indexed array into assoc array by combining the + * given values with the list of attributes of the object. + * + * @param array $source + * + * @return array + */ + private function prepareAttributes(array $source): array { // check if the array is indexed or associative. $isIndexed = fn($source): bool => ([] !== $source) && array_keys($source) === range(0, count($source) - 1); - $source = $isIndexed($source) + /** @var array $source */ + return $isIndexed($source) // combine the attributes keys with the provided source values. ? array_combine(array_slice(static::attributes(), 0, count($source)), $source) // return the already mapped array source. : $source; - - - $this->hydrate($source); - $this->check($onFail); - - return $this; } /** diff --git a/src/Traits/HasAttributes.php b/src/Traits/HasAttributes.php index 0ef6185..32d2428 100644 --- a/src/Traits/HasAttributes.php +++ b/src/Traits/HasAttributes.php @@ -47,7 +47,7 @@ final public function values(): array /** * Populate the object recursively. * - * @param iterable $source + * @param iterable $source */ final protected function hydrate(iterable $source): void { @@ -68,8 +68,8 @@ final protected function get(string $attribute): mixed if (in_array($attribute, static::attributes())) { $method = $this->getStringKey($attribute, 'get', 'Value'); - return ($this->canCall($method)) - ? call_user_func_array([$this, $method], [$this->{$attribute}]) + return method_exists($this, $method) + ? $this->{$method}($this->{$attribute}) : $this->{$attribute}; } @@ -87,8 +87,8 @@ final protected function set(string $attribute, mixed $value): void if (in_array($attribute, $this->attributes())) { $method = $this->getStringKey($attribute, 'set', 'Value'); - $this->{$attribute} = ($this->canCall($method)) - ? call_user_func_array([$this, $method], [$value]) + $this->{$attribute} = method_exists($this, $method) + ? $this->{$method}($value) : $value; } } @@ -116,24 +116,12 @@ protected function getStringKey(string $id, string $prefix = '', string $suffix ); } - /** - * Check if the required method name is callable. - * - * @param string $method - * - * @return bool - */ - protected function canCall(string $method): bool - { - return method_exists($this, $method); - } - /** * Dynamic method to access each attribute as method, i.e: * $user->name() will access the private attribute name. * * @param string $attribute - * @param array $_ + * @param array $_ * @return mixed|null * @deprecated will be removed in version 3.0 */ diff --git a/src/Traits/HasImmutability.php b/src/Traits/HasImmutability.php index 310de4d..0c864ac 100644 --- a/src/Traits/HasImmutability.php +++ b/src/Traits/HasImmutability.php @@ -23,7 +23,7 @@ trait HasImmutability * Enforces the immutability by blocking any attempts of update any property. * * @param string $name - * @param $_ + * @param mixed $_ * @return void */ final public function __set(string $name, $_): void diff --git a/src/Traits/HasInvariants.php b/src/Traits/HasInvariants.php index c3d9cb7..b297c47 100644 --- a/src/Traits/HasInvariants.php +++ b/src/Traits/HasInvariants.php @@ -25,13 +25,12 @@ final public static function invariants(): array $invariants = []; foreach (get_class_methods(static::class) as $invariant) { if (str_starts_with($invariant, 'invariant') && !in_array($invariant, ['invariants', 'invariantHandler'])) { - $invariants[$invariant] = str_replace( - 'invariant ', - '', - strtolower( - preg_replace('/[A-Z]([A-Z](?![a-z]))*/', ' $0', $invariant) - ) - ); + $invariantRuleName = preg_replace('/[A-Z]([A-Z](?![a-z]))*/', ' $0', $invariant); + if (is_null($invariantRuleName)) { + continue; + } + + $invariants[$invariant] = str_replace('invariant ', '', strtolower($invariantRuleName)); } } @@ -55,18 +54,29 @@ final public static function invariants(): array * $onFail function must have the following signature: * fn(array) => void * - * @param callable|null $onFail + * @param string|callable $onFail * * @return void */ - private function check(callable $onFail = null): void + private function check(string|callable $onFail = 'invariantHandler'): void { - $handler = 'invariantHandler'; + $violations = $this->computeInvariantViolations(); + if (!empty($violations)) { + call_user_func_array($this->computeInvariantHandler($onFail), [$violations]); + } + } + /** + * Computes the list of invariant violations. + * + * @return array + */ + private function computeInvariantViolations(): array + { $violations = []; foreach (static::invariants() as $invariant => $rule) { try { - if (!call_user_func_array([$this, $invariant], [])) { + if (!$this->{$invariant}()) { $violations[$invariant] = $rule; } } catch (Exception $e) { @@ -74,29 +84,28 @@ private function check(callable $onFail = null): void } } - if (!empty($violations)) { - if (is_null($onFail)) { - $customizedHandler = function (array $violations) use ($handler): void { - call_user_func_array([$this, $handler], [$violations]); - }; - - $defaultHandler = function (array $violations): void { - throw new InvariantViolation( - sprintf( - "Unable to create %s due %s", - basename(str_replace('\\', '/', static::class)), - implode(",", $violations), + return $violations; + } - ) - ); - }; + private function computeInvariantHandler(string|callable $handlerFn): callable + { + if (!is_string($handlerFn)) { + return $handlerFn; + } - $onFail = (method_exists($this, $handler)) - ? $customizedHandler - : $defaultHandler; + return method_exists($this, $handlerFn) + ? function (array $violations) use ($handlerFn): void { + $this->{$handlerFn}($violations); } + : function (array $violations): void { + throw new InvariantViolation( + sprintf( + "Unable to create %s due %s", + basename(str_replace('\\', '/', static::class)), + implode(",", $violations), - $onFail($violations); - } + ) + ); + }; } } diff --git a/src/TypedCollection.php b/src/TypedCollection.php index 7e738a2..ab7eb57 100644 --- a/src/TypedCollection.php +++ b/src/TypedCollection.php @@ -14,6 +14,10 @@ * * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Domain + * + * @template TKey of array-key + * @template-covariant TValue + * @extends Collection */ class TypedCollection extends Collection { @@ -37,7 +41,7 @@ class TypedCollection extends Collection /** * TypedCollection constructor. * - * @param array $items + * @param array $items */ public function __construct(array $items = []) { @@ -138,7 +142,7 @@ public function push(...$values): static * * @throws InvariantViolation */ - public function offsetSet(mixed $key, mixed $value): void + public function offsetSet($key, $value): void { if ($this->keyType !== 'mixed') { $this->checkKeyType($key); @@ -187,10 +191,9 @@ public function add(mixed $item): static /** * Get the values of a given key. * - * @param string|array|int|null $value + * @param string|int|array $value * @param string|null $key - * - * @return Collection + * @return Collection */ public function pluck($value, $key = null): Collection { @@ -200,7 +203,7 @@ public function pluck($value, $key = null): Collection /** * Get the keys of the collection items. * - * @return Collection + * @return Collection */ public function keys(): Collection { diff --git a/src/ValueObjects/ArrayValue.php b/src/ValueObjects/ArrayValue.php index 57efdd6..855f38d 100644 --- a/src/ValueObjects/ArrayValue.php +++ b/src/ValueObjects/ArrayValue.php @@ -18,15 +18,17 @@ * * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects + * @implements IteratorAggregate + * @implements ArrayAccess */ abstract class ArrayValue extends Value implements IteratorAggregate, ArrayAccess, Serializable, Countable { /** * The value storage. * - * @var array + * @var array */ - protected array $value; + protected array $value = []; /** * Define the min amount of items for the array. @@ -52,7 +54,7 @@ abstract class ArrayValue extends Value implements IteratorAggregate, ArrayAcces /** * ArrayValue constructor. * - * @param array $value + * @param array $value */ public function __construct(array $value = []) { @@ -125,7 +127,7 @@ protected function invariantMustHaveMaximumNumberOfElements(): bool /** * Retrieve an external iterator. * - * @return Traversable + * @return Traversable */ public function getIterator(): Traversable { @@ -202,6 +204,10 @@ public function unserialize(string $data): void $this->value = unserialize($data); } + /** + * @param array $data + * @return void + */ public function __unserialize(array $data): void { $this->initialize($data); @@ -224,6 +230,7 @@ public function count(): int */ public function __toString(): string { - return json_encode($this->value); + $string = json_encode($this->value); + return is_string($string) ? $string : "[]"; } } diff --git a/src/ValueObjects/EnumValue.php b/src/ValueObjects/EnumValue.php index 92a18b5..3049652 100644 --- a/src/ValueObjects/EnumValue.php +++ b/src/ValueObjects/EnumValue.php @@ -27,7 +27,7 @@ abstract class EnumValue extends Value /** * Internal cache. * - * @var array + * @var array> */ protected static array $cache = []; @@ -44,7 +44,7 @@ public function __construct(mixed $value) /** * Returns the cached constant data of the class. * - * @return array + * @return array */ private static function cache(): array { @@ -81,7 +81,7 @@ public static function isValid(mixed $value): bool /** * Return the available labels. * - * @return string[] + * @return array */ public static function getLabels(): array {