diff --git a/lib/Doctrine/DBAL/DBALException.php b/lib/Doctrine/DBAL/DBALException.php index 02cad00cc43..4961af2f35f 100644 --- a/lib/Doctrine/DBAL/DBALException.php +++ b/lib/Doctrine/DBAL/DBALException.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Driver\ExceptionConverterDriver; use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type; use Exception; use Throwable; use function array_map; @@ -18,6 +19,7 @@ use function is_string; use function json_encode; use function preg_replace; +use function spl_object_hash; use function sprintf; class DBALException extends Exception @@ -277,4 +279,16 @@ public static function typeNotFound($name) { return new self('Type to be overwritten ' . $name . ' does not exist.'); } + + public static function typeNotRegistered(Type $type) : self + { + return new self(sprintf('Type of the class %s@%s is not registered.', get_class($type), spl_object_hash($type))); + } + + public static function typeAlreadyRegistered(Type $type) : self + { + return new self( + sprintf('Type of the class %s@%s is already registered.', get_class($type), spl_object_hash($type)) + ); + } } diff --git a/lib/Doctrine/DBAL/Types/Type.php b/lib/Doctrine/DBAL/Types/Type.php index 025eb14cbda..f6aa3dde046 100644 --- a/lib/Doctrine/DBAL/Types/Type.php +++ b/lib/Doctrine/DBAL/Types/Type.php @@ -5,6 +5,8 @@ use Doctrine\DBAL\DBALException; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; +use function array_map; +use function get_class; use function str_replace; use function strrpos; use function substr; @@ -16,76 +18,70 @@ */ abstract class Type { - public const TARRAY = 'array'; - public const SIMPLE_ARRAY = 'simple_array'; - public const JSON_ARRAY = 'json_array'; - public const JSON = 'json'; public const BIGINT = 'bigint'; + public const BINARY = 'binary'; + public const BLOB = 'blob'; public const BOOLEAN = 'boolean'; + public const DATE = 'date'; + public const DATE_IMMUTABLE = 'date_immutable'; + public const DATEINTERVAL = 'dateinterval'; public const DATETIME = 'datetime'; public const DATETIME_IMMUTABLE = 'datetime_immutable'; public const DATETIMETZ = 'datetimetz'; public const DATETIMETZ_IMMUTABLE = 'datetimetz_immutable'; - public const DATE = 'date'; - public const DATE_IMMUTABLE = 'date_immutable'; - public const TIME = 'time'; - public const TIME_IMMUTABLE = 'time_immutable'; public const DECIMAL = 'decimal'; + public const FLOAT = 'float'; + public const GUID = 'guid'; public const INTEGER = 'integer'; + public const JSON = 'json'; + public const JSON_ARRAY = 'json_array'; public const OBJECT = 'object'; + public const SIMPLE_ARRAY = 'simple_array'; public const SMALLINT = 'smallint'; public const STRING = 'string'; + public const TARRAY = 'array'; public const TEXT = 'text'; - public const BINARY = 'binary'; - public const BLOB = 'blob'; - public const FLOAT = 'float'; - public const GUID = 'guid'; - public const DATEINTERVAL = 'dateinterval'; - - /** - * Map of already instantiated type objects. One instance per type (flyweight). - * - * @var self[] - */ - private static $_typeObjects = []; + public const TIME = 'time'; + public const TIME_IMMUTABLE = 'time_immutable'; /** * The map of supported doctrine mapping types. - * - * @var string[] */ - private static $_typesMap = [ - self::TARRAY => ArrayType::class, - self::SIMPLE_ARRAY => SimpleArrayType::class, - self::JSON_ARRAY => JsonArrayType::class, - self::JSON => JsonType::class, - self::OBJECT => ObjectType::class, - self::BOOLEAN => BooleanType::class, - self::INTEGER => IntegerType::class, - self::SMALLINT => SmallIntType::class, - self::BIGINT => BigIntType::class, - self::STRING => StringType::class, - self::TEXT => TextType::class, - self::DATETIME => DateTimeType::class, - self::DATETIME_IMMUTABLE => DateTimeImmutableType::class, - self::DATETIMETZ => DateTimeTzType::class, + private const BUILTIN_TYPES_MAP = [ + self::BIGINT => BigIntType::class, + self::BINARY => BinaryType::class, + self::BLOB => BlobType::class, + self::BOOLEAN => BooleanType::class, + self::DATE => DateType::class, + self::DATE_IMMUTABLE => DateImmutableType::class, + self::DATEINTERVAL => DateIntervalType::class, + self::DATETIME => DateTimeType::class, + self::DATETIME_IMMUTABLE => DateTimeImmutableType::class, + self::DATETIMETZ => DateTimeTzType::class, self::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class, - self::DATE => DateType::class, - self::DATE_IMMUTABLE => DateImmutableType::class, - self::TIME => TimeType::class, - self::TIME_IMMUTABLE => TimeImmutableType::class, - self::DECIMAL => DecimalType::class, - self::FLOAT => FloatType::class, - self::BINARY => BinaryType::class, - self::BLOB => BlobType::class, - self::GUID => GuidType::class, - self::DATEINTERVAL => DateIntervalType::class, + self::DECIMAL => DecimalType::class, + self::FLOAT => FloatType::class, + self::GUID => GuidType::class, + self::INTEGER => IntegerType::class, + self::JSON => JsonType::class, + self::JSON_ARRAY => JsonArrayType::class, + self::OBJECT => ObjectType::class, + self::SIMPLE_ARRAY => SimpleArrayType::class, + self::SMALLINT => SmallIntType::class, + self::STRING => StringType::class, + self::TARRAY => ArrayType::class, + self::TEXT => TextType::class, + self::TIME => TimeType::class, + self::TIME_IMMUTABLE => TimeImmutableType::class, ]; + /** @var TypeRegistry|null */ + private static $typeRegistry; + /** - * Prevents instantiation and forces use of the factory method. + * @internal Do not instantiate directly - use {@see Type::addType()} method instead. */ - final private function __construct() + final public function __construct() { } @@ -148,6 +144,29 @@ abstract public function getSQLDeclaration(array $fieldDeclaration, AbstractPlat */ abstract public function getName(); + /** + * @internal This method is only to be used within DBAL for forward compatibility purposes. Do not use directly. + */ + final public static function getTypeRegistry() : TypeRegistry + { + if (self::$typeRegistry === null) { + self::$typeRegistry = self::createTypeRegistry(); + } + + return self::$typeRegistry; + } + + private static function createTypeRegistry() : TypeRegistry + { + $registry = new TypeRegistry(); + + foreach (self::BUILTIN_TYPES_MAP as $name => $class) { + $registry->register($name, new $class()); + } + + return $registry; + } + /** * Factory method to create type instances. * Type instances are implemented as flyweights. @@ -160,14 +179,7 @@ abstract public function getName(); */ public static function getType($name) { - if (! isset(self::$_typeObjects[$name])) { - if (! isset(self::$_typesMap[$name])) { - throw DBALException::unknownColumnType($name); - } - self::$_typeObjects[$name] = new self::$_typesMap[$name](); - } - - return self::$_typeObjects[$name]; + return self::getTypeRegistry()->get($name); } /** @@ -182,11 +194,7 @@ public static function getType($name) */ public static function addType($name, $className) { - if (isset(self::$_typesMap[$name])) { - throw DBALException::typeExists($name); - } - - self::$_typesMap[$name] = $className; + self::getTypeRegistry()->register($name, new $className()); } /** @@ -198,7 +206,7 @@ public static function addType($name, $className) */ public static function hasType($name) { - return isset(self::$_typesMap[$name]); + return self::getTypeRegistry()->has($name); } /** @@ -213,15 +221,7 @@ public static function hasType($name) */ public static function overrideType($name, $className) { - if (! isset(self::$_typesMap[$name])) { - throw DBALException::typeNotFound($name); - } - - if (isset(self::$_typeObjects[$name])) { - unset(self::$_typeObjects[$name]); - } - - self::$_typesMap[$name] = $className; + self::getTypeRegistry()->override($name, new $className()); } /** @@ -245,7 +245,12 @@ public function getBindingType() */ public static function getTypesMap() { - return self::$_typesMap; + return array_map( + static function (Type $type) : string { + return get_class($type); + }, + self::getTypeRegistry()->getMap() + ); } /** diff --git a/lib/Doctrine/DBAL/Types/TypeRegistry.php b/lib/Doctrine/DBAL/Types/TypeRegistry.php new file mode 100644 index 00000000000..7e8093cf749 --- /dev/null +++ b/lib/Doctrine/DBAL/Types/TypeRegistry.php @@ -0,0 +1,118 @@ + Map of type names and their corresponding flyweight objects. */ + private $instances = []; + + /** + * Finds a type by the given name. + * + * @throws DBALException + */ + public function get(string $name) : Type + { + if (! isset($this->instances[$name])) { + throw DBALException::unknownColumnType($name); + } + + return $this->instances[$name]; + } + + /** + * Finds a name for the given type. + * + * @throws DBALException + */ + public function lookupName(Type $type) : string + { + $name = $this->findTypeName($type); + + if ($name === null) { + throw DBALException::typeNotRegistered($type); + } + + return $name; + } + + /** + * Checks if there is a type of the given name. + */ + public function has(string $name) : bool + { + return isset($this->instances[$name]); + } + + /** + * Registers a custom type to the type map. + * + * @throws DBALException + */ + public function register(string $name, Type $type) : void + { + if (isset($this->instances[$name])) { + throw DBALException::typeExists($name); + } + + if ($this->findTypeName($type) !== null) { + throw DBALException::typeAlreadyRegistered($type); + } + + $this->instances[$name] = $type; + } + + /** + * Overrides an already defined type to use a different implementation. + * + * @throws DBALException + */ + public function override(string $name, Type $type) : void + { + if (! isset($this->instances[$name])) { + throw DBALException::typeNotFound($name); + } + + if (! in_array($this->findTypeName($type), [$name, null], true)) { + throw DBALException::typeAlreadyRegistered($type); + } + + $this->instances[$name] = $type; + } + + /** + * Gets the map of all registered types and their corresponding type instances. + * + * @internal + * + * @return array + */ + public function getMap() : array + { + return $this->instances; + } + + private function findTypeName(Type $type) : ?string + { + $name = array_search($type, $this->instances, true); + + if ($name === false) { + return null; + } + + return $name; + } +} diff --git a/tests/Doctrine/Tests/DBAL/Types/TypeRegistryTest.php b/tests/Doctrine/Tests/DBAL/Types/TypeRegistryTest.php new file mode 100644 index 00000000000..14cd7497385 --- /dev/null +++ b/tests/Doctrine/Tests/DBAL/Types/TypeRegistryTest.php @@ -0,0 +1,154 @@ +testType = new BlobType(); + $this->otherTestType = new BinaryType(); + + $this->registry = new TypeRegistry(); + $this->registry->register(self::TEST_TYPE_NAME, $this->testType); + $this->registry->register(self::OTHER_TEST_TYPE_NAME, $this->otherTestType); + } + + public function testGet() : void + { + self::assertSame($this->testType, $this->registry->get(self::TEST_TYPE_NAME)); + self::assertSame($this->otherTestType, $this->registry->get(self::OTHER_TEST_TYPE_NAME)); + + $this->expectException(DBALException::class); + $this->registry->get('unknown'); + } + + public function testGetReturnsSameInstances() : void + { + self::assertSame( + $this->registry->get(self::TEST_TYPE_NAME), + $this->registry->get(self::TEST_TYPE_NAME) + ); + } + + public function testLookupName() : void + { + self::assertSame( + self::TEST_TYPE_NAME, + $this->registry->lookupName($this->testType) + ); + self::assertSame( + self::OTHER_TEST_TYPE_NAME, + $this->registry->lookupName($this->otherTestType) + ); + + $this->expectException(DBALException::class); + $this->registry->lookupName(new TextType()); + } + + public function testHas() : void + { + self::assertTrue($this->registry->has(self::TEST_TYPE_NAME)); + self::assertTrue($this->registry->has(self::OTHER_TEST_TYPE_NAME)); + self::assertFalse($this->registry->has('unknown')); + } + + public function testRegister() : void + { + $newType = new TextType(); + + $this->registry->register('some', $newType); + + self::assertTrue($this->registry->has('some')); + self::assertSame($newType, $this->registry->get('some')); + } + + public function testRegisterWithAlradyRegisteredName() : void + { + $this->registry->register('some', new TextType()); + + $this->expectException(DBALException::class); + $this->registry->register('some', new TextType()); + } + + public function testRegisterWithAlreadyRegisteredInstance() : void + { + $newType = new TextType(); + + $this->registry->register('some', $newType); + + $this->expectException(DBALException::class); + $this->registry->register('other', $newType); + } + + public function testOverride() : void + { + $baseType = new TextType(); + $overrideType = new StringType(); + + $this->registry->register('some', $baseType); + $this->registry->override('some', $overrideType); + + self::assertSame($overrideType, $this->registry->get('some')); + } + + public function testOverrideAllowsExistingInstance() : void + { + $type = new TextType(); + + $this->registry->register('some', $type); + $this->registry->override('some', $type); + + self::assertSame($type, $this->registry->get('some')); + } + + public function testOverrideWithAlreadyRegisteredInstance() : void + { + $newType = new TextType(); + + $this->registry->register('first', $newType); + $this->registry->register('second', new StringType()); + + $this->expectException(DBALException::class); + $this->registry->override('second', $newType); + } + + public function testOverrideWithUnknownType() : void + { + $this->expectException(DBALException::class); + $this->registry->override('unknown', new TextType()); + } + + public function testGetMap() : void + { + $registeredTypes = $this->registry->getMap(); + + self::assertCount(2, $registeredTypes); + self::assertArrayHasKey(self::TEST_TYPE_NAME, $registeredTypes); + self::assertArrayHasKey(self::OTHER_TEST_TYPE_NAME, $registeredTypes); + self::assertSame($this->testType, $registeredTypes[self::TEST_TYPE_NAME]); + self::assertSame($this->otherTestType, $registeredTypes[self::OTHER_TEST_TYPE_NAME]); + } +} diff --git a/tests/Doctrine/Tests/DBAL/Types/TypeTest.php b/tests/Doctrine/Tests/DBAL/Types/TypeTest.php new file mode 100644 index 00000000000..59440f062c6 --- /dev/null +++ b/tests/Doctrine/Tests/DBAL/Types/TypeTest.php @@ -0,0 +1,34 @@ +getReflectionConstants() as $constant) { + if (! $constant->isPublic()) { + continue; + } + + yield [$constant->getValue()]; + } + } +}