Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions documentation/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,21 @@ flow_telemetry:
- 'cache.http_client.pool'
```

### 9) `flow-php/types` - a `Type` implementation's generic parameter is the value it represents

| Before | After |
|----------------------------------|------------------------------------------|
| `ListType<string>` | `ListType<list<string>>` |
| `MapType<string, int>` | `MapType<array<string, int>>` |
| `StructureType<mixed>` | `StructureType<array<array-key, mixed>>` |
| `ClassStringType<Foo>` | `ClassStringType<class-string<Foo>>` |
| `ListType::element(): Type<T>` | `Type<value-of<T>>` |
| `MapType::key(): Type<TKey>` | `Type<key-of<T>>` |
| `MapType::value(): Type<TValue>` | `Type<value-of<T>>` |
| `type_string(): Type` | `type_string(): StringType` |

Update `ListType`, `MapType`, `StructureType` and `ClassStringType` parameters in your own docblocks.

---

## Upgrading from 0.41.x to 0.42.x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,7 @@ private function typeToJsonSchema(Type $type): array
}

/**
* @param MapType<array-key, mixed> $type
* @param MapType<array<array-key, mixed>> $type
*
* @return array<string, mixed>
*/
Expand All @@ -1041,7 +1041,7 @@ private function mapToJsonSchema(MapType $type): array
}

/**
* @param StructureType<mixed> $type
* @param StructureType<array<array-key, mixed>> $type
*
* @return array<string, mixed>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ public function toParquet(Schema $schema): ParquetSchema
*/
private function flowToParquet(string $name, Type $type, bool $nullable): Column
{
if ($type instanceof StructureType && count($type->optionalElements())) {
throw new RuntimeException(sprintf(
'Parquet schema does not support structure optional elements, given: %s',
$type->toString(),
));
}

$repetition = $nullable ? ParquetSchema\Repetition::OPTIONAL : ParquetSchema\Repetition::REQUIRED;

return match ($type::class) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Flow\ETL\Adapter\Parquet\Tests\Unit;

use Flow\ETL\Adapter\Parquet\SchemaConverter;
use Flow\ETL\Exception\RuntimeException;
use Flow\ETL\Tests\FlowTestCase;
use Flow\Parquet\ParquetFile\Schema as ParquetSchema;
use Flow\Parquet\ParquetFile\Schema\FlatColumn;
Expand Down Expand Up @@ -82,4 +83,28 @@ public function test_convert_etl_entries_to_parquet_fields(): void
)),
);
}

public function test_converting_structure_with_optional_elements_throws(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage(
'Parquet schema does not support structure optional elements, given: structure{a: string, b?: string}',
);

(new SchemaConverter())->toParquet(schema(structure_schema('structure', type_structure(['a' => type_string()], [
'b' => type_string(),
]))));
}

public function test_converting_nested_structure_with_optional_elements_throws(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage(
'Parquet schema does not support structure optional elements, given: structure{b: string, c?: string}',
);

(new SchemaConverter())->toParquet(schema(structure_schema('structure', type_structure([
'a' => type_structure(['b' => type_string()], ['c' => type_string()]),
]))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ private function flag(Metadata $metadata, SealMetadata $key, bool $default): boo
*/
private function flowToSealField(string $name, Type $type, bool $multiple, Metadata $metadata): AbstractField
{
if ($type instanceof StructureType && count($type->optionalElements())) {
throw new RuntimeException(sprintf(
'Seal schema does not support structure optional elements, given: %s',
$type->toString(),
));
}

return match ($type::class) {
EnumType::class,
HTMLElementType::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ public function test_throws_when_no_identifier_is_provided(): void
to_seal_schema(schema(str_schema('name')), 'index');
}

public function test_throws_when_structure_has_optional_elements(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage(
'Seal schema does not support structure optional elements, given: structure{name: string, nickname?: string}',
);

to_seal_schema(
schema(
str_schema('id'),
structure_schema('author', type_structure(['name' => type_string()], ['nickname' => type_string()])),
),
'index',
);
}

public function test_using_metadata_to_override_default_field_flags(): void
{
$sealSchema = to_seal_schema(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ public function encode(array $batch): array
*/
private function normalize(string $name, Type $type, mixed $value): XMLNode|XMLAttribute
{
if ($type instanceof StructureType && count($type->optionalElements())) {
throw new RuntimeException(sprintf(
'XML encoder does not support structure optional elements, given: %s',
$type->toString(),
));
}

if (str_starts_with($name, $this->attributePrefix)) {
return new XMLAttribute(substr($name, strlen($this->attributePrefix)), type_string()->cast($value));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use DateTimeImmutable;
use Flow\ETL\Adapter\XML\XMLEncoder;
use Flow\ETL\Adapter\XML\XMLWriter\DOMDocumentWriter;
use Flow\ETL\Exception\RuntimeException;
use Flow\ETL\Row\TypedRowValues;
use Flow\ETL\Tests\Fixtures\Enum\BackedIntEnum;
use Flow\ETL\Tests\FlowTestCase;
Expand Down Expand Up @@ -156,6 +157,20 @@ public function test_encodes_structure_into_nested_nodes(): void
);
}

public function test_encoding_structure_with_optional_elements_throws(): void
{
$encoder = new XMLEncoder(new DOMDocumentWriter());

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage(
'XML encoder does not support structure optional elements, given: structure{city: string, zip?: string}',
);

$encoder->encode([new TypedRowValues(['address' => ['city' => 'Krakow']], ['address' => type_structure([
'city' => type_string(),
], ['zip' => type_string()])])]);
}

public function test_encodes_null_scalar_as_an_empty_node(): void
{
$encoder = new XMLEncoder(new DOMDocumentWriter());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function test_constructor_throws_exception_when_mask_sanitizer_has_invali
{
$this->expectException(InvalidTypeException::class);
$this->expectExceptionMessage(
'Expected type "structure{type: \'mask\', character?: string, offset?: integer}", got "map<string, string>"',
'Expected type "structure{type: \'mask\', character?: string, offset?: integer}", got "structure{type: string, character: string, offset: string}"',
);

new RequestConfig(sanitizers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function test_constructor_throws_exception_when_mask_sanitizer_has_invali
{
$this->expectException(InvalidTypeException::class);
$this->expectExceptionMessage(
'Expected type "structure{type: \'mask\', character?: string, offset?: integer}", got "map<string, string>"',
'Expected type "structure{type: \'mask\', character?: string, offset?: integer}", got "structure{type: string, character: string, offset: string}"',
);

new ResponseConfig(sanitizers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function test_from_array_throws_exception_when_offset_is_not_an_integer()
{
$this->expectException(InvalidTypeException::class);
$this->expectExceptionMessage(
'Expected type "structure{type: \'mask\', character?: string, offset?: integer}", got "map<string, string>"',
'Expected type "structure{type: \'mask\', character?: string, offset?: integer}", got "structure{type: string, character: string, offset: string}"',
);

Mask::fromArray([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function test_throws_exception_when_offset_is_not_an_integer(): void
{
$this->expectException(InvalidTypeException::class);
$this->expectExceptionMessage(
'Expected type "structure{type: \'mask\', character?: string, offset?: integer}", got "map<string, string>"',
'Expected type "structure{type: \'mask\', character?: string, offset?: integer}", got "structure{type: string, character: string, offset: string}"',
);

SanitizerFactory::fromArray([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ public function test_from_openapi_throws_exception_for_invalid_property_spec():
];

$this->expectException(InvalidTypeException::class);
$this->expectExceptionMessage('Expected type "map<string, array<mixed>>", got "map<string, string>".');
$this->expectExceptionMessage(
'Expected type "map<string, array<mixed>>", got "structure{invalid_prop: string}".',
);

$converter->fromOpenAPI($openApiSpec);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Flow\Bridge\Symfony\TelemetryBundle\Instrumentation\Cache\TraceableCacheAdapter;
use Flow\Bridge\Symfony\TelemetryBundle\Tests\Fixtures\Cache\ArrayCacheAdapter;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RequiresMethod;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -17,6 +18,7 @@
#[CoversClass(InterfaceAliasRepointer::class)]
final class InterfaceAliasRepointerTest extends TestCase
{
#[RequiresMethod(NamespacedPoolInterface::class, 'withSubNamespace')]
public function test_alias_the_decorator_cannot_satisfy_is_pointed_at_the_inner_service(): void
{
$container = new ContainerBuilder();
Expand Down Expand Up @@ -50,6 +52,7 @@ public function test_alias_the_decorator_satisfies_is_left_alone(): void
static::assertSame('cache.app', (string) $container->getAlias(CacheInterface::class));
}

#[RequiresMethod(NamespacedPoolInterface::class, 'withSubNamespace')]
public function test_alias_pointing_at_another_service_is_left_alone(): void
{
$container = new ContainerBuilder();
Expand Down Expand Up @@ -80,6 +83,7 @@ public function test_alias_that_is_not_an_interface_is_left_alone(): void
static::assertSame('cache.app', (string) $container->getAlias('cache.app.alias'));
}

#[RequiresMethod(NamespacedPoolInterface::class, 'withSubNamespace')]
public function test_public_visibility_is_preserved(): void
{
$container = new ContainerBuilder();
Expand All @@ -95,6 +99,7 @@ public function test_public_visibility_is_preserved(): void
static::assertTrue($container->getAlias(NamespacedPoolInterface::class)->isPublic());
}

#[RequiresMethod(NamespacedPoolInterface::class, 'withSubNamespace')]
public function test_private_visibility_is_preserved(): void
{
$container = new ContainerBuilder();
Expand All @@ -110,6 +115,7 @@ public function test_private_visibility_is_preserved(): void
static::assertFalse($container->getAlias(NamespacedPoolInterface::class)->isPublic());
}

#[RequiresMethod(NamespacedPoolInterface::class, 'withSubNamespace')]
public function test_deprecation_is_preserved(): void
{
$container = new ContainerBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,19 +369,19 @@ public function test_run_schema_with_table_output_on_parquet(): void
$tester->assertCommandIsSuccessful();

self::assertCommandOutputIdentical(<<<'OUTPUT'
+------------+----------+----------+----------+
| name | type | nullable | metadata |
+------------+----------+----------+----------+
| order_id | uuid | false | [] |
| created_at | datetime | false | [] |
| updated_at | datetime | true | [] |
| discount | float | true | [] |
| email | string | false | [] |
| customer | string | false | [] |
| address | map | false | [] |
| notes | list | false | [] |
| items | list | false | [] |
+------------+----------+----------+----------+
+------------+-----------+----------+----------+
| name | type | nullable | metadata |
+------------+-----------+----------+----------+
| order_id | uuid | false | [] |
| created_at | datetime | false | [] |
| updated_at | datetime | true | [] |
| discount | float | true | [] |
| email | string | false | [] |
| customer | string | false | [] |
| address | structure | false | [] |
| notes | list | false | [] |
| items | list | false | [] |
+------------+-----------+----------+----------+
9 rows

OUTPUT, $tester->getDisplay());
Expand Down
16 changes: 8 additions & 8 deletions src/core/etl/src/Flow/ETL/DSL/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,7 @@ function entries(Entry ...$entries): Entries
* @template TShape of array<array-key, mixed>
*
* @param ?TShape $value
* @param StructureType<mixed>|Type<TShape> $type
* @param StructureType<array<array-key, mixed>>|Type<TShape> $type
*
* @return ($value is null ? Entry<null> : Entry<TShape>)
*/
Expand All @@ -1006,7 +1006,7 @@ function struct_entry(string $name, ?array $value, Type $type, ?Metadata $metada
* @template TShape of array<array-key, mixed>
*
* @param ?TShape $value
* @param StructureType<mixed>|Type<TShape> $type
* @param StructureType<array<array-key, mixed>>|Type<TShape> $type
*
* @return ($value is null ? Entry<null> : Entry<TShape>)
*/
Expand Down Expand Up @@ -1992,21 +1992,21 @@ function float_schema(string $name, bool $nullable = false, ?Metadata $metadata
* @template TKey of array-key
* @template TValue
*
* @param MapType<TKey, TValue>|Type<array<TKey, TValue>> $type
* @param MapType<array<TKey, TValue>>|Type<array<TKey, TValue>> $type
*
* @return MapDefinition<TKey, TValue>
*/
#[DocumentationDSL(module: Module::CORE, type: DSLType::SCHEMA)]
function map_schema(string $name, MapType|Type $type, bool $nullable = false, ?Metadata $metadata = null): MapDefinition
{
/** @var MapType<TKey, TValue> $type */
/** @var MapType<array<TKey, TValue>> $type */
return new MapDefinition($name, $type, $nullable, $metadata);
}

/**
* @template T
*
* @param ListType<T>|Type<list<T>> $type
* @param ListType<list<T>>|Type<list<T>> $type
*
* @return ListDefinition<T>
*/
Expand All @@ -2017,7 +2017,7 @@ function list_schema(
bool $nullable = false,
?Metadata $metadata = null,
): ListDefinition {
/** @var ListType<T> $type */
/** @var ListType<list<T>> $type */
return new ListDefinition($name, $type, $nullable, $metadata);
}

Expand Down Expand Up @@ -2091,7 +2091,7 @@ function xml_element_schema(string $name, bool $nullable = false, ?Metadata $met
/**
* @template T
*
* @param StructureType<T>|Type<array<string, T>> $type
* @param StructureType<array<array-key, T>>|Type<array<array-key, T>> $type
*
* @return StructureDefinition<T>
*/
Expand All @@ -2102,7 +2102,7 @@ function structure_schema(
bool $nullable = false,
?Metadata $metadata = null,
): StructureDefinition {
/** @var StructureType<T> $type */
/** @var StructureType<array<array-key, T>> $type */
return new StructureDefinition($name, $type, $nullable, $metadata);
}

Expand Down
4 changes: 2 additions & 2 deletions src/core/etl/src/Flow/ETL/Row/Entry/ListEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ final class ListEntry implements Entry

/**
* @param TList $value
* @param ListType<T> $type
* @param ListType<list<T>> $type
*
* @throws InvalidArgumentException
*/
Expand Down Expand Up @@ -123,7 +123,7 @@ public function toString(): string
}

/**
* @return ListType<T>
* @return ListType<list<T>>
*/
public function type(): ListType
{
Expand Down
Loading
Loading