Library for auto-registering value objects (crates, DTOs) as Doctrine types.
Wouldn't it be nice if you could use your Value Objects as entity type directly? With AutoType you do not have to create doctrine type for every single value object type.
composer require martingold/autotypeYou have few options:
- Add
#[ValueGetter]and#[Constructor](not required) attributes to your ValueObject, DTO, Crate, ... - Make your objects implement ValueObject interface
- Create your own driver (implement your own
TypeDefinitionDriver). See below 👇
Example using attributes:
finalreadonlyclass Url
{
privatefunction__construct(
privatestring$value,
) {
}
// Regular constructor is used when attribute not found
#[Constructor]
publicstaticfunctioncreate(string$url): self
{
if (filter_var($value, FILTER_VALIDATE_URL) === false) {
throw MalformedUrl::fromString($value);
}
returnnew self($url)
}
#[ValueGetter]
publicfunctiongetValue(): string
{
return$this->value;
}
publicfunctionisSecure(): bool
{
returnstr_starts_with('https://', $this->value);
}
}Register types at the entry point of your application (kernel boot when using symfony):
// Get a PSR-6 cache instance$cache = $this->container->get(CacheItemPoolInterface::class);
// Alternatively, use Doctrine's PSR-6 metadata cache$entityManager = $this->container->get(EntityManagerInterface::class);
$cache = $entityManager->getConfiguration()->getMetadataCache();
// Create a type provider$cachedTypeFinder = newCachedTypeDefinitionProvider(
newDefaultTypeDefinitionProvider(__DIR__ . '/../ValueObject'),
$cache
);
// Register dynamic types
(newDynamicTypeRegistry($cachedTypeFinder))->register();Use the value object directly in your entities.
#[Entity]
class Company
{
#[Column]
privatestring$name;
#[Column(type: Url::class)]
privateUrl$url;
}See tests/Entity for the example usage of the drivers. The library comes with two default drivers:
This driver registers all classes with a #[ValueGetter] method as Doctrine types. If a static factory
method is needed, add the #[Constructor] to the method which should be used for constructing the object back from
database value.
This driver registers all classes implementing ValueObject interface.
If you have existing value objects based on your project's conventions and do not want to add additional interfaces or custom attributes, you can implement your own driver and use it during type registration:
$typeDefinitionProvider = newCachedTypeDefinitionProvider(
newScanTypeDefinitionProvider($sourceFolder, [
newCustomTypeDefinitionDriver(),
]),
$cache,
);
(newDynamicTypeRegistry($typeDefinitionProvider))->register();The possibilities are endless. You can even specify your own custom dynamic types in
case you have special requirements like column length or database-specific optimizations.
See AttributeTypeDefinitionDriver and InterfaceTypeDefinitionDriver for more examples.
class CustomTypeDefinitionDriver implements TypeDefinitionDriver
{
/** * Should be the class treated as doctrine type? * @param ReflectionClass<object> $class */publicfunctionsupports(ReflectionClass$class): bool
{
returnstr_ends_with($class->getShortName(), 'Crate');
}
/** * Get dynamic type class. Whether it is value a string or int. * @param ReflectionClass<object> $class * * @return class-string<DynamicType&Type> */publicfunctiongetDynamicTypeClass(ReflectionClass$class): string
{
returnmatch ($this->getValueMethodReturnType($class)) {
'string' => StringDynamicType::class,
'int' => IntegerDynamicType::class,
default => thrownewUnsupportedType('Only string|int type is supported.'),
};
}
/** * Name of the method which should be used when persisting object to database. * @param ReflectionClass<object> $class */publicfunctiongetValueMethodName(ReflectionClass$class): string
{
return'getValue';
}
/** * Method to use when creating the object from database value. Must have single argument. * When null returned, the regular constructor is used. */publicfunctiongetConstructorMethodName(ReflectionClass$class): string|null
{
return$class->hasMethod('of') ? 'of' : null;
}
/** * Get value getter method return type to determine if database value should be string or int * @param ReflectionClass<object> $class * * @throws UnsupportedType */privatefunctiongetValueMethodReturnType(ReflectionClass$class): string
{
$returnType = $class->getMethod('getValue')->getReturnType();
if (!$returnTypeinstanceof ReflectionNamedType) {
thrownewUnsupportedType("Intersection or union return type not supported in method {$class->getName()}::getValue()");
}
if (!$returnType->isBuiltin()) {
thrownewUnsupportedType("Only scalar return types are supported in {$class->getName()}::getValue()");
}
return$returnType->getName();
}
}