Add TypeRegistry to Configuration and use it in all internal type resolution#7342
Add TypeRegistry to Configuration and use it in all internal type resolution#7342GromNaN wants to merge 10 commits intodoctrine:4.5.xfrom
TypeRegistry to Configuration and use it in all internal type resolution#7342Conversation
…instance-based lookups All internal type resolution (Connection, Statement, AbstractPlatform, SchemaManagers, MetadataProviders) now goes through Configuration::getTypeRegistry() instead of the global Type::getType() / Type::hasType() / Type::getTypesMap() static methods. Table receives an optional Configuration so addColumn() uses the instance registry when available, falling back to Type::getType() for user code without a Configuration. ColumnEditor::setTypeName() similarly falls back to Type::getType() for user code; internal callers (MetadataProviders) now use setType() with the configuration registry.
…in types by default The constant is now TypeRegistry::BUILTIN_TYPES_MAP (public). Any new TypeRegistry() is pre-populated with all built-in type instances; additional types passed to the constructor are registered on top and may override built-ins. Type::getTypeRegistry() is simplified to new TypeRegistry() with no arguments.
7ca5478 to
b5bd71c
Compare
SchemaConfig now carries a TypeRegistry that is populated by AbstractSchemaManager::createSchemaConfig() from the connection configuration. Schema::createTable() forwards it to each Table so that Table::addColumn() resolves types from the per-connection TypeRegistry instead of falling back to the global static registry. Table's constructor parameter is changed from ?Configuration to ?TypeRegistry directly, since Configuration was only needed to reach its TypeRegistry. A TODO comment is added to ColumnDiff::hasTypeChanged() noting that the current class-based comparison is insufficient now that types are services: same-class aliases (json / json_object) produce false negatives, and distinct service instances of the same class would not be detected. The fix (identity comparison) is left for a follow-up.
9e8eef6 to
adbd44c
Compare
|
As discussed during the SymfonyLive, it would be great to support lazy-loading of type instances injected in the TypeRegistry, to reduce the cost of instantiation the connection service (especially when types have dependencies, which might lead to instantiating a bigger object graph). As far as DoctrineBundle is concerned, the easier way would probably involve injecting a PSR-12 ContainerInterface (with ids being the type names) and a list of type names (or a map of type names to ids in the container to allow more flexibility about those ids). This would allow us to use the Symfony ServiceLocator which performs such lazy-loading (it would of course mean that the constructor should not retrieve type instances, as that would defeat the lazy-loading). |
|
Thanks for the reminder @stof! I implemented both approaches:
Both paths converge into a unified |
- Accept ServiceProviderInterface<Type> or array<string, Type|ContainerInterface> as constructor argument; built-in types are now lazy too - Store unresolved types as class-string (built-ins) or ContainerInterface in a $factories array; instances are created on first get() and cached - Replace $instancesReverseIndex spl_object_id map with WeakMap<Type, string> for O(1) reverse lookup: 84 ns/op vs 86 ns/op (spl_id) vs 171 ns/op (array_search) for 30 registered types, while also being GC-friendly
d87fe57 to
0b6f833
Compare
| private array $factories = []; | ||
|
|
||
| /** @var WeakMap<Type, string> */ | ||
| private WeakMap $instancesReverseIndex; |
There was a problem hiding this comment.
I want to get rid of this $instancesReverseIndex by removing the lookupName function. This means there will no longer be a restriction preventing you from registering the same type instance under multiple names.
|
I find it weird to pass multiple ContainerInterface. A single container can hold all the types (under different indexes). |
Continues #6705. The existing
TypeRegistryclass already supports scoped, instance-basedtype management; this PR wires it into the rest of DBAL so it is actually used.
A PR to Doctrine ORM and DoctrineBundle will follow.
This is already being done in the same way for Doctrine MongoDB ODM: doctrine/mongodb-odm#2966
Motivation
Two goals:
Dependency injection into type instances. Custom types sometimes need access to
services (e.g. a serializer, an encryption service). With the global static registry this is impossible
without static state. A per-connection
TypeRegistrycan hold fully-constructed typeinstances.
Prevent global side-effects.
Type::addType()andType::overrideType()mutate aprocess-wide singleton, so a test or a bundle changing a type affects every connection.
A scoped registry isolates those changes.
What changed
TypeRegistrynew TypeRegistry()alreadycontains all built-in types. Custom types passed as constructor arguments are layered
on top and may override built-ins by name.
ConfigurationgetTypeRegistry(): TypeRegistryreturns the registry for this connection, lazilydefaulting to the global one (
Type::getTypeRegistry()).setTypeRegistry(TypeRegistry $registry): selfinjects a custom registry scoped tothis connection.
Internal type resolution
All of the following now resolve types through
$configuration->getTypeRegistry()insteadof the static
Type::*methods:ConnectionconvertToDatabaseValue(),convertToPHPValue(),getBindingInfo()StatementAbstractPlatforminitializeAllDoctrineTypeMappings(),registerDoctrineTypeMapping()*SchemaManager(×6)_getPortableTableColumnDefinition()*MetadataProvider(×6)->setTypeName()to->setType())TableaddColumn()uses the TypeRegistry fromSchemaConfigwhen availableSchemacreateTable()forwards the TypeRegistry fromSchemaConfigto eachTableSchemaConfigTypeRegistry; populated byAbstractSchemaManager::createSchemaConfig()from the connection configurationAbstractPlatformreceives itsConfigurationvia a newsetConfiguration()methodcalled by
Connection::getDatabasePlatform()after platform creation.AbstractSchemaManager::createSchemaConfig()now sets the TypeRegistry from the connectionso that every
Tablecreated viaSchema::createTable()(including from ORM'sSchemaTool)resolves types through the per-connection registry.
Static
Type::*methods are keptType::getType(),Type::addType(),Type::overrideType(),Type::hasType(), andType::getTypesMap()still delegate to the global singleton and remain the fallback foruser code that creates
TableorColumnEditorwithout aConfiguration.Trade-offs
Table::addColumn()falls back to the global registry when no TypeRegistry is presentin the
SchemaConfig. Enforcing the instance registry here would require a breaking change.Internal callers have been updated to go through
SchemaConfig, so the fallback is onlyhit from user code that constructs
Tabledirectly without aSchemaConfig.ColumnDiff::hasTypeChanged()still compares by class name, which is insufficient nowthat two distinct service instances can share the same class (e.g. two differently
configured
MoneyTypeinstances), and already incorrect for built-in aliases likejsonand
json_objectwhich shareJsonType::classbut are distinct registered names.The correct fix is to compare by instance identity (
===) once both sides of the diffare guaranteed to resolve from the same
TypeRegistry. This is left as a follow-up.Custom types registered via
Type::addType()are invisible to connections that use acustom
TypeRegistry. This is intentional: it is the isolation the feature provides.Users who set a custom registry are responsible for registering all types they need in it.
The global singleton is preserved.
Type::getTypeRegistry()still returns theprocess-wide registry. Connections that do not call
setTypeRegistry()continue to behaveexactly as before.