From 28fa964a7b2df7145718845f6094f683d314f5f8 Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:06:08 +0200 Subject: [PATCH] Customers name and surname can not be empty strings and e-mail must be valid e-mail address --- composer.json | 1 + src/Model/CreatePaymentCustomer.php | 19 ++++--- src/ValueObject/Amount.php | 6 +- src/ValueObject/BaseValueObject.php | 49 +++++++++++++++- src/ValueObject/CountryCode.php | 6 +- src/ValueObject/CurrencyCode.php | 6 +- src/ValueObject/EmailAddress.php | 20 +++++++ src/ValueObject/EnumValueObject.php | 6 +- src/ValueObject/Identifier.php | 6 +- src/ValueObject/LanguageCode.php | 6 +- src/ValueObject/NonEmptyString.php | 17 ++++++ src/ValueObject/PhoneNumber.php | 3 + src/ValueObject/SecureUrl.php | 3 + src/ValueObject/StringValue.php | 31 ++++------ src/ValueObject/Url.php | 3 + tests/ValueObject/BaseValueObjectTestCase.php | 57 +++++++++++++++++++ tests/ValueObject/EmailAddressTest.php | 34 +++++++++++ tests/ValueObject/NonEmptyStringTest.php | 33 +++++++++++ tests/ValueObject/StringValueTest.php | 29 ++++++++++ 19 files changed, 284 insertions(+), 51 deletions(-) create mode 100644 src/ValueObject/EmailAddress.php create mode 100644 src/ValueObject/NonEmptyString.php create mode 100644 tests/ValueObject/BaseValueObjectTestCase.php create mode 100644 tests/ValueObject/EmailAddressTest.php create mode 100644 tests/ValueObject/NonEmptyStringTest.php create mode 100644 tests/ValueObject/StringValueTest.php diff --git a/composer.json b/composer.json index 9e4ace2..479f191 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "require": { "php": "~7.4|~8.0", "ext-json": "*", + "egulias/email-validator": "^4.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.0|^2.0" diff --git a/src/Model/CreatePaymentCustomer.php b/src/Model/CreatePaymentCustomer.php index 205a97f..8446a4d 100644 --- a/src/Model/CreatePaymentCustomer.php +++ b/src/Model/CreatePaymentCustomer.php @@ -3,16 +3,17 @@ namespace ThePay\ApiClient\Model; use InvalidArgumentException; +use ThePay\ApiClient\ValueObject\EmailAddress; +use ThePay\ApiClient\ValueObject\NonEmptyString; use ThePay\ApiClient\ValueObject\PhoneNumber; -use ThePay\ApiClient\ValueObject\StringValue; final class CreatePaymentCustomer { private string $name; private string $surname; - /** @var StringValue|null */ + /** @var string|null */ private $email; - /** @var PhoneNumber|null */ + /** @var string|null */ private $phone; /** @var Address|null */ private $billingAddress; @@ -31,10 +32,10 @@ public function __construct(string $name, string $surname, $email, $phone, Addre throw new InvalidArgumentException('At least one of $email and $phone is required.'); } - $this->name = $name; - $this->surname = $surname; - $this->email = $email === null ? null : new StringValue($email); - $this->phone = $phone === null ? null : new PhoneNumber($phone); + $this->name = (new NonEmptyString($name))->getValue(); + $this->surname = (new NonEmptyString($surname))->getValue(); + $this->email = $email === null ? null : (new EmailAddress($email))->getValue(); + $this->phone = $phone === null ? null : (new PhoneNumber($phone))->getValue(); $this->billingAddress = $billingAddress; $this->shippingAddress = $shippingAddress; } @@ -54,7 +55,7 @@ public function getSurname(): string */ public function getEmail() { - return $this->email === null ? null : $this->email->getValue(); + return $this->email; } /** @@ -62,7 +63,7 @@ public function getEmail() */ public function getPhone() { - return $this->phone === null ? null : $this->phone->getValue(); + return $this->phone; } /** diff --git a/src/ValueObject/Amount.php b/src/ValueObject/Amount.php index 1056318..5d7d71a 100644 --- a/src/ValueObject/Amount.php +++ b/src/ValueObject/Amount.php @@ -4,11 +4,11 @@ use InvalidArgumentException; +/** + * @extends BaseValueObject<int> + */ final class Amount extends BaseValueObject { - /** @var int */ - private $value; - /** * Amount constructor. * diff --git a/src/ValueObject/BaseValueObject.php b/src/ValueObject/BaseValueObject.php index e4440eb..20a0d94 100644 --- a/src/ValueObject/BaseValueObject.php +++ b/src/ValueObject/BaseValueObject.php @@ -2,13 +2,27 @@ namespace ThePay\ApiClient\ValueObject; +use InvalidArgumentException; + +/** + * @template TValue of mixed + */ abstract class BaseValueObject implements ValueObject { /** - * BaseValueObject constructor. - * @param mixed $value + * @var TValue + */ + protected $value; + + /** + * @param TValue|mixed $value + * + * @throws InvalidArgumentException */ - abstract public function __construct($value); + public function __construct($value) + { + $this->value = static::filter($value); + } /** * @param mixed $value @@ -30,4 +44,33 @@ public function equals(ValueObject $object) return $this->getValue() === $object->getValue(); } + + /** + * @return TValue + */ + public function getValue() + { + return $this->value; + } + + /** + * @internal should be abstract + * + * @param TValue|mixed $value + * + * @return TValue + * + * @throws InvalidArgumentException + */ + protected static function filter($value) + { + throw self::invalidValue('expected'); + } + + protected static function invalidValue(string $expected, ?string $actual = null): InvalidArgumentException + { + return new InvalidArgumentException( + 'Value ' . ($actual === null ? '' : '"' . $actual . '" ') . 'is not ' . $expected, + ); + } } diff --git a/src/ValueObject/CountryCode.php b/src/ValueObject/CountryCode.php index dc9275e..9fad216 100644 --- a/src/ValueObject/CountryCode.php +++ b/src/ValueObject/CountryCode.php @@ -2,11 +2,11 @@ namespace ThePay\ApiClient\ValueObject; +/** + * @extends BaseValueObject<string> + */ final class CountryCode extends BaseValueObject { - /** @var string */ - private $value; - /** * @param string $value */ diff --git a/src/ValueObject/CurrencyCode.php b/src/ValueObject/CurrencyCode.php index 653de62..5276f78 100644 --- a/src/ValueObject/CurrencyCode.php +++ b/src/ValueObject/CurrencyCode.php @@ -4,11 +4,11 @@ use InvalidArgumentException; +/** + * @extends BaseValueObject<string> + */ final class CurrencyCode extends BaseValueObject { - /** @var string */ - private $value; - /** * CurrencyCode constructor. * diff --git a/src/ValueObject/EmailAddress.php b/src/ValueObject/EmailAddress.php new file mode 100644 index 0000000..6c62647 --- /dev/null +++ b/src/ValueObject/EmailAddress.php @@ -0,0 +1,20 @@ +<?php + +namespace ThePay\ApiClient\ValueObject; + +use Egulias\EmailValidator\EmailValidator; +use Egulias\EmailValidator\Validation\RFCValidation; + +class EmailAddress extends NonEmptyString +{ + public static function filter($value) + { + $nonEmptyString = parent::filter($value); + + if ((new EmailValidator())->isValid($nonEmptyString, new RFCValidation()) === false) { + throw self::invalidValue('e-mail address', $nonEmptyString); + } + + return $nonEmptyString; + } +} diff --git a/src/ValueObject/EnumValueObject.php b/src/ValueObject/EnumValueObject.php index 529c0c8..a12e831 100644 --- a/src/ValueObject/EnumValueObject.php +++ b/src/ValueObject/EnumValueObject.php @@ -2,11 +2,11 @@ namespace ThePay\ApiClient\ValueObject; +/** + * @extends BaseValueObject<string> + */ abstract class EnumValueObject extends BaseValueObject { - /** @var string */ - protected $value; - /** * @param string $value */ diff --git a/src/ValueObject/Identifier.php b/src/ValueObject/Identifier.php index ed86a22..ee77934 100644 --- a/src/ValueObject/Identifier.php +++ b/src/ValueObject/Identifier.php @@ -4,11 +4,11 @@ use InvalidArgumentException; +/** + * @extends BaseValueObject<string> + */ final class Identifier extends BaseValueObject { - /** @var string */ - private $value; - /** * Uid constructor. * diff --git a/src/ValueObject/LanguageCode.php b/src/ValueObject/LanguageCode.php index f384464..6a5348a 100644 --- a/src/ValueObject/LanguageCode.php +++ b/src/ValueObject/LanguageCode.php @@ -4,11 +4,11 @@ use InvalidArgumentException; +/** + * @extends BaseValueObject<string> + */ final class LanguageCode extends BaseValueObject { - /** @var string */ - private $value; - /** * CurrencyCode constructor. * diff --git a/src/ValueObject/NonEmptyString.php b/src/ValueObject/NonEmptyString.php new file mode 100644 index 0000000..6e54759 --- /dev/null +++ b/src/ValueObject/NonEmptyString.php @@ -0,0 +1,17 @@ +<?php + +namespace ThePay\ApiClient\ValueObject; + +class NonEmptyString extends StringValue +{ + protected static function filter($value) + { + $string = parent::filter($value); + + if (trim($string) === '') { + throw self::invalidValue('non-empty string', $string); + } + + return $string; + } +} diff --git a/src/ValueObject/PhoneNumber.php b/src/ValueObject/PhoneNumber.php index 311d9f2..ba94ecd 100644 --- a/src/ValueObject/PhoneNumber.php +++ b/src/ValueObject/PhoneNumber.php @@ -4,6 +4,9 @@ use InvalidArgumentException; +/** + * @extends BaseValueObject<string> + */ final class PhoneNumber extends BaseValueObject { /** @var string */ diff --git a/src/ValueObject/SecureUrl.php b/src/ValueObject/SecureUrl.php index ed9ff78..9193d0a 100644 --- a/src/ValueObject/SecureUrl.php +++ b/src/ValueObject/SecureUrl.php @@ -2,6 +2,9 @@ namespace ThePay\ApiClient\ValueObject; +/** + * @extends BaseValueObject<string> + */ final class SecureUrl extends BaseValueObject { /** @var string */ diff --git a/src/ValueObject/StringValue.php b/src/ValueObject/StringValue.php index da0401a..50443bb 100644 --- a/src/ValueObject/StringValue.php +++ b/src/ValueObject/StringValue.php @@ -2,33 +2,22 @@ namespace ThePay\ApiClient\ValueObject; -final class StringValue extends BaseValueObject +/** + * @extends BaseValueObject<string> + */ +class StringValue extends BaseValueObject { - /** @var string */ - private $value; - - public function __construct($value) - { - if ( ! is_string($value)) { - throw new \InvalidArgumentException('type of value: ' . (string) $value . ' is not string'); - } - - $this->value = $value; - } - - /** - * @return string - */ public function __toString() { return $this->value; } - /** - * @return string - */ - public function getValue() + protected static function filter($value) { - return $this->value; + if ( ! is_string($value)) { + throw self::invalidValue('string'); + } + + return $value; } } diff --git a/src/ValueObject/Url.php b/src/ValueObject/Url.php index dc6d970..cb5831c 100644 --- a/src/ValueObject/Url.php +++ b/src/ValueObject/Url.php @@ -4,6 +4,9 @@ use InvalidArgumentException; +/** + * @extends BaseValueObject<string> + */ final class Url extends BaseValueObject { /** @var string */ diff --git a/tests/ValueObject/BaseValueObjectTestCase.php b/tests/ValueObject/BaseValueObjectTestCase.php new file mode 100644 index 0000000..b5f90f1 --- /dev/null +++ b/tests/ValueObject/BaseValueObjectTestCase.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +namespace ThePay\ApiClient\Tests\ValueObject; + +use PHPUnit\Framework\TestCase; +use ThePay\ApiClient\ValueObject\BaseValueObject; + +abstract class BaseValueObjectTestCase extends TestCase +{ + /** + * @dataProvider validValuesDataProvider + * + * @param mixed $value + */ + public function testCreatesWorkingInstanceWithValidValue($value): void + { + $className = static::getClassName(); + $a = $className::create($value); + $b = new $className($value); + + self::assertTrue($a->equals($b)); + self::assertTrue($b->equals($a)); + self::assertSame($value, $a->getValue()); + self::assertSame((string) $value, (string) $a); + } + + /** + * @dataProvider invalidValuesAndMessagesDataProvider + * + * @param mixed $value + */ + public function testThrowsWithInvalidValue($value, string $message): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($message); + + $className = static::getClassName(); + $className::create($value); + } + + /** + * @return class-string<BaseValueObject> + */ + abstract protected static function getClassName(): string; + + /** + * @return array<array<mixed>>|array<string, array<mixed> + */ + abstract public static function validValuesDataProvider(): array; + + /** + * @return array<array<mixed|string>>|array<string, array<mixed|string> + */ + abstract public static function invalidValuesAndMessagesDataProvider(): array; +} diff --git a/tests/ValueObject/EmailAddressTest.php b/tests/ValueObject/EmailAddressTest.php new file mode 100644 index 0000000..8f0da5a --- /dev/null +++ b/tests/ValueObject/EmailAddressTest.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace ThePay\ApiClient\Tests\ValueObject; + +use ThePay\ApiClient\ValueObject\EmailAddress; + +final class EmailAddressTest extends BaseValueObjectTestCase +{ + protected static function getClassName(): string + { + return EmailAddress::class; + } + + public static function validValuesDataProvider(): array + { + return [ + ['user@example.com'], + ['user+tag@example.com'], + ['idn@测试假域名.com'], + ]; + } + + public static function invalidValuesAndMessagesDataProvider(): array + { + return array_merge( + NonEmptyStringTest::invalidValuesAndMessagesDataProvider(), + [ + ['foo', 'Value "foo" is not e-mail address'], + ], + ); + } +} diff --git a/tests/ValueObject/NonEmptyStringTest.php b/tests/ValueObject/NonEmptyStringTest.php new file mode 100644 index 0000000..f700803 --- /dev/null +++ b/tests/ValueObject/NonEmptyStringTest.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace ThePay\ApiClient\Tests\ValueObject; + +use ThePay\ApiClient\ValueObject\NonEmptyString; + +final class NonEmptyStringTest extends BaseValueObjectTestCase +{ + protected static function getClassName(): string + { + return NonEmptyString::class; + } + + public static function validValuesDataProvider(): array + { + return [ + ['string'], + ]; + } + + public static function invalidValuesAndMessagesDataProvider(): array + { + return array_merge( + StringValueTest::invalidValuesAndMessagesDataProvider(), + [ + ['', 'Value "" is not non-empty string'], + [' ', 'Value " " is not non-empty string'], + ], + ); + } +} diff --git a/tests/ValueObject/StringValueTest.php b/tests/ValueObject/StringValueTest.php new file mode 100644 index 0000000..06e9124 --- /dev/null +++ b/tests/ValueObject/StringValueTest.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace ThePay\ApiClient\Tests\ValueObject; + +use ThePay\ApiClient\ValueObject\StringValue; + +final class StringValueTest extends BaseValueObjectTestCase +{ + protected static function getClassName(): string + { + return StringValue::class; + } + + public static function validValuesDataProvider(): array + { + return [ + ['string'], + ]; + } + + public static function invalidValuesAndMessagesDataProvider(): array + { + return [ + [null, 'Value is not string'], + ]; + } +}