Skip to content

Commit e53ffd4

Browse files
committed
Improve array config reader
1 parent 1365f64 commit e53ffd4

13 files changed

+516
-251
lines changed

src/Mapping/Driver/ArrayConfigDriver.php

Lines changed: 58 additions & 239 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@
55
namespace TypeLang\Mapper\Mapping\Driver;
66

77
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
8-
use Symfony\Component\ExpressionLanguage\ParsedExpression;
9-
use TypeLang\Mapper\Exception\Definition\PropertyTypeNotFoundException;
10-
use TypeLang\Mapper\Exception\Definition\TypeNotFoundException;
11-
use TypeLang\Mapper\Exception\Environment\ComposerPackageRequiredException;
8+
use TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver\ClassConfigLoaderInterface;
9+
use TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver\ErrorMessagePropertyConfigLoader;
10+
use TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver\NamePropertyConfigLoader;
11+
use TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver\PropertyConfigLoaderInterface;
1212
use TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver\SchemaValidator;
13+
use TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver\SkipConditionsPropertyConfigLoader;
14+
use TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver\TypePropertyConfigLoader;
15+
use TypeLang\Mapper\Mapping\Driver\AttributeDriver\DiscriminatorMapClassConfigLoader;
16+
use TypeLang\Mapper\Mapping\Driver\AttributeDriver\NormalizeAsArrayClassConfigLoader;
1317
use TypeLang\Mapper\Mapping\Metadata\ClassMetadata;
14-
use TypeLang\Mapper\Mapping\Metadata\DiscriminatorMapMetadata;
15-
use TypeLang\Mapper\Mapping\Metadata\EmptyConditionMetadata;
16-
use TypeLang\Mapper\Mapping\Metadata\ExpressionConditionMetadata;
17-
use TypeLang\Mapper\Mapping\Metadata\NullConditionMetadata;
18-
use TypeLang\Mapper\Mapping\Metadata\TypeMetadata;
1918
use TypeLang\Mapper\Runtime\Parser\TypeParserInterface;
2019
use TypeLang\Mapper\Runtime\Repository\TypeRepositoryInterface;
2120

