Skip to content

Commit 435c5c5

Browse files
committed
Auto-generated types for standard int/string/uuid objects
Not sure if this should become part of the main package. The API still needs work, and there is currently no interface for the string/int/uuid objects to enforce the correct methods.
1 parent 05ea3ab commit 435c5c5

13 files changed

+430
-0
lines changed

config/services.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
5+
6+
return function (ContainerConfigurator $configurator): void
7+
{
8+
$services = $configurator->services()
9+
->defaults()
10+
->autowire()
11+
->autoconfigure()
12+
;
13+
14+
$services->load('Headsnet\\DoctrineToolsBundle\\', '../src/*');
15+
};

src/Attribute/DoctrineType.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Headsnet\DoctrineToolsBundle\Attribute;
5+
6+
use Attribute;
7+
8+
#[Attribute(Attribute::TARGET_CLASS)]
9+
final class DoctrineType
10+
{
11+
public function __construct(
12+
public readonly string $name,
13+
public readonly string $type
14+
) {
15+
}
16+
}

src/HeadsnetDoctrineToolsBundle.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Headsnet\DoctrineToolsBundle\Mapping\CarbonTypeMappingsCompilerPass;
66
use Headsnet\DoctrineToolsBundle\Mapping\DoctrineTypeMappingsCompilerPass;
7+
use Headsnet\DoctrineToolsBundle\Types\DoctrineTypesCompilerPass;
78
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
89
use Symfony\Component\DependencyInjection\ContainerBuilder;
910
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
@@ -15,6 +16,15 @@ public function configure(DefinitionConfigurator $definition): void
1516
{
1617
$definition->rootNode()
1718
->children()
19+
->scalarNode('root_namespace')->cannotBeEmpty()->end()
20+
->arrayNode('preset_types')
21+
->canBeDisabled()
22+
->children()
23+
->arrayNode('scan_dirs')
24+
->defaultValue(['src/'])->scalarPrototype()->end()
25+
->end()
26+
->end()
27+
->end() // End preset_types
1828
->arrayNode('custom_types')
1929
->children()
2030
->arrayNode('scan_dirs')
@@ -33,13 +43,19 @@ public function configure(DefinitionConfigurator $definition): void
3343

3444
/**
3545
* @param array{
46+
* root_namespace: string,
47+
* preset_types: array{scan_dirs: array<string>},
3648
* custom_types: array{scan_dirs: array<string>},
3749
* carbon_types: array{enabled: boolean, replace: boolean}
3850
* } $config
3951
*/
4052
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
4153
{
54+
$container->import('../config/services.php');
55+
4256
$container->parameters()
57+
->set('headsnet_doctrine_tools.root_namespace', $config['root_namespace'])
58+
->set('headsnet_doctrine_tools.preset_types.scan_dirs', $config['preset_types']['scan_dirs'])
4359
->set('headsnet_doctrine_tools.custom_types.scan_dirs', $config['custom_types']['scan_dirs'])
4460
->set('headsnet_doctrine_tools.carbon_types.enabled', $config['carbon_types']['enabled'])
4561
->set('headsnet_doctrine_tools.carbon_types.replace', $config['carbon_types']['replace'])
@@ -50,6 +66,10 @@ public function build(ContainerBuilder $container): void
5066
{
5167
parent::build($container);
5268

69+
$container->addCompilerPass(
70+
new DoctrineTypesCompilerPass()
71+
);
72+
5373
$container->addCompilerPass(
5474
new DoctrineTypeMappingsCompilerPass()
5575
);

src/Types/CandidateType.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Headsnet\DoctrineToolsBundle\Types;
5+
6+
final class CandidateType
7+
{
8+
private string $baseTypeClass;
9+
10+
/**
11+
* @param class-string $objectClass
12+
*/
13+
public function __construct(
14+
public readonly string $typeName,
15+
public readonly string $typeClass,
16+
public readonly string $baseType,
17+
public readonly string $objectClass,
18+
) {
19+
}
20+
21+
public function setBaseTypeClass(string $baseTypeClass): void
22+
{
23+
$this->baseTypeClass = $baseTypeClass;
24+
}
25+
26+
public function getBaseTypeClass(): string
27+
{
28+
return $this->baseTypeClass;
29+
}
30+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Headsnet\DoctrineToolsBundle\Types;
5+
6+
use Headsnet\DoctrineToolsBundle\Attribute\DoctrineType;
7+
use Headsnet\DoctrineToolsBundle\Types\StandardTypes\MappingPrototype;
8+
use League\ConstructFinder\ConstructFinder;
9+
use ReflectionClass;
10+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
11+
use Symfony\Component\DependencyInjection\ContainerBuilder;
12+
13+
/**
14+
* Automatically create Doctrine types for any class that implements #[DoctrineType].
15+
*/
16+
final class DoctrineTypesCompilerPass implements CompilerPassInterface
17+
{
18+
private const TYPE_DEFINITION_PARAMETER = 'doctrine.dbal.connection_factory.types';
19+
20+
private string $rootNamespace;
21+
22+
public function process(ContainerBuilder $container): void
23+
{
24+
if (!$container->hasParameter(self::TYPE_DEFINITION_PARAMETER)) {
25+
return;
26+
}
27+
28+
/** @var array<string, array{class: class-string}> $typeDefinitions */
29+
$typeDefinitions = $container->getParameter(self::TYPE_DEFINITION_PARAMETER);
30+
/** @var array<string> $scanDirs */
31+
$scanDirs = $container->getParameter('headsnet_doctrine_tools.preset_types.scan_dirs');
32+
$this->rootNamespace = $container->getParameter('headsnet_doctrine_tools.root_namespace'); // @phpstan-ignore-line
33+
34+
$objectsToRegister = $this->findObjectsToRegister($scanDirs);
35+
36+
$prototypeTypes = array_keys($container->findTaggedServiceIds(MappingPrototype::TAG));
37+
38+
foreach ($objectsToRegister as $candidate) {
39+
// Do not add the type if it's been manually defined already
40+
if (array_key_exists($candidate->typeName, $typeDefinitions)) {
41+
continue;
42+
}
43+
44+
/** @var MappingPrototype $prototypeType */
45+
foreach ($prototypeTypes as $prototypeType) {
46+
if ($prototypeType::supports($candidate->baseType)) {
47+
$candidate->setBaseTypeClass(
48+
$prototypeType::mappedBy()
49+
);
50+
}
51+
}
52+
53+
if (!$candidate->getBaseTypeClass()) {
54+
throw new \RuntimeException('Unsupported base type for Doctrine!');
55+
}
56+
57+
$this->writeClassToFile($candidate);
58+
59+
$typeDefinitions[$candidate->typeName] = [
60+
'class' => sprintf(
61+
'%s\_generated\HeadsnetDoctrineTools\Types\\%s',
62+
$this->rootNamespace,
63+
$candidate->typeClass
64+
),
65+
];
66+
}
67+
68+
$container->setParameter(self::TYPE_DEFINITION_PARAMETER, $typeDefinitions);
69+
}
70+
71+
/**
72+
* @param array<string> $scanDirs
73+
*
74+
* @return iterable<CandidateType>
75+
*/
76+
private function findObjectsToRegister(array $scanDirs): iterable
77+
{
78+
$classNames = ConstructFinder::locatedIn(...$scanDirs)->findClassNames();
79+
80+
foreach ($classNames as $className) {
81+
$reflection = new ReflectionClass($className);
82+
83+
// Skip any abstract parent types
84+
if ($reflection->isAbstract()) {
85+
continue;
86+
}
87+
88+
// Only register types that have the #[DoctrineType] attribute
89+
if ($reflection->getAttributes(DoctrineType::class)) {
90+
$attribute = $reflection->getAttributes(DoctrineType::class)[0];
91+
$attributeArgs = $attribute->getArguments();
92+
93+
yield new CandidateType(
94+
typeName: $attributeArgs['name'],
95+
typeClass: $reflection->getShortName() . 'Type',
96+
baseType: $attributeArgs['type'],
97+
objectClass: $className
98+
);
99+
}
100+
}
101+
}
102+
103+
private function generateClass(CandidateType $candidate): string
104+
{
105+
return <<<PHP
106+
<?php
107+
108+
namespace $this->rootNamespace\_generated\HeadsnetDoctrineTools\Types;
109+
110+
class $candidate->typeClass extends \\{$candidate->getBaseTypeClass()} {
111+
public function getName(): string
112+
{
113+
return '$candidate->typeName';
114+
}
115+
116+
public function getClass(): string
117+
{
118+
return '$candidate->objectClass';
119+
}
120+
}
121+
PHP;
122+
}
123+
124+
private function writeClassToFile(CandidateType $candidate): void
125+
{
126+
$classCode = $this->generateClass($candidate);
127+
128+
$filePath = sprintf('src/_generated/HeadsnetDoctrineTools/Types/%s.php', $candidate->typeClass);
129+
130+
if (!is_dir(dirname($filePath))) {
131+
mkdir(dirname($filePath), 0777, true);
132+
}
133+
134+
file_put_contents($filePath, $classCode);
135+
}
136+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Headsnet\DoctrineToolsBundle\Types\StandardTypes;
5+
6+
use Doctrine\DBAL\Platforms\AbstractPlatform;
7+
use Doctrine\DBAL\Types\Type;
8+
9+
abstract class AbstractIntegerMappingType extends Type
10+
{
11+
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
12+
{
13+
return $platform->getIntegerTypeDeclarationSQL($column);
14+
}
15+
16+
/**
17+
* @param int|null $value
18+
*/
19+
public function convertToPHPValue($value, AbstractPlatform $platform): ?object
20+
{
21+
if ($value === null) {
22+
return null;
23+
}
24+
25+
$class = $this->getClass();
26+
27+
return $class::create($value);
28+
}
29+
30+
/**
31+
* @param object|null $value
32+
*/
33+
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?int
34+
{
35+
if ($value === null) {
36+
return null;
37+
}
38+
39+
return $value->asInteger(); // @phpstan-ignore-line
40+
}
41+
42+
abstract public function getName(): string;
43+
44+
abstract public function getClass(): string;
45+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Headsnet\DoctrineToolsBundle\Types\StandardTypes;
5+
6+
use Doctrine\DBAL\Platforms\AbstractPlatform;
7+
use Doctrine\DBAL\Types\Type;
8+
9+
abstract class AbstractStringMappingType extends Type
10+
{
11+
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
12+
{
13+
return $platform->getStringTypeDeclarationSQL($column);
14+
}
15+
16+
/**
17+
* @param string|null $value
18+
*/
19+
public function convertToPHPValue($value, AbstractPlatform $platform): ?object
20+
{
21+
if ($value === null) {
22+
return null;
23+
}
24+
25+
$class = $this->getClass();
26+
27+
return $class::create($value);
28+
}
29+
30+
/**
31+
* @param object|null $value
32+
*/
33+
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
34+
{
35+
if ($value === null) {
36+
return null;
37+
}
38+
39+
return $value->asString(); // @phpstan-ignore-line
40+
}
41+
42+
abstract public function getName(): string;
43+
44+
abstract public function getClass(): string;
45+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Headsnet\DoctrineToolsBundle\Types\StandardTypes;
5+
6+
use Doctrine\DBAL\Platforms\AbstractPlatform;
7+
use Doctrine\DBAL\Types\Type;
8+
9+
abstract class AbstractUuidMappingType extends Type
10+
{
11+
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
12+
{
13+
return $platform->getGuidTypeDeclarationSQL($column);
14+
}
15+
16+
/**
17+
* @param string|null $value
18+
*/
19+
public function convertToPHPValue($value, AbstractPlatform $platform): ?object
20+
{
21+
if ($value === null) {
22+
return null;
23+
}
24+
25+
$class = $this->getClass();
26+
27+
return $class::fromString($value);
28+
}
29+
30+
/**
31+
* @param object|string|null $value
32+
*/
33+
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
34+
{
35+
if ($value === null) {
36+
return null;
37+
}
38+
39+
if (is_string($value)) {
40+
return $value;
41+
}
42+
43+
return $value->asString(); // @phpstan-ignore-line
44+
}
45+
46+
abstract public function getName(): string;
47+
48+
abstract public function getClass(): string;
49+
}

0 commit comments

Comments
 (0)