├── .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 |
--------------------------------------------------------------------------------