@@ -28,13 +27,11 @@
2827
* undefined_error_message?: non-empty-string,
2928
* ...
3029
* }
31-
*
3230
* @phpstan-type ClassDiscriminatorConfigType array{
3331
* field: non-empty-string,
3432
* map: array<non-empty-string, non-empty-string>,
3533
* otherwise?: non-empty-string,
3634
* }
37-
*
3835
* @phpstan-type ClassConfigType array{
3936
* normalize_as_array?: bool,
4037
* discriminator?: ClassDiscriminatorConfigType,
@@ -45,15 +42,52 @@ abstract class ArrayConfigDriver extends LoadableDriver
4542
{
4643
private static ?bool $supportsSchemaValidation = null;
4744

45+
/**
46+
* @var list<ClassConfigLoaderInterface>
47+
*/
48+
private readonly array $classConfigLoaders;
49+
50+
/**
51+
* @var list<PropertyConfigLoaderInterface>
52+
*/
53+
private readonly array $propertyConfigLoaders;
54+
4855
public function __construct(
4956
DriverInterface $delegate = new NullDriver(),
50-
private ?ExpressionLanguage $expression = null,
57+
private readonly ?ExpressionLanguage $expression = null,
5158
) {
5259
self::$supportsSchemaValidation ??= SchemaValidator::isAvailable();
5360

61+
$this->classConfigLoaders = $this->createClassLoaders();
62+
$this->propertyConfigLoaders = $this->createPropertyLoaders();
63+
5464
parent::__construct($delegate);
5565
}
5666

67+
/**
68+
* @return list<ClassConfigLoaderInterface>
69+
*/
70+
private function createClassLoaders(): array
71+
{
72+
return [
73+
new NormalizeAsArrayClassConfigLoader(),
74+
new DiscriminatorMapClassConfigLoader(),
75+
];
76+
}
77+
78+
/**
79+
* @return list<PropertyConfigLoaderInterface>
80+
*/
81+
private function createPropertyLoaders(): array
82+
{
83+
return [
84+
new TypePropertyConfigLoader(),
85+
new NamePropertyConfigLoader(),
86+
new ErrorMessagePropertyConfigLoader(),
87+
new SkipConditionsPropertyConfigLoader($this->expression),
88+
];
89+
}
90+
5791
/**
5892
* @param \ReflectionClass<object> $class
5993
*
@@ -109,82 +143,22 @@ protected function load(
109143
return;
110144
}
111145

112-
// ---------------------------------------------------------------------
113-
// > start: Normalize as array
114-
// ---------------------------------------------------------------------
115-
116-
if (\array_key_exists('normalize_as_array', $classConfig)) {
117-
// @phpstan-ignore-next-line : Additional DbC invariant
118-
assert(\is_bool($classConfig['normalize_as_array']));
119-
120-
$class->isNormalizeAsArray = $classConfig['normalize_as_array'];
121-
}
122-
123-
// ---------------------------------------------------------------------
124-
// end: Normalize as array
125-
// > start: Discriminator Map
126-
// ---------------------------------------------------------------------
127-
128-
if (\array_key_exists('discriminator', $classConfig)) {
129-
// @phpstan-ignore-next-line : Additional DbC invariant
130-
assert(\is_array($classConfig['discriminator']));
131-
132-
$discriminatorConfig = $classConfig['discriminator'];
133-
134-
// @phpstan-ignore-next-line : Additional DbC invariant
135-
assert(\array_key_exists('field', $discriminatorConfig));
136-
137-
$discriminator = new DiscriminatorMapMetadata(
138-
field: $discriminatorConfig['field'],
139-
);
140-
141-
// @phpstan-ignore-next-line : Additional DbC invariant
142-
assert(\array_key_exists('map', $discriminatorConfig));
143-
144-
// @phpstan-ignore-next-line : Additional DbC invariant
145-
assert(\is_array($discriminatorConfig['map']));
146-
147-
foreach ($discriminatorConfig['map'] as $discriminatorValue => $discriminatorType) {
148-
// @phpstan-ignore-next-line : Additional DbC invariant
149-
assert(\is_string($discriminatorValue));
150-
151-
// @phpstan-ignore-next-line : Additional DbC invariant
152-
assert(\is_string($discriminatorType));
153-
154-
$discriminator->addType(
155-
fieldValue: $discriminatorValue,
156-
type: $this->createDiscriminatorType(
157-
type: $discriminatorType,
158-
class: $reflection,
159-
types: $types,
160-
parser: $parser,
161-
),
162-
);
163-
}
164-
165-
// @phpstan-ignore-next-line : Additional DbC invariant
166-
assert(\array_key_exists('otherwise', $discriminatorConfig));
167-
168-
// @phpstan-ignore-next-line : Additional DbC invariant
169-
assert(\is_string($discriminatorConfig['otherwise']));
170-
171-
$discriminator->default = $this->createDiscriminatorType(
172-
type: $discriminatorConfig['otherwise'],
146+
foreach ($this->classConfigLoaders as $classConfigLoader) {
147+
$classConfigLoader->load(
148+
config: $classConfig,
173149
class: $reflection,
150+
metadata: $class,
174151
types: $types,
175152
parser: $parser,
176153
);
177154
}
178155

179-
// ---------------------------------------------------------------------
180-
// end: Discriminator Map
181-
// > start: Properties
182-
// ---------------------------------------------------------------------
156+
$classConfig['properties'] ??= [];
183157

184158
// @phpstan-ignore-next-line : Additional DbC invariant
185-
assert(\is_array($classConfig['properties'] ?? []));
159+
assert(\is_array($classConfig['properties']));
186160

187-
foreach ($classConfig['properties'] ?? [] as $propertyName => $propertyConfig) {
161+
foreach ($classConfig['properties'] as $propertyName => $propertyConfig) {
188162
// Prepare: Normalize config in case of config contains string
189163
if (\is_string($propertyConfig)) {
190164
$propertyConfig = ['type' => $propertyConfig];
@@ -198,170 +172,15 @@ class: $reflection,
198172

199173
$metadata = $class->getPropertyOrCreate($propertyName);
200174

201-
// ---------------------------------------------------------------------
202-
// start: Property Error Message
203-
// ---------------------------------------------------------------------
204-
205-
if (\array_key_exists('type_error_message', $propertyConfig)) {
206-
// @phpstan-ignore-next-line : Additional DbC invariant
207-
assert(\is_string($propertyConfig['type_error_message']));
208-
209-
$metadata->typeErrorMessage = $propertyConfig['type_error_message'];
210-
}
211-
212-
if (\array_key_exists('undefined_error_message', $propertyConfig)) {
213-
// @phpstan-ignore-next-line : Additional DbC invariant
214-
assert(\is_string($propertyConfig['undefined_error_message']));
215-
216-
$metadata->undefinedErrorMessage = $propertyConfig['undefined_error_message'];
217-
}
218-
219-
// -----------------------------------------------------------------
220-
// start: Property Type
221-
// -----------------------------------------------------------------
222-
223-
if (\array_key_exists('type', $propertyConfig)) {
224-
// @phpstan-ignore-next-line : Additional DbC invariant
225-
assert(\is_string($propertyConfig['type']));
226-
227-
$metadata->type = $this->createPropertyType(
228-
class: $reflection,
229-
propertyName: $propertyName,
230-
propertyType: $propertyConfig['type'],
175+
foreach ($this->propertyConfigLoaders as $propertyConfigLoader) {
176+
$propertyConfigLoader->load(
177+
config: $propertyConfig,
178+
property: new \ReflectionProperty($class, $propertyName),
179+
metadata: $metadata,
231180
types: $types,
232181
parser: $parser,
233182
);
234183
}
235-
236-
// -----------------------------------------------------------------
237-
// end: Property Type
238-
// > start: Property Name
239-
// -----------------------------------------------------------------
240-
241-
if (\array_key_exists('name', $propertyConfig)) {
242-
// @phpstan-ignore-next-line : Additional DbC invariant
243-
assert(\is_string($propertyConfig['name']));
244-
245-
$metadata->alias = $propertyConfig['name'];
246-
}
247-
248-
// -----------------------------------------------------------------
249-
// end: Property Name
250-
// > start: Property Skip Behaviour
251-
// -----------------------------------------------------------------
252-
253-
if (\array_key_exists('skip', $propertyConfig)) {
254-
if (\is_string($propertyConfig['skip'])) {
255-
$propertyConfig['skip'] = [$propertyConfig['skip']];
256-
}
257-
258-
// @phpstan-ignore-next-line : Additional DbC invariant
259-
assert(\is_array($propertyConfig['skip']));
260-
261-
foreach ($propertyConfig['skip'] as $propertyConfigSkip) {
262-
// @phpstan-ignore-next-line : Additional DbC invariant
263-
assert(\is_string($propertyConfigSkip));
264-
265-
$metadata->addSkipCondition(match ($propertyConfigSkip) {
266-
'null' => new NullConditionMetadata(),
267-
'empty' => new EmptyConditionMetadata(),
268-
default => new ExpressionConditionMetadata(
269-
expression: $this->createExpression($propertyConfigSkip, [
270-
ExpressionConditionMetadata::DEFAULT_CONTEXT_VARIABLE_NAME,
271-
]),
272-
variable: ExpressionConditionMetadata::DEFAULT_CONTEXT_VARIABLE_NAME,
273-
)
274-
});
275-
}
276-
}
277-
278-
// -----------------------------------------------------------------
279-
// end: Property Skip Behaviour
280-
// -----------------------------------------------------------------
281-
}
282-
283-
// ---------------------------------------------------------------------
284-
// end: Properties
285-
// ---------------------------------------------------------------------
286-
}
287-
288-
/**
289-
* @param \ReflectionClass<object> $class
290-
* @param non-empty-string $propertyName
291-
* @param non-empty-string $propertyType
292-
*
293-
* @throws PropertyTypeNotFoundException in case of property type not found
294-
* @throws \Throwable in case of internal error occurs
295-
*/
296-
private function createPropertyType(
297-
\ReflectionClass $class,
298-
string $propertyName,
299-
string $propertyType,
300-
TypeRepositoryInterface $types,
301-
TypeParserInterface $parser,
302-
): TypeMetadata {
303-
$statement = $parser->getStatementByDefinition($propertyType);
304-
305-
try {
306-
$instance = $types->getTypeByStatement($statement, $class);
307-
} catch (TypeNotFoundException $e) {
308-
throw PropertyTypeNotFoundException::becauseTypeOfPropertyNotDefined(
309-
class: $class->getName(),
310-
property: $propertyName,
311-
type: $e->getType(),
312-
previous: $e,
313-
);
314-
}
315-
316-
return new TypeMetadata($instance, $statement);
317-
}
318-
319-
/**
320-
* @param non-empty-string $type
321-
* @param \ReflectionClass<object> $class
322-
*
323-
* @throws PropertyTypeNotFoundException
324-
* @throws \Throwable
325-
*/
326-
private function createDiscriminatorType(
327-
string $type,
328-
\ReflectionClass $class,
329-
TypeRepositoryInterface $types,
330-
TypeParserInterface $parser,
331-
): TypeMetadata {
332-
$statement = $parser->getStatementByDefinition($type);
333-
334-
// TODO Add custom "discriminator type exception"
335-
$instance = $types->getTypeByStatement($statement, $class);
336-
337-
return new TypeMetadata($instance, $statement);
338-
}
339-
340-
/**
341-
* @param non-empty-string $expression
342-
* @param list<non-empty-string> $names
343-
*
344-
* @throws ComposerPackageRequiredException
345-
*/
346-
private function createExpression(string $expression, array $names): ParsedExpression
347-
{
348-
$parser = ($this->expression ??= $this->createDefaultExpressionLanguage());
349-
350-
return $parser->parse($expression, $names);
351-
}
352-
353-
/**
354-
* @throws ComposerPackageRequiredException
355-
*/
356-
private function createDefaultExpressionLanguage(): ExpressionLanguage
357-
{
358-
if (!\class_exists(ExpressionLanguage::class)) {
359-
throw ComposerPackageRequiredException::becausePackageNotInstalled(
360-
package: 'symfony/expression-language',
361-
purpose: 'expressions support',
362-
);
363184
}
364-
365-
return new ExpressionLanguage();
366185
}
367186
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver;
6+
7+
abstract class ClassConfigLoader implements ClassConfigLoaderInterface {}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver;
6+
7+
use TypeLang\Mapper\Mapping\Driver\ArrayConfigDriver;
8+
use TypeLang\Mapper\Mapping\Metadata\ClassMetadata;
9+
use TypeLang\Mapper\Runtime\Parser\TypeParserInterface;
10+
use TypeLang\Mapper\Runtime\Repository\TypeRepositoryInterface;
11+
12+
/**
13+
* @phpstan-import-type ClassConfigType from ArrayConfigDriver
14+
*/
15+
interface ClassConfigLoaderInterface
16+
{
17+
/**
18+
* @template T of object
19+
*
20+
* @param ClassConfigType $config
21+
* @param \ReflectionClass<T> $class
22+
* @param ClassMetadata<T> $metadata
23+
*/
24+
public function load(
25+
array $config,
26+
\ReflectionClass $class,
27+
ClassMetadata $metadata,
28+
TypeRepositoryInterface $types,
29+
TypeParserInterface $parser,
30+
): void;
31+
}

0 commit comments

Comments
 (0)