diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
old mode 100644
new mode 100755
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fa112b8f..8d5e7c505 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
-## [Unreleased] - 2023-08-29
+## [Unreleased] - 2023-09-06
### Added
+- [#474](https://github.com/flow-php/flow/pull/474) - **XMLEntry** - [@norberttech](https://github.com/norberttech)
+- [#474](https://github.com/flow-php/flow/pull/474) - **XMLNodeEntry** - [@norberttech](https://github.com/norberttech)
+- [#474](https://github.com/flow-php/flow/pull/474) - **ref('...')->xpath('...') - for extracting specific nodes from XMLEntry** - [@norberttech](https://github.com/norberttech)
+- [#474](https://github.com/flow-php/flow/pull/474) - **ref('...')->domNodeAttribute('...') - for extracting value of attribute** - [@norberttech](https://github.com/norberttech)
+- [#474](https://github.com/flow-php/flow/pull/474) - **ref('...')->domNodeValue('...') - for extracting value of node** - [@norberttech](https://github.com/norberttech)
- [#450](https://github.com/flow-php/flow/pull/450) - **Add new `ulid()` expression based on Symfony Uid** - [@stloyd](https://github.com/stloyd)
- [#445](https://github.com/flow-php/flow/pull/445) - **Add `fig/log-test` package for mock logger** - [@stloyd](https://github.com/stloyd)
- [#440](https://github.com/flow-php/flow/pull/440) - **Add MariaDB to supported platforms for Doctrine adapter** - [@stloyd](https://github.com/stloyd)
@@ -58,6 +63,7 @@
- [#388](https://github.com/flow-php/flow/pull/388) - **Added `ext-hash` PHP extension as required for Flow** - [@stloyd](https://github.com/stloyd)
### Changed
+- [#474](https://github.com/flow-php/flow/pull/474) - **XMLReaderExtractor is now returning XMLEntry type instead of casting XML's to array** - [@norberttech](https://github.com/norberttech)
- [#445](https://github.com/flow-php/flow/pull/445) - **Allow usage of `psr/log` v2 & v3`** - [@stloyd](https://github.com/stloyd)
- [#438](https://github.com/flow-php/flow/pull/438) - **Mark methods on DataFrame api as @lazy or @trigger** - [@norberttech](https://github.com/norberttech)
- [#436](https://github.com/flow-php/flow/pull/436) - **Moved limit functionality into LimitingPipeline** - [@norberttech](https://github.com/norberttech)
diff --git a/composer.lock b/composer.lock
index 8f87e3c58..63cbf47e4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -89,16 +89,16 @@
},
{
"name": "amphp/byte-stream",
- "version": "v2.0.1",
+ "version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/amphp/byte-stream.git",
- "reference": "7e7a77579f3e90c6fbd56e49628e6ace02d8f88a"
+ "reference": "408a3b4fc4f4c7604575dc8704f18c1bd91c3ceb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/amphp/byte-stream/zipball/7e7a77579f3e90c6fbd56e49628e6ace02d8f88a",
- "reference": "7e7a77579f3e90c6fbd56e49628e6ace02d8f88a",
+ "url": "https://api.github.com/repos/amphp/byte-stream/zipball/408a3b4fc4f4c7604575dc8704f18c1bd91c3ceb",
+ "reference": "408a3b4fc4f4c7604575dc8704f18c1bd91c3ceb",
"shasum": ""
},
"require": {
@@ -119,7 +119,8 @@
"type": "library",
"autoload": {
"files": [
- "src/functions.php"
+ "src/functions.php",
+ "src/Internal/functions.php"
],
"psr-4": {
"Amp\\ByteStream\\": "src"
@@ -151,7 +152,7 @@
],
"support": {
"issues": "https://github.com/amphp/byte-stream/issues",
- "source": "https://github.com/amphp/byte-stream/tree/v2.0.1"
+ "source": "https://github.com/amphp/byte-stream/tree/v2.0.2"
},
"funding": [
{
@@ -159,7 +160,7 @@
"type": "github"
}
],
- "time": "2023-02-03T04:06:20+00:00"
+ "time": "2023-09-01T04:41:26+00:00"
},
{
"name": "amphp/cache",
@@ -1546,16 +1547,16 @@
},
{
"name": "google/apiclient-services",
- "version": "v0.313.0",
+ "version": "v0.314.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
- "reference": "e41289c4488563af75bd291972f0fa00949e084a"
+ "reference": "fe2f7513dc5a4a6cf82715fd0edf7589423d6535"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/e41289c4488563af75bd291972f0fa00949e084a",
- "reference": "e41289c4488563af75bd291972f0fa00949e084a",
+ "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/fe2f7513dc5a4a6cf82715fd0edf7589423d6535",
+ "reference": "fe2f7513dc5a4a6cf82715fd0edf7589423d6535",
"shasum": ""
},
"require": {
@@ -1584,9 +1585,9 @@
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
- "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.313.0"
+ "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.314.0"
},
- "time": "2023-08-25T01:10:13+00:00"
+ "time": "2023-09-03T01:04:12+00:00"
},
{
"name": "google/auth",
@@ -2293,20 +2294,20 @@
},
{
"name": "league/uri",
- "version": "7.1.0",
+ "version": "7.2.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri.git",
- "reference": "c0bf6dfa86b7804fe870b3f3d9c653e35a2c9e3e"
+ "reference": "8b644f8ff80352530bbc0ea467d5b5a89b60d832"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/uri/zipball/c0bf6dfa86b7804fe870b3f3d9c653e35a2c9e3e",
- "reference": "c0bf6dfa86b7804fe870b3f3d9c653e35a2c9e3e",
+ "url": "https://api.github.com/repos/thephpleague/uri/zipball/8b644f8ff80352530bbc0ea467d5b5a89b60d832",
+ "reference": "8b644f8ff80352530bbc0ea467d5b5a89b60d832",
"shasum": ""
},
"require": {
- "league/uri-interfaces": "^7.1",
+ "league/uri-interfaces": "^7.2",
"php": "^8.1"
},
"conflict": {
@@ -2371,7 +2372,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
- "source": "https://github.com/thephpleague/uri/tree/7.1.0"
+ "source": "https://github.com/thephpleague/uri/tree/7.2.1"
},
"funding": [
{
@@ -2379,20 +2380,20 @@
"type": "github"
}
],
- "time": "2023-08-21T20:15:03+00:00"
+ "time": "2023-08-30T21:06:57+00:00"
},
{
"name": "league/uri-interfaces",
- "version": "7.1.0",
+ "version": "7.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri-interfaces.git",
- "reference": "c3ea9306c67c9a1a72312705e8adfcb9cf167310"
+ "reference": "43fa071050fcba89aefb5d4789a4a5a73874c44b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c3ea9306c67c9a1a72312705e8adfcb9cf167310",
- "reference": "c3ea9306c67c9a1a72312705e8adfcb9cf167310",
+ "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/43fa071050fcba89aefb5d4789a4a5a73874c44b",
+ "reference": "43fa071050fcba89aefb5d4789a4a5a73874c44b",
"shasum": ""
},
"require": {
@@ -2455,7 +2456,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
- "source": "https://github.com/thephpleague/uri-interfaces/tree/7.1.0"
+ "source": "https://github.com/thephpleague/uri-interfaces/tree/7.2.0"
},
"funding": [
{
@@ -2463,7 +2464,7 @@
"type": "github"
}
],
- "time": "2023-08-21T20:15:03+00:00"
+ "time": "2023-08-30T19:43:38+00:00"
},
{
"name": "monolog/monolog",
@@ -4207,16 +4208,16 @@
},
{
"name": "aws/aws-sdk-php",
- "version": "3.279.8",
+ "version": "3.280.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "47a454538ec6bf38cf658cf5573585c64915691a"
+ "reference": "d68b83b3bc39b70bf33e9b8b5166facbe3e4fe9b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/47a454538ec6bf38cf658cf5573585c64915691a",
- "reference": "47a454538ec6bf38cf658cf5573585c64915691a",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d68b83b3bc39b70bf33e9b8b5166facbe3e4fe9b",
+ "reference": "d68b83b3bc39b70bf33e9b8b5166facbe3e4fe9b",
"shasum": ""
},
"require": {
@@ -4296,9 +4297,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
- "source": "https://github.com/aws/aws-sdk-php/tree/3.279.8"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.280.2"
},
- "time": "2023-08-28T18:14:34+00:00"
+ "time": "2023-09-01T18:06:10+00:00"
},
{
"name": "brick/math",
@@ -5510,16 +5511,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "1.10.32",
+ "version": "1.10.33",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44"
+ "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c47e47d3ab03137c0e121e77c4d2cb58672f6d44",
- "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1",
+ "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1",
"shasum": ""
},
"require": {
@@ -5568,7 +5569,7 @@
"type": "tidelift"
}
],
- "time": "2023-08-24T21:54:50+00:00"
+ "time": "2023-09-04T12:20:53+00:00"
},
{
"name": "psr/container",
diff --git a/examples/data/salaries.xml b/examples/data/salaries.xml
new file mode 100644
index 000000000..ab637155d
--- /dev/null
+++ b/examples/data/salaries.xml
@@ -0,0 +1,104 @@
+
+
+
+ 71883
+
+
+ 192644
+
+
+ 174187
+
+
+ 179932
+
+
+ 52056
+
+
+
+
+ 102342
+
+
+ 111102
+
+
+ 81938
+
+
+ 132202
+
+
+ 173225
+
+
+
+
+ 79619
+
+
+ 99387
+
+
+ 198847
+
+
+ 50550
+
+
+ 98212
+
+
+
+
+ 69721
+
+
+ 151826
+
+
+ 158168
+
+
+ 111872
+
+
+ 172334
+
+
+
+
+ 174220
+
+
+ 164086
+
+
+ 104257
+
+
+ 105817
+
+
+ 145490
+
+
+
+
+ 127383
+
+
+ 52592
+
+
+ 71732
+
+
+ 165083
+
+
+ 85138
+
+
+
\ No newline at end of file
diff --git a/examples/data/simple_items.xml b/examples/data/simple_items.xml
new file mode 100644
index 000000000..ed8dc7bbf
--- /dev/null
+++ b/examples/data/simple_items.xml
@@ -0,0 +1,10 @@
+
+
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+
+
\ No newline at end of file
diff --git a/examples/topics/types/xml/reading.php b/examples/topics/types/xml/reading.php
new file mode 100644
index 000000000..3c27d7874
--- /dev/null
+++ b/examples/topics/types/xml/reading.php
@@ -0,0 +1,16 @@
+read(XML::from(__FLOW_DATA__ . '/simple_items.xml', 'root/items/item'))
+ ->write(To::output(false))
+ ->run();
diff --git a/examples/topics/types/xml/salaries.php b/examples/topics/types/xml/salaries.php
new file mode 100644
index 000000000..2efe1f867
--- /dev/null
+++ b/examples/topics/types/xml/salaries.php
@@ -0,0 +1,29 @@
+read(XML::from(__FLOW_DATA__ . '/salaries.xml'))
+ ->withEntry('months', ref('row')->xpath('/Salaries/Month'))
+ ->withEntry('month', ref('months')->expand())
+ ->withEntry('month_name', ref('month')->domNodeAttribute('name'))
+ ->withEntry('departments', ref('month')->xpath('/Month/Department'))
+ ->withEntry('department', ref('departments')->expand())
+ ->withEntry('department_name', ref('department')->domNodeAttribute('name'))
+ ->withEntry('department_salary', ref('department')->xpath('/Department/TotalSalary')->domNodeValue())
+ ->drop('row', 'months', 'month', 'departments', 'department')
+ ->groupBy(ref('month_name'))
+ ->aggregate(Aggregation::sum(ref('department_salary')))
+ ->rename('department_salary_sum', 'total_monthly_salaries')
+ ->write(To::output(false))
+ ->run();
diff --git a/src/adapter/etl-adapter-xml/README.md b/src/adapter/etl-adapter-xml/README.md
index 7790aec58..71ec42a60 100644
--- a/src/adapter/etl-adapter-xml/README.md
+++ b/src/adapter/etl-adapter-xml/README.md
@@ -40,22 +40,36 @@ Memory safe XML extractor
read(XML::from_file(__DIR__ . '/xml/simple_items.xml', 'root/items/item'))
- ->fetch()
+ ->read(XML::from(__FLOW_DATA__ . '/simple_items.xml', 'root/items/item'))
+ ->write(To::output(false))
+ ->run()
+;
```
Above code will generate Rows with 5 entries like the one below:
-```php
-- 1
|
+| - 2
|
+| - 3
|
+| - 4
|
+| - 5
|
+| - 6
|
++----------------------------------------------+
+```
+
+Each entry will be an XMLEntry type.
+From there you can use built in expressions to extract data from XML.
+
+- `ref('row')->xpath('...');`
+- `ref('row')->domNodeAttribute('...');`
+- `ref('row')->domNodeValue('...');`
+
+When working with collections XPath will return an ListEntry with XMLEntries inside.
+From there you can for example unpack or expand them.
+
+For more examples please look into `/examples/topics/xml` directory in [flow monorepo](https://github.com/flow-php/flow)
-Row::create(
- Entry::array('row', [
- 'item' => [
- 'id' => [
- '@value' => 1
- ]
- ]
- ])
-)
-```
\ No newline at end of file
diff --git a/src/adapter/etl-adapter-xml/src/Flow/ETL/Adapter/XML/XMLReaderExtractor.php b/src/adapter/etl-adapter-xml/src/Flow/ETL/Adapter/XML/XMLReaderExtractor.php
index 82fc2e7bc..f2d2f972c 100644
--- a/src/adapter/etl-adapter-xml/src/Flow/ETL/Adapter/XML/XMLReaderExtractor.php
+++ b/src/adapter/etl-adapter-xml/src/Flow/ETL/Adapter/XML/XMLReaderExtractor.php
@@ -51,11 +51,11 @@ public function extract(FlowContext $context) : \Generator
if ($xmlReader->nodeType === \XMLReader::ELEMENT) {
if ($previousDepth === $xmlReader->depth) {
\array_pop($currentPathBreadCrumbs);
- \array_push($currentPathBreadCrumbs, $xmlReader->name);
+ $currentPathBreadCrumbs[] = $xmlReader->name;
}
if ($xmlReader->depth > $previousDepth) {
- \array_push($currentPathBreadCrumbs, $xmlReader->name);
+ $currentPathBreadCrumbs[] = $xmlReader->name;
}
if ($xmlReader->depth < $previousDepth) {
@@ -71,11 +71,11 @@ public function extract(FlowContext $context) : \Generator
if ($context->config->shouldPutInputIntoRows()) {
$rows[] = Row::create(
- Entry::array($this->rowEntryName, $this->convertDOMDocument($node)),
+ Entry::xml($this->rowEntryName, $node),
Entry::string('input_file_uri', $filePath->uri())
);
} else {
- $rows[] = Row::create(Entry::array($this->rowEntryName, $this->convertDOMDocument($node)));
+ $rows[] = Row::create(Entry::xml($this->rowEntryName, $node));
}
if (\count($rows) >= $this->rowsInBatch) {
@@ -95,90 +95,4 @@ public function extract(FlowContext $context) : \Generator
}
}
}
-
- /**
- * @param \DOMDocument $document
- *
- * @return array
- */
- private function convertDOMDocument(\DOMDocument $document) : array
- {
- $xmlArray = [];
-
- if ($document->hasChildNodes()) {
- $children = $document->childNodes;
-
- foreach ($children as $child) {
- /** @psalm-suppress ArgumentTypeCoercion */
- $xmlArray[$child->nodeName] = $this->convertDOMElement($child);
- }
- }
-
- return $xmlArray;
- }
-
- /**
- * @psalm-suppress ArgumentTypeCoercion
- * @psalm-suppress PossiblyNullArgument
- * @psalm-suppress UnnecessaryVarAnnotation
- * @psalm-suppress PossiblyNullIterator
- *
- * @return array
- */
- private function convertDOMElement(\DOMElement|\DOMNode $element) : array
- {
- $xmlArray = [];
-
- if ($element->hasAttributes()) {
- /**
- * @var \DOMAttr $attribute
- *
- * @phpstan-ignore-next-line
- */
- foreach ($element->attributes as $attribute) {
- $xmlArray['@attributes'][$attribute->name] = $attribute->value;
- }
- }
-
- foreach ($element->childNodes as $childNode) {
- if ($childNode->nodeType === XML_TEXT_NODE) {
- /** @phpstan-ignore-next-line */
- if (\trim($childNode->nodeValue)) {
- $xmlArray['@value'] = $childNode->nodeValue;
- }
- }
-
- if ($childNode->nodeType === XML_ELEMENT_NODE) {
- if ($this->isElementCollection($element)) {
- /** @phpstan-ignore-next-line */
- $xmlArray[$childNode->nodeName][] = $this->convertDOMElement($childNode);
- } else {
- $xmlArray[$childNode->nodeName] = $this->convertDOMElement($childNode);
- }
- }
- }
-
- return $xmlArray;
- }
-
- private function isElementCollection(\DOMElement|\DOMNode $element) : bool
- {
- if ($element->childNodes->count() <= 1) {
- return false;
- }
-
- $nodeNames = [];
- /** @var \DOMElement $childNode */
- foreach ($element->childNodes as $childNode) {
- if ($childNode->nodeType === XML_ELEMENT_NODE) {
- $nodeNames[] = $childNode->nodeName;
- }
- }
-
- if (!\count($nodeNames) || \count($nodeNames) === 1) {
- return false;
- }
-
- return \count(\array_unique($nodeNames)) === 1;
- }
}
diff --git a/src/adapter/etl-adapter-xml/tests/Flow/ETL/Tests/Integration/Adapter/XML/XMLReaderExtractorTest.php b/src/adapter/etl-adapter-xml/tests/Flow/ETL/Tests/Integration/Adapter/XML/XMLReaderExtractorTest.php
index 6e254d6f7..4da95a772 100644
--- a/src/adapter/etl-adapter-xml/tests/Flow/ETL/Tests/Integration/Adapter/XML/XMLReaderExtractorTest.php
+++ b/src/adapter/etl-adapter-xml/tests/Flow/ETL/Tests/Integration/Adapter/XML/XMLReaderExtractorTest.php
@@ -13,70 +13,15 @@
final class XMLReaderExtractorTest extends TestCase
{
- public function test_reading_xml_collection() : void
+ public function test_reading_xml() : void
{
+ $xml = new \DOMDocument();
+ $xml->load(__DIR__ . '/xml/simple_items.xml');
+
$this->assertEquals(
- new Rows(
- Row::create(
- Entry::array('row', [
- 'items' => [
- 'item' => [
- [
- 'id' => [
- '@value' => 1,
- '@attributes' => [
- 'id_attribute_01' => '1',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '1'],
- ],
- [
- 'id' => [
- '@value' => 2,
- '@attributes' => [
- 'id_attribute_01' => '2',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '2'],
- ],
- [
- 'id' => [
- '@value' => 3,
- '@attributes' => [
- 'id_attribute_01' => '3',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '3'],
- ],
- [
- 'id' => [
- '@value' => 4,
- '@attributes' => [
- 'id_attribute_01' => '4',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '4'],
- ],
- [
- 'id' => [
- '@value' => 5,
- '@attributes' => [
- 'id_attribute_01' => '5',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '5'],
- ],
- ],
- '@attributes' => [
- 'items_attribute_01' => '1',
- 'items_attribute_02' => '2',
- ],
- ],
- ])
- )
- ),
+ (new Rows(Row::create(Entry::xml('row', $xml)))),
(new Flow())
- ->read(XML::from(__DIR__ . '/xml/simple_items.xml', 'root/items'))
+ ->read(XML::from(__DIR__ . '/xml/simple_items.xml'))
->fetch()
);
}
@@ -85,198 +30,46 @@ public function test_reading_xml_each_collection_item() : void
{
$this->assertEquals(
new Rows(
- Row::create(
- Entry::array('row', [
- 'item' => [
- 'id' => [
- '@value' => 1,
- '@attributes' => [
- 'id_attribute_01' => '1',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '1'],
- ],
- ])
- ),
- Row::create(
- Entry::array('row', [
- 'item' => [
- 'id' => [
- '@value' => 2,
- '@attributes' => [
- 'id_attribute_01' => '2',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '2'],
- ],
- ])
- ),
- Row::create(
- Entry::array('row', [
- 'item' => [
- 'id' => [
- '@value' => 3,
- '@attributes' => [
- 'id_attribute_01' => '3',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '3'],
- ],
- ])
- ),
- Row::create(
- Entry::array('row', [
- 'item' => [
- 'id' => [
- '@value' => 4,
- '@attributes' => [
- 'id_attribute_01' => '4',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '4'],
- ],
- ])
- ),
- Row::create(
- Entry::array('row', [
- 'item' => [
- 'id' => [
- '@value' => 5,
- '@attributes' => [
- 'id_attribute_01' => '5',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '5'],
- ],
- ])
- )
+ Row::create(Entry::xml('row', '- 1
')),
+ Row::create(Entry::xml('row', '- 2
')),
+ Row::create(Entry::xml('row', '- 3
')),
+ Row::create(Entry::xml('row', '- 4
')),
+ Row::create(Entry::xml('row', '- 5
')),
),
(new Flow())
- ->read(XML::from(__DIR__ . '/xml/simple_items.xml', 'root/items/item'))
+ ->read(XML::from(__DIR__ . '/xml/simple_items_flat.xml', 'root/items/item'))
->fetch()
);
}
- public function test_reading_xml_each_collection_item_id() : void
+ public function test_reading_xml_from_path() : void
{
- $this->assertEquals(
- new Rows(
- Row::create(
- Entry::array('row', [
- 'id' => [
- '@value' => '1',
- '@attributes' => ['id_attribute_01' => '1'],
- ],
- ])
- ),
- Row::create(
- Entry::array('row', [
- 'id' => [
- '@value' => '2',
- '@attributes' => ['id_attribute_01' => '2'],
- ],
- ])
- ),
- Row::create(
- Entry::array('row', [
- 'id' => [
- '@value' => '3',
- '@attributes' => ['id_attribute_01' => '3'],
- ],
- ])
- ),
- Row::create(
- Entry::array('row', [
- 'id' => [
- '@value' => '4',
- '@attributes' => ['id_attribute_01' => '4'],
- ],
- ])
- ),
- Row::create(
- Entry::array('row', [
- 'id' => [
- '@value' => '5',
- '@attributes' => ['id_attribute_01' => '5'],
- ],
- ])
- )
- ),
- (new Flow())
- ->read(XML::from(__DIR__ . '/xml/simple_items.xml', 'root/items/item/id'))
- ->fetch()
- );
- }
+ $xml = new \DOMDocument();
+ $xml->loadXML(<<<'XML'
+
+
+ -
+ 1
+
+ -
+ 2
+
+ -
+ 3
+
+ -
+ 4
+
+ -
+ 5
+
+
- public function test_reading_xml_root() : void
- {
+XML);
$this->assertEquals(
- new Rows(
- Row::create(
- Entry::array('row', [
- 'root' => [
- 'items' => [
- 'item' => [
- [
- 'id' => [
- '@value' => 1,
- '@attributes' => [
- 'id_attribute_01' => '1',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '1'],
- ],
- [
- 'id' => [
- '@value' => 2,
- '@attributes' => [
- 'id_attribute_01' => '2',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '2'],
- ],
- [
- 'id' => [
- '@value' => 3,
- '@attributes' => [
- 'id_attribute_01' => '3',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '3'],
- ],
- [
- 'id' => [
- '@value' => 4,
- '@attributes' => [
- 'id_attribute_01' => '4',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '4'],
- ],
- [
- 'id' => [
- '@value' => 5,
- '@attributes' => [
- 'id_attribute_01' => '5',
- ],
- ],
- '@attributes' => ['item_attribute_01' => '5'],
- ],
- ],
- '@attributes' => [
- 'items_attribute_01' => '1',
- 'items_attribute_02' => '2',
- ],
- ],
- '@attributes' => [
- 'root_attribute_01' => '1',
- ],
- ],
- ])
- )
- ),
+ new Rows(Row::create(Entry::xml('row', $xml))),
(new Flow())
- ->read(XML::from(__DIR__ . '/xml/simple_items.xml'))
+ ->read(XML::from(__DIR__ . '/xml/simple_items.xml', 'root/items'))
->fetch()
);
}
diff --git a/src/adapter/etl-adapter-xml/tests/Flow/ETL/Tests/Integration/Adapter/XML/xml/simple_items_flat.xml b/src/adapter/etl-adapter-xml/tests/Flow/ETL/Tests/Integration/Adapter/XML/xml/simple_items_flat.xml
new file mode 100644
index 000000000..8c5c2d66f
--- /dev/null
+++ b/src/adapter/etl-adapter-xml/tests/Flow/ETL/Tests/Integration/Adapter/XML/xml/simple_items_flat.xml
@@ -0,0 +1 @@
+- 1
- 2
- 3
- 4
- 5
\ No newline at end of file
diff --git a/src/core/etl/src/Flow/ETL/DSL/Entry.php b/src/core/etl/src/Flow/ETL/DSL/Entry.php
index 024baa3b0..6d06ab325 100644
--- a/src/core/etl/src/Flow/ETL/DSL/Entry.php
+++ b/src/core/etl/src/Flow/ETL/DSL/Entry.php
@@ -289,4 +289,17 @@ final public static function structure(string $name, RowEntry ...$entries) : Row
{
return new RowEntry\StructureEntry($name, ...$entries);
}
+
+ /**
+ * @return RowEntry\XMLEntry
+ */
+ final public static function xml(string $name, \DOMDocument|string $data) : RowEntry
+ {
+ return new RowEntry\XMLEntry($name, $data);
+ }
+
+ final public static function xml_node(string $name, \DOMNode $data) : RowEntry
+ {
+ return new RowEntry\XMLNodeEntry($name, $data);
+ }
}
diff --git a/src/core/etl/src/Flow/ETL/Formatter/ASCII/ASCIIValue.php b/src/core/etl/src/Flow/ETL/Formatter/ASCII/ASCIIValue.php
index 30ce77822..96fac80b1 100644
--- a/src/core/etl/src/Flow/ETL/Formatter/ASCII/ASCIIValue.php
+++ b/src/core/etl/src/Flow/ETL/Formatter/ASCII/ASCIIValue.php
@@ -94,6 +94,10 @@ private function stringValue() : string
if ($val instanceof Entry) {
$this->stringValue = $val->toString();
+ if ($val instanceof Entry\XMLEntry || $val instanceof Entry\XMLNodeEntry) {
+ $this->stringValue = \str_replace("\n", '', $this->stringValue);
+ }
+
return $this->stringValue;
}
diff --git a/src/core/etl/src/Flow/ETL/Row/Entry/XMLEntry.php b/src/core/etl/src/Flow/ETL/Row/Entry/XMLEntry.php
new file mode 100644
index 000000000..eb0865d43
--- /dev/null
+++ b/src/core/etl/src/Flow/ETL/Row/Entry/XMLEntry.php
@@ -0,0 +1,106 @@
+
+ */
+final class XMLEntry implements \Stringable, Entry
+{
+ use EntryRef;
+
+ private readonly \DOMDocument $value;
+
+ public function __construct(private readonly string $name, \DOMDocument|string $value)
+ {
+ if (\is_string($value)) {
+ $doc = new \DOMDocument();
+
+ if (!@$doc->loadXML($value)) {
+ throw new InvalidArgumentException(\sprintf('Given string "%s" is not valid XML', $value));
+ }
+
+ $this->value = $doc;
+ } else {
+ $this->value = $value;
+ }
+ }
+
+ public function __serialize() : array
+ {
+ return [
+ 'name' => $this->name,
+ 'value' => $this->value,
+ ];
+ }
+
+ public function __toString() : string
+ {
+ /** @phpstan-ignore-next-line */
+ return $this->value->saveXML();
+ }
+
+ public function __unserialize(array $data) : void
+ {
+ $this->name = $data['name'];
+ $this->value = $data['value'];
+ }
+
+ public function definition() : Definition
+ {
+ return Definition::xml($this->ref(), false);
+ }
+
+ public function is(Reference|string $name) : bool
+ {
+ if ($name instanceof Reference) {
+ return $this->name === $name->name();
+ }
+
+ return $this->name === $name;
+ }
+
+ public function isEqual(Entry $entry) : bool
+ {
+ if (!$entry instanceof self || !$this->is($entry->name())) {
+ return false;
+ }
+
+ if ($entry->value->documentElement === null && $this->value->documentElement === null) {
+ return true;
+ }
+
+ return $entry->value()->C14N() === $this->value->C14N();
+ }
+
+ public function map(callable $mapper) : Entry
+ {
+ return new self($this->name, $mapper($this->value()));
+ }
+
+ public function name() : string
+ {
+ return $this->name;
+ }
+
+ public function rename(string $name) : Entry
+ {
+ return new self($name, $this->value);
+ }
+
+ public function toString() : string
+ {
+ /** @phpstan-ignore-next-line */
+ return $this->value->saveXML();
+ }
+
+ public function value() : \DOMDocument
+ {
+ return $this->value;
+ }
+}
diff --git a/src/core/etl/src/Flow/ETL/Row/Entry/XMLNodeEntry.php b/src/core/etl/src/Flow/ETL/Row/Entry/XMLNodeEntry.php
new file mode 100644
index 000000000..28f0486e8
--- /dev/null
+++ b/src/core/etl/src/Flow/ETL/Row/Entry/XMLNodeEntry.php
@@ -0,0 +1,96 @@
+
+ */
+final class XMLNodeEntry implements \Stringable, Entry
+{
+ use EntryRef;
+
+ public function __construct(private readonly string $name, private readonly \DOMNode $value)
+ {
+ }
+
+ public function __serialize() : array
+ {
+ return [
+ 'name' => $this->name,
+ 'value' => $this->value,
+ ];
+ }
+
+ public function __toString() : string
+ {
+ /**
+ * @psalm-suppress PossiblyNullReference
+ *
+ * @phpstan-ignore-next-line
+ */
+ return $this->value->ownerDocument->saveXML($this->value);
+ }
+
+ public function __unserialize(array $data) : void
+ {
+ $this->name = $data['name'];
+ $this->value = $data['value'];
+ }
+
+ public function definition() : Definition
+ {
+ return Definition::xml_node($this->ref(), false);
+ }
+
+ public function is(Reference|string $name) : bool
+ {
+ if ($name instanceof Reference) {
+ return $this->name === $name->name();
+ }
+
+ return $this->name === $name;
+ }
+
+ public function isEqual(Entry $entry) : bool
+ {
+ if (!$entry instanceof self || !$this->is($entry->name())) {
+ return false;
+ }
+
+ return $this->value->C14N() === $entry->value->C14N();
+ }
+
+ public function map(callable $mapper) : Entry
+ {
+ return new self($this->name, $mapper($this->value()));
+ }
+
+ public function name() : string
+ {
+ return $this->name;
+ }
+
+ public function rename(string $name) : Entry
+ {
+ return new self($name, $this->value);
+ }
+
+ public function toString() : string
+ {
+ /**
+ * @psalm-suppress PossiblyNullReference
+ *
+ * @phpstan-ignore-next-line
+ */
+ return $this->value->ownerDocument->saveXML($this->value);
+ }
+
+ public function value() : \DOMNode
+ {
+ return $this->value;
+ }
+}
diff --git a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php
index a4c4679cf..ae86ed0fa 100644
--- a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php
+++ b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php
@@ -57,6 +57,10 @@ public function create(string $entryName, mixed $value) : Entry
return Row\Entry\JsonEntry::fromJsonString($entryName, $value);
}
+ if ($this->isXML($value)) {
+ return new Entry\XMLEntry($entryName, $value);
+ }
+
return new Row\Entry\StringEntry($entryName, $value);
}
@@ -73,6 +77,14 @@ public function create(string $entryName, mixed $value) : Entry
}
if (\is_object($value)) {
+ if ($value instanceof \DOMDocument) {
+ return new Row\Entry\XMLEntry($entryName, $value);
+ }
+
+ if ($value instanceof \DOMNode) {
+ return new Row\Entry\XMLNodeEntry($entryName, $value);
+ }
+
if ($value instanceof \DateTimeImmutable) {
return new Row\Entry\DateTimeEntry($entryName, $value);
}
@@ -121,6 +133,11 @@ public function create(string $entryName, mixed $value) : Entry
if ($class === \DateTimeImmutable::class || $class === \DateTime::class) {
$class = \DateTimeInterface::class;
}
+
+ if ($class === \DOMElement::class) {
+ $class = \DOMNode::class;
+ }
+
/**
* @psalm-suppress PossiblyNullArgument
*/
@@ -174,6 +191,10 @@ private function fromDefinition(Schema\Definition $definition, mixed $value) : E
}
}
+ if ($type === Entry\XMLEntry::class && (\is_string($value) || $value instanceof \DOMDocument)) {
+ return EntryDSL::xml($definition->entry()->name(), $value);
+ }
+
if ($type === Entry\ObjectEntry::class && \is_object($value)) {
return EntryDSL::object($definition->entry()->name(), $value);
}
@@ -267,4 +288,24 @@ private function isJson(string $string) : bool
return false;
}
}
+
+ private function isXML(string $string) : bool
+ {
+ try {
+ \libxml_use_internal_errors(true);
+
+ $doc = new \DOMDocument();
+ $result = $doc->loadXML($string);
+ \libxml_clear_errors(); // Clear any errors if needed
+ \libxml_use_internal_errors(false); // Restore standard error handling
+
+ /** @psalm-suppress RedundantCastGivenDocblockType */
+ return (bool) $result;
+ } catch (\Exception) {
+ \libxml_clear_errors(); // Clear any errors if needed
+ \libxml_use_internal_errors(false); // Restore standard error handling
+
+ return false;
+ }
+ }
}
diff --git a/src/core/etl/src/Flow/ETL/Row/Reference/EntryExpression.php b/src/core/etl/src/Flow/ETL/Row/Reference/EntryExpression.php
index 1d82f3d59..d7fb670f5 100644
--- a/src/core/etl/src/Flow/ETL/Row/Reference/EntryExpression.php
+++ b/src/core/etl/src/Flow/ETL/Row/Reference/EntryExpression.php
@@ -96,6 +96,16 @@ public function divide(Expression $ref) : Expression|EntryReference
return new Expressions(new Divide($this, $ref));
}
+ public function domNodeAttribute(string $attribute) : Expression|EntryReference
+ {
+ return new Expressions(new Expression\DOMNodeAttribute($this, $attribute));
+ }
+
+ public function domNodeValue() : Expression|EntryReference
+ {
+ return new Expressions(new Expression\DOMNodeValue($this));
+ }
+
public function endsWith(Expression $needle) : Expression|EntryReference
{
return new Expressions(new EndsWith($this, $needle));
@@ -376,4 +386,9 @@ public function upper() : Expression|EntryReference
{
return new Expressions(new Expression\ToUpper($this));
}
+
+ public function xpath(string $string) : Expression|EntryReference
+ {
+ return new Expressions(new Expression\XPath($this, $string));
+ }
}
diff --git a/src/core/etl/src/Flow/ETL/Row/Reference/Expression/Cast.php b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/Cast.php
index 944bf4adf..a62b85206 100644
--- a/src/core/etl/src/Flow/ETL/Row/Reference/Expression/Cast.php
+++ b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/Cast.php
@@ -46,12 +46,41 @@ public function eval(Row $row) : mixed
default => (string) $value
},
'bool', 'boolean' => (bool) $value,
- 'array' => (array) $value,
+ 'array' => $this->toArray($value),
'object' => (object) $value,
'null' => null,
'json' => \json_encode($value, JSON_THROW_ON_ERROR),
'json_pretty' => \json_encode($value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
+ 'xml' => $this->toXML($value),
default => throw new InvalidArgumentException("Unknown cast type '{$this->type}'")
};
}
+
+ private function toArray(mixed $data) : array
+ {
+ if ($data instanceof \DOMDocument) {
+ return (new Cast\XMLConverter())->toArray($data);
+ }
+
+ return (array) $data;
+ }
+
+ private function toXML(mixed $value) : \DOMDocument
+ {
+ if (\is_string($value)) {
+ $doc = new \DOMDocument();
+
+ if (!@$doc->load($value)) {
+ throw new InvalidArgumentException('Invalid XML string given: ' . $value);
+ }
+
+ return $doc;
+ }
+
+ if ($value instanceof \DOMDocument) {
+ return $value;
+ }
+
+ throw new InvalidArgumentException(\sprintf('Cannot cast %s to XML', \gettype($value)));
+ }
}
diff --git a/src/core/etl/src/Flow/ETL/Row/Reference/Expression/Cast/XMLConverter.php b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/Cast/XMLConverter.php
new file mode 100644
index 000000000..5bfcd3453
--- /dev/null
+++ b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/Cast/XMLConverter.php
@@ -0,0 +1,88 @@
+
+ */
+ public function toArray(\DOMDocument $document) : array
+ {
+ $xmlArray = [];
+
+ if ($document->hasChildNodes()) {
+ foreach ($document->childNodes as $child) {
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $xmlArray[$child->nodeName] = $this->convertDOMElement($child);
+ }
+ }
+
+ return $xmlArray;
+ }
+
+ /**
+ * @psalm-suppress ArgumentTypeCoercion
+ * @psalm-suppress PossiblyNullArgument
+ * @psalm-suppress UnnecessaryVarAnnotation
+ * @psalm-suppress PossiblyNullIterator
+ */
+ private function convertDOMElement(\DOMElement|\DOMNode $element) : array
+ {
+ $xmlArray = [];
+
+ if ($element->hasAttributes()) {
+ /**
+ * @var \DOMAttr $attribute
+ *
+ * @phpstan-ignore-next-line
+ */
+ foreach ($element->attributes as $attribute) {
+ $xmlArray['@attributes'][$attribute->name] = $attribute->value;
+ }
+ }
+
+ foreach ($element->childNodes as $childNode) {
+ if ($childNode->nodeType === XML_TEXT_NODE) {
+ /** @phpstan-ignore-next-line */
+ if (\trim($childNode->nodeValue)) {
+ $xmlArray['@value'] = $childNode->nodeValue;
+ }
+ }
+
+ if ($childNode->nodeType === XML_ELEMENT_NODE) {
+ if ($this->isElementCollection($element)) {
+ /** @phpstan-ignore-next-line */
+ $xmlArray[$childNode->nodeName][] = $this->convertDOMElement($childNode);
+ } else {
+ $xmlArray[$childNode->nodeName] = $this->convertDOMElement($childNode);
+ }
+ }
+ }
+
+ return $xmlArray;
+ }
+
+ private function isElementCollection(\DOMElement|\DOMNode $element) : bool
+ {
+ if ($element->childNodes->count() <= 1) {
+ return false;
+ }
+
+ $nodeNames = [];
+ /** @var \DOMElement $childNode */
+ foreach ($element->childNodes as $childNode) {
+ if ($childNode->nodeType === XML_ELEMENT_NODE) {
+ $nodeNames[] = $childNode->nodeName;
+ }
+ }
+
+ if (\count($nodeNames) <= 1) {
+ return false;
+ }
+
+ return \count(\array_unique($nodeNames)) === 1;
+ }
+}
diff --git a/src/core/etl/src/Flow/ETL/Row/Reference/Expression/DOMNodeAttribute.php b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/DOMNodeAttribute.php
new file mode 100644
index 000000000..a4ccdb27b
--- /dev/null
+++ b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/DOMNodeAttribute.php
@@ -0,0 +1,45 @@
+ref->eval($row);
+
+ if (!$value instanceof \DOMNode) {
+ return null;
+ }
+
+ if (!$value->hasAttributes()) {
+ return null;
+ }
+
+ $attributes = $value->attributes;
+
+ /**
+ * @psalm-suppress PossiblyNullReference
+ *
+ * @phpstan-ignore-next-line
+ */
+ if (!$attributes->getNamedItem($this->attribute)) {
+ return null;
+ }
+
+ /**
+ * @psalm-suppress PossiblyNullPropertyFetch
+ *
+ * @phpstan-ignore-next-line
+ */
+ return $attributes->getNamedItem($this->attribute)->nodeValue;
+ }
+}
diff --git a/src/core/etl/src/Flow/ETL/Row/Reference/Expression/DOMNodeValue.php b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/DOMNodeValue.php
new file mode 100644
index 000000000..c1c636b58
--- /dev/null
+++ b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/DOMNodeValue.php
@@ -0,0 +1,25 @@
+ref->eval($row);
+
+ if (!$value instanceof \DOMNode) {
+ return null;
+ }
+
+ return $value->nodeValue;
+ }
+}
diff --git a/src/core/etl/src/Flow/ETL/Row/Reference/Expression/XPath.php b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/XPath.php
new file mode 100644
index 000000000..d14417eec
--- /dev/null
+++ b/src/core/etl/src/Flow/ETL/Row/Reference/Expression/XPath.php
@@ -0,0 +1,54 @@
+ref->eval($row);
+
+ if ($value instanceof \DOMNode && !$value instanceof \DOMDocument) {
+ $newDom = new \DOMDocument();
+ $newNode = $newDom->importNode($value, true);
+ $newDom->append($newNode);
+
+ $value = $newDom;
+ }
+
+ if (!$value instanceof \DOMDocument) {
+ return null;
+ }
+
+ $xpath = new \DOMXPath($value);
+ $result = @$xpath->query($this->path);
+
+ if ($result === false) {
+ return null;
+ }
+
+ if ($result->length === 0) {
+ return null;
+ }
+
+ if ($result->length === 1) {
+ return $result->item(0);
+ }
+
+ $nodes = [];
+
+ foreach ($result as $node) {
+ $nodes[] = $node;
+ }
+
+ return $nodes;
+ }
+}
diff --git a/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php b/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php
index 7d9c98d89..c132554cb 100644
--- a/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php
+++ b/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php
@@ -20,6 +20,7 @@
use Flow\ETL\Row\Entry\StringEntry;
use Flow\ETL\Row\Entry\StructureEntry;
use Flow\ETL\Row\Entry\TypedCollection\Type;
+use Flow\ETL\Row\Entry\XMLEntry;
use Flow\ETL\Row\EntryReference;
use Flow\ETL\Row\Schema\Constraint\Any;
use Flow\ETL\Row\Schema\Constraint\VoidConstraint;
@@ -162,6 +163,16 @@ public static function union(string|EntryReference $entry, array $entryClasses,
return new self($entry, $types, $constraint, $metadata);
}
+ public static function xml(string|EntryReference $entry, bool $nullable = false, ?Constraint $constraint = null, ?Metadata $metadata = null) : self
+ {
+ return new self($entry, ($nullable) ? [XMLEntry::class, NullEntry::class] : [XMLEntry::class], $constraint, $metadata);
+ }
+
+ public static function xml_node(string|EntryReference $entry, bool $nullable = false, ?Constraint $constraint = null, ?Metadata $metadata = null) : self
+ {
+ return new self($entry, ($nullable) ? [Entry\XMLNodeEntry::class, NullEntry::class] : [Entry\XMLNodeEntry::class], $constraint, $metadata);
+ }
+
// @codeCoverageIgnoreStart
public function __serialize() : array
{
diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/DataFrameTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/DataFrameTest.php
index d57d8bcd5..e5180ce65 100644
--- a/src/core/etl/tests/Flow/ETL/Tests/Unit/DataFrameTest.php
+++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/DataFrameTest.php
@@ -326,7 +326,8 @@ public function extract(FlowContext $context) : \Generator
new Row\Entries(new IntegerEntry('item-id', 3), new StringEntry('name', 'three'))
),
new Row\Entry\ObjectEntry('object', new \ArrayIterator([1, 2, 3])),
- new Row\Entry\EnumEntry('enum', BackedStringEnum::three)
+ new Row\Entry\EnumEntry('enum', BackedStringEnum::three),
+ new Row\Entry\XMLEntry('xml', 'testbar'),
),
);
}
@@ -336,15 +337,15 @@ public function extract(FlowContext $context) : \Generator
$this->assertSame(
<<<'ASCIITABLE'
-+------+--------+-----+---------+----------------------+-------+----------------------+----------------------+----------------------+----------------------+-------+
-| id | price | 100 | deleted | created-at | phase | array | items | tags | object | enum |
-+------+--------+-----+---------+----------------------+-------+----------------------+----------------------+----------------------+----------------------+-------+
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+ | null | [{"id":1,"status":"N | {"item-id":"1","name | [{"item-id":"1","nam | ArrayIterator Object | three |
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+ | null | [{"id":1,"status":"N | {"item-id":"1","name | [{"item-id":"1","nam | ArrayIterator Object | three |
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+ | null | [{"id":1,"status":"N | {"item-id":"1","name | [{"item-id":"1","nam | ArrayIterator Object | three |
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+ | null | [{"id":1,"status":"N | {"item-id":"1","name | [{"item-id":"1","nam | ArrayIterator Object | three |
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+ | null | [{"id":1,"status":"N | {"item-id":"1","name | [{"item-id":"1","nam | ArrayIterator Object | three |
-+------+--------+-----+---------+----------------------+-------+----------------------+----------------------+----------------------+----------------------+-------+
++------+--------+-----+---------+----------------------+-------+----------------------+----------------------+----------------------+----------------------+-------+----------------------+
+| id | price | 100 | deleted | created-at | phase | array | items | tags | object | enum | xml |
++------+--------+-----+---------+----------------------+-------+----------------------+----------------------+----------------------+----------------------+-------+----------------------+
+| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+ | null | [{"id":1,"status":"N | {"item-id":"1","name | [{"item-id":"1","nam | ArrayIterator Object | three | assertSame(
<<<'ASCIITABLE'
-+------+--------+-----+---------+---------------------------+-------+-------------------------------------------------------+------------------------------+--------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------+-------+
-| id | price | 100 | deleted | created-at | phase | array | items | tags | object | enum |
-+------+--------+-----+---------+---------------------------+-------+-------------------------------------------------------+------------------------------+--------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------+-------+
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three |
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three |
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three |
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three |
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three |
-| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three |
-+------+--------+-----+---------+---------------------------+-------+-------------------------------------------------------+------------------------------+--------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------+-------+
++------+--------+-----+---------+---------------------------+-------+-------------------------------------------------------+------------------------------+--------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------+-------+--------------------------------------------------------------------------+
+| id | price | 100 | deleted | created-at | phase | array | items | tags | object | enum | xml |
++------+--------+-----+---------+---------------------------+-------+-------------------------------------------------------+------------------------------+--------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------+-------+--------------------------------------------------------------------------+
+| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three | testbar |
+| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three | testbar |
+| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three | testbar |
+| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three | testbar |
+| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three | testbar |
+| 1234 | 123.45 | 100 | false | 2020-07-13T15:00:00+00:00 | null | [{"id":1,"status":"NEW"},{"id":2,"status":"PENDING"}] | {"item-id":"1","name":"one"} | [{"item-id":"1","name":"one"},{"item-id":"2","name":"two"},{"item-id":"3","name":"three"}] | ArrayIterator Object( [storage:ArrayIterator:private] => Array ( [0] => 1 [1] => 2 [2] => 3 )) | three | testbar |
++------+--------+-----+---------+---------------------------+-------+-------------------------------------------------------+------------------------------+--------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------+-------+--------------------------------------------------------------------------+
6 rows
ASCIITABLE,
diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Entry/XMLEntryTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Entry/XMLEntryTest.php
new file mode 100644
index 000000000..fe1cb3885
--- /dev/null
+++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Entry/XMLEntryTest.php
@@ -0,0 +1,136 @@
+loadXML('123');
+ $doc2 = new \DOMDocument();
+ $doc2->loadXML('123');
+
+ yield 'equal names and equal simple xml documents' => [
+ true,
+ new XMLEntry('name', $doc1),
+ new XMLEntry('name', $doc2),
+ ];
+
+ $doc1 = new \DOMDocument();
+ $doc1->loadXML('123');
+ $doc2 = new \DOMDocument();
+ $doc2->loadXML('123');
+
+ yield 'equal names and equal simple xml documents with different order of attributes' => [
+ true,
+ new XMLEntry('name', $doc1),
+ new XMLEntry('name', $doc2),
+ ];
+
+ $doc1 = new \DOMDocument();
+ $doc1->loadXML('123');
+ $doc2 = new \DOMDocument();
+ $doc2->loadXML('123');
+
+ yield 'equal nodes but different attributes' => [
+ false,
+ new XMLEntry('name', $doc1),
+ new XMLEntry('name', $doc2),
+ ];
+
+ $doc1 = new \DOMDocument();
+ $doc1->loadXML('123');
+ $doc2 = new \DOMDocument();
+ $doc2->loadXML('23');
+
+ yield 'equal attributes but different nodes' => [
+ false,
+ new XMLEntry('name', $doc1),
+ new XMLEntry('name', $doc2),
+ ];
+
+ $doc1 = new \DOMDocument();
+ $doc1->loadXML('123');
+ $doc2 = new \DOMDocument();
+
+ yield 'compare with empty document' => [
+ false,
+ new XMLEntry('name', $doc1),
+ new XMLEntry('name', $doc2),
+ ];
+
+ $doc1 = new \DOMDocument();
+ $doc2 = new \DOMDocument();
+
+ yield 'compare twp empty documents' => [
+ true,
+ new XMLEntry('name', $doc1),
+ new XMLEntry('name', $doc2),
+ ];
+ }
+
+ /**
+ * The C14N() method in PHP's DOMDocument class does not provide an option to remove all whitespace between nodes;
+ * it's designed to produce a canonical form of the XML document according to the Canonical XML standard,
+ * which generally preserves whitespace within text nodes.
+ */
+ public function test_canonicalization() : void
+ {
+ $doc = new \DOMDocument();
+ $doc->loadXML('- 1
');
+
+ $doc2 = new \DOMDocument();
+ $doc2->loadXML(<<<'XML'
+-
+ 1
+
+XML);
+
+ $this->assertNotEquals(
+ Entry::xml('row', $doc),
+ Entry::xml('row', $doc2),
+ );
+ }
+
+ public function test_creating_entry_from_invalid_xml_string() : void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Given string "foo" is not valid XML');
+
+ new XMLEntry('name', 'foo');
+ }
+
+ public function test_creating_entry_from_valid_xml_string() : void
+ {
+ $entry = new XMLEntry('name', '123');
+
+ $this->assertSame('name', $entry->name());
+ $this->assertSame("\n123\n", $entry->__toString());
+ }
+
+ public function test_creating_xml_entry_with_empty_dom_document() : void
+ {
+ $doc = new \DOMDocument();
+ $entry = new XMLEntry('name', $doc);
+
+ $this->assertSame('name', $entry->name());
+ $this->assertSame($doc, $entry->value());
+ $this->assertSame("\n", $entry->__toString());
+ }
+
+ /**
+ * @dataProvider is_equal_data_provider
+ */
+ public function test_is_equal(bool $equals, XMLEntry $entry, XMLEntry $nextEntry) : void
+ {
+ $this->assertSame($equals, $entry->isEqual($nextEntry));
+ }
+}
diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php
index 37e593524..29942f58c 100644
--- a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php
+++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php
@@ -268,4 +268,30 @@ public function test_string_with_schema() : void
(new NativeEntryFactory(new Schema(Schema\Definition::string('e'))))->create('e', 'string')
);
}
+
+ public function test_xml_from_dom_document() : void
+ {
+ $doc = new \DOMDocument();
+ $doc->loadXML($xml = '123');
+ $this->assertEquals(
+ Entry::xml('e', $xml),
+ (new NativeEntryFactory())->create('e', $doc)
+ );
+ }
+
+ public function test_xml_from_string() : void
+ {
+ $this->assertEquals(
+ Entry::xml('e', $xml = '123'),
+ (new NativeEntryFactory())->create('e', $xml)
+ );
+ }
+
+ public function test_xml_string_with_xml_definition_provided() : void
+ {
+ $this->assertEquals(
+ Entry::xml('e', $xml = '123'),
+ (new NativeEntryFactory(new Schema(Schema\Definition::xml('e'))))->create('e', $xml)
+ );
+ }
}
diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/CastTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/CastTest.php
index bec7d9164..a73a97fbb 100644
--- a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/CastTest.php
+++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/CastTest.php
@@ -6,6 +6,7 @@
use function Flow\ETL\DSL\cast;
use function Flow\ETL\DSL\ref;
+use Flow\ETL\Exception\InvalidArgumentException;
use Flow\ETL\Row;
use Flow\ETL\Row\Factory\NativeEntryFactory;
use PHPUnit\Framework\TestCase;
@@ -14,6 +15,9 @@ final class CastTest extends TestCase
{
public static function cast_provider() : array
{
+ $xml = new \DOMDocument();
+ $xml->loadXML($xmlString = 'bar');
+
return [
'int' => ['1', 'int', 1],
'integer' => ['1', 'integer', 1],
@@ -28,6 +32,8 @@ public static function cast_provider() : array
'null' => ['1', 'null', null],
'json' => [[1], 'json', '[1]'],
'json_pretty' => [[1], 'json_pretty', "[\n 1\n]"],
+ 'xml_to_array' => [$xml, 'array', ['root' => ['foo' => ['@attributes' => ['baz' => 'buz'], '@value' => 'bar']]]],
+ 'string_to_xml' => [$xmlString, 'xml', $xml],
];
}
@@ -45,4 +51,20 @@ public function test_cast(mixed $from, string $to, mixed $expected) : void
cast(ref('value'), $to)->eval(Row::create((new NativeEntryFactory())->create('value', $from)))
);
}
+
+ public function test_casting_integer_to_xml() : void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Cannot cast integer to XML');
+
+ ref('value')->cast('xml')->eval(Row::create((new NativeEntryFactory())->create('value', 1)));
+ }
+
+ public function test_casting_non_xml_string_to_xml() : void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid XML string given: foo');
+
+ ref('value')->cast('xml')->eval(Row::create((new NativeEntryFactory())->create('value', 'foo')));
+ }
}
diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/DOMNodeAttributeTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/DOMNodeAttributeTest.php
new file mode 100644
index 000000000..218f35c61
--- /dev/null
+++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/DOMNodeAttributeTest.php
@@ -0,0 +1,32 @@
+loadXML('bar');
+
+ $this->assertEquals(
+ 'buz',
+ ref('value')->domNodeAttribute('baz')->eval(Row::create((new NativeEntryFactory())->create('value', $xml->documentElement->firstChild)))
+ );
+ }
+
+ public function test_extracting_non_existing_attribute_from_dom_node_entry() : void
+ {
+ $xml = new \DOMDocument();
+ $xml->loadXML('bar');
+
+ $this->assertNull(
+ ref('value')->domNodeAttribute('bar')->eval(Row::create((new NativeEntryFactory())->create('value', $xml->documentElement->firstChild)))
+ );
+ }
+}
diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/DOMNodeValueTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/DOMNodeValueTest.php
new file mode 100644
index 000000000..f31a09ee1
--- /dev/null
+++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/DOMNodeValueTest.php
@@ -0,0 +1,33 @@
+loadXML('baz');
+
+ $this->assertEquals(
+ 'baz',
+ ref('value')->domNodeValue()->eval(Row::create((new NativeEntryFactory())->create('value', $xml->documentElement->firstChild)))
+ );
+ }
+
+ public function test_getting_simple_node_value() : void
+ {
+ $xml = new \DOMDocument();
+ $xml->loadXML('bar');
+
+ $this->assertEquals(
+ 'bar',
+ ref('value')->domNodeValue()->eval(Row::create((new NativeEntryFactory())->create('value', $xml->documentElement->firstChild)))
+ );
+ }
+}
diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/XPathTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/XPathTest.php
new file mode 100644
index 000000000..bea74775b
--- /dev/null
+++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Reference/Expression/XPathTest.php
@@ -0,0 +1,56 @@
+loadXML('bar');
+
+ $this->assertEquals(
+ $xml->documentElement->firstChild,
+ ref('value')->xpath('/root/foo')->eval(Row::create((new NativeEntryFactory())->create('value', $xml)))
+ );
+ }
+
+ public function test_xpath_when_there_are_more_than_one_elements_under_given_path() : void
+ {
+ $xml = new \DOMDocument();
+ $xml->loadXML('barbar');
+
+ $this->assertEquals(
+ [
+ $xml->documentElement->firstChild,
+ $xml->documentElement->lastChild,
+ ],
+ ref('value')->xpath('/root/foo')->eval(Row::create((new NativeEntryFactory())->create('value', $xml)))
+ );
+ }
+
+ public function test_xpath_with_invalid_path_syntax() : void
+ {
+ $xml = new \DOMDocument();
+ $xml->loadXML('bar');
+
+ $this->assertNull(
+ ref('value')->xpath('/root/foo/@')->eval(Row::create((new NativeEntryFactory())->create('value', $xml)))
+ );
+ }
+
+ public function test_xpath_with_non_existing_path() : void
+ {
+ $xml = new \DOMDocument();
+ $xml->loadXML('bar');
+
+ $this->assertNull(
+ ref('value')->xpath('/root/bar')->eval(Row::create((new NativeEntryFactory())->create('value', $xml)))
+ );
+ }
+}
diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Schema/Formatter/ASCIISchemaFormatterTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Schema/Formatter/ASCIISchemaFormatterTest.php
index a63de95b0..af4a14d70 100644
--- a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Schema/Formatter/ASCIISchemaFormatterTest.php
+++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Schema/Formatter/ASCIISchemaFormatterTest.php
@@ -18,7 +18,8 @@ public function test_format_schema() : void
Schema\Definition::union('number', [IntegerEntry::class, FloatEntry::class]),
Schema\Definition::string('name', nullable: true),
Schema\Definition::array('tags', nullable: false),
- Schema\Definition::boolean('active', false)
+ Schema\Definition::boolean('active', false),
+ Schema\Definition::xml('xml', false)
);
$this->assertSame(
@@ -28,6 +29,7 @@ public function test_format_schema() : void
|-- name: [Flow\ETL\Row\Entry\StringEntry, Flow\ETL\Row\Entry\NullEntry] (nullable = true)
|-- number: [Flow\ETL\Row\Entry\IntegerEntry, Flow\ETL\Row\Entry\FloatEntry] (nullable = false)
|-- tags: Flow\ETL\Row\Entry\ArrayEntry (nullable = false)
+|-- xml: Flow\ETL\Row\Entry\XMLEntry (nullable = false)
SCHEMA,
(new ASCIISchemaFormatter())->format($schema)
diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Transformer/EntryExpressionEvalTransformerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Transformer/EntryExpressionEvalTransformerTest.php
index bc1002c1a..b6f9223c5 100644
--- a/src/core/etl/tests/Flow/ETL/Tests/Unit/Transformer/EntryExpressionEvalTransformerTest.php
+++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Transformer/EntryExpressionEvalTransformerTest.php
@@ -81,4 +81,26 @@ public function test_plus_expression_on_non_existing_rows() : void
->toArray()
);
}
+
+ public function test_xml_xpath_expression_when_there_is_more_than_one_node_under_given_path() : void
+ {
+ $xml = 'barbaz';
+ $document = new \DOMDocument();
+ $document->loadXML($xml);
+ $xpath = new \DOMXPath($document);
+
+ $this->assertEquals(
+ Entry::list_of_objects('xpath', \DOMNode::class, [
+ $xpath->query('/root/foo')->item(0),
+ $xpath->query('/root/foo')->item(1),
+ ]),
+ (new EntryExpressionEvalTransformer('xpath', ref('xml')->xpath('/root/foo')))
+ ->transform(
+ new Rows(Row::create(Entry::xml('xml', $xml))),
+ new FlowContext(Config::default())
+ )
+ ->first()
+ ->get(ref('xpath'))
+ );
+ }
}
diff --git a/tools/phpstan/composer.lock b/tools/phpstan/composer.lock
index d19e27b3b..83a1eeedd 100644
--- a/tools/phpstan/composer.lock
+++ b/tools/phpstan/composer.lock
@@ -9,16 +9,16 @@
"packages-dev": [
{
"name": "phpstan/phpstan",
- "version": "1.10.32",
+ "version": "1.10.33",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44"
+ "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c47e47d3ab03137c0e121e77c4d2cb58672f6d44",
- "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1",
+ "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1",
"shasum": ""
},
"require": {
@@ -67,7 +67,7 @@
"type": "tidelift"
}
],
- "time": "2023-08-24T21:54:50+00:00"
+ "time": "2023-09-04T12:20:53+00:00"
}
],
"aliases": [],