Skip to content

Commit e598f67

Browse files
committed
Added intersection type
1 parent 5330938 commit e598f67

10 files changed

Lines changed: 506 additions & 10 deletions

File tree

documentation/components/libs/types.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ By default all types are not nullabe, in order to achieve nullability two types
8787

8888
- `type_optional(type_string())` - results in nullable string '?string'
8989
- `type_union(type_string(),type_integer(),type_null())` - results in `union<string,integer,null>` which is the same as `string|integer|null`
90+
- `type_intersection(type_integer(),type_string())` - results in `intersection<integer,string>` which is the same as `integer&string`, requiring values to be valid for ALL types in the intersection
9091

9192
#### Lists
9293

documentation/contributing/guidelines.md

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,19 @@ The project is structured as follows:
4646
- `bridge` contains bridges to connect flow libs with other libraries and frameworks.
4747
- `cli` contains the command line interface application.
4848
- `core` contains the core functionality of the project, it holds the entire DataFrame.
49-
- `lib` contains standalone libraries that can be used independently of the project, like `doctrine-dbal-bulk` and `parquet`.
49+
- `lib` contains standalone libraries that can be used independently of the project, like `doctrine-dbal-bulk` and
50+
`parquet`.
5051
- `tools` contains tools used during development.
51-
- `tools` contains tools used during development, like the `phpstan`, `phpunit` and others, to not pollute project autoloader and
52+
- `tools` contains tools used during development, like the `phpstan`, `phpunit` and others, to not pollute project
53+
autoloader and
5254
to keep tools outside of project dependencies.
5355
- `var` contains temporary files, like cache and logs.
5456
- `vendor` contains the dependencies of the project, managed by Composer.
5557
- `web`
56-
- `landing` contains the landing page of the project, which is a symfony application that is automatically dumped to static HTML files
57-
and served by GitHub Pages. It has it's own composer.json that defines the dependencies for the landing page and commands.
58+
- `landing` contains the landing page of the project, which is a symfony application that is automatically dumped to
59+
static HTML files
60+
and served by GitHub Pages. It has it's own composer.json that defines the dependencies for the landing page and
61+
commands.
5862

5963
## Monorepo packages
6064

@@ -91,7 +95,8 @@ as wide as possible, so we don't block other projects by our constraints.
9195
Packages from this monorepo can depend on each other, but ther are strict rules about that:
9296

9397
- `lib` - libraries can depend only on other `lib` packages, never on anything else.
94-
- `adapter` - adapters can depend on `lib` / `bridge` and they always depend on `core`. Adpaters should not depend on `cli`
98+
- `adapter` - adapters can depend on `lib` / `bridge` and they always depend on `core`. Adpaters should not depend on
99+
`cli`
95100
- `bridge` - bridges can depend only on `lib`
96101
- `cli` - CLI can depend on `core`, `lib` and `adapter` and `bridge`, `cli` always depends on `core`
97102
- `core` - core can depend on `lib` or `bridge`, but should should never depend on `adapter`, or `cli`
@@ -100,7 +105,8 @@ The above rules apply also on namespaces. So for example Adapter for `CSV` can't
100105

101106
# PHP Versions
102107

103-
This project supports only the latest three PHP versions, for example: 8.2, 8.3, and 8.4. (assuming that 8.4 is the latest version).
108+
This project supports only the latest three PHP versions, for example: 8.2, 8.3, and 8.4. (assuming that 8.4 is the
109+
latest version).
104110
Development is done using the lowest supported version, which is currently PHP 8.2.
105111
The project is tested against all three versions, so you can use any of them for development.
106112

