diff --git a/composer.json b/composer.json index 31c5fef..26eade3 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ ], "require": { "php": "^7.2|^8.0", - "consistence/consistence": "^1.0 || ^2.0", "nette/utils": "^3.0 || ^2.4", "nette/http": "^3.0 || ^2.4", "jschaedl/iban-validation": "^1.0", diff --git a/src/Enum.php b/src/Enum.php index 199f4ab..869a7a0 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -4,23 +4,276 @@ namespace SmartEmailing\Types; -use Consistence\Enum\InvalidEnumValueException; +use ReflectionClass; +use ReflectionClassConstant; -abstract class Enum extends \Consistence\Enum\Enum +abstract class Enum { + /** + * @var mixed + */ + private $value; + + /** + * @var array indexed by enum and value + */ + private static $instances = []; + + /** + * @var array + */ + private static $availableValues = []; + + /** + * @param mixed $value + */ + final private function __construct( + $value + ) + { + static::checkValue($value); + $this->value = $value; + } + /** * @param mixed $value + * @return static + */ + final public static function get( + $value + ): self + { + $index = \sprintf('%s::%s', static::class, self::getValueIndex($value)); + + if (!isset(self::$instances[$index])) { + self::$instances[$index] = new static($value); + } + + return self::$instances[$index]; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + public function equals( + self $that + ): bool + { + $this->checkSameEnum($that); + + return $this === $that; + } + + /** + * @param mixed $value + * @return bool + */ + public function equalsValue( + $value + ): bool + { + return $this->getValue() === $value; + } + + /** + * @param mixed $value + * @throws \Exception */ public static function checkValue( $value ): void { - try { - parent::checkValue($value); - } catch (InvalidEnumValueException $e) { - throw new InvalidTypeException($e->getMessage()); + if (!\is_subclass_of(static::class, self::class)) { + throw new \Exception(\sprintf( + '"%s" is not a subclass of "%s"', + static::class, + self::class + )); + } + + if (!static::isValidValue($value)) { + $availableValues = static::getAvailableValues(); + + throw new InvalidTypeException( + \sprintf( + '%s [%s] is not a valid value for %s, accepted values: %s', + $value, + \gettype($value), + static::class, + \implode(', ', $availableValues) + ) + ); + } + } + + /** + * @return array + */ + public static function getAvailableValues(): array + { + $index = static::class; + + if (!isset(self::$availableValues[$index])) { + $availableValues = self::getEnumConstants(); + static::checkAvailableValues($availableValues); + self::$availableValues[$index] = $availableValues; + } + + return self::$availableValues[$index]; + } + + /** + * @return array + */ + public static function getAvailableEnums(): array + { + $values = static::getAvailableValues(); + $out = []; + + foreach ($values as $value) { + $out[$value] = static::get($value); + } + + return $out; + } + + /** + * @param mixed $value + * @return bool + */ + public static function isValidValue( + $value + ): bool + { + return \in_array($value, self::getAvailableValues(), true); + } + + protected function checkSameEnum( + self $that + ): void + { + if (\get_class($that) !== static::class) { + throw new InvalidTypeException(\sprintf('Operation supported only for enum of same class: %s given, %s expected', \get_class($that), static::class)); + } + } + + /** + * @param array $availableValues + */ + protected static function checkAvailableValues( + array $availableValues + ): void + { + $index = []; + + foreach ($availableValues as $value) { + self::checkType($value); + $key = self::getValueIndex($value); + + if (isset($index[$key])) { + throw new InvalidTypeException( + \sprintf( + 'Value %s [%s] is specified in %s\'s available values multiple times', + $value, + \gettype($value), + static::class + ) + ); + } + + $index[$key] = true; + } + } + + /** + * @param mixed $value + */ + private static function checkType( + $value + ): void + { + if (\is_scalar($value) || $value === null) { + return; } + + $valueType = \gettype($value); + $printableValue = $value; + + if (\is_object($value)) { + $valueType = \get_class($value); + $printableValue = \get_class($value); + } + + if (\is_array($value)) { + $valueType = ''; + } + + throw new InvalidTypeException( + \sprintf('%s expected, %s [%s] given', 'int|string|float|bool|null', $printableValue, $valueType) + ); + } + + /** + * @param \ReflectionClass $classReflection + * @return array + */ + private static function getDeclaredConstants( + ReflectionClass $classReflection + ): array + { + $constants = $classReflection->getReflectionConstants(); + $className = $classReflection->getName(); + + return \array_filter( + $constants, + static function (ReflectionClassConstant $constant) use ($className): bool { + return $constant->getDeclaringClass()->getName() === $className; + } + ); + } + + /** + * @param mixed $value + * @return string + */ + private static function getValueIndex( + $value + ): string + { + $type = \gettype($value); + + return $value . \sprintf('[%s]', $type); + } + + /** + * @return array + */ + private static function getEnumConstants(): array + { + $classReflection = new ReflectionClass(static::class); + $declaredConstants = self::getDeclaredConstants($classReflection); + $declaredPublicConstants = \array_filter( + $declaredConstants, + static function (ReflectionClassConstant $constant): bool { + return $constant->isPublic(); + } + ); + + $out = []; + + foreach ($declaredPublicConstants as $publicConstant) { + \assert($publicConstant instanceof \ReflectionClassConstant); + + $out[$publicConstant->getName()] = $publicConstant->getValue(); + } + + return $out; } } diff --git a/tests/CountryCodeTest.phpt b/tests/CountryCodeTest.phpt index 2c7aad4..cd657ce 100644 --- a/tests/CountryCodeTest.phpt +++ b/tests/CountryCodeTest.phpt @@ -5,28 +5,39 @@ declare(strict_types = 1); namespace SmartEmailing\Types; use Tester\Assert; +use Tester\TestCase; require_once __DIR__ . '/bootstrap.php'; -$countrySK = CountryCode::from('SK'); -Assert::equal('SK', $countrySK->getValue()); +class CountryCodeTest extends TestCase +{ -$countryGB = CountryCode::extractOrNull(['currency_code' => 'GB'], 'currency_code'); -Assert::equal('GB', $countryGB->getValue()); + public function testDefaults(): void + { + $countrySK = CountryCode::from('SK'); + Assert::equal('SK', $countrySK->getValue()); -$countryPL = CountryCode::extract(['currency_code' => 'PL'], 'currency_code'); -Assert::equal('PL', $countryPL->getValue()); -Assert::equal('PL', (string) $countryPL); + $countryGB = CountryCode::extractOrNull(['currency_code' => 'GB'], 'currency_code'); + Assert::equal('GB', $countryGB->getValue()); -Assert::true($countryPL->equalsValue(CountryCode::PL)); -Assert::false($countryPL->equals($countryGB)); + $countryPL = CountryCode::extract(['currency_code' => 'PL'], 'currency_code'); + Assert::equal('PL', $countryPL->getValue()); + Assert::equal('PL', (string) $countryPL); -$enums = CountryCode::getAvailableEnums(); -Assert::type('array', $enums); + Assert::true($countryPL->equalsValue(CountryCode::PL)); + Assert::false($countryPL->equals($countryGB)); -$values = CountryCode::getAvailableValues(); -Assert::type('array', $values); + $enums = CountryCode::getAvailableEnums(); + Assert::type('array', $enums); -$country = CountryCode::from('CZ'); -Assert::type(CountryCode::class, $country); -Assert::type(CountryCode::class, $country); + $values = CountryCode::getAvailableValues(); + Assert::type('array', $values); + + $country = CountryCode::from('CZ'); + Assert::type(CountryCode::class, $country); + Assert::type(CountryCode::class, $country); + } + +} + +(new CountryCodeTest())->run(); diff --git a/tests/PhoneNumberTest.phpt b/tests/PhoneNumberTest.phpt index 7f6d1af..9006ad3 100644 --- a/tests/PhoneNumberTest.phpt +++ b/tests/PhoneNumberTest.phpt @@ -5,48 +5,67 @@ declare(strict_types = 1); namespace SmartEmailing\Types; use Tester\Assert; +use Tester\TestCase; require __DIR__ . '/bootstrap.php'; -$invalidValues = [ - 'Tel: +421 903 111 111', - 'abcd', - '++720182158', - '+42072274957503260901821580', - '+42072018215a', - 'xxx', -]; - -foreach ($invalidValues as $invalidValue) { - Assert::throws( - static function () use ($invalidValue): void { - PhoneNumber::from($invalidValue); - echo 'FAIL, ' . $invalidValue . ' should be invalid.' . \PHP_EOL; - }, - InvalidTypeException::class - ); -} +class PhoneNumberTest extends TestCase +{ + + public function testInvalidValues(): void + { + $invalidValues = [ + 'Tel: +421 903 111 111', + 'abcd', + '++720182158', + '+42072274957503260901821580', + '+42072018215a', + 'xxx', + ]; + + foreach ($invalidValues as $invalidValue) { + + Assert::throws( + static function () use ($invalidValue): void { + PhoneNumber::from($invalidValue); + echo 'FAIL, ' . $invalidValue . ' should be invalid.' . \PHP_EOL; + }, + InvalidTypeException::class + ); + } + } + + public function testValidValues(): void + { + $validValues = [ + '+385915809952' => CountryCode::HR, + '+420(608)111111' => CountryCode::CZ, + '00421 254111111' => CountryCode::SK, + '00421 905 111 111' => CountryCode::SK, + '+420 950 111 111' => CountryCode::CZ, + '+420720182158' => CountryCode::CZ, + '+391234567891234' => CountryCode::IT, + '+905322002020' => CountryCode::TR, + '+972546589568' => CountryCode::IL, + '+35796562049' => CountryCode::CY, + ]; + + foreach ($validValues as $number => $country) { + $phone = PhoneNumber::from($number); + Assert::type(PhoneNumber::class, $phone); + Assert::equal( + $country, + $phone->guessCountry() + ->getValue() + ); + echo 'Phone number ' + . $phone->getValue() + . ' belongs to ' + . $phone->guessCountry() + ->getValue() + . \PHP_EOL; + } + } -$validValues = [ - '+385915809952' => CountryCode::HR, - '+420(608)111111' => CountryCode::CZ, - '00421 254111111' => CountryCode::SK, - '00421 905 111 111' => CountryCode::SK, - '+420 950 111 111' => CountryCode::CZ, - '+420720182158' => CountryCode::CZ, - '+391234567891234' => CountryCode::IT, - '+905322002020' => CountryCode::TR, - '+972546589568' => CountryCode::IL, - '+35796562049' => CountryCode::CY, -]; - -foreach ($validValues as $number => $country) { - $phone = PhoneNumber::from($number); - Assert::type(PhoneNumber::class, $phone); - Assert::equal($country, $phone->guessCountry()->getValue()); - echo 'Phone number ' - . $phone->getValue() - . ' belongs to ' - . $phone->guessCountry()->getValue() - . \PHP_EOL; } +(new PhoneNumberTest())->run();