Skip to content

Commit 96280bd

Browse files
committed
Improve Schema API
1 parent e551b0f commit 96280bd

26 files changed

Lines changed: 398 additions & 186 deletions

docs/9.0/connections/instantiation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Alternatively, you can use the <code>fromStream</code> method.</p>
6161
```php
6262
public static AbstractCsv::fromStream(SplFileObject|resource $stream): self
6363
```
64+
6465
Creates a new object from a stream resource or a streaming object.
6566

6667
```php

docs/9.0/reader/record-mapping.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ description: Converts your CSV records into PHP objects using PHP's powerful Ref
88

99
<p class="message-notice">New in version <code>9.12.0</code></p>
1010

11-
If you are working with a class which implements the `TabularDataReader` interface you can now deserialize
11+
If you are working with a class which implements the `TabularData` interface you can now deserialize
1212
your data using the `TabularDataReader::getRecordsAsObject` method. The method will convert your document records
1313
into objects using PHP's powerful Reflection API.
1414

src/Buffer.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,14 @@ public function reduce(callable $callback, mixed $initial = null): mixed
224224
return $initial;
225225
}
226226

227-
public function schema(?Inspector $inspector = null): Schema
227+
public function inferSchema(?Inspector $inspector = null, array $header = []): Schema
228228
{
229-
return ($inspector ?? Inspector::default())->schema($this);
229+
return ($inspector ?? Inspector::default())->schema($this, $header);
230+
}
231+
232+
public function inferRecords(?Inspector $inspector = null, array $header = []): Iterator
233+
{
234+
return $this->inferSchema($inspector, $header)->parse($this);
230235
}
231236

232237
/**

src/Reader.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,9 +418,14 @@ public function map(callable $callback): Iterator
418418
return MapIterator::fromIterable($this, $callback);
419419
}
420420

421-
public function schema(?Inspector $inspector = null): Schema
421+
public function inferSchema(?Inspector $inspector = null, array $header = []): Schema
422422
{
423-
return ($inspector ?? Inspector::default())->schema($this);
423+
return ($inspector ?? Inspector::default())->schema($this, $header);
424+
}
425+
426+
public function inferRecords(?Inspector $inspector = null, array $header = []): Iterator
427+
{
428+
return $this->inferSchema($inspector, $header)->parse($this);
424429
}
425430

426431
/**

src/ResultSet.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,14 @@ public function map(callable $callback): Iterator
208208
return MapIterator::fromIterable($this, $callback);
209209
}
210210

211-
public function schema(?Inspector $inspector = null): Schema
211+
public function inferSchema(?Inspector $inspector = null, array $header = []): Schema
212212
{
213-
return ($inspector ?? Inspector::default())->schema($this);
213+
return ($inspector ?? Inspector::default())->schema($this, $header);
214+
}
215+
216+
public function inferRecords(?Inspector $inspector = null, array $header = []): Iterator
217+
{
218+
return $this->inferSchema($inspector, $header)->parse($this);
214219
}
215220

216221
/**

src/Schema/AbstractField.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ public function evaluate(mixed $value): int
6969
return null !== $this->parse($value) ? 1 : -1;
7070
}
7171

72-
public function metadata(): Metadata
72+
public function metadata(): FieldMetadata
7373
{
74-
return new Metadata();
74+
return new FieldMetadata();
7575
}
7676
}

src/Schema/CustomField.php

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,50 @@
1818

1919
use function preg_match;
2020

21+
/**
22+
* @template T
23+
*/
2124
final class CustomField extends AbstractField
2225
{
23-
/**
24-
* The closure MUST return null if it fails to parse the value.
25-
* It should return the parsed value according to
26-
* the callable logic.
27-
*
28-
* @var Closure(mixed): ?mixed
29-
*/
30-
private readonly Closure $parser;
26+
private readonly FieldParser $parser;
3127
/** @var non-empty-string */
3228
public readonly string $name;
3329

3430
public function __construct(
35-
callable $parser,
31+
FieldParser|Closure|callable $parser,
3632
string $name,
3733
float $confidenceThreshold = 0.8
3834
) {
3935
('' !== $name && 1 === preg_match('/^[a-z]+(?:_[a-z0-9]+)*$/', $name)) || throw new ValueError('The name "'.$name.'" is not a valid snake case variable name.');
36+
$fieldParser = self::resolveParser($parser);
4037
parent::__construct($confidenceThreshold);
4138

42-
$this->parser = !$parser instanceof Closure
43-
? $parser(...)
44-
: $parser;
45-
39+
$this->parser = $fieldParser;
4640
$this->name = $name;
4741
}
4842

43+
private static function resolveParser(FieldParser|Closure|callable $parser): FieldParser
44+
{
45+
if ($parser instanceof FieldParser) {
46+
return $parser;
47+
}
48+
49+
if (!$parser instanceof Closure) {
50+
$parser = Closure::fromCallable($parser);
51+
}
52+
53+
return new class ($parser) implements FieldParser {
54+
public function __construct(private Closure $parser)
55+
{
56+
}
57+
58+
public function parse(mixed $value): mixed
59+
{
60+
return ($this->parser)($value);
61+
}
62+
};
63+
}
64+
4965
public function type(): FieldType
5066
{
5167
return FieldType::Custom;
@@ -56,8 +72,11 @@ public function name(): string
5672
return $this->name;
5773
}
5874

75+
/**
76+
* @return ?T
77+
*/
5978
public function parse(mixed $value): mixed
6079
{
61-
return ($this->parser)($value);
80+
return $this->parser->parse($value);
6281
}
6382
}

src/Schema/DateField.php

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use DateTimeImmutable;
1717
use DateTimeInterface;
18+
use DateTimeZone;
1819
use Exception;
1920
use ValueError;
2021

@@ -23,22 +24,85 @@
2324

2425
final class DateField extends AbstractField
2526
{
26-
public function __construct(public readonly string $format = '', float $confidenceThreshold = 0.8)
27+
public readonly string $format;
28+
public readonly ?DateTimeZone $timezone;
29+
30+
public function __construct(string $format, DateTimeZone|string|null $timezone = null, float $confidenceThreshold = 0.8)
2731
{
32+
$format = trim($format);
33+
'' !== $format || throw new ValueError('The date field format can not be empty.');
34+
$timezone = self::filterTimezone($timezone);
35+
2836
parent::__construct($confidenceThreshold);
37+
$this->format = $format;
38+
$this->timezone = $timezone;
2939
}
3040

31-
public static function native(float $confidenceThreshold = 0.8): self
41+
public static function common(DateTimeZone|string|null $timezone = null): FieldList
3242
{
33-
return new self(format: '', confidenceThreshold: $confidenceThreshold);
43+
return self::machine($timezone)->append(self::localized($timezone));
3444
}
3545

36-
public static function withFormat(string $format, float $confidenceThreshold = 0.8): self
46+
public static function machine(DateTimeZone|string|null $timezone = null): FieldList
3747
{
38-
$format = trim($format);
39-
'' !== $format || throw new ValueError('The date field strategy format can not be empty.');
48+
$formats = [
49+
'Y-m-d',
50+
'Y-m-d H:i:s',
51+
'Y-m-d\TH:i:s',
52+
DateTimeInterface::RFC3339,
53+
DateTimeInterface::RFC3339_EXTENDED,
54+
DateTimeInterface::ISO8601_EXPANDED,
55+
];
56+
57+
return self::fromFormat($formats, $timezone, .8);
58+
}
59+
60+
public static function localized(DateTimeZone|string|null $timezone = null): FieldList
61+
{
62+
$formats = [
63+
'd/m/Y',
64+
'd-m-Y',
65+
'd.m.Y',
66+
'm/d/Y',
67+
'm-d-Y',
68+
'm.d.Y',
69+
];
70+
71+
return self::fromFormat($formats, $timezone, .7);
72+
}
73+
74+
/**
75+
* @param iterable<non-empty-string> $formats
76+
*/
77+
public static function fromFormat(
78+
iterable $formats,
79+
DateTimeZone|string|null $timezone = null,
80+
float $confidenceThreshold = 0.8
81+
): FieldList {
82+
83+
$fields = [];
84+
foreach ($formats as $format) {
85+
$fields[] = new DateField($format, $timezone, $confidenceThreshold);
86+
}
87+
88+
return new FieldList(...$fields);
89+
}
4090

41-
return new self(format: $format, confidenceThreshold: $confidenceThreshold);
91+
private static function filterTimezone(DateTimeZone|string|null $timeZone): ?DateTimeZone
92+
{
93+
if (null === $timeZone) {
94+
return null;
95+
}
96+
97+
if ($timeZone instanceof DateTimeZone) {
98+
return $timeZone;
99+
}
100+
101+
try {
102+
return new DateTimeZone($timeZone);
103+
} catch (Exception $exception) {
104+
throw new ValueError('The date field timezone value `'.$timeZone.'` is invalid.', previous: $exception);
105+
}
42106
}
43107

44108
public function type(): FieldType
@@ -51,11 +115,6 @@ public function name(): string
51115
return FieldType::Date->value;
52116
}
53117

54-
public function format(): string
55-
{
56-
return $this->format;
57-
}
58-
59118
public function parse(mixed $value): ?DateTimeImmutable
60119
{
61120
if ($value instanceof DateTimeInterface) {
@@ -72,22 +131,30 @@ public function parse(mixed $value): ?DateTimeImmutable
72131
}
73132

74133
try {
75-
if ('' !== $this->format) {
76-
$value = DateTimeImmutable::createFromFormat($this->format, $value);
134+
$value = DateTimeImmutable::createFromFormat($this->format, $value, $this->timezone);
135+
if (false === $value) {
136+
return null;
137+
}
77138

78-
return false === $value ? null : $value;
139+
$errors = DateTimeImmutable::getLastErrors();
140+
if (
141+
(isset($errors['warning_count']) && 0 < $errors['warning_count']) ||
142+
(isset($errors['error_count']) && 0 < $errors['error_count'])
143+
) {
144+
return null;
79145
}
80146

81-
return new DateTimeImmutable($value);
82-
} catch (Exception) {
147+
return $value;
148+
} catch (ValueError) {
83149
return null;
84150
}
85151
}
86152

87-
public function metadata(): Metadata
153+
public function metadata(): FieldMetadata
88154
{
89-
return new Metadata([
155+
return new FieldMetadata([
90156
'format' => $this->format,
157+
'timezone' => $this->timezone?->getName(),
91158
]);
92159
}
93160
}

src/Schema/DateFieldTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ final class DateFieldTest extends TestCase
2525

2626
protected function setUp(): void
2727
{
28-
$this->field = DateField::native();
28+
$this->field = new DateField('Y-m-d');
2929
}
3030

3131
public function testParseUsesNativeConstructorWhenFormatIsEmpty(): void
@@ -38,7 +38,7 @@ public function testParseUsesNativeConstructorWhenFormatIsEmpty(): void
3838

3939
public function testParseUsesCreateFromFormatWhenFormatIsProvided(): void
4040
{
41-
$field = DateField::withFormat('d-m-Y');
41+
$field = new DateField('d-m-Y');
4242
$result = $field->parse('01-01-2024');
4343

4444
self::assertInstanceOf(DateTimeImmutable::class, $result);

src/Schema/EnumField.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ public function parse(mixed $value): ?UnitEnum
107107
return $enumClass::tryFrom($value);
108108
}
109109

110-
public function metadata(): Metadata
110+
public function metadata(): FieldMetadata
111111
{
112-
return new Metadata([
112+
return new FieldMetadata([
113113
'class' => $this->enumClass,
114114
]);
115115
}

0 commit comments

Comments
 (0)