@@ -111,7 +117,8 @@ Most of them are available as Composer scripts, so you can run them using `compo
111117

112118
- `composer static:analyze` runs static analysis tools like PHPStan to check the code for errors and potential issues.
113119
- `composer cs:php:fix` runs the PHP CS Fixer and Rector to automatically fix coding standards issues in the code.
114-
- `composer test` runs all tests in the project, including unit tests, functional tests, and integration tests. It's a combination of all other test commands:
120+
- `composer test` runs all tests in the project, including unit tests, functional tests, and integration tests. It's
121+
a combination of all other test commands:
115122
- `composer test:core`
116123
- `composer test:cli`
117124
- `composer test:lib:array-dot`
@@ -120,7 +127,8 @@ Most of them are available as Composer scripts, so you can run them using `compo
120127
- ...
121128
- `composer test:adapter:xml`
122129
- ...
123-
- `composer test:benchmark` runs the benchmark tests to measure the performance of the certain parts of the project.
130+
- `composer test:benchmark` runs the benchmark tests to measure the performance of the certain parts of the
131+
project.
124132
- `composer test:website` runs the tests for the website
125133
- `composer test:examples` runs all examples
126134
- `composer test:mutation` runs the mutation tests to check the quality of the tests.
@@ -135,3 +143,23 @@ All functions defined in DSL (usually in `functions.php` files) are following th
135143
snake_case is used for function names and arguments in DSL. The only other place where snake_case is used is in the
136144
tests, where it is used for test method names.
137145

146+
To make sure, that the whole project is aligned with the codding standards, run following commands:
147+
148+
```
149+
composer cs:php:fix
150+
composer static:analyze
151+
```
152+
153+
Only when both commands pass, you should commit your changes.
154+
155+
# Testing
156+
157+
- Tests in the project are divided into
158+
- `Unit` - tests a single behavior in isolation, without any dependencies.
159+
- `Integration` - tests a single behavior with dependencies, like database or external services.
160+
- Test cases of all packages should extends `\Flow\ETL\Tests\FlowTestCase` class, the only exceptions are `lib` and
161+
`bridge` packages, which can use their own test cases.
162+
- Each test method should test only one scenario, when one behavior needs to be tested against multiple input data, use
163+
`PHPUnit\Framework\Attributes\TestWith` or `PHPUnit\Framework\Attributes\DataProvider` attributes.
164+
165+

src/lib/types/src/Flow/Types/DSL/functions.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
EnumType,
2929
FloatType,
3030
IntegerType,
31+
IntersectionType,
3132
MixedType,
3233
NullType,
3334
ObjectType,
@@ -70,6 +71,27 @@ function type_union(Type $first, Type $second, Type ...$types) : UnionType
7071
return $type;
7172
}
7273

