Unverified Commit 11b67bbd authored by Sergei Morozov's avatar Sergei Morozov Committed by GitHub

Merge pull request #3354 from Majkl578/type-registry

Extract type factory and registry from Type into TypeRegistry
parents 773275d1 089f32e6
...@@ -6,6 +6,7 @@ use Doctrine\DBAL\Driver\DriverException as DriverExceptionInterface; ...@@ -6,6 +6,7 @@ use Doctrine\DBAL\Driver\DriverException as DriverExceptionInterface;
use Doctrine\DBAL\Driver\ExceptionConverterDriver; use Doctrine\DBAL\Driver\ExceptionConverterDriver;
use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Exception; use Exception;
use Throwable; use Throwable;
use function array_map; use function array_map;
...@@ -18,6 +19,7 @@ use function is_resource; ...@@ -18,6 +19,7 @@ use function is_resource;
use function is_string; use function is_string;
use function json_encode; use function json_encode;
use function preg_replace; use function preg_replace;
use function spl_object_hash;
use function sprintf; use function sprintf;
class DBALException extends Exception class DBALException extends Exception
...@@ -277,4 +279,16 @@ class DBALException extends Exception ...@@ -277,4 +279,16 @@ class DBALException extends Exception
{ {
return new self('Type to be overwritten ' . $name . ' does not exist.'); 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))
);
}
} }
...@@ -5,6 +5,8 @@ namespace Doctrine\DBAL\Types; ...@@ -5,6 +5,8 @@ namespace Doctrine\DBAL\Types;
use Doctrine\DBAL\DBALException; use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform;
use function array_map;
use function get_class;
use function str_replace; use function str_replace;
use function strrpos; use function strrpos;
use function substr; use function substr;
...@@ -16,76 +18,70 @@ use function substr; ...@@ -16,76 +18,70 @@ use function substr;
*/ */
abstract class Type 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 BIGINT = 'bigint';
public const BINARY = 'binary';
public const BLOB = 'blob';
public const BOOLEAN = 'boolean'; 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 = 'datetime';
public const DATETIME_IMMUTABLE = 'datetime_immutable'; public const DATETIME_IMMUTABLE = 'datetime_immutable';
public const DATETIMETZ = 'datetimetz'; public const DATETIMETZ = 'datetimetz';
public const DATETIMETZ_IMMUTABLE = 'datetimetz_immutable'; 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 DECIMAL = 'decimal';
public const FLOAT = 'float';
public const GUID = 'guid';
public const INTEGER = 'integer'; public const INTEGER = 'integer';
public const JSON = 'json';
public const JSON_ARRAY = 'json_array';
public const OBJECT = 'object'; public const OBJECT = 'object';
public const SIMPLE_ARRAY = 'simple_array';
public const SMALLINT = 'smallint'; public const SMALLINT = 'smallint';
public const STRING = 'string'; public const STRING = 'string';
public const TARRAY = 'array';
public const TEXT = 'text'; public const TEXT = 'text';
public const BINARY = 'binary'; public const TIME = 'time';
public const BLOB = 'blob'; public const TIME_IMMUTABLE = 'time_immutable';
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 = [];
/** /**
* The map of supported doctrine mapping types. * The map of supported doctrine mapping types.
*
* @var string[]
*/ */
private static $_typesMap = [ private const BUILTIN_TYPES_MAP = [
self::TARRAY => ArrayType::class, self::BIGINT => BigIntType::class,
self::SIMPLE_ARRAY => SimpleArrayType::class, self::BINARY => BinaryType::class,
self::JSON_ARRAY => JsonArrayType::class, self::BLOB => BlobType::class,
self::JSON => JsonType::class, self::BOOLEAN => BooleanType::class,
self::OBJECT => ObjectType::class, self::DATE => DateType::class,
self::BOOLEAN => BooleanType::class, self::DATE_IMMUTABLE => DateImmutableType::class,
self::INTEGER => IntegerType::class, self::DATEINTERVAL => DateIntervalType::class,
self::SMALLINT => SmallIntType::class, self::DATETIME => DateTimeType::class,
self::BIGINT => BigIntType::class, self::DATETIME_IMMUTABLE => DateTimeImmutableType::class,
self::STRING => StringType::class, self::DATETIMETZ => DateTimeTzType::class,
self::TEXT => TextType::class,
self::DATETIME => DateTimeType::class,
self::DATETIME_IMMUTABLE => DateTimeImmutableType::class,
self::DATETIMETZ => DateTimeTzType::class,
self::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class, self::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class,
self::DATE => DateType::class, self::DECIMAL => DecimalType::class,
self::DATE_IMMUTABLE => DateImmutableType::class, self::FLOAT => FloatType::class,
self::TIME => TimeType::class, self::GUID => GuidType::class,
self::TIME_IMMUTABLE => TimeImmutableType::class, self::INTEGER => IntegerType::class,
self::DECIMAL => DecimalType::class, self::JSON => JsonType::class,
self::FLOAT => FloatType::class, self::JSON_ARRAY => JsonArrayType::class,
self::BINARY => BinaryType::class, self::OBJECT => ObjectType::class,
self::BLOB => BlobType::class, self::SIMPLE_ARRAY => SimpleArrayType::class,
self::GUID => GuidType::class, self::SMALLINT => SmallIntType::class,
self::DATEINTERVAL => DateIntervalType::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 class Type ...@@ -148,6 +144,29 @@ abstract class Type
*/ */
abstract public function getName(); 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. * Factory method to create type instances.
* Type instances are implemented as flyweights. * Type instances are implemented as flyweights.
...@@ -160,14 +179,7 @@ abstract class Type ...@@ -160,14 +179,7 @@ abstract class Type
*/ */
public static function getType($name) public static function getType($name)
{ {
if (! isset(self::$_typeObjects[$name])) { return self::getTypeRegistry()->get($name);
if (! isset(self::$_typesMap[$name])) {
throw DBALException::unknownColumnType($name);
}
self::$_typeObjects[$name] = new self::$_typesMap[$name]();
}
return self::$_typeObjects[$name];
} }
/** /**
...@@ -182,11 +194,7 @@ abstract class Type ...@@ -182,11 +194,7 @@ abstract class Type
*/ */
public static function addType($name, $className) public static function addType($name, $className)
{ {
if (isset(self::$_typesMap[$name])) { self::getTypeRegistry()->register($name, new $className());
throw DBALException::typeExists($name);
}
self::$_typesMap[$name] = $className;
} }
/** /**
...@@ -198,7 +206,7 @@ abstract class Type ...@@ -198,7 +206,7 @@ abstract class Type
*/ */
public static function hasType($name) public static function hasType($name)
{ {
return isset(self::$_typesMap[$name]); return self::getTypeRegistry()->has($name);
} }
/** /**
...@@ -213,15 +221,7 @@ abstract class Type ...@@ -213,15 +221,7 @@ abstract class Type
*/ */
public static function overrideType($name, $className) public static function overrideType($name, $className)
{ {
if (! isset(self::$_typesMap[$name])) { self::getTypeRegistry()->override($name, new $className());
throw DBALException::typeNotFound($name);
}
if (isset(self::$_typeObjects[$name])) {
unset(self::$_typeObjects[$name]);
}
self::$_typesMap[$name] = $className;
} }
/** /**
...@@ -245,7 +245,12 @@ abstract class Type ...@@ -245,7 +245,12 @@ abstract class Type
*/ */
public static function getTypesMap() public static function getTypesMap()
{ {
return self::$_typesMap; return array_map(
static function (Type $type) : string {
return get_class($type);
},
self::getTypeRegistry()->getMap()
);
} }
/** /**
......
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Types;
use Doctrine\DBAL\DBALException;
use function array_search;
use function in_array;
/**
* The type registry is responsible for holding a map of all known DBAL types.
* The types are stored using the flyweight pattern so that one type only exists as exactly one instance.
*
* @internal TypeRegistry exists for forward compatibility, its API should not be considered stable.
*/
final class TypeRegistry
{
/** @var array<string, Type> 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<string, Type>
*/
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;
}
}
<?php
declare(strict_types=1);
namespace Doctrine\Tests\DBAL\Types;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Types\BinaryType;
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Types\TypeRegistry;
use PHPUnit\Framework\TestCase;
class TypeRegistryTest extends TestCase
{
private const TEST_TYPE_NAME = 'test';
private const OTHER_TEST_TYPE_NAME = 'other';
/** @var TypeRegistry */
private $registry;
/** @var BlobType */
private $testType;
/** @var BinaryType */
private $otherTestType;
protected function setUp() : void
{
$this->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]);
}
}
<?php
declare(strict_types=1);
namespace Doctrine\Tests\DBAL\Types;
use Doctrine\DBAL\Types\Type;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
class TypeTest extends TestCase
{
/**
* @dataProvider defaultTypesProvider()
*/
public function testDefaultTypesAreRegistered(string $name) : void
{
self::assertTrue(Type::hasType($name));
}
/**
* @return string[][]
*/
public function defaultTypesProvider() : iterable
{
foreach ((new ReflectionClass(Type::class))->getReflectionConstants() as $constant) {
if (! $constant->isPublic()) {
continue;
}
yield [$constant->getValue()];
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment