├── .styleci.yml ├── .githooks ├── pre-commit.sh └── init.sh ├── .gitignore ├── .travis.yml ├── ruleset.xml ├── docker-compose.yml ├── src ├── Type │ ├── Downcasting │ │ ├── ToIntConvertibleInterface.php │ │ ├── ToBoolConvertibleInterface.php │ │ ├── ToArrayConvertibleInterface.php │ │ ├── ToFloatConvertibleInterface.php │ │ └── ToStringConvertibleInterface.php │ ├── Upcasting │ │ ├── FromIntConstructableInterface.php │ │ ├── FromArrayConstructableInterface.php │ │ ├── FromBoolConstructableInterface.php │ │ ├── FromFloatConstructableInterface.php │ │ ├── FromStringConstructableInterface.php │ │ ├── FromAnyConstructableInterface.php │ │ └── FromObjectConstructableInterface.php │ ├── TypeInterface.php │ ├── TypeViolation.php │ ├── NullableType.php │ ├── VoidType.php │ ├── TypeExpectationInterface.php │ ├── IntType.php │ ├── BoolType.php │ ├── ArrayType.php │ ├── FloatType.php │ ├── StringType.php │ ├── NullableTypeExpectation.php │ ├── TypeExpectation.php │ └── ClassType.php ├── Floats │ ├── FloatViolationInterface.php │ ├── FloatViolation.php │ ├── BoundedFloat.php │ ├── FloatTooBig.php │ ├── FloatTooSmall.php │ ├── FloatValue.php │ └── FloatOutOfBounds.php ├── Strings │ ├── StringViolationInterface.php │ ├── StringViolation.php │ ├── MaxRawLengthString.php │ ├── MinRawLengthString.php │ ├── MaxMbLengthString.php │ ├── MinMbLengthString.php │ ├── StringLengthViolation.php │ ├── MultiByteString.php │ ├── RegexTemplateString.php │ ├── BoundedRawLengthString.php │ ├── BoundedMbLengthString.php │ ├── StringValue.php │ ├── StringPatternViolation.php │ ├── StringTooLong.php │ ├── StringTooShort.php │ └── StringLengthOutOfBounds.php ├── Integers │ ├── IntegerViolationInterface.php │ ├── NegativeInteger.php │ ├── PositiveInteger.php │ ├── UnsignedInteger.php │ ├── IntegerViolation.php │ ├── LowerBoundInteger.php │ ├── UpperBoundInteger.php │ ├── BoundedInteger.php │ ├── IntegerTooBig.php │ ├── IntegerValue.php │ ├── IntegerTooSmall.php │ └── IntegerOutOfBounds.php ├── Collections │ ├── MissingValue.php │ ├── ListOfInts.php │ ├── ListOfFloats.php │ ├── ListOfStrings.php │ ├── NestedViolationInterface.php │ ├── ImmutableArrayAccessTrait.php │ ├── DataStructure.php │ ├── ArrayMap.php │ ├── ImmutableArrayIterator.php │ ├── ArrayList.php │ ├── DataTransferObject.php │ ├── NestedViolation.php │ └── FromArrayConstructor.php ├── Enum │ ├── StringEnumViolationInterface.php │ ├── ConstantStringValuesEnum.php │ ├── ClassConstantsReflection.php │ ├── StringEnumViolation.php │ ├── ConstantStringValuesWeakEnum.php │ ├── StringEnumBase.php │ └── NamedConstructorsEnum.php ├── Violation.php ├── ViolationExceptionInterface.php ├── Standard │ └── Email │ │ ├── EmailAddressViolation.php │ │ └── EmailAddress.php ├── ImmutableObjectTrait.php ├── DateTime │ ├── DateTimeFormatViolation.php │ ├── FromDateTimeImmutableConstructableInterface.php │ └── DateTimeValue.php ├── ViolationException.php ├── ViolationInterface.php └── Type.php ├── tests ├── Enum │ ├── ConstantStringValuesEnumFixture.php │ ├── NamedConstructorsEnumFixture.php │ ├── NamedConstructorsEnumTest.php │ └── ConstantStringValuesEnumTest.php ├── Collections │ ├── DataStructureFixture.php │ ├── DataStructureTest.php │ ├── ArrayMapTest.php │ ├── ListOfIntsTest.php │ ├── DataTransferObjectTest.php │ └── FromArrayConstructorTest.php ├── Type │ ├── ClassTypeTest.php │ ├── NullableTypeTest.php │ ├── VoidTypeTest.php │ ├── BoolTypeTest.php │ ├── ArrayTypeTest.php │ ├── StringTypeTest.php │ ├── IntTypeTest.php │ └── FloatTypeTest.php ├── TypeTest.php ├── Strings │ └── StringValueTest.php ├── Floats │ ├── FloatValueTest.php │ └── BoundedFloatTest.php ├── Integers │ ├── IntegerValueTest.php │ ├── LowerBoundIntegerTest.php │ └── BoundedIntegerTest.php └── DateTime │ └── DateTimeValueTest.php ├── psalm.xml ├── Makefile ├── composer.json └── LICENSE /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | -------------------------------------------------------------------------------- /.githooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | make ci 4 | exit $? 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | composer.lock 4 | .idea 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.4 5 | 6 | before_install: 7 | - composer self-update 8 | 9 | install: 10 | - make _install 11 | 12 | script: 13 | - make _ci 14 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | php: 5 | image: composer:latest 6 | container_name: php 7 | volumes: 8 | - .:/app:delegated 9 | working_dir: /app 10 | 11 | -------------------------------------------------------------------------------- /src/Type/Downcasting/ToIntConvertibleInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public function getAllowedValues(): array; 13 | } 14 | -------------------------------------------------------------------------------- /src/Floats/FloatViolation.php: -------------------------------------------------------------------------------- 1 | message = $message; 12 | } 13 | 14 | final public function getMessage(): string 15 | { 16 | return $this->message; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Enum/ConstantStringValuesEnumFixture.php: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Integers/UpperBoundInteger.php: -------------------------------------------------------------------------------- 1 | $maxValue) { 13 | throw IntegerTooBig::exception($maxValue); 14 | } 15 | parent::__construct($value); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Strings/MaxMbLengthString.php: -------------------------------------------------------------------------------- 1 | length = $length; 12 | parent::__construct($message ?: "Unexpected string of length $length."); 13 | } 14 | 15 | final public function getLength(): int 16 | { 17 | return $this->length; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Floats/BoundedFloat.php: -------------------------------------------------------------------------------- 1 | length = $length; 16 | parent::__construct($value); 17 | } 18 | 19 | public function getLength(): int 20 | { 21 | return $this->length; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Strings/RegexTemplateString.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function getViolations(): array; 20 | } 21 | -------------------------------------------------------------------------------- /src/Integers/BoundedInteger.php: -------------------------------------------------------------------------------- 1 | $maxValue) { 16 | throw IntegerOutOfBounds::exception($minValue, $maxValue); 17 | } 18 | 19 | parent::__construct($value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Standard/Email/EmailAddressViolation.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class ListOfInts extends ArrayList implements 12 | ToArrayConvertibleInterface, 13 | FromArrayConstructableInterface 14 | { 15 | public function current(): int 16 | { 17 | return parent::current(); 18 | } 19 | 20 | public static function fromArray(array $value): self 21 | { 22 | return new self($value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Collections/ListOfFloats.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class ListOfFloats extends ArrayList implements 12 | ToArrayConvertibleInterface, 13 | FromArrayConstructableInterface 14 | { 15 | public function current(): float 16 | { 17 | return parent::current(); 18 | } 19 | 20 | public static function fromArray(array $value): self 21 | { 22 | return new self($value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Type/Upcasting/FromObjectConstructableInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class ListOfStrings extends ArrayList implements 12 | ToArrayConvertibleInterface, 13 | FromArrayConstructableInterface 14 | { 15 | public function current(): string 16 | { 17 | return parent::current(); 18 | } 19 | 20 | public static function fromArray(array $value): self 21 | { 22 | return new self($value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Type/NullableType.php: -------------------------------------------------------------------------------- 1 | type = $type; 12 | } 13 | 14 | public function getExpectation(): TypeExpectationInterface 15 | { 16 | return new NullableTypeExpectation($this->type->getExpectation()); 17 | } 18 | 19 | public function prepareValue($value) 20 | { 21 | if ($value === null) { 22 | return null; 23 | } 24 | 25 | return $this->type->prepareValue($value); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Collections/DataStructureFixture.php: -------------------------------------------------------------------------------- 1 | x = $x; 16 | $this->y = $y; 17 | $this->z = $z; 18 | } 19 | 20 | public function getY(): float 21 | { 22 | return $this->y; 23 | } 24 | 25 | public function getZ(): string 26 | { 27 | return $this->z; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ImmutableObjectTrait.php: -------------------------------------------------------------------------------- 1 | expectation = new TypeExpectation( 12 | null, 13 | false, 14 | false, 15 | false, 16 | false, 17 | false 18 | ); 19 | } 20 | 21 | public function getExpectation(): TypeExpectationInterface 22 | { 23 | return $this->expectation; 24 | } 25 | 26 | public function prepareValue($value) 27 | { 28 | throw TypeViolation::exception(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Type/ClassTypeTest.php: -------------------------------------------------------------------------------- 1 | prepareValue($input); 16 | self::assertInstanceOf(DateTimeValue::class, $value); 17 | self::assertSame('2020-05-26T13:14:15+00:00', (string) $value); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Collections/NestedViolationInterface.php: -------------------------------------------------------------------------------- 1 | upperBound = $upperBound; 15 | parent::__construct($message ?: "Expected value no greater then $upperBound."); 16 | } 17 | 18 | public function getUpperBound(): float 19 | { 20 | return $this->upperBound; 21 | } 22 | 23 | public static function exception(float $upperBound): ViolationExceptionInterface 24 | { 25 | return ViolationException::for(new self($upperBound)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Floats/FloatTooSmall.php: -------------------------------------------------------------------------------- 1 | lowerBound = $lowerBound; 15 | parent::__construct($message ?: "Expected value no smaller then $lowerBound."); 16 | } 17 | 18 | public function getLowerBound(): float 19 | { 20 | return $this->lowerBound; 21 | } 22 | 23 | public static function exception(float $lowerBound): ViolationExceptionInterface 24 | { 25 | return ViolationException::for(new self($lowerBound)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Integers/IntegerTooBig.php: -------------------------------------------------------------------------------- 1 | upperBound = $upperBound; 15 | parent::__construct($message ?: "Expected integer no greater then $upperBound."); 16 | } 17 | 18 | public function getUpperBound(): int 19 | { 20 | return $this->upperBound; 21 | } 22 | 23 | public static function exception(int $upperBound): ViolationExceptionInterface 24 | { 25 | return ViolationException::for(new self($upperBound)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Strings/StringValue.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | final public function __toString(): string 20 | { 21 | return $this->value; 22 | } 23 | 24 | final public function jsonSerialize(): string 25 | { 26 | return $this->value; 27 | } 28 | 29 | public function getLength(): int 30 | { 31 | return \strlen($this->value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Collections/ImmutableArrayAccessTrait.php: -------------------------------------------------------------------------------- 1 | format = $format; 16 | parent::__construct($message ?: "Expected date in format \"$format\"."); 17 | } 18 | 19 | public function getFormat(): string 20 | { 21 | return $this->format; 22 | } 23 | 24 | public static function exception(string $format): ViolationExceptionInterface 25 | { 26 | return ViolationException::for(new self($format)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Floats/FloatValue.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | final public function toFloat(): float 20 | { 21 | return $this->value; 22 | } 23 | 24 | public function toInt(): int 25 | { 26 | return (int) $this->value; 27 | } 28 | 29 | public function __toString(): string 30 | { 31 | return (string) $this->value; 32 | } 33 | 34 | final public function jsonSerialize(): float 35 | { 36 | return $this->value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Integers/IntegerValue.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | final public function toInt(): int 20 | { 21 | return $this->value; 22 | } 23 | 24 | final public function toFloat(): float 25 | { 26 | return (float) $this->value; 27 | } 28 | 29 | final public function __toString(): string 30 | { 31 | return (string) $this->value; 32 | } 33 | 34 | final public function jsonSerialize(): int 35 | { 36 | return $this->value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .phony: install test cs-check cs-fix psalm 2 | 3 | DOCKER=docker-compose run --rm php 4 | CS=--standard=ruleset.xml src tests 5 | 6 | INSTALL=composer install --no-interaction 7 | UPDATE=composer update 8 | TEST=vendor/bin/phpunit --colors=always tests 9 | CSCHECK=vendor/bin/phpcs $(CS) 10 | CSFIX=vendor/bin/phpcbf $(CS) 11 | PSALM=vendor/bin/psalm --show-info=false 12 | 13 | install: 14 | $(DOCKER) $(INSTALL) 15 | 16 | update: 17 | $(DOCKER) $(UPDATE) 18 | 19 | githooks: 20 | .githooks/init.sh 21 | 22 | test: 23 | $(DOCKER) $(TEST) 24 | 25 | cs-check: 26 | $(DOCKER) $(CSCHECK) 27 | 28 | cs-fix: 29 | $(DOCKER) $(CSFIX) 30 | 31 | psalm: 32 | $(DOCKER) $(PSALM) 33 | 34 | ci: cs-check psalm test 35 | 36 | # inside docker 37 | 38 | _install: 39 | $(INSTALL) 40 | 41 | _test: 42 | $(TEST) 43 | 44 | _cs-check: 45 | $(CSCHECK) 46 | 47 | _cs-fix: 48 | $(CSFIX) 49 | 50 | _psalm: 51 | $(PSALM) 52 | 53 | _ci: _cs-check _psalm _test 54 | 55 | -------------------------------------------------------------------------------- /src/Type/TypeExpectationInterface.php: -------------------------------------------------------------------------------- 1 | =7.4", 15 | "ext-json": "*" 16 | }, 17 | "require-dev": { 18 | "ext-mbstring": "*", 19 | "vimeo/psalm": "^3.11", 20 | "phpunit/phpunit": "^9.1", 21 | "squizlabs/php_codesniffer": "^3.5" 22 | }, 23 | "suggest": { 24 | "ext-mbstring": "Install if you want to use the multi-byte string length value objects." 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Slepic\\ValueObject\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Slepic\\Tests\\ValueObject\\": "tests" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Standard/Email/EmailAddress.php: -------------------------------------------------------------------------------- 1 | expectation = new TypeExpectation( 14 | null, 15 | false, 16 | true, 17 | false, 18 | false, 19 | false, 20 | ); 21 | } 22 | 23 | public function getExpectation(): TypeExpectationInterface 24 | { 25 | return $this->expectation; 26 | } 27 | 28 | public function prepareValue($value): int 29 | { 30 | if (\is_int($value)) { 31 | return $value; 32 | } 33 | 34 | if (\is_object($value) && $value instanceof ToIntConvertibleInterface) { 35 | return $value->toInt(); 36 | } 37 | 38 | throw TypeViolation::exception(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Type/BoolType.php: -------------------------------------------------------------------------------- 1 | expectation = new TypeExpectation( 14 | null, 15 | false, 16 | false, 17 | false, 18 | true, 19 | false, 20 | ); 21 | } 22 | 23 | public function getExpectation(): TypeExpectationInterface 24 | { 25 | return $this->expectation; 26 | } 27 | 28 | public function prepareValue($value): bool 29 | { 30 | if (\is_bool($value)) { 31 | return $value; 32 | } 33 | 34 | if (\is_object($value) && $value instanceof ToBoolConvertibleInterface) { 35 | return $value->toBool(); 36 | } 37 | 38 | throw TypeViolation::exception(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Integers/IntegerTooSmall.php: -------------------------------------------------------------------------------- 1 | lowerBound = $lowerBound; 15 | parent::__construct($message ?: "Expected integer no smaller then $lowerBound."); 16 | } 17 | 18 | public function getLowerBound(): int 19 | { 20 | return $this->lowerBound; 21 | } 22 | 23 | public static function exception(int $lowerBound): ViolationExceptionInterface 24 | { 25 | return ViolationException::for(new self($lowerBound)); 26 | } 27 | 28 | public static function check(int $lowerBound, int $value): void 29 | { 30 | if ($value < $lowerBound) { 31 | throw self::exception($lowerBound); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Type/ArrayType.php: -------------------------------------------------------------------------------- 1 | expectation = new TypeExpectation( 14 | null, 15 | false, 16 | false, 17 | false, 18 | false, 19 | true, 20 | ); 21 | } 22 | 23 | public function getExpectation(): TypeExpectationInterface 24 | { 25 | return $this->expectation; 26 | } 27 | 28 | public function prepareValue($value): array 29 | { 30 | if (\is_array($value)) { 31 | return $value; 32 | } 33 | 34 | if (\is_object($value) && $value instanceof ToArrayConvertibleInterface) { 35 | return $value->toArray(); 36 | } 37 | 38 | throw TypeViolation::exception(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Type/FloatType.php: -------------------------------------------------------------------------------- 1 | expectation = new TypeExpectation( 14 | null, 15 | false, 16 | false, 17 | true, 18 | false, 19 | false, 20 | ); 21 | } 22 | 23 | public function getExpectation(): TypeExpectationInterface 24 | { 25 | return $this->expectation; 26 | } 27 | 28 | public function prepareValue($value): float 29 | { 30 | if (\is_float($value)) { 31 | return $value; 32 | } 33 | 34 | if (\is_object($value) && $value instanceof ToFloatConvertibleInterface) { 35 | return $value->toFloat(); 36 | } 37 | 38 | throw TypeViolation::exception(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Integers/IntegerOutOfBounds.php: -------------------------------------------------------------------------------- 1 | minValue = $minValue; 16 | $this->maxValue = $maxValue; 17 | parent::__construct($message ?: "Integer value is out of bounds [$minValue, $maxValue]."); 18 | } 19 | 20 | public function getMinValue(): int 21 | { 22 | return $this->minValue; 23 | } 24 | 25 | public function getMaxValue(): int 26 | { 27 | return $this->maxValue; 28 | } 29 | 30 | public static function exception(int $minValue, int $maxValue): ViolationExceptionInterface 31 | { 32 | return ViolationException::for(new self($minValue, $maxValue)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Type/StringType.php: -------------------------------------------------------------------------------- 1 | expectation = new TypeExpectation( 14 | null, 15 | true, 16 | false, 17 | false, 18 | false, 19 | false, 20 | ); 21 | } 22 | 23 | public function getExpectation(): TypeExpectationInterface 24 | { 25 | return $this->expectation; 26 | } 27 | 28 | public function prepareValue($value): string 29 | { 30 | if (\is_string($value)) { 31 | return $value; 32 | } 33 | 34 | if (\is_object($value) && $value instanceof ToStringConvertibleInterface) { 35 | return (string) $value; 36 | } 37 | 38 | throw TypeViolation::exception(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Strings/StringPatternViolation.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern; 15 | parent::__construct($message ?: "Expected value to satisfy the pattern: $pattern."); 16 | } 17 | 18 | public function getPattern(): string 19 | { 20 | return $this->pattern; 21 | } 22 | 23 | public static function exception(string $pattern): ViolationExceptionInterface 24 | { 25 | return ViolationException::for(new self($pattern)); 26 | } 27 | 28 | public static function check(string $pattern, string $value): void 29 | { 30 | if (1 !== \preg_match($pattern, $value)) { 31 | throw self::exception($pattern); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Strings/StringTooLong.php: -------------------------------------------------------------------------------- 1 | maxLength = $maxLength; 15 | parent::__construct($length, $message ?: "Expected at most $maxLength characters."); 16 | } 17 | 18 | public function getMaxLength(): int 19 | { 20 | return $this->maxLength; 21 | } 22 | 23 | public static function exception(int $maxLength, int $length): ViolationExceptionInterface 24 | { 25 | return ViolationException::for(new self($maxLength, $length)); 26 | } 27 | 28 | public static function check(int $maxLength, int $length): void 29 | { 30 | if ($length > $maxLength) { 31 | throw self::exception($maxLength, $length); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Strings/StringTooShort.php: -------------------------------------------------------------------------------- 1 | minLength = $minLength; 15 | parent::__construct($length, $message ?: "Expected at least $minLength characters."); 16 | } 17 | 18 | public function getMinLength(): int 19 | { 20 | return $this->minLength; 21 | } 22 | 23 | public static function exception(int $minLength, int $length): ViolationExceptionInterface 24 | { 25 | return ViolationException::for(new self($minLength, $length)); 26 | } 27 | 28 | public static function check(int $minLength, int $length): void 29 | { 30 | if ($length < $minLength) { 31 | throw self::exception($minLength, $length); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Enum/ClassConstantsReflection.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | public static function getUniqueStringConstantValues(\ReflectionClass $reflection): array 12 | { 13 | $all = []; 14 | $constants = $reflection->getReflectionConstants(); 15 | foreach ($constants as $constant) { 16 | if ($constant->isPublic()) { 17 | $name = $constant->getName(); 18 | $value = $constant->getValue(); 19 | if (!\is_string($value)) { 20 | throw new \UnexpectedValueException("Constant \"$name\" does not have a string value."); 21 | } 22 | if (isset($all[$value])) { 23 | throw new \UnexpectedValueException("Constant \"$name\" has duplicate value."); 24 | } 25 | $all[$value] = $value; 26 | } 27 | } 28 | return $all; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 slepic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/DateTime/FromDateTimeImmutableConstructableInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $allowedValues; 15 | 16 | /** 17 | * @param array $allowedValues 18 | * @param string $message 19 | */ 20 | public function __construct(array $allowedValues, string $message = '') 21 | { 22 | $this->allowedValues = $allowedValues; 23 | parent::__construct($message ?: ('Expected one of: ' . \implode(', ', $allowedValues))); 24 | } 25 | 26 | public function getAllowedValues(): array 27 | { 28 | return $this->allowedValues; 29 | } 30 | 31 | /** 32 | * @param array $allowedValues 33 | * @return ViolationExceptionInterface 34 | */ 35 | public static function exception(array $allowedValues): ViolationExceptionInterface 36 | { 37 | return ViolationException::for(new self($allowedValues)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Type/NullableTypeExpectation.php: -------------------------------------------------------------------------------- 1 | expectation = $expectation; 12 | } 13 | 14 | public function acceptsNull(): bool 15 | { 16 | return true; 17 | } 18 | 19 | public function acceptsClass(string $class): bool 20 | { 21 | return $this->expectation->acceptsClass($class); 22 | } 23 | 24 | public function acceptsString(): bool 25 | { 26 | return $this->expectation->acceptsString(); 27 | } 28 | 29 | public function acceptsInt(): bool 30 | { 31 | return $this->expectation->acceptsInt(); 32 | } 33 | 34 | public function acceptsFloat(): bool 35 | { 36 | return $this->expectation->acceptsFloat(); 37 | } 38 | 39 | public function acceptsBool(): bool 40 | { 41 | return $this->expectation->acceptsBool(); 42 | } 43 | 44 | public function acceptsArray(): bool 45 | { 46 | return $this->expectation->acceptsArray(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Floats/FloatOutOfBounds.php: -------------------------------------------------------------------------------- 1 | minValue = $minValue; 16 | $this->maxValue = $maxValue; 17 | parent::__construct($message ?: "Expected value between $minValue and $maxValue."); 18 | } 19 | 20 | public function getMinValue(): float 21 | { 22 | return $this->minValue; 23 | } 24 | 25 | public function getMaxValue(): float 26 | { 27 | return $this->maxValue; 28 | } 29 | 30 | public static function exception(float $minValue, float $maxValue): ViolationExceptionInterface 31 | { 32 | return ViolationException::for(new self($minValue, $maxValue)); 33 | } 34 | 35 | public static function check(float $minValue, float $maxValue, float $value): void 36 | { 37 | if ($value < $minValue || $value > $maxValue) { 38 | throw self::exception($minValue, $maxValue); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Collections/DataStructure.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | public function toArray(): array 37 | { 38 | return FromArrayConstructor::extractConstructorArguments($this); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ViolationException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | private array $violations; 11 | 12 | /** 13 | * @param array $violations 14 | * @param string $message 15 | * @param int $code 16 | * @param \Throwable|null $previous 17 | */ 18 | public function __construct(array $violations, string $message = "", int $code = 0, \Throwable $previous = null) 19 | { 20 | $violation = \reset($violations); 21 | if (!$violation instanceof ViolationInterface) { 22 | throw new \InvalidArgumentException('Expected nonempty array of ViolationInterface instances.'); 23 | } 24 | $this->violations = $violations; 25 | parent::__construct($message ?: $violation->getMessage(), $code, $previous); 26 | } 27 | 28 | public static function for(ViolationInterface ...$violations): self 29 | { 30 | return new self($violations); 31 | } 32 | 33 | /** 34 | * @return array 35 | */ 36 | public function getViolations(): array 37 | { 38 | return $this->violations; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/TypeTest.php: -------------------------------------------------------------------------------- 1 | minLength = $minLength; 16 | $this->maxLength = $maxLength; 17 | parent::__construct( 18 | $length, 19 | $message ?: "Value of length $length is out of boundaries [$minLength, $maxLength]." 20 | ); 21 | } 22 | 23 | public function getMinLength(): int 24 | { 25 | return $this->minLength; 26 | } 27 | 28 | public function getMaxLength(): int 29 | { 30 | return $this->maxLength; 31 | } 32 | 33 | public static function exception(int $minLength, int $maxLength, int $actualLength): ViolationExceptionInterface 34 | { 35 | return ViolationException::for(new self($minLength, $maxLength, $actualLength)); 36 | } 37 | 38 | public static function check(int $minLength, int $maxLength, int $actualLength): void 39 | { 40 | if ($actualLength < $minLength || $actualLength > $maxLength) { 41 | throw self::exception($minLength, $maxLength, $actualLength); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Enum/ConstantStringValuesWeakEnum.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | public static function allowedValues(): array 33 | { 34 | $reflection = new \ReflectionClass(static::class); 35 | return ClassConstantsReflection::getUniqueStringConstantValues($reflection); 36 | } 37 | 38 | /** 39 | * @param mixed $other 40 | * @return bool 41 | */ 42 | public function is($other): bool 43 | { 44 | if (\is_string($other)) { 45 | return ((string) $this) === $other; 46 | } 47 | 48 | if (\is_object($other) && \is_a($other, static::class)) { 49 | return ((string) $this) === ((string) $other); 50 | } 51 | 52 | throw new \InvalidArgumentException('Expected string or instance of ' . static::class); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Strings/StringValueTest.php: -------------------------------------------------------------------------------- 1 | getLength()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Enum/StringEnumBase.php: -------------------------------------------------------------------------------- 1 | >|null 12 | */ 13 | private static array $all = []; 14 | 15 | private function __construct(string $value) 16 | { 17 | parent::__construct($value); 18 | } 19 | 20 | /** 21 | * @return array 22 | */ 23 | final public static function all(): array 24 | { 25 | if (!isset(static::$all[static::class])) { 26 | $all = []; 27 | foreach (static::createAllUniqueValues() as $value) { 28 | $all[$value] = new static($value); 29 | } 30 | static::$all[static::class] = $all; 31 | } 32 | return static::$all[static::class]; 33 | } 34 | 35 | /** 36 | * @param string $value 37 | * @return static 38 | */ 39 | final public static function fromString(string $value): self 40 | { 41 | $all = static::all(); 42 | if (isset($all[$value])) { 43 | return $all[$value]; 44 | } 45 | throw StringEnumViolation::exception(\array_keys($all)); 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | abstract protected static function createAllUniqueValues(): array; 52 | 53 | /** 54 | * @throws \BadMethodCallException 55 | */ 56 | public function __clone() 57 | { 58 | throw new \BadMethodCallException('StrongEnum cannot be cloned.'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Type/TypeExpectation.php: -------------------------------------------------------------------------------- 1 | acceptsClass = $acceptsClass; 24 | $this->acceptsString = $acceptsString; 25 | $this->acceptsInt = $acceptsInt; 26 | $this->acceptsFloat = $acceptsFloat; 27 | $this->acceptsBool = $acceptsBool; 28 | $this->acceptsArray = $acceptsArray; 29 | } 30 | 31 | public function acceptsNull(): bool 32 | { 33 | return false; 34 | } 35 | 36 | public function acceptsClass(string $class): bool 37 | { 38 | if ($this->acceptsClass === null) { 39 | return false; 40 | } 41 | return \is_a($class, $this->acceptsClass); 42 | } 43 | 44 | public function acceptsString(): bool 45 | { 46 | return $this->acceptsString; 47 | } 48 | 49 | public function acceptsInt(): bool 50 | { 51 | return $this->acceptsInt; 52 | } 53 | 54 | public function acceptsFloat(): bool 55 | { 56 | return $this->acceptsFloat; 57 | } 58 | 59 | public function acceptsBool(): bool 60 | { 61 | return $this->acceptsBool; 62 | } 63 | 64 | public function acceptsArray(): bool 65 | { 66 | return $this->acceptsArray; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Collections/ArrayMap.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class ArrayMap extends ImmutableArrayIterator implements \JsonSerializable 14 | { 15 | /** 16 | * @psalm-param array $input 17 | * @param array $input 18 | * @throws ViolationException 19 | */ 20 | public function __construct(array $input) 21 | { 22 | $reflection = new \ReflectionClass(static::class); 23 | $type = Type::forMethodReturnType($reflection->getMethod('current')); 24 | 25 | $items = []; 26 | $violations = []; 27 | 28 | foreach ($input as $key => $value) { 29 | try { 30 | /** @psalm-var TValue $item */ 31 | $item = $type->prepareValue($value); 32 | $items[$key] = $item; 33 | } catch (ViolationExceptionInterface $e) { 34 | $violations[] = NestedViolation::invalidProperty( 35 | (string) $key, 36 | $type->getExpectation(), 37 | $value, 38 | $e->getViolations() 39 | ); 40 | } 41 | } 42 | 43 | if (\count($violations) !== 0) { 44 | throw new ViolationException($violations); 45 | } 46 | 47 | parent::__construct($items); 48 | } 49 | 50 | public function key(): string 51 | { 52 | return (string) parent::key(); 53 | } 54 | 55 | public function jsonSerialize(): \stdClass 56 | { 57 | return (object) $this->toArray(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Floats/FloatValueTest.php: -------------------------------------------------------------------------------- 1 | toInt()); 30 | } 31 | 32 | public function testToFloat(): void 33 | { 34 | $object = new FloatValue(11.1); 35 | self::assertSame(11.1, $object->toFloat()); 36 | } 37 | 38 | public function testToString(): void 39 | { 40 | $object = new FloatValue(11.1); 41 | self::assertSame('11.1', (string) $object); 42 | } 43 | 44 | public function testJsonSerialization(): void 45 | { 46 | $object = new FloatValue(11.1); 47 | self::assertSame('11.1', \json_encode($object)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Integers/IntegerValueTest.php: -------------------------------------------------------------------------------- 1 | toInt()); 30 | } 31 | public function testToFloat(): void 32 | { 33 | $object = new IntegerValue(11); 34 | self::assertSame(11.0, $object->toFloat()); 35 | } 36 | 37 | public function testToString(): void 38 | { 39 | $object = new IntegerValue(10); 40 | self::assertSame('10', (string) $object); 41 | } 42 | 43 | public function testJsonSerialization(): void 44 | { 45 | $object = new IntegerValue(10); 46 | self::assertSame('10', \json_encode($object)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Collections/ImmutableArrayIterator.php: -------------------------------------------------------------------------------- 1 | 10 | * @template-implements \ArrayAccess 11 | */ 12 | class ImmutableArrayIterator implements \Iterator, \Countable, \ArrayAccess 13 | { 14 | use ImmutableArrayAccessTrait; 15 | 16 | /** 17 | * @psalm-var \ArrayIterator 18 | * @var \ArrayIterator 19 | */ 20 | private \ArrayIterator $items; 21 | 22 | /** 23 | * @param array $items 24 | * @psalm-param array $items 25 | */ 26 | public function __construct(array $items) 27 | { 28 | $this->items = new \ArrayIterator($items); 29 | } 30 | 31 | public function rewind(): void 32 | { 33 | $this->items->rewind(); 34 | } 35 | 36 | public function next(): void 37 | { 38 | $this->items->next(); 39 | } 40 | 41 | public function valid(): bool 42 | { 43 | return $this->items->valid(); 44 | } 45 | 46 | public function key() 47 | { 48 | return $this->items->key(); 49 | } 50 | 51 | public function current() 52 | { 53 | return $this->items->current(); 54 | } 55 | 56 | /** 57 | * @return array 58 | * @psalm-return array 59 | */ 60 | public function toArray(): array 61 | { 62 | return $this->items->getArrayCopy(); 63 | } 64 | 65 | public function count(): int 66 | { 67 | return $this->items->count(); 68 | } 69 | 70 | public function offsetExists($offset): bool 71 | { 72 | return $this->items->offsetExists($offset); 73 | } 74 | 75 | public function offsetGet($offset) 76 | { 77 | return $this->items->offsetGet($offset); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Integers/LowerBoundIntegerTest.php: -------------------------------------------------------------------------------- 1 | toInt()); 21 | } 22 | 23 | public function testAtBoundSucceeds(): void 24 | { 25 | $value = new class (10) extends LowerBoundInteger { 26 | final protected static function minValue(): int 27 | { 28 | return 10; 29 | } 30 | }; 31 | self::assertSame(10, $value->toInt()); 32 | } 33 | 34 | public function testBelowBoundFails(): void 35 | { 36 | try { 37 | new class (9) extends LowerBoundInteger { 38 | final protected static function minValue(): int 39 | { 40 | return 10; 41 | } 42 | }; 43 | self::assertTrue(false, 'Exception not thrown.'); 44 | } catch (ViolationExceptionInterface $e) { 45 | $violations = $e->getViolations(); 46 | self::assertCount(1, $violations); 47 | $violation = \reset($violations); 48 | if ($violation instanceof IntegerTooSmall) { 49 | self::assertSame(10, $violation->getLowerBound()); 50 | } else { 51 | self::assertTrue(false, 'Bad violation type.'); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Collections/ArrayList.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | abstract class ArrayList extends ImmutableArrayIterator implements \JsonSerializable 19 | { 20 | /** 21 | * @psalm-param array $input 22 | * @param array $input 23 | * @throws ViolationExceptionInterface 24 | */ 25 | public function __construct(array $input) 26 | { 27 | $reflection = new \ReflectionClass($this); 28 | $type = Type::forMethodReturnType($reflection->getMethod('current')); 29 | 30 | $index = 0; 31 | $items = []; 32 | $violations = []; 33 | foreach ($input as $key => $value) { 34 | if ($key !== $index) { 35 | throw TypeViolation::exception('Expected 0-based indices.'); 36 | } 37 | 38 | try { 39 | /** @psalm-var TValue $item */ 40 | $item = $type->prepareValue($value); 41 | $items[] = $item; 42 | } catch (ViolationExceptionInterface $e) { 43 | $violations[] = NestedViolation::invalidItem( 44 | $key, 45 | $type->getExpectation(), 46 | $value, 47 | $e->getViolations() 48 | ); 49 | } 50 | 51 | ++$index; 52 | } 53 | 54 | if (\count($violations) > 0) { 55 | throw new ViolationException($violations); 56 | } 57 | 58 | parent::__construct($items); 59 | } 60 | 61 | public function key(): int 62 | { 63 | return parent::key(); 64 | } 65 | 66 | public function jsonSerialize(): array 67 | { 68 | return $this->toArray(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Collections/DataTransferObject.php: -------------------------------------------------------------------------------- 1 | $data 21 | * @throws ViolationExceptionInterface 22 | */ 23 | public function __construct(array $data) 24 | { 25 | $reflection = new \ReflectionClass(static::class); 26 | 27 | $violations = []; 28 | $properties = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC); 29 | foreach ($properties as $property) { 30 | if ($property->isStatic()) { 31 | continue; 32 | }; 33 | 34 | $key = $property->getName(); 35 | $type = Type::forPropertyReflection($property); 36 | 37 | if (\array_key_exists($key, $data)) { 38 | $value = $data[$key]; 39 | try { 40 | $this->$key = $type->prepareValue($value); 41 | } catch (ViolationExceptionInterface $e) { 42 | $violations[] = NestedViolation::invalidProperty( 43 | $key, 44 | $type->getExpectation(), 45 | $value, 46 | $e->getViolations() 47 | ); 48 | } 49 | unset($data[$key]); 50 | } else { 51 | if (!$property->isInitialized($this)) { 52 | $violations[] = NestedViolation::missingRequiredProperty($key, $type->getExpectation()); 53 | } 54 | } 55 | } 56 | 57 | if (!static::IGNORE_UNKNOWN_PROPERTIES) { 58 | foreach ($data as $key => $value) { 59 | $violations[] = NestedViolation::unknownProperty($key, $value); 60 | } 61 | } 62 | 63 | if (\count($violations) !== 0) { 64 | throw new ViolationException($violations); 65 | } 66 | } 67 | 68 | public function toArray(): array 69 | { 70 | return \get_object_vars($this); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Enum/NamedConstructorsEnumTest.php: -------------------------------------------------------------------------------- 1 | getViolations(); 62 | self::assertCount(1, $violations); 63 | self::assertInstanceOf(StringEnumViolation::class, \reset($violations)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Enum/ConstantStringValuesEnumTest.php: -------------------------------------------------------------------------------- 1 | getViolations(); 62 | self::assertCount(1, $violations); 63 | self::assertInstanceOf(StringEnumViolation::class, \reset($violations)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Integers/BoundedIntegerTest.php: -------------------------------------------------------------------------------- 1 | toInt()); 26 | } 27 | 28 | public function testTooBig(): void 29 | { 30 | try { 31 | new class (11) extends BoundedInteger { 32 | protected static function minValue(): int 33 | { 34 | return 9; 35 | } 36 | 37 | protected static function maxValue(): int 38 | { 39 | return 10; 40 | } 41 | }; 42 | self::assertTrue(false, 'Exception not thrown'); 43 | } catch (ViolationExceptionInterface $e) { 44 | $violations = $e->getViolations(); 45 | self::assertCount(1, $violations); 46 | $violation = \reset($violations); 47 | if ($violation instanceof IntegerOutOfBounds) { 48 | self::assertSame(9, $violation->getMinValue()); 49 | self::assertSame(10, $violation->getMaxValue()); 50 | } else { 51 | self::assertTrue(false, 'Bad violation ' . \get_class($violation)); 52 | } 53 | } 54 | } 55 | 56 | public function testTooSmall(): void 57 | { 58 | try { 59 | new class (8) extends BoundedInteger { 60 | protected static function minValue(): int 61 | { 62 | return 9; 63 | } 64 | 65 | protected static function maxValue(): int 66 | { 67 | return 10; 68 | } 69 | }; 70 | self::assertTrue(false, 'Exception not thrown'); 71 | } catch (ViolationExceptionInterface $e) { 72 | $violations = $e->getViolations(); 73 | self::assertCount(1, $violations); 74 | $violation = \reset($violations); 75 | if ($violation instanceof IntegerOutOfBounds) { 76 | self::assertSame(9, $violation->getMinValue()); 77 | self::assertSame(10, $violation->getMaxValue()); 78 | } else { 79 | self::assertTrue(false, 'Bad violation ' . \get_class($violation)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Floats/BoundedFloatTest.php: -------------------------------------------------------------------------------- 1 | toFloat()); 26 | } 27 | 28 | public function testTooBig(): void 29 | { 30 | try { 31 | new class (11.1) extends BoundedFloat { 32 | protected static function minValue(): float 33 | { 34 | return 9.0; 35 | } 36 | 37 | protected static function maxValue(): float 38 | { 39 | return 11.09; 40 | } 41 | }; 42 | self::assertTrue(false, 'Exception not thrown'); 43 | } catch (ViolationExceptioninterface $e) { 44 | $violations = $e->getViolations(); 45 | self::assertCount(1, $violations); 46 | $violation = \reset($violations); 47 | if ($violation instanceof FloatOutOfBounds) { 48 | self::assertSame(9.0, $violation->getMinValue()); 49 | self::assertSame(11.09, $violation->getMaxValue()); 50 | } else { 51 | self::assertTrue(false, 'Bad violation ' . \get_class($violation)); 52 | } 53 | } 54 | } 55 | 56 | public function testTooSmall(): void 57 | { 58 | try { 59 | new class (8.9) extends BoundedFloat { 60 | protected static function minValue(): float 61 | { 62 | return 9.0; 63 | } 64 | 65 | protected static function maxValue(): float 66 | { 67 | return 11.1; 68 | } 69 | }; 70 | self::assertTrue(false, 'Exception not thrown'); 71 | } catch (ViolationExceptioninterface $e) { 72 | $violations = $e->getViolations(); 73 | self::assertCount(1, $violations); 74 | $violation = \reset($violations); 75 | if ($violation instanceof FloatOutOfBounds) { 76 | self::assertSame(9.0, $violation->getMinValue()); 77 | self::assertSame(11.1, $violation->getMaxValue()); 78 | } else { 79 | self::assertTrue(false, 'Bad violation ' . \get_class($violation)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Collections/DataStructureTest.php: -------------------------------------------------------------------------------- 1 | 10, 16 | 'y' => 11.1, 17 | 'z' => 'value', 18 | ]); 19 | 20 | self::assertInstanceOf(DataStructureFixture::class, $structure); 21 | self::assertSame(10, $structure->x); 22 | self::assertSame(11.1, $structure->getY()); 23 | self::assertSame('value', $structure->getZ()); 24 | } 25 | 26 | public function testThatCanModifyAll(): void 27 | { 28 | $input = new DataStructureFixture(1, 11.1, 'value'); 29 | $structure = $input->with([ 30 | 'x' => 10, 31 | ]); 32 | 33 | self::assertSame(10, $structure->x); 34 | self::assertSame(11.1, $structure->getY()); 35 | self::assertSame('value', $structure->getZ()); 36 | } 37 | 38 | public function testThatAllViolationsAreThrown(): void 39 | { 40 | try { 41 | DataStructureFixture::fromArray([ 42 | 'x' => 10, 43 | 'y' => 10, 44 | 'extra' => 'null', 45 | ]); 46 | self::assertTrue(false, 'Exception not thrown.'); 47 | } catch (ViolationExceptionInterface $e) { 48 | $violations = $e->getViolations(); 49 | self::assertCount(3, $violations); 50 | 51 | $violation = \array_shift($violations); 52 | if ($violation instanceof NestedViolationInterface) { 53 | self::assertSame('y', $violation->getKey()); 54 | self::assertSame(10, $violation->getValue()); 55 | $subViolations = $violation->getViolations(); 56 | self::assertCount(1, $subViolations); 57 | $subViolation = \reset($subViolations); 58 | self::assertInstanceOf(TypeViolation::class, $subViolation); 59 | } else { 60 | self::assertTrue(false, 'Bad violation type.'); 61 | } 62 | 63 | $violation = \array_shift($violations); 64 | if ($violation instanceof NestedViolationInterface) { 65 | self::assertSame('z', $violation->getKey()); 66 | } else { 67 | self::assertTrue(false, 'Bad violation type.'); 68 | } 69 | 70 | $violation = \array_shift($violations); 71 | if ($violation instanceof NestedViolationInterface) { 72 | self::assertSame('extra', $violation->getKey()); 73 | self::assertSame('null', $violation->getValue()); 74 | } else { 75 | self::assertTrue(false, 'Bad violation type.'); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/DateTime/DateTimeValueTest.php: -------------------------------------------------------------------------------- 1 | getTimezone()->getName()); 25 | } 26 | 27 | public function testFromImmutable(): void 28 | { 29 | $immutable = new \DateTimeImmutable('2020-05-26 13:14:15', new \DateTimeZone('Europe/Prague')); 30 | $value = DateTimeValue::fromDateTimeImmutable($immutable); 31 | self::assertSame($immutable->getTimestamp(), $value->getTimestamp()); 32 | self::assertSame('UTC', $value->getTimezone()->getName()); 33 | } 34 | 35 | public function testFromImmutableAsObject(): void 36 | { 37 | $immutable = new \DateTimeImmutable('2020-05-26 13:14:15', new \DateTimeZone('Europe/Prague')); 38 | $value = DateTimeValue::fromObject($immutable); 39 | self::assertSame($immutable->getTimestamp(), $value->getTimestamp()); 40 | self::assertSame('UTC', $value->getTimezone()->getName()); 41 | } 42 | 43 | public function testFromMutable(): void 44 | { 45 | $mutable = new \DateTime('2020-05-26 13:14:15', new \DateTimeZone('Europe/Prague')); 46 | $value = DateTimeValue::fromDateTime($mutable); 47 | self::assertSame($mutable->getTimestamp(), $value->getTimestamp()); 48 | self::assertSame('UTC', $value->getTimezone()->getName()); 49 | } 50 | 51 | public function testFromMutableAsObject(): void 52 | { 53 | $mutable = new \DateTime('2020-05-26 13:14:15', new \DateTimeZone('Europe/Prague')); 54 | $value = DateTimeValue::fromObject($mutable); 55 | self::assertSame($mutable->getTimestamp(), $value->getTimestamp()); 56 | self::assertSame('UTC', $value->getTimezone()->getName()); 57 | } 58 | 59 | public function testFromFormat(): void 60 | { 61 | $value = DateTimeValue::fromFormat('d.m.Y H:i:s', '26.5.2020 13:14:15'); 62 | self::assertSame('2020-05-26T13:14:15+00:00', (string) $value); 63 | self::assertSame('UTC', $value->getTimezone()->getName()); 64 | } 65 | 66 | public function testJsonSerialization(): void 67 | { 68 | $value = DateTimeValue::fromString('2020-05-26 13:14:15'); 69 | self::assertSame('"2020-05-26T13:14:15+00:00"', \json_encode($value)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Type/ClassType.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 21 | $this->expectation = new TypeExpectation( 22 | $this->reflection->getName(), 23 | $this->reflection->implementsInterface(FromStringConstructableInterface::class), 24 | $this->reflection->implementsInterface(FromIntConstructableInterface::class), 25 | $this->reflection->implementsInterface(FromFloatConstructableInterface::class), 26 | $this->reflection->implementsInterface(FromBoolConstructableInterface::class), 27 | $this->reflection->implementsInterface(FromArrayConstructableInterface::class), 28 | ); 29 | } 30 | 31 | public function getExpectation(): TypeExpectationInterface 32 | { 33 | return $this->expectation; 34 | } 35 | 36 | public function prepareValue($value) 37 | { 38 | $expectation = $this->getExpectation(); 39 | 40 | if (\is_string($value)) { 41 | if ($expectation->acceptsString()) { 42 | return $this->reflection->getMethod('fromString')->invoke(null, $value); 43 | } 44 | } elseif (\is_int($value)) { 45 | if ($expectation->acceptsInt()) { 46 | return $this->reflection->getMethod('fromInt')->invoke(null, $value); 47 | } 48 | } elseif (\is_float($value)) { 49 | if ($expectation->acceptsFloat()) { 50 | return $this->reflection->getMethod('fromFloat')->invoke(null, $value); 51 | } 52 | } elseif (\is_array($value)) { 53 | if ($expectation->acceptsArray()) { 54 | return $this->reflection->getMethod('fromArray')->invoke(null, $value); 55 | } 56 | } elseif (\is_bool($value)) { 57 | if ($expectation->acceptsBool()) { 58 | return $this->reflection->getMethod('fromBool')->invoke(null, $value); 59 | } 60 | } elseif (\is_object($value)) { 61 | if ($this->reflection->implementsInterface(FromObjectConstructableInterface::class)) { 62 | return $this->reflection->getMethod('fromObject')->invoke(null, $value); 63 | } 64 | 65 | if (\is_a($value, $this->reflection->getName())) { 66 | return $value; 67 | } 68 | } elseif ($this->reflection->implementsInterface(FromAnyConstructableInterface::class)) { 69 | return $this->reflection->getMethod('fromAny')->invoke(null, $value); 70 | } 71 | 72 | throw TypeViolation::exception(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Enum/NamedConstructorsEnum.php: -------------------------------------------------------------------------------- 1 | getCode(), 50 | $e 51 | ); 52 | } 53 | } 54 | 55 | final protected static function createAllUniqueValues(): array 56 | { 57 | $all = []; 58 | $reflection = new \ReflectionClass(static::class); 59 | $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_STATIC); 60 | foreach ($methods as $method) { 61 | if (static::isUsableNamedConstructor($method)) { 62 | $methodName = $method->getName(); 63 | $all[] = $methodName; 64 | } 65 | } 66 | return $all; 67 | } 68 | 69 | /** 70 | * @param \ReflectionMethod $method 71 | * @return bool 72 | */ 73 | private static function isUsableNamedConstructor(\ReflectionMethod $method): bool 74 | { 75 | $parameters = $method->getParameters(); 76 | if (\count($parameters) !== 0) { 77 | return false; 78 | } 79 | $returnType = $method->getReturnType(); 80 | if ($returnType && $returnType instanceof \ReflectionNamedType) { 81 | $returnTypeName = $returnType->getName(); 82 | if ($returnTypeName === 'self' || $returnTypeName === 'static' || $returnTypeName === static::class) { 83 | return true; 84 | } 85 | } 86 | return false; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Collections/NestedViolation.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class NestedViolation extends Violation implements NestedViolationInterface 14 | { 15 | /** 16 | * @psalm-var TKey 17 | * @var mixed 18 | */ 19 | private $key; 20 | 21 | /** 22 | * @var mixed 23 | */ 24 | private $value; 25 | private TypeExpectationInterface $expectation; 26 | private array $violations; 27 | 28 | /** 29 | * @psalm-param TKey $key 30 | * @param mixed $key 31 | * @param TypeExpectationInterface $expectation 32 | * @param array $violations 33 | * @param mixed $value 34 | * @param string $message 35 | */ 36 | public function __construct( 37 | $key, 38 | TypeExpectationInterface $expectation, 39 | array $violations, 40 | $value = null, 41 | string $message = '' 42 | ) { 43 | $this->key = $key; 44 | $this->expectation = $expectation; 45 | $this->violations = $violations; 46 | $this->value = $value; 47 | parent::__construct($message ?: 'The collection is invalid.'); 48 | } 49 | 50 | public function getKey() 51 | { 52 | return $this->key; 53 | } 54 | 55 | public function getValue() 56 | { 57 | return $this->value; 58 | } 59 | 60 | public function getExpectation(): TypeExpectationInterface 61 | { 62 | return $this->expectation; 63 | } 64 | 65 | public function getViolations(): array 66 | { 67 | return $this->violations; 68 | } 69 | 70 | /** 71 | * @param int $key 72 | * @param TypeExpectationInterface $expectation 73 | * @param mixed $value 74 | * @param array $violations 75 | * @return self 76 | */ 77 | public static function invalidItem(int $key, TypeExpectationInterface $expectation, $value, array $violations): self 78 | { 79 | return new self($key, $expectation, $violations, $value); 80 | } 81 | 82 | /** 83 | * @param string $key 84 | * @param TypeExpectationInterface $expectation 85 | * @param mixed $value 86 | * @param array $violations 87 | * @return self 88 | */ 89 | public static function invalidProperty( 90 | string $key, 91 | TypeExpectationInterface $expectation, 92 | $value, 93 | array $violations 94 | ): self { 95 | return new self($key, $expectation, $violations, $value); 96 | } 97 | 98 | public static function missingRequiredProperty(string $key, TypeExpectationInterface $expectation): self 99 | { 100 | return new self($key, $expectation, [new MissingValue()]); 101 | } 102 | 103 | /** 104 | * @param string $key 105 | * @param mixed $value 106 | * @return self 107 | */ 108 | public static function unknownProperty(string $key, $value): self 109 | { 110 | return new self( 111 | $key, 112 | Type::forBuiltinType('void')->getExpectation(), 113 | [new Type\TypeViolation()], 114 | $value 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/Type/NullableTypeTest.php: -------------------------------------------------------------------------------- 1 | innerType = self::createMock(TypeInterface::class); 22 | $this->type = new NullableType($this->innerType); 23 | } 24 | 25 | public function testCreatesNullableExpectation(): void 26 | { 27 | $expectation = self::createMock(TypeExpectationInterface::class); 28 | $this->innerType->expects(self::once()) 29 | ->method('getExpectation') 30 | ->willReturn($expectation); 31 | self::assertInstanceOf(NullableTypeExpectation::class, $this->type->getExpectation()); 32 | } 33 | 34 | public function testAcceptsNull(): void 35 | { 36 | $this->innerType->expects(self::never())->method('getExpectation'); 37 | $this->innerType->expects(self::never())->method('prepareValue'); 38 | self::assertNull($this->type->prepareValue(null)); 39 | } 40 | 41 | public function testAcceptsBool(): void 42 | { 43 | $input = true; 44 | $this->innerType->expects(self::once())->method('prepareValue') 45 | ->with($input) 46 | ->willReturn($input); 47 | self::assertSame($input, $this->type->prepareValue($input)); 48 | } 49 | 50 | public function testAcceptsInt(): void 51 | { 52 | $input = 10; 53 | $this->innerType->expects(self::once())->method('prepareValue') 54 | ->with($input) 55 | ->willReturn($input); 56 | self::assertSame($input, $this->type->prepareValue($input)); 57 | } 58 | 59 | public function testAcceptsFloat(): void 60 | { 61 | $input = 11.1; 62 | $this->innerType->expects(self::once())->method('prepareValue') 63 | ->with($input) 64 | ->willReturn($input); 65 | self::assertSame($input, $this->type->prepareValue($input)); 66 | } 67 | 68 | public function testAcceptsString(): void 69 | { 70 | $input = 'test'; 71 | $this->innerType->expects(self::once())->method('prepareValue') 72 | ->with($input) 73 | ->willReturn($input); 74 | self::assertSame($input, $this->type->prepareValue($input)); 75 | } 76 | 77 | public function testAcceptsArray(): void 78 | { 79 | $input = ['value']; 80 | $this->innerType->expects(self::once())->method('prepareValue') 81 | ->with($input) 82 | ->willReturn($input); 83 | self::assertSame($input, $this->type->prepareValue($input)); 84 | } 85 | 86 | public function testAcceptsObject(): void 87 | { 88 | $input = (object) ['test' => 'value']; 89 | $this->innerType->expects(self::once())->method('prepareValue') 90 | ->with($input) 91 | ->willReturn($input); 92 | self::assertSame($input, $this->type->prepareValue($input)); 93 | } 94 | 95 | public function testNotAcceptsString(): void 96 | { 97 | $input = 'test'; 98 | $e = TypeViolation::exception(); 99 | $this->innerType->method('prepareValue') 100 | ->with($input) 101 | ->willThrowException($e); 102 | 103 | try { 104 | $this->type->prepareValue($input); 105 | self::assertTrue(false, 'Not thrown.'); 106 | } catch (ViolationExceptionInterface $caught) { 107 | self::assertSame($e, $caught); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/Collections/ArrayMapTest.php: -------------------------------------------------------------------------------- 1 | toArray()); 36 | } 37 | 38 | public function testThatCanConstructFromSameValueTypeArray(): void 39 | { 40 | $input = ['a' => 1, 'b' => 2, 'c' => 3]; 41 | $map = new class ($input) extends ArrayMap { 42 | public function current(): int 43 | { 44 | return parent::current(); 45 | } 46 | }; 47 | 48 | self::assertSame($input, $map->toArray()); 49 | } 50 | 51 | public function testThatCanConstructFromUsingDowncasting(): void 52 | { 53 | $input = [ 54 | 'a' => new IntegerValue(1), 55 | 'b' => new IntegerValue(2), 56 | 'c' => new IntegerValue(3), 57 | ]; 58 | $map = new class ($input) extends ArrayMap { 59 | public function current(): int 60 | { 61 | return parent::current(); 62 | } 63 | }; 64 | 65 | self::assertEquals(['a' => 1, 'b' => 2, 'c' => 3], $map->toArray()); 66 | } 67 | 68 | public function testThatCannotCreateWithInvalidItems(): void 69 | { 70 | try { 71 | new class (['a' => 1, 'b' => 2.0, 'c' => '3']) extends ArrayMap { 72 | public function current(): int 73 | { 74 | return parent::current(); 75 | } 76 | }; 77 | } catch (ViolationExceptionInterface $e) { 78 | $violations = $e->getViolations(); 79 | self::assertCount(2, $violations); 80 | 81 | $violation = \array_shift($violations); 82 | if ($violation instanceof NestedViolationInterface) { 83 | self::assertSame('b', $violation->getKey()); 84 | self::assertSame(2.0, $violation->getValue()); 85 | $subViolations = $violation->getViolations(); 86 | self::assertCount(1, $subViolations); 87 | $subViolation = \reset($subViolations); 88 | self::assertInstanceOf(TypeViolation::class, $subViolation); 89 | } else { 90 | self::assertTrue(false, 'Bad violation type.'); 91 | } 92 | 93 | $violation = \array_shift($violations); 94 | if ($violation instanceof NestedViolationInterface) { 95 | self::assertSame('c', $violation->getKey()); 96 | self::assertSame('3', $violation->getValue()); 97 | $subViolations = $violation->getViolations(); 98 | self::assertCount(1, $subViolations); 99 | $subViolation = \reset($subViolations); 100 | self::assertInstanceOf(TypeViolation::class, $subViolation); 101 | } else { 102 | self::assertTrue(false, 'Bad violation type.'); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Collections/ListOfIntsTest.php: -------------------------------------------------------------------------------- 1 | toArray()); 17 | } 18 | 19 | public function testOneElementOk(): void 20 | { 21 | $list = new ListOfInts([10]); 22 | self::assertSame([10], $list->toArray()); 23 | } 24 | 25 | public function testManyElementOk(): void 26 | { 27 | $list = new ListOfInts([10, 100, 5]); 28 | self::assertSame([10, 100, 5], $list->toArray()); 29 | } 30 | 31 | public function testJsonSerialization(): void 32 | { 33 | $list = new ListOfInts([10, 100, 5]); 34 | self::assertSame(\json_encode([10, 100, 5]), \json_encode($list)); 35 | } 36 | 37 | public function testStringElementFails(): void 38 | { 39 | try { 40 | new ListOfInts(['test']); 41 | self::assertTrue(false, 'Exception not thrown.'); 42 | } catch (ViolationExceptionInterface $e) { 43 | $violations = $e->getViolations(); 44 | self::assertCount(1, $violations); 45 | $violation = \reset($violations); 46 | if ($violation instanceof NestedViolationInterface) { 47 | self::assertSame(0, $violation->getKey()); 48 | self::assertSame('test', $violation->getValue()); 49 | $subViolations = $violation->getViolations(); 50 | self::assertCount(1, $subViolations); 51 | $subViolation = \reset($subViolations); 52 | self::assertInstanceOf(TypeViolation::class, $subViolation); 53 | } else { 54 | self::assertTrue(false, 'Invalid violation type'); 55 | } 56 | } 57 | } 58 | 59 | public function testMultipleElementViolations(): void 60 | { 61 | try { 62 | new ListOfInts([1, 2, 'test', 5, 11.1, 10]); 63 | self::assertTrue(false, 'Exception not thrown.'); 64 | } catch (ViolationExceptionInterface $e) { 65 | $violations = $e->getViolations(); 66 | self::assertCount(2, $violations); 67 | $violation = \array_shift($violations); 68 | if ($violation instanceof NestedViolationInterface) { 69 | self::assertSame(2, $violation->getKey()); 70 | self::assertSame('test', $violation->getValue()); 71 | $subViolations = $violation->getViolations(); 72 | self::assertCount(1, $subViolations); 73 | $subViolation = \reset($subViolations); 74 | self::assertInstanceOf(TypeViolation::class, $subViolation); 75 | } else { 76 | self::assertTrue(false, 'Invalid violation type'); 77 | } 78 | $violation = \array_shift($violations); 79 | if ($violation instanceof NestedViolationInterface) { 80 | self::assertSame(4, $violation->getKey()); 81 | self::assertSame(11.1, $violation->getValue()); 82 | $subViolations = $violation->getViolations(); 83 | self::assertCount(1, $subViolations); 84 | $subViolation = \reset($subViolations); 85 | self::assertInstanceOf(TypeViolation::class, $subViolation); 86 | } else { 87 | self::assertTrue(false, 'Invalid violation type'); 88 | } 89 | } 90 | } 91 | 92 | public function testAssociativeArrayFails(): void 93 | { 94 | try { 95 | new ListOfInts([1, 2, 3, 'test' => 10, 10, 11]); 96 | self::assertTrue(false, 'Exception not thrown.'); 97 | } catch (ViolationExceptionInterface $e) { 98 | $violations = $e->getViolations(); 99 | self::assertCount(1, $violations); 100 | $violation = \reset($violations); 101 | self::assertInstanceOf(TypeViolation::class, $violation); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Type.php: -------------------------------------------------------------------------------- 1 | isBuiltin()) { 31 | $type = self::forBuiltinType($reflectionType->getName()); 32 | } else { 33 | /** @psalm-suppress ArgumentTypeCoercion */ 34 | $type = self::forClass($reflectionType->getName()); 35 | } 36 | 37 | if ($reflectionType->allowsNull()) { 38 | return new NullableType($type); 39 | } 40 | 41 | return $type; 42 | } 43 | 44 | /** 45 | * @psalm-param class-string $class 46 | * @param string $class 47 | * @return TypeInterface 48 | */ 49 | public static function forClass(string $class): TypeInterface 50 | { 51 | $reflection = new \ReflectionClass($class); 52 | return self::forClassReflection($reflection); 53 | } 54 | 55 | public static function forBuiltinType(string $name): TypeInterface 56 | { 57 | switch ($name) { 58 | case self::PHP_STRING: 59 | return new StringType(); 60 | case self::PHP_INT: 61 | return new IntType(); 62 | case self::PHP_FLOAT: 63 | return new FloatType(); 64 | case self::PHP_BOOL: 65 | return new BoolType(); 66 | case self::PHP_ARRAY: 67 | return new ArrayType(); 68 | case self::PHP_VOID: 69 | return new VoidType(); 70 | default: 71 | throw new \InvalidArgumentException("Not a builtin type: $name."); 72 | } 73 | } 74 | 75 | private static function forClassReflection(\ReflectionClass $class): TypeInterface 76 | { 77 | return new ClassType($class); 78 | } 79 | 80 | public static function forPropertyReflection(\ReflectionProperty $property): TypeInterface 81 | { 82 | $key = $property->getName(); 83 | 84 | if (!$property->hasType()) { 85 | throw new \RuntimeException( 86 | "Property $key is missing type hint." 87 | ); 88 | } 89 | 90 | $type = $property->getType(); 91 | if (!$type instanceof \ReflectionNamedType) { 92 | throw new \RuntimeException('ReflectionNamedType is not supported.'); 93 | } 94 | 95 | return self::forTypeReflection($type); 96 | } 97 | 98 | public static function forMethodReturnType(\ReflectionMethod $method): TypeInterface 99 | { 100 | $type = $method->getReturnType(); 101 | if ($type === null) { 102 | throw new \UnexpectedValueException( 103 | 'Method ' . $method->getName() . ' does not have a return type.' 104 | ); 105 | } 106 | 107 | if (!$type instanceof \ReflectionNamedType) { 108 | throw new \RuntimeException('ReflectionNamedType not supported.'); 109 | } 110 | 111 | return self::forTypeReflection($type); 112 | } 113 | 114 | 115 | public static function forMethodParameterType(\ReflectionParameter $parameter): TypeInterface 116 | { 117 | $type = $parameter->getType(); 118 | if ($type === null) { 119 | throw new \UnexpectedValueException('Method current does not have a return type.'); 120 | } 121 | 122 | if (!$type instanceof \ReflectionNamedType) { 123 | throw new \RuntimeException('ReflectionNamedType not supported.'); 124 | } 125 | 126 | return self::forTypeReflection($type); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/Type/VoidTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new VoidType(); 18 | } 19 | 20 | public function testTypeExpectation(): void 21 | { 22 | $expectation = $this->type->getExpectation(); 23 | self::assertFalse($expectation->acceptsNull()); 24 | self::assertFalse($expectation->acceptsInt()); 25 | self::assertFalse($expectation->acceptsString()); 26 | self::assertFalse($expectation->acceptsFloat()); 27 | self::assertFalse($expectation->acceptsBool()); 28 | self::assertFalse($expectation->acceptsArray()); 29 | // @todo objects 30 | } 31 | 32 | public function testIntFails(): void 33 | { 34 | try { 35 | $this->type->prepareValue(10); 36 | self::assertTrue(false, 'Exception not thrown.'); 37 | } catch (ViolationExceptionInterface $e) { 38 | $violations = $e->getViolations(); 39 | self::assertCount(1, $violations); 40 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 41 | } 42 | } 43 | 44 | public function testNullFails(): void 45 | { 46 | try { 47 | $this->type->prepareValue(null); 48 | self::assertTrue(false, 'Exception not thrown.'); 49 | } catch (ViolationExceptionInterface $e) { 50 | $violations = $e->getViolations(); 51 | self::assertCount(1, $violations); 52 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 53 | } 54 | } 55 | 56 | public function testStringFails(): void 57 | { 58 | try { 59 | $this->type->prepareValue(''); 60 | self::assertTrue(false, 'Exception not thrown.'); 61 | } catch (ViolationExceptionInterface $e) { 62 | $violations = $e->getViolations(); 63 | self::assertCount(1, $violations); 64 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 65 | } 66 | } 67 | 68 | public function testFloatFails(): void 69 | { 70 | try { 71 | $this->type->prepareValue(11.1); 72 | self::assertTrue(false, 'Exception not thrown.'); 73 | } catch (ViolationExceptionInterface $e) { 74 | $violations = $e->getViolations(); 75 | self::assertCount(1, $violations); 76 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 77 | } 78 | } 79 | 80 | public function testBoolFails(): void 81 | { 82 | try { 83 | $this->type->prepareValue(true); 84 | self::assertTrue(false, 'Exception not thrown.'); 85 | } catch (ViolationExceptionInterface $e) { 86 | $violations = $e->getViolations(); 87 | self::assertCount(1, $violations); 88 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 89 | } 90 | } 91 | 92 | public function testArrayFails(): void 93 | { 94 | try { 95 | $this->type->prepareValue([]); 96 | self::assertTrue(false, 'Exception not thrown.'); 97 | } catch (ViolationExceptionInterface $e) { 98 | $violations = $e->getViolations(); 99 | self::assertCount(1, $violations); 100 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 101 | } 102 | } 103 | 104 | public function testObjectFails(): void 105 | { 106 | try { 107 | $this->type->prepareValue((object) []); 108 | self::assertTrue(false, 'Exception not thrown.'); 109 | } catch (ViolationExceptionInterface $e) { 110 | $violations = $e->getViolations(); 111 | self::assertCount(1, $violations); 112 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 113 | } 114 | } 115 | 116 | public function testStringableObjectFails(): void 117 | { 118 | try { 119 | $this->type->prepareValue(new class () { 120 | public function __toString(): string 121 | { 122 | return '10'; 123 | } 124 | }); 125 | self::assertTrue(false, 'Exception not thrown.'); 126 | } catch (ViolationExceptionInterface $e) { 127 | $violations = $e->getViolations(); 128 | self::assertCount(1, $violations); 129 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Type/BoolTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new BoolType(); 19 | } 20 | 21 | public function testTypeExpectation(): void 22 | { 23 | $expectation = $this->type->getExpectation(); 24 | self::assertFalse($expectation->acceptsNull()); 25 | self::assertFalse($expectation->acceptsInt()); 26 | self::assertFalse($expectation->acceptsString()); 27 | self::assertFalse($expectation->acceptsFloat()); 28 | self::assertTrue($expectation->acceptsBool()); 29 | self::assertFalse($expectation->acceptsArray()); 30 | // @todo objects 31 | } 32 | 33 | public function testBoolSuccess(): void 34 | { 35 | self::assertSame(true, $this->type->prepareValue(true)); 36 | } 37 | 38 | public function testConvertibleToBoolSuccess(): void 39 | { 40 | $input = new class () implements ToBoolConvertibleInterface { 41 | public function toBool(): bool 42 | { 43 | return true; 44 | } 45 | }; 46 | self::assertSame(true, $this->type->prepareValue($input)); 47 | } 48 | 49 | public function testConvertibleToBoolWithoutInterfaceFails(): void 50 | { 51 | $input = new class () { 52 | public function toBool(): bool 53 | { 54 | return true; 55 | } 56 | }; 57 | try { 58 | $this->type->prepareValue($input); 59 | self::assertTrue(false, 'Exception not thrown.'); 60 | } catch (ViolationExceptionInterface $e) { 61 | $violations = $e->getViolations(); 62 | self::assertCount(1, $violations); 63 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 64 | } 65 | } 66 | 67 | public function testNullFails(): void 68 | { 69 | try { 70 | $this->type->prepareValue(null); 71 | self::assertTrue(false, 'Exception not thrown.'); 72 | } catch (ViolationExceptionInterface $e) { 73 | $violations = $e->getViolations(); 74 | self::assertCount(1, $violations); 75 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 76 | } 77 | } 78 | 79 | public function testIntFails(): void 80 | { 81 | try { 82 | $this->type->prepareValue(10); 83 | self::assertTrue(false, 'Exception not thrown.'); 84 | } catch (ViolationExceptionInterface $e) { 85 | $violations = $e->getViolations(); 86 | self::assertCount(1, $violations); 87 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 88 | } 89 | } 90 | 91 | public function testFloatFails(): void 92 | { 93 | try { 94 | $this->type->prepareValue(11.1); 95 | self::assertTrue(false, 'Exception not thrown.'); 96 | } catch (ViolationExceptionInterface $e) { 97 | $violations = $e->getViolations(); 98 | self::assertCount(1, $violations); 99 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 100 | } 101 | } 102 | 103 | public function testStringFails(): void 104 | { 105 | try { 106 | $this->type->prepareValue('true'); 107 | self::assertTrue(false, 'Exception not thrown.'); 108 | } catch (ViolationExceptionInterface $e) { 109 | $violations = $e->getViolations(); 110 | self::assertCount(1, $violations); 111 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 112 | } 113 | } 114 | 115 | public function testArrayFails(): void 116 | { 117 | try { 118 | $this->type->prepareValue([]); 119 | self::assertTrue(false, 'Exception not thrown.'); 120 | } catch (ViolationExceptionInterface $e) { 121 | $violations = $e->getViolations(); 122 | self::assertCount(1, $violations); 123 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 124 | } 125 | } 126 | 127 | public function testObjectFails(): void 128 | { 129 | try { 130 | $this->type->prepareValue((object) []); 131 | self::assertTrue(false, 'Exception not thrown.'); 132 | } catch (ViolationExceptionInterface $e) { 133 | $violations = $e->getViolations(); 134 | self::assertCount(1, $violations); 135 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/Type/ArrayTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new ArrayType(); 19 | } 20 | 21 | public function testTypeExpectation(): void 22 | { 23 | $expectation = $this->type->getExpectation(); 24 | self::assertFalse($expectation->acceptsNull()); 25 | self::assertFalse($expectation->acceptsInt()); 26 | self::assertFalse($expectation->acceptsString()); 27 | self::assertFalse($expectation->acceptsFloat()); 28 | self::assertFalse($expectation->acceptsBool()); 29 | self::assertTrue($expectation->acceptsArray()); 30 | // @todo objects 31 | } 32 | 33 | public function testArraySuccess(): void 34 | { 35 | self::assertSame(['value'], $this->type->prepareValue(['value'])); 36 | } 37 | 38 | public function testConvertibleToArraySuccess(): void 39 | { 40 | $input = new class () implements ToArrayConvertibleInterface { 41 | public function toArray(): array 42 | { 43 | return ['value']; 44 | } 45 | }; 46 | self::assertSame(['value'], $this->type->prepareValue($input)); 47 | } 48 | 49 | public function testConvertibleToArrayWithoutInterfaceFails(): void 50 | { 51 | $input = new class () { 52 | public function toArray(): array 53 | { 54 | return ['value']; 55 | } 56 | }; 57 | 58 | try { 59 | $this->type->prepareValue($input); 60 | self::assertTrue(false, 'Exception not thrown.'); 61 | } catch (ViolationExceptionInterface $e) { 62 | $violations = $e->getViolations(); 63 | self::assertCount(1, $violations); 64 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 65 | } 66 | } 67 | 68 | public function testNullFails(): void 69 | { 70 | try { 71 | $this->type->prepareValue(null); 72 | self::assertTrue(false, 'Exception not thrown.'); 73 | } catch (ViolationExceptionInterface $e) { 74 | $violations = $e->getViolations(); 75 | self::assertCount(1, $violations); 76 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 77 | } 78 | } 79 | 80 | public function testIntFails(): void 81 | { 82 | try { 83 | $this->type->prepareValue(10); 84 | self::assertTrue(false, 'Exception not thrown.'); 85 | } catch (ViolationExceptionInterface $e) { 86 | $violations = $e->getViolations(); 87 | self::assertCount(1, $violations); 88 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 89 | } 90 | } 91 | 92 | public function testFloatFails(): void 93 | { 94 | try { 95 | $this->type->prepareValue(11.1); 96 | self::assertTrue(false, 'Exception not thrown.'); 97 | } catch (ViolationExceptionInterface $e) { 98 | $violations = $e->getViolations(); 99 | self::assertCount(1, $violations); 100 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 101 | } 102 | } 103 | 104 | public function testBoolFails(): void 105 | { 106 | try { 107 | $this->type->prepareValue(true); 108 | self::assertTrue(false, 'Exception not thrown.'); 109 | } catch (ViolationExceptionInterface $e) { 110 | $violations = $e->getViolations(); 111 | self::assertCount(1, $violations); 112 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 113 | } 114 | } 115 | 116 | public function testStringFails(): void 117 | { 118 | try { 119 | $this->type->prepareValue(''); 120 | self::assertTrue(false, 'Exception not thrown.'); 121 | } catch (ViolationExceptionInterface $e) { 122 | $violations = $e->getViolations(); 123 | self::assertCount(1, $violations); 124 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 125 | } 126 | } 127 | 128 | public function testObjectFails(): void 129 | { 130 | try { 131 | $this->type->prepareValue((object) []); 132 | self::assertTrue(false, 'Exception not thrown.'); 133 | } catch (ViolationExceptionInterface $e) { 134 | $violations = $e->getViolations(); 135 | self::assertCount(1, $violations); 136 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/Type/StringTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new StringType(); 19 | } 20 | 21 | public function testTypeExpectation(): void 22 | { 23 | $expectation = $this->type->getExpectation(); 24 | self::assertFalse($expectation->acceptsNull()); 25 | self::assertFalse($expectation->acceptsInt()); 26 | self::assertTrue($expectation->acceptsString()); 27 | self::assertFalse($expectation->acceptsFloat()); 28 | self::assertFalse($expectation->acceptsBool()); 29 | self::assertFalse($expectation->acceptsArray()); 30 | // @todo objects 31 | } 32 | 33 | public function testStringSuccess(): void 34 | { 35 | self::assertSame('test', $this->type->prepareValue('test')); 36 | } 37 | 38 | public function testConvertibleToStringSuccess(): void 39 | { 40 | $input = new class () implements ToStringConvertibleInterface { 41 | public function __toString(): string 42 | { 43 | return 'test'; 44 | } 45 | }; 46 | self::assertSame('test', $this->type->prepareValue($input)); 47 | } 48 | 49 | public function testConvertibleToStringWithoutInterfaceFails(): void 50 | { 51 | $input = new class () { 52 | public function __toString(): string 53 | { 54 | return 'test'; 55 | } 56 | }; 57 | 58 | try { 59 | $this->type->prepareValue($input); 60 | self::assertTrue(false, 'Exception not thrown.'); 61 | } catch (ViolationExceptionInterface $e) { 62 | $violations = $e->getViolations(); 63 | self::assertCount(1, $violations); 64 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 65 | } 66 | } 67 | 68 | public function testNullFails(): void 69 | { 70 | try { 71 | $this->type->prepareValue(null); 72 | self::assertTrue(false, 'Exception not thrown.'); 73 | } catch (ViolationExceptionInterface $e) { 74 | $violations = $e->getViolations(); 75 | self::assertCount(1, $violations); 76 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 77 | } 78 | } 79 | 80 | public function testIntFails(): void 81 | { 82 | try { 83 | $this->type->prepareValue(10); 84 | self::assertTrue(false, 'Exception not thrown.'); 85 | } catch (ViolationExceptionInterface $e) { 86 | $violations = $e->getViolations(); 87 | self::assertCount(1, $violations); 88 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 89 | } 90 | } 91 | 92 | public function testFloatFails(): void 93 | { 94 | try { 95 | $this->type->prepareValue(11.1); 96 | self::assertTrue(false, 'Exception not thrown.'); 97 | } catch (ViolationExceptionInterface $e) { 98 | $violations = $e->getViolations(); 99 | self::assertCount(1, $violations); 100 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 101 | } 102 | } 103 | 104 | public function testBoolFails(): void 105 | { 106 | try { 107 | $this->type->prepareValue(true); 108 | self::assertTrue(false, 'Exception not thrown.'); 109 | } catch (ViolationExceptionInterface $e) { 110 | $violations = $e->getViolations(); 111 | self::assertCount(1, $violations); 112 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 113 | } 114 | } 115 | 116 | public function testArrayFails(): void 117 | { 118 | try { 119 | $this->type->prepareValue([]); 120 | self::assertTrue(false, 'Exception not thrown.'); 121 | } catch (ViolationExceptionInterface $e) { 122 | $violations = $e->getViolations(); 123 | self::assertCount(1, $violations); 124 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 125 | } 126 | } 127 | 128 | public function testObjectFails(): void 129 | { 130 | try { 131 | $this->type->prepareValue((object) []); 132 | self::assertTrue(false, 'Exception not thrown.'); 133 | } catch (ViolationExceptionInterface $e) { 134 | $violations = $e->getViolations(); 135 | self::assertCount(1, $violations); 136 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/Type/IntTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new IntType(); 19 | } 20 | 21 | public function testTypeExpectation(): void 22 | { 23 | $expectation = $this->type->getExpectation(); 24 | self::assertFalse($expectation->acceptsNull()); 25 | self::assertTrue($expectation->acceptsInt()); 26 | self::assertFalse($expectation->acceptsString()); 27 | self::assertFalse($expectation->acceptsFloat()); 28 | self::assertFalse($expectation->acceptsBool()); 29 | self::assertFalse($expectation->acceptsArray()); 30 | // @todo objects 31 | } 32 | 33 | public function testIntSuccess(): void 34 | { 35 | self::assertSame(10, $this->type->prepareValue(10)); 36 | } 37 | 38 | public function testConvertibleToIntSuccess(): void 39 | { 40 | $input = new class () implements ToIntConvertibleInterface { 41 | public function toInt(): int 42 | { 43 | return 10; 44 | } 45 | }; 46 | self::assertSame(10, $this->type->prepareValue($input)); 47 | } 48 | 49 | public function testConvertibleToIntWithoutInterfaceFails(): void 50 | { 51 | $input = new class () { 52 | public function toInt(): int 53 | { 54 | return 10; 55 | } 56 | }; 57 | try { 58 | $this->type->prepareValue($input); 59 | self::assertTrue(false, 'Exception not thrown.'); 60 | } catch (ViolationExceptionInterface $e) { 61 | $violations = $e->getViolations(); 62 | self::assertCount(1, $violations); 63 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 64 | } 65 | } 66 | 67 | public function testNullFails(): void 68 | { 69 | try { 70 | $this->type->prepareValue(null); 71 | self::assertTrue(false, 'Exception not thrown.'); 72 | } catch (ViolationExceptionInterface $e) { 73 | $violations = $e->getViolations(); 74 | self::assertCount(1, $violations); 75 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 76 | } 77 | } 78 | 79 | public function testStringFails(): void 80 | { 81 | try { 82 | $this->type->prepareValue(''); 83 | self::assertTrue(false, 'Exception not thrown.'); 84 | } catch (ViolationExceptionInterface $e) { 85 | $violations = $e->getViolations(); 86 | self::assertCount(1, $violations); 87 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 88 | } 89 | } 90 | 91 | public function testFloatFails(): void 92 | { 93 | try { 94 | $this->type->prepareValue(11.1); 95 | self::assertTrue(false, 'Exception not thrown.'); 96 | } catch (ViolationExceptionInterface $e) { 97 | $violations = $e->getViolations(); 98 | self::assertCount(1, $violations); 99 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 100 | } 101 | } 102 | 103 | public function testBoolFails(): void 104 | { 105 | try { 106 | $this->type->prepareValue(true); 107 | self::assertTrue(false, 'Exception not thrown.'); 108 | } catch (ViolationExceptionInterface $e) { 109 | $violations = $e->getViolations(); 110 | self::assertCount(1, $violations); 111 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 112 | } 113 | } 114 | 115 | public function testArrayFails(): void 116 | { 117 | try { 118 | $this->type->prepareValue([]); 119 | self::assertTrue(false, 'Exception not thrown.'); 120 | } catch (ViolationExceptionInterface $e) { 121 | $violations = $e->getViolations(); 122 | self::assertCount(1, $violations); 123 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 124 | } 125 | } 126 | 127 | public function testObjectFails(): void 128 | { 129 | try { 130 | $this->type->prepareValue((object) []); 131 | self::assertTrue(false, 'Exception not thrown.'); 132 | } catch (ViolationExceptionInterface $e) { 133 | $violations = $e->getViolations(); 134 | self::assertCount(1, $violations); 135 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 136 | } 137 | } 138 | 139 | public function testStringableObjectFails(): void 140 | { 141 | try { 142 | $this->type->prepareValue(new class () { 143 | public function __toString(): string 144 | { 145 | return '10'; 146 | } 147 | }); 148 | self::assertTrue(false, 'Exception not thrown.'); 149 | } catch (ViolationExceptionInterface $e) { 150 | $violations = $e->getViolations(); 151 | self::assertCount(1, $violations); 152 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/Type/FloatTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new FloatType(); 19 | } 20 | 21 | public function testTypeExpectation(): void 22 | { 23 | $expectation = $this->type->getExpectation(); 24 | self::assertFalse($expectation->acceptsNull()); 25 | self::assertFalse($expectation->acceptsInt()); 26 | self::assertFalse($expectation->acceptsString()); 27 | self::assertTrue($expectation->acceptsFloat()); 28 | self::assertFalse($expectation->acceptsBool()); 29 | self::assertFalse($expectation->acceptsArray()); 30 | // @todo objects 31 | } 32 | 33 | public function testFloatSuccess(): void 34 | { 35 | self::assertSame(11.1, $this->type->prepareValue(11.1)); 36 | } 37 | 38 | public function testConvertibleToFloatSuccess(): void 39 | { 40 | $input = new class () implements ToFloatConvertibleInterface { 41 | public function toFloat(): float 42 | { 43 | return 11.1; 44 | } 45 | }; 46 | self::assertSame(11.1, $this->type->prepareValue($input)); 47 | } 48 | 49 | public function testConvertibleToFloatWithoutInterfaceFails(): void 50 | { 51 | $input = new class () { 52 | public function toFloat(): float 53 | { 54 | return 11.1; 55 | } 56 | }; 57 | try { 58 | $this->type->prepareValue($input); 59 | self::assertTrue(false, 'Exception not thrown.'); 60 | } catch (ViolationExceptionInterface $e) { 61 | $violations = $e->getViolations(); 62 | self::assertCount(1, $violations); 63 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 64 | } 65 | } 66 | 67 | public function testNullFails(): void 68 | { 69 | try { 70 | $this->type->prepareValue(null); 71 | self::assertTrue(false, 'Exception not thrown.'); 72 | } catch (ViolationExceptionInterface $e) { 73 | $violations = $e->getViolations(); 74 | self::assertCount(1, $violations); 75 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 76 | } 77 | } 78 | 79 | public function testStringFails(): void 80 | { 81 | try { 82 | $this->type->prepareValue(''); 83 | self::assertTrue(false, 'Exception not thrown.'); 84 | } catch (ViolationExceptionInterface $e) { 85 | $violations = $e->getViolations(); 86 | self::assertCount(1, $violations); 87 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 88 | } 89 | } 90 | 91 | public function testIntFails(): void 92 | { 93 | try { 94 | $this->type->prepareValue(10); 95 | self::assertTrue(false, 'Exception not thrown.'); 96 | } catch (ViolationExceptionInterface $e) { 97 | $violations = $e->getViolations(); 98 | self::assertCount(1, $violations); 99 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 100 | } 101 | } 102 | 103 | public function testBoolFails(): void 104 | { 105 | try { 106 | $this->type->prepareValue(true); 107 | self::assertTrue(false, 'Exception not thrown.'); 108 | } catch (ViolationExceptionInterface $e) { 109 | $violations = $e->getViolations(); 110 | self::assertCount(1, $violations); 111 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 112 | } 113 | } 114 | 115 | public function testArrayFails(): void 116 | { 117 | try { 118 | $this->type->prepareValue([]); 119 | self::assertTrue(false, 'Exception not thrown.'); 120 | } catch (ViolationExceptionInterface $e) { 121 | $violations = $e->getViolations(); 122 | self::assertCount(1, $violations); 123 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 124 | } 125 | } 126 | 127 | public function testObjectFails(): void 128 | { 129 | try { 130 | $this->type->prepareValue((object) []); 131 | self::assertTrue(false, 'Exception not thrown.'); 132 | } catch (ViolationExceptionInterface $e) { 133 | $violations = $e->getViolations(); 134 | self::assertCount(1, $violations); 135 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 136 | } 137 | } 138 | 139 | public function testStringableObjectFails(): void 140 | { 141 | try { 142 | $this->type->prepareValue(new class () { 143 | public function __toString(): string 144 | { 145 | return '11.1'; 146 | } 147 | }); 148 | self::assertTrue(false, 'Exception not thrown.'); 149 | } catch (ViolationExceptionInterface $e) { 150 | $violations = $e->getViolations(); 151 | self::assertCount(1, $violations); 152 | self::assertInstanceOf(TypeViolation::class, \reset($violations)); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/DateTime/DateTimeValue.php: -------------------------------------------------------------------------------- 1 | value = $value; 25 | } 26 | 27 | private static function timezone(): \DateTimeZone 28 | { 29 | return new \DateTimeZone((string) static::TIMEZONE); 30 | } 31 | 32 | /** 33 | * @param string $value 34 | * @return static 35 | * @throws \Slepic\ValueObject\ViolationExceptionInterface 36 | */ 37 | public static function fromString(string $value): self 38 | { 39 | try { 40 | $dateTime = new \DateTimeImmutable($value, static::timezone()); 41 | } catch (\Exception $e) { 42 | throw DateTimeFormatViolation::exception((string) static::FORMAT); 43 | } 44 | 45 | return new static($dateTime); 46 | } 47 | 48 | /** 49 | * @param string $format 50 | * @param string $value 51 | * @return static 52 | * @throws \Slepic\ValueObject\ViolationExceptionInterface 53 | */ 54 | final public static function fromFormat(string $format, string $value): self 55 | { 56 | $dateTime = \DateTimeImmutable::createFromFormat($format, $value); 57 | 58 | if ($dateTime === false) { 59 | throw DateTimeFormatViolation::exception($format); 60 | } 61 | 62 | return new static($dateTime); 63 | } 64 | 65 | final public static function fromDateTimeImmutable(\DateTimeImmutable $value): self 66 | { 67 | if ($value->getTimezone()->getName() !== 'UTC') { 68 | $value = $value->setTimezone(static::timezone()); 69 | } 70 | return new static($value); 71 | } 72 | 73 | final public static function fromDateTime(\DateTime $value): self 74 | { 75 | $immutable = \DateTimeImmutable::createFromMutable($value); 76 | return static::fromDateTimeImmutable($immutable); 77 | } 78 | 79 | final public static function fromDateTimeInterface(\DateTimeInterface $value): self 80 | { 81 | $dateTime = new \DateTimeImmutable('now', static::timezone()); 82 | $dateTime = $dateTime->setTimestamp($value->getTimestamp()); 83 | return new static($dateTime); 84 | } 85 | 86 | /** 87 | * @psalm-suppress LessSpecificImplementedReturnType 88 | * @param object $value 89 | * @return self 90 | * @throws \Slepic\ValueObject\ViolationExceptionInterface 91 | */ 92 | public static function fromObject(object $value): self 93 | { 94 | if ($value instanceof \DateTimeImmutable) { 95 | return static::fromDateTimeImmutable($value); 96 | } 97 | 98 | if ($value instanceof \DateTime) { 99 | return static::fromDateTime($value); 100 | } 101 | 102 | if ($value instanceof \DateTimeInterface) { 103 | return static::fromDateTimeInterface($value); 104 | } 105 | 106 | throw ViolationException::for(new TypeViolation()); 107 | } 108 | 109 | /** 110 | * @psalm-suppress InvalidToString 111 | * @return string 112 | */ 113 | final public function __toString(): string 114 | { 115 | return $this->value->format((string) static::FORMAT); 116 | } 117 | 118 | final public function jsonSerialize(): string 119 | { 120 | return (string) $this; 121 | } 122 | 123 | final public function toDateTimeImmutable(): \DateTimeImmutable 124 | { 125 | return $this->value; 126 | } 127 | 128 | final public function getOffset(): int 129 | { 130 | return $this->value->getOffset(); 131 | } 132 | 133 | final public function getTimestamp(): int 134 | { 135 | return $this->value->getTimestamp(); 136 | } 137 | 138 | final public function setTimestamp(int $timestamp): self 139 | { 140 | return new static($this->value->setTimestamp($timestamp)); 141 | } 142 | 143 | final public function getTimezone(): \DateTimeZone 144 | { 145 | return $this->value->getTimezone(); 146 | } 147 | 148 | final public function format(string $format): string 149 | { 150 | return $this->value->format($format); 151 | } 152 | 153 | /** 154 | * @param \DateTimeInterface|DateTimeValue $datetime2 155 | * @param bool $absolute 156 | * @return \DateInterval 157 | */ 158 | final public function diff($datetime2, bool $absolute = false): \DateInterval 159 | { 160 | if ($datetime2 instanceof DateTimeValue) { 161 | $datetime2 = $datetime2->toDateTimeImmutable(); 162 | } else { 163 | /** @psalm-suppress DocblockTypeContradiction check in runtime too*/ 164 | if (!$datetime2 instanceof \DateTimeInterface) { 165 | throw new \InvalidArgumentException(); 166 | } 167 | } 168 | 169 | return $this->value->diff($datetime2, $absolute); 170 | } 171 | 172 | final public function modify(string $modifier): self 173 | { 174 | return new static($this->value->modify($modifier)); 175 | } 176 | 177 | final public function add(\DateInterval $interval): self 178 | { 179 | return new static($this->value->add($interval)); 180 | } 181 | 182 | final public function sub(\DateInterval $interval): self 183 | { 184 | return new static($this->value->sub($interval)); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Collections/FromArrayConstructor.php: -------------------------------------------------------------------------------- 1 | $class 19 | * @psalm-return T 20 | * @param string $class 21 | * @param array $input 22 | * @param bool $ignoreExtraProperties 23 | * @return object 24 | * @throws ViolationExceptionInterface 25 | */ 26 | public static function constructFromArray(string $class, array $input, bool $ignoreExtraProperties = false): object 27 | { 28 | $reflection = new \ReflectionClass($class); 29 | $arguments = []; 30 | $violations = []; 31 | $parameters = self::getPublicConstructorParameters($reflection); 32 | foreach ($parameters as $parameter) { 33 | $key = $parameter->getName(); 34 | $type = Type::forMethodParameterType($parameter); 35 | 36 | if (\array_key_exists($key, $input)) { 37 | $value = $input[$key]; 38 | try { 39 | $arguments[] = $type->prepareValue($value); 40 | } catch (ViolationExceptionInterface $e) { 41 | $violations[] = NestedViolation::invalidProperty( 42 | $key, 43 | $type->getExpectation(), 44 | $value, 45 | $e->getViolations() 46 | ); 47 | } 48 | unset($input[$key]); 49 | } elseif ($parameter->isDefaultValueAvailable()) { 50 | $arguments[] = $parameter->getDefaultValue(); 51 | } else { 52 | $violations[] = NestedViolation::missingRequiredProperty($key, $type->getExpectation()); 53 | } 54 | } 55 | 56 | if (!$ignoreExtraProperties) { 57 | foreach ($input as $key => $value) { 58 | $violations[] = NestedViolation::unknownProperty((string) $key, $value); 59 | } 60 | } 61 | 62 | if (\count($violations) > 0) { 63 | throw new ViolationException($violations); 64 | } 65 | 66 | return $reflection->newInstanceArgs($arguments); 67 | } 68 | 69 | /** 70 | * @psalm-template T of object 71 | * @psalm-param T $source 72 | * @psalm-return T 73 | * @param object $source 74 | * @param array $input 75 | * @param bool $ignoreExtraProperties 76 | * @return object 77 | * @throws ViolationExceptionInterface 78 | */ 79 | public static function combineWithArray(object $source, array $input, bool $ignoreExtraProperties = false): object 80 | { 81 | /** @var class-string $class */ 82 | $class = \get_class($source); 83 | $reflection = new \ReflectionClass($class); 84 | $arguments = []; 85 | $violations = []; 86 | $parameters = self::getPublicConstructorParameters($reflection); 87 | foreach ($parameters as $parameter) { 88 | $key = $parameter->getName(); 89 | $type = Type::forMethodParameterType($parameter); 90 | 91 | if (\array_key_exists($key, $input)) { 92 | $value = $input[$key]; 93 | try { 94 | $arguments[] = $type->prepareValue($value); 95 | } catch (ViolationExceptionInterface $e) { 96 | $violations[] = NestedViolation::invalidProperty( 97 | $key, 98 | $type->getExpectation(), 99 | $value, 100 | $e->getViolations() 101 | ); 102 | } 103 | unset($input[$key]); 104 | } elseif ($reflection->hasProperty($key)) { 105 | $property = $reflection->getProperty($key); 106 | if ($property->isStatic()) { 107 | throw new \LogicException("Property $key cannot be static."); 108 | } 109 | 110 | $accessible = $property->isPublic(); 111 | if (!$accessible) { 112 | $property->setAccessible(true); 113 | } 114 | $value = $property->getValue($source); 115 | if (!$accessible) { 116 | $property->setAccessible(false); 117 | } 118 | 119 | try { 120 | $arguments[] = $type->prepareValue($value); 121 | } catch (ViolationExceptionInterface $e) { 122 | throw new \LogicException( 123 | "Property $key must be compatible with constructor parameter with the same name.", 124 | 0, 125 | $e 126 | ); 127 | } 128 | } else { 129 | throw new \LogicException( 130 | "Constructor parameter $key must have a corresponding non-static property." 131 | ); 132 | } 133 | } 134 | 135 | if (!$ignoreExtraProperties) { 136 | foreach ($input as $key => $value) { 137 | $violations[] = NestedViolation::unknownProperty((string) $key, $value); 138 | } 139 | } 140 | 141 | if (\count($violations) > 0) { 142 | throw new ViolationException($violations); 143 | } 144 | 145 | return $reflection->newInstanceArgs($arguments); 146 | } 147 | 148 | /** 149 | * @psalm-template T of object 150 | * @psalm-param T $source 151 | * @param object $source 152 | * @return array 153 | */ 154 | public static function extractConstructorArguments(object $source): array 155 | { 156 | /** @var class-string $class */ 157 | $class = \get_class($source); 158 | $reflection = new \ReflectionClass($class); 159 | $parameters = self::getPublicConstructorParameters($reflection); 160 | $output = []; 161 | foreach ($parameters as $parameter) { 162 | $key = $parameter->getName(); 163 | $property = $reflection->getProperty($key); 164 | $accessible = $property->isPublic(); 165 | if (!$accessible) { 166 | $property->setAccessible(true); 167 | } 168 | $output[$key] = $property->getValue($source); 169 | if (!$accessible) { 170 | $property->setAccessible(false); 171 | } 172 | } 173 | return $output; 174 | } 175 | 176 | /** 177 | * @param \ReflectionClass $reflection 178 | * @return \ReflectionParameter[] 179 | */ 180 | private static function getPublicConstructorParameters(\ReflectionClass $reflection): array 181 | { 182 | $class =$reflection->getName(); 183 | 184 | if (!$reflection->isInstantiable()) { 185 | throw new \LogicException("Class $class is not instantiable."); 186 | } 187 | 188 | $constructor = $reflection->getConstructor(); 189 | if (!$constructor) { 190 | throw new \LogicException("Class $class does not have a constructor."); 191 | } 192 | 193 | if (!$constructor->isPublic()) { 194 | throw new \LogicException("Class $class does not have a public constructor."); 195 | } 196 | 197 | return $constructor->getParameters(); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/Collections/DataTransferObjectTest.php: -------------------------------------------------------------------------------- 1 | isAbstract()); 18 | } 19 | 20 | public function testThatIntIsCopiedToIntPropertyWithoutDefault(): void 21 | { 22 | $input = ['xyz' => 10]; 23 | $dto = new class($input) extends DataTransferObject { 24 | public int $xyz; 25 | }; 26 | self::assertSame(10, $dto->xyz); 27 | } 28 | 29 | public function testThatIntIsCopiedToIntPropertyWithDefault(): void 30 | { 31 | $input = ['xyz' => 10]; 32 | $dto = new class($input) extends DataTransferObject { 33 | public int $xyz = 5; 34 | }; 35 | self::assertSame(10, $dto->xyz); 36 | } 37 | 38 | public function testThatIntIsCopiedToNullableIntPropertyWithoutDefault(): void 39 | { 40 | $input = ['xyz' => 10]; 41 | $dto = new class($input) extends DataTransferObject { 42 | public ?int $xyz; 43 | }; 44 | self::assertSame(10, $dto->xyz); 45 | } 46 | 47 | public function testThatNullIsCopiedToNullableIntPropertyWithoutDefault(): void 48 | { 49 | $input = ['xyz' => null]; 50 | $dto = new class($input) extends DataTransferObject { 51 | public ?int $xyz; 52 | }; 53 | self::assertSame(null, $dto->xyz); 54 | } 55 | 56 | public function testThatNullIsCopiedToNullableIntPropertyWithDefault(): void 57 | { 58 | $input = ['xyz' => null]; 59 | $dto = new class($input) extends DataTransferObject { 60 | public ?int $xyz = 10; 61 | }; 62 | self::assertSame(null, $dto->xyz); 63 | } 64 | 65 | public function testThatMissingRequiredPropertyThrows(): void 66 | { 67 | $input = []; 68 | try { 69 | new class($input) extends DataTransferObject { 70 | public int $xyz; 71 | }; 72 | self::assertTrue(false, 'Exception not thrown.'); 73 | } catch (ViolationExceptionInterface $e) { 74 | $violations = $e->getViolations(); 75 | self::assertCount(1, $violations); 76 | $violation = \reset($violations); 77 | if ($violation instanceof NestedViolationInterface) { 78 | self::assertSame('xyz', $violation->getKey()); 79 | } else { 80 | self::assertTrue(false, 'Violation has incorrect type.'); 81 | } 82 | } 83 | } 84 | 85 | public function testThatNonValueObjectCanBeCopied(): void 86 | { 87 | $value = new \DateTimeImmutable(); 88 | $input = [ 89 | 'xyz' => $value, 90 | ]; 91 | $dto = new class($input) extends DataTransferObject { 92 | public \DateTimeImmutable $xyz; 93 | }; 94 | 95 | self::assertSame($value, $dto->xyz); 96 | } 97 | 98 | public function testThatNonValueObjectCannotBeConstructedFromPrimitive(): void 99 | { 100 | $input = [ 101 | 'xyz' => \date(\DATE_ATOM), 102 | ]; 103 | try { 104 | new class($input) extends DataTransferObject { 105 | public \DateTimeImmutable $xyz; 106 | }; 107 | self::assertTrue(false, 'Exception not thrown.'); 108 | } catch (ViolationExceptionInterface $e) { 109 | $violations = $e->getViolations(); 110 | self::assertCount(1, $violations); 111 | $violation = \reset($violations); 112 | if ($violation instanceof NestedViolationInterface) { 113 | self::assertSame('xyz', $violation->getKey()); 114 | self::assertSame($input['xyz'], $violation->getValue()); 115 | $subViolations = $violation->getViolations(); 116 | self::assertCount(1, $subViolations); 117 | $subViolation = \reset($subViolations); 118 | self::assertInstanceOf(TypeViolation::class, $subViolation); 119 | } else { 120 | self::assertTrue(false, 'Invalid violation type.'); 121 | } 122 | } 123 | } 124 | 125 | public function testThatValueObjectCanBeCreatedFromPrimitive(): void 126 | { 127 | $input = [ 128 | 'xyz' => [1, 2, 3], 129 | ]; 130 | $dto = new class($input) extends DataTransferObject { 131 | public ListOfInts $xyz; 132 | }; 133 | self::assertSame([1, 2, 3], $dto->xyz->toArray()); 134 | } 135 | 136 | public function testThatValueObjectPropertyWithoutDefaultDoesNotAcceptNull(): void 137 | { 138 | $input = [ 139 | 'xyz' => null, 140 | ]; 141 | try { 142 | new class($input) extends DataTransferObject { 143 | public ListOfInts $xyz; 144 | }; 145 | self::assertTrue(false, 'Exception not thrown.'); 146 | } catch (ViolationExceptionInterface $e) { 147 | $violations = $e->getViolations(); 148 | self::assertCount(1, $violations); 149 | $violation = \reset($violations); 150 | if ($violation instanceof NestedViolationInterface) { 151 | self::assertSame('xyz', $violation->getKey()); 152 | self::assertSame(null, $violation->getValue()); 153 | $subViolations = $violation->getViolations(); 154 | self::assertCount(1, $subViolations); 155 | $subViolation = \reset($subViolations); 156 | self::assertInstanceOf(TypeViolation::class, $subViolation); 157 | } else { 158 | self::assertTrue(false, 'Invalid violation type'); 159 | } 160 | } 161 | } 162 | 163 | public function testThatMultipleViolationsCanBeCreated(): void 164 | { 165 | $input = [ 166 | 'int' => 'string', 167 | 'string' => 10, 168 | ]; 169 | 170 | try { 171 | new class($input) extends DataTransferObject { 172 | public int $int; 173 | public string $string; 174 | }; 175 | self::assertTrue(false, 'Exception not thrown.'); 176 | } catch (ViolationExceptionInterface $e) { 177 | $violations = $e->getViolations(); 178 | self::assertCount(2, $violations); 179 | $violation = \array_shift($violations); 180 | if ($violation instanceof NestedViolationInterface) { 181 | self::assertSame('int', $violation->getKey()); 182 | self::assertSame('string', $violation->getValue()); 183 | $subViolations = $violation->getViolations(); 184 | self::assertCount(1, $subViolations); 185 | $subViolation = \reset($subViolations); 186 | self::assertInstanceOf(TypeViolation::class, $subViolation); 187 | } else { 188 | self::assertTrue(false, 'Invalid violation type'); 189 | } 190 | $violation = \array_shift($violations); 191 | if ($violation instanceof NestedViolationInterface) { 192 | self::assertSame('string', $violation->getKey()); 193 | self::assertSame(10, $violation->getValue()); 194 | $subViolations = $violation->getViolations(); 195 | self::assertCount(1, $subViolations); 196 | $subViolation = \reset($subViolations); 197 | self::assertInstanceOf(TypeViolation::class, $subViolation); 198 | } else { 199 | self::assertTrue(false, 'Invalid violation type'); 200 | } 201 | } 202 | } 203 | 204 | public function testThatUnknownPropertiesAreViolationsByDefault(): void 205 | { 206 | $input = [ 207 | 'extra' => 'value', 208 | ]; 209 | 210 | try { 211 | new class ($input) extends DataTransferObject { 212 | public ?int $id = null; 213 | }; 214 | self::assertTrue(false, 'Exception not thrown.'); 215 | } catch (ViolationExceptionInterface $e) { 216 | $violations = $e->getViolations(); 217 | self::assertCount(1, $violations); 218 | $violation = \reset($violations); 219 | if ($violation instanceof NestedViolationInterface) { 220 | self::assertSame('extra', $violation->getKey()); 221 | self::assertSame('value', $violation->getValue()); 222 | } else { 223 | self::assertTrue(false, 'Bad violation type.'); 224 | } 225 | } 226 | } 227 | 228 | public function testThatUnknownPropertiesAreIgnoredWhenOverridenConstantFlag(): void 229 | { 230 | $input = [ 231 | 'id' => 5, 232 | 'extra' => 'value', 233 | ]; 234 | 235 | $dto = new class ($input) extends DataTransferObject { 236 | protected const IGNORE_UNKNOWN_PROPERTIES = true; 237 | public ?int $id = null; 238 | }; 239 | 240 | self::assertSame(5, $dto->id); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /tests/Collections/FromArrayConstructorTest.php: -------------------------------------------------------------------------------- 1 | x = $x; 24 | $this->y = $y; 25 | } 26 | }; 27 | 28 | $class = \get_class($dummy); 29 | 30 | $output = FromArrayConstructor::constructFromArray($class, [ 31 | 'x' => 'value', 32 | 'y' => 10, 33 | ]); 34 | 35 | self::assertInstanceOf($class, $output); 36 | self::assertSame('value', $output->x); 37 | self::assertSame(10, $output->y); 38 | } 39 | 40 | public function testThatDefaultsAreRespected(): void 41 | { 42 | $dummy = new class ('x', 1) { 43 | public ?string $x; 44 | public int $y; 45 | 46 | public function __construct(?string $x = null, int $y = 10) 47 | { 48 | $this->x = $x; 49 | $this->y = $y; 50 | } 51 | }; 52 | 53 | $class = \get_class($dummy); 54 | 55 | $output = FromArrayConstructor::constructFromArray($class, []); 56 | 57 | self::assertInstanceOf($class, $output); 58 | self::assertSame(null, $output->x); 59 | self::assertSame(10, $output->y); 60 | } 61 | 62 | public function testThatCanCreateWithUpcastingAndDowncasting(): void 63 | { 64 | $dummy = new class (DateTimeValue::fromString('2020-06-20T00:15:16+00:00'), 1) { 65 | public DateTimeValue $x; 66 | public int $y; 67 | 68 | public function __construct(DateTimeValue $x, int $y) 69 | { 70 | $this->x = $x; 71 | $this->y = $y; 72 | } 73 | }; 74 | 75 | $class = \get_class($dummy); 76 | 77 | $output = FromArrayConstructor::constructFromArray($class, [ 78 | 'x' => new \DateTimeImmutable('2020-06-15T05:04:03+00:00'), 79 | 'y' => new IntegerValue(11), 80 | ]); 81 | 82 | self::assertInstanceOf($class, $output); 83 | self::assertSame('2020-06-15T05:04:03+00:00', (string) $output->x); 84 | self::assertSame(11, $output->y); 85 | } 86 | 87 | public function testThatConstructFromArrayReportsAllViolations(): void 88 | { 89 | $dummy = new class ('x', 1) { 90 | public string $x; 91 | public int $y; 92 | 93 | public function __construct(string $x, int $y) 94 | { 95 | $this->x = $x; 96 | $this->y = $y; 97 | } 98 | }; 99 | 100 | $class = \get_class($dummy); 101 | 102 | try { 103 | FromArrayConstructor::constructFromArray($class, [ 104 | 'x' => 10, 105 | 'z' => 'extra', 106 | ]); 107 | self::assertTrue(false, 'Exception not thrown.'); 108 | } catch (ViolationExceptionInterface $e) { 109 | $violations = $e->getViolations(); 110 | self::assertCount(3, $violations); 111 | 112 | $violation = \array_shift($violations); 113 | if ($violation instanceof NestedViolationInterface) { 114 | self::assertSame('x', $violation->getKey()); 115 | self::assertSame(10, $violation->getValue()); 116 | $subViolations = $violation->getViolations(); 117 | self::assertCount(1, $subViolations); 118 | $subViolation = \reset($subViolations); 119 | self::assertInstanceOf(TypeViolation::class, $subViolation); 120 | } else { 121 | self::assertTrue(false, 'Unexpected violation type.'); 122 | } 123 | 124 | $violation = \array_shift($violations); 125 | if ($violation instanceof NestedViolationInterface) { 126 | self::assertSame('y', $violation->getKey()); 127 | } else { 128 | self::assertTrue(false, 'Unexpected violation type.'); 129 | } 130 | 131 | $violation = \array_shift($violations); 132 | if ($violation instanceof NestedViolationInterface) { 133 | self::assertSame('z', $violation->getKey()); 134 | self::assertSame('extra', $violation->getValue()); 135 | } else { 136 | self::assertTrue(false, 'Unexpected violation type.'); 137 | } 138 | } 139 | } 140 | 141 | public function testThatCanCombineWithArrayOfModifiedProperties(): void 142 | { 143 | $dummy = new class ('x', 1, 11.1) { 144 | public string $x; 145 | public int $y; 146 | public float $z; 147 | 148 | public function __construct(string $x, int $y, float $z) 149 | { 150 | $this->x = $x; 151 | $this->y = $y; 152 | $this->z = $z; 153 | } 154 | }; 155 | 156 | $output = FromArrayConstructor::combineWithArray($dummy, [ 157 | 'x' => 'value', 158 | 'y' => 10, 159 | ]); 160 | 161 | self::assertInstanceOf(\get_class($dummy), $output); 162 | self::assertSame('value', $output->x); 163 | self::assertSame(10, $output->y); 164 | self::assertSame(11.1, $output->z); 165 | } 166 | 167 | public function testThatCombineWithArrayOfBadClassDefinitionIsErrorButNotViolation(): void 168 | { 169 | $dummy = new class ('x', 1, 11.1) { 170 | public string $x; 171 | public int $y; 172 | 173 | public function __construct(string $x, int $y, float $z) 174 | { 175 | $this->x = $x; 176 | $this->y = $y; 177 | } 178 | }; 179 | 180 | try { 181 | FromArrayConstructor::combineWithArray($dummy, [ 182 | 'x' => 'value', 183 | 'y' => 10, 184 | ]); 185 | self::assertTrue(false, 'Exception not thrown.'); 186 | } catch (\Throwable $e) { 187 | self::assertNotInstanceOf(ViolationExceptionInterface::class, $e); 188 | } 189 | } 190 | 191 | public function testThatCombineWithArrayReportsAllViolations(): void 192 | { 193 | $dummy = new class ('x', 1, 11.1) { 194 | public string $x; 195 | public int $y; 196 | public float $z; 197 | 198 | public function __construct(string $x, int $y, float $z) 199 | { 200 | $this->x = $x; 201 | $this->y = $y; 202 | $this->z = $z; 203 | } 204 | }; 205 | 206 | try { 207 | FromArrayConstructor::combineWithArray($dummy, [ 208 | 'x' => 10, 209 | 'w' => 'extra', 210 | ]); 211 | self::assertTrue(false, 'Exception not thrown.'); 212 | } catch (ViolationExceptionInterface $e) { 213 | $violations = $e->getViolations(); 214 | self::assertCount(2, $violations); 215 | 216 | $violation = \array_shift($violations); 217 | if ($violation instanceof NestedViolationInterface) { 218 | self::assertSame('x', $violation->getKey()); 219 | self::assertSame(10, $violation->getValue()); 220 | $subViolations = $violation->getViolations(); 221 | self::assertCount(1, $subViolations); 222 | $subViolation = \reset($subViolations); 223 | self::assertInstanceOf(TypeViolation::class, $subViolation); 224 | } else { 225 | self::assertTrue(false, 'Unexpected violation type.'); 226 | } 227 | 228 | $violation = \array_shift($violations); 229 | if ($violation instanceof NestedViolationInterface) { 230 | self::assertSame('w', $violation->getKey()); 231 | self::assertSame('extra', $violation->getValue()); 232 | } else { 233 | self::assertTrue(false, 'Unexpected violation type.'); 234 | } 235 | } 236 | } 237 | 238 | public function testThatCanExtractConstructorArguments(): void 239 | { 240 | $dummy = new class ('value', 1, 11.1) { 241 | private string $x; 242 | private int $y; 243 | private float $z; 244 | private array $w = []; 245 | 246 | public function __construct(string $x, int $y, float $z) 247 | { 248 | $this->x = $x; 249 | $this->y = $y; 250 | $this->z = $z; 251 | } 252 | }; 253 | 254 | $output = FromArrayConstructor::extractConstructorArguments($dummy); 255 | 256 | self::assertCount(3, $output); 257 | self::assertTrue(isset($output['x'], $output['y'], $output['z'])); 258 | self::assertSame('value', $output['x']); 259 | self::assertSame(1, $output['y']); 260 | self::assertSame(11.1, $output['z']); 261 | } 262 | } 263 | --------------------------------------------------------------------------------