74+
/**
75+
* @template T
76+
*
77+
* @param \Flow\Types\Type<T> $first
78+
* @param Type<T> $second
79+
* @param \Flow\Types\Type<T> ...$types
80+
*
81+
* @return IntersectionType<T, T>
82+
*/
83+
#[DocumentationDSL(module: Module::TYPES, type: DSLType::TYPE)]
84+
function type_intersection(Type $first, Type $second, Type ...$types) : IntersectionType
85+
{
86+
$type = new IntersectionType($first, $second);
87+
88+
foreach ($types as $t) {
89+
$type = new IntersectionType($type, $t);
90+
}
91+
92+
return $type;
93+
}
94+
7395
/**
7496
* @template T
7597
*
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Types\Type\Native;
6+
7+
use Flow\Types\Exception\{CastingException};
8+
use Flow\Types\Exception\InvalidTypeException;
9+
use Flow\Types\Type;
10+
use Flow\Types\Type\{Logical\OptionalType, TypeFactory, Types};
11+
12+
/**
13+
* @template TLeft
14+
* @template TRight
15+
*
16+
* @implements Type<TLeft&TRight>
17+
*/
18+
final readonly class IntersectionType implements Type
19+
{
20+
private Types $flatTypes;
21+
22+
/**
23+
* @param Type<TLeft> $left
24+
* @param Type<TRight> $right
25+
*/
26+
public function __construct(private Type $left, private Type $right)
27+
{
28+
if ($left instanceof MixedType || $right instanceof MixedType) {
29+
throw new InvalidTypeException('IntersectionType cannot be mixed with MixedType, mixed is a standalone type');
30+
}
31+
32+
$types = [];
33+
34+
if ($this->left instanceof self) {
35+
$types = [...$types, ...$this->left->types()->all()];
36+
} else {
37+
$types[] = $this->left;
38+
}
39+
40+
if ($this->right instanceof self) {
41+
$types = [...$types, ...$this->right->types()->all()];
42+
} else {
43+
$types[] = $this->right;
44+
}
45+
46+
$this->flatTypes = \Flow\Types\DSL\types(...$types);
47+
}
48+
49+
/**
50+
* @param array{type: 'intersection', left: array, right: array} $data
51+
*
52+
* @return type<TLeft&TRight>
53+
*/
54+
public static function fromArray(array $data) : Type
55+
{
56+
return new self(
57+
TypeFactory::fromArray($data['left']),
58+
TypeFactory::fromArray($data['right']),
59+
);
60+
}
61+
62+
/**
63+
* @return TLeft&TRight
64+
*/
65+
public function assert(mixed $value) : mixed
66+
{
67+
if (!$this->isValid($value)) {
68+
throw InvalidTypeException::value($value, $this);
69+
}
70+
71+
return $value;
72+
}
73+
74+
public function cast(mixed $value) : mixed
75+
{
76+
if ($this->isValid($value)) {
77+
return $value;
78+
}
79+
80+
try {
81+
$leftCasted = $this->left->cast($value);
82+
83+
if ($this->right->isValid($leftCasted)) {
84+
return $leftCasted;
85+
}
86+
} catch (CastingException) {
87+
}
88+
89+
try {
90+
$rightCasted = $this->right->cast($value);
91+
92+
if ($this->left->isValid($rightCasted)) {
93+
return $rightCasted;
94+
}
95+
} catch (CastingException) {
96+
}
97+
98+
throw new CastingException($value, $this);
99+
}
100+
101+
public function isValid(mixed $value) : bool
102+
{
103+
return $this->left->isValid($value) && $this->right->isValid($value);
104+
}
105+
106+
/**
107+
* @return array{type: 'intersection', left: array, right: array}
108+
*/
109+
public function normalize() : array
110+
{
111+
return [
112+
'type' => 'intersection',
113+
'left' => $this->left->normalize(),
114+
'right' => $this->right->normalize(),
115+
];
116+
}
117+
118+
public function toString() : string
119+
{
120+
$stringTypes = [];
121+
122+
foreach ($this->flatTypes->deduplicate()->all() as $type) {
123+
if ($type instanceof OptionalType) {
124+
if (!\in_array($type->base()->toString(), $stringTypes, true)) {
125+
$stringTypes[] = $type->base()->toString();
126+
}
127+
128+
if (!\in_array('null', $stringTypes, true)) {
129+
$stringTypes[] = 'null';
130+
}
131+
132+
continue;
133+
}
134+
135+
$stringTypes[] = $type->toString();
136+
}
137+
138+
asort($stringTypes);
139+
140+
return 'intersection<' . \implode('&', $stringTypes) . '>';
141+
}
142+
143+
public function types() : Types
144+
{
145+
return $this->flatTypes;
146+
}
147+
}

src/lib/types/src/Flow/Types/Type/TypeFactory.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
use Flow\Types\Exception\InvalidArgumentException;
2828
use Flow\Types\Type;
2929
use Flow\Types\Type\Logical\{InstanceOfType, ListType, MapType, OptionalType, StructureType};
30-
use Flow\Types\Type\Native\{EnumType, UnionType};
30+
use Flow\Types\Type\Native\{EnumType, IntersectionType, UnionType};
3131

3232
final class TypeFactory
3333
{
@@ -74,6 +74,8 @@ public static function fromArray(array $data) : Type
7474
/** @phpstan-ignore argument.type */
7575
'union' => UnionType::fromArray($data),
7676
/** @phpstan-ignore argument.type */
77+
'intersection' => IntersectionType::fromArray($data),
78+
/** @phpstan-ignore argument.type */
7779
'optional' => OptionalType::fromArray($data),
7880
'scalar' => type_scalar(),
7981
'mixed' => type_mixed(),
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Types\Tests\Unit\Type\Fixtures\Intersection;
6+
7+
interface Date
8+
{
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Types\Tests\Unit\Type\Fixtures\Intersection;
6+
7+
final class DateOrTime implements Date, Time
8+
{
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Types\Tests\Unit\Type\Fixtures\Intersection;
6+
7+
interface Time
8+
{
9+
}

0 commit comments

Comments
 (0)