From 8cb7f400fd86dba292bbc75127e33ea6f61bc37a Mon Sep 17 00:00:00 2001 From: diffhead <82263657+diffhead@users.noreply.github.com> Date: Sun, 16 Nov 2025 01:51:25 +0400 Subject: [PATCH] Version 1.0.0 --- .editorconfig | 22 + .gitignore | 3 + LICENSE | 19 + README.md | 230 ++++ composer.json | 36 + composer.lock | 1882 ++++++++++++++++++++++++++ phpunit.xml | 27 + src/Builder.php | 54 + src/Enricher.php | 20 + src/Exception/InvalidRequest.php | 17 + src/Exception/PayloadIsNotJson.php | 9 + src/Exception/RepositoryNotFound.php | 17 + src/Exception/TargetIsNull.php | 9 + src/Header.php | 10 + src/Interface/Enrichment.php | 12 + src/Interface/Entity.php | 10 + src/Interface/Parser.php | 12 + src/Interface/Repository.php | 16 + src/Interface/Serializer.php | 12 + src/Message.php | 85 ++ src/Object/Item.php | 23 + src/Object/ItemsBag.php | 29 + src/Object/Request.php | 23 + src/Object/Target.php | 23 + src/Service/Enrichment.php | 153 +++ src/Service/Parser.php | 57 + src/Service/Serializer.php | 40 + src/Storage/Repositories.php | 45 + src/Storage/Requests.php | 54 + src/Utility/Arr.php | 174 +++ tests/BuilderTest.php | 57 + tests/EnricherTest.php | 116 ++ tests/MessageTest.php | 127 ++ tests/Object/ItemTest.php | 24 + tests/Object/ItemsBagTest.php | 28 + tests/Object/RequestTest.php | 28 + tests/Object/TargetTest.php | 24 + tests/Service/EnrichmentTest.php | 403 ++++++ tests/Service/ParserTest.php | 66 + tests/Service/SerializerTest.php | 55 + tests/Storage/RepositoriesTest.php | 44 + tests/Storage/RequestsTest.php | 45 + tests/Utility/ArrTest.php | 322 +++++ 43 files changed, 4462 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 phpunit.xml create mode 100644 src/Builder.php create mode 100644 src/Enricher.php create mode 100644 src/Exception/InvalidRequest.php create mode 100644 src/Exception/PayloadIsNotJson.php create mode 100644 src/Exception/RepositoryNotFound.php create mode 100644 src/Exception/TargetIsNull.php create mode 100644 src/Header.php create mode 100644 src/Interface/Enrichment.php create mode 100644 src/Interface/Entity.php create mode 100644 src/Interface/Parser.php create mode 100644 src/Interface/Repository.php create mode 100644 src/Interface/Serializer.php create mode 100644 src/Message.php create mode 100644 src/Object/Item.php create mode 100644 src/Object/ItemsBag.php create mode 100644 src/Object/Request.php create mode 100644 src/Object/Target.php create mode 100644 src/Service/Enrichment.php create mode 100644 src/Service/Parser.php create mode 100644 src/Service/Serializer.php create mode 100644 src/Storage/Repositories.php create mode 100644 src/Storage/Requests.php create mode 100644 src/Utility/Arr.php create mode 100644 tests/BuilderTest.php create mode 100644 tests/EnricherTest.php create mode 100644 tests/MessageTest.php create mode 100644 tests/Object/ItemTest.php create mode 100644 tests/Object/ItemsBagTest.php create mode 100644 tests/Object/RequestTest.php create mode 100644 tests/Object/TargetTest.php create mode 100644 tests/Service/EnrichmentTest.php create mode 100644 tests/Service/ParserTest.php create mode 100644 tests/Service/SerializerTest.php create mode 100644 tests/Storage/RepositoriesTest.php create mode 100644 tests/Storage/RequestsTest.php create mode 100644 tests/Utility/ArrTest.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e994499 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[*.php] +indent_style = space +indent_size = 4 + +[*.yml] +indent_size = 2 + +[*.json] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7dd5252 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor/ + +.phpunit.cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c1acc01 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2025 Viktor S. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the “Software”), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd52ed6 --- /dev/null +++ b/README.md @@ -0,0 +1,230 @@ +# Data Enrichment Kit + +A collection of components for building microservice-based applications. +This library provides tools for enriching data during communication +between system components. + +# Installation + +```bash +# Install the package +composer require diffhead/php-data-enrichment-kit + +# Run library tests +composer test +``` + +# Usage + +To use this library, you first need to create repositories +that implement the `Repository` interface: + +```php +namespace Diffhead\PHP\DataEnrichmentKit\Interface; + +interface Repository +{ + /** + * @param string $field + * @param array $values + * + * @return iterable + */ + public function getByFieldValues(string $field, array $values): iterable; +} +``` + +Each entity must implement the `Entity` interface: + +```php +namespace Diffhead\PHP\DataEnrichmentKit\Interface; + +interface Entity extends \ArrayAccess, \JsonSerializable {} +``` + +### Creating Enrichment Requests and Attaching to HTTP Messages + +```php +use Diffhead\PHP\DataEnrichmentKit\Builder; +use Diffhead\PHP\DataEnrichmentKit\Message; +use Diffhead\PHP\DataEnrichmentKit\Header; +use Diffhead\PHP\DataEnrichmentKit\Service\Serializer; +use Diffhead\PHP\DataEnrichmentKit\Service\Parser; +use Diffhead\PHP\DataEnrichmentKit\Storage\Requests; + +$builder = Builder::withTarget('user', 'id'); +$builder + ->item('data.posts.*.creator_id', 'creator') + ->item('data.posts.*.moderator_id', 'moderator'); + +$requests = new Requests([ + $builder->build() +]); + +$message = new Message(new Serializer(), new Parser()); + +/** + * @var \Psr\Http\Message\MessageInterface $psrMessage + * + * Message contains the following body data: + * + * {"data":{"posts":[{"id":1,"title":"String","creator_id":1,"moderator_id":3}]}} + * + * This call will attach enrichment requests to a message + */ +$message->setRequests($psrMessage, Header::XEnrichmentRequest, $requests); +``` + +### Receiving Data and Performing Enrichment + +```php +use Diffhead\PHP\DataEnrichmentKit\Enricher; +use Diffhead\PHP\DataEnrichmentKit\Message; +use Diffhead\PHP\DataEnrichmentKit\Header; +use Diffhead\PHP\DataEnrichmentKit\Service\Enrichment; +use Diffhead\PHP\DataEnrichmentKit\Service\Serializer; +use Diffhead\PHP\DataEnrichmentKit\Service\Parser; +use Diffhead\PHP\DataEnrichmentKit\Storage\Repositories; +use Diffhead\PHP\DataEnrichmentKit\Interface\Repository; + +/** + * Repository implementation example + */ +$repository = new class() implements Repository +{ + private array $items = [ + ['id' => 1, 'name' => 'Antony'], + ['id' => 3, 'name' => 'Martin'] + ]; + + public function getByFieldValues(string $field, array $values): iterable + { + return array_filter($this->items, fn(array $item) => in_array($item[$field], $values)); + } +}; + +$repositories = new Repositories([ + 'user' => $repository +]); + +/** + * Initialize services + */ +$enricher = new Enricher(new Enrichment($repositories)); +$message = new Message(new Serializer(), new Parser()); + +/** + * Getting enrichment requests from psr message + */ +$requests = $message->getRequests($psrMessage, Header::XEnrichmentRequest); + +/** + * Getting array payload from message + */ +$payload = $message->getPayload($psrMessage); + +/** + * Enrich the payload using the registered repositories + * Expected enriched structure: + * + * [ + * "data" => [ + * "posts" => [ + * [ + * "id" => 1, + * "title" => "String", + * "creator_id" => 1, + * "moderator_id" => 3, + * "creator" => ["id" => 1, "name" => "Antony"], + * "moderator" => ["id" => 3, "name" => "Martin"] + * ] + * ] + * ] + * ] + */ +$enriched = $enricher->enrich($payload, $requests); + +/** + * Set new payload to psr message + */ +$psrMessage = $message->setPayload($psrMessage, $enriched); +``` + +# Anatomy + +This library consists of the following **high-level service components**: + +* **Enricher** – a self-explanatory service that enriches data. +* **Message** – a PSR-compatible component for interacting with +HTTP requests and responses using `MessageInterface`. +* **Builder** – helps build enrichment request objects. + +**Lower-level components** for building custom enrichment algorithms: + +* **Enrichment** – implements the `Enrichment` interface and contains +the enrichment logic. +* **Parser** – parses raw and returns `Request` objects. +* **Serializer** – serializes `Request` objects to strings. + +**Value objects:** + +* **Item** – a unit describing an enrichment reference and alias. +* **ItemsBag** – stores multiple `Item` instances. +* **Request** – represents a single enrichment request. +* **Target** – specifies the entity to enrich. + +**Storage objects:** + +* **Requests** – contains enrichment requests and implements `IteratorAggregate`. +* **Repositories** – stores a map of entity repositories. + +# Customize + +You can extend the library by implementing your own: + +### Data enrichment logic + +```php +namespace Diffhead\PHP\DataEnrichmentKit\Interface; + +use Diffhead\PHP\DataEnrichmentKit\Storage\Requests; + +interface Enrichment +{ + public function enrich(array $data, Requests $requests): array; +} +``` + +### Request parsing + +```php +namespace Diffhead\PHP\DataEnrichmentKit\Interface; + +use Diffhead\PHP\DataEnrichmentKit\Object\Request; + +interface Parser +{ + public function parse(string $value): Request; +} +``` + +### Request serialization + +```php +namespace Diffhead\PHP\DataEnrichmentKit\Interface; + +use Diffhead\PHP\DataEnrichmentKit\Object\Request; + +interface Serializer +{ + public function toString(Request $request): string; +} +``` + +# Notes + +* Designed primarily for HTTP, but low-level components allow you +to quickly implement high-level integrations for any data source. + +* Supports PSR-compliant messages and can be integrated into +frameworks like Laravel with custom adapters. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5fbdfe2 --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "name": "diffhead/php-data-enrichment-kit", + "description": "Data enrichment library. A suitable component for microservice architectures.", + "type": "library", + "license": "MIT", + "version": "1.0.0", + "keywords": [ + "php", "data enrichment", "data transformation", "psr", + "pipeline", "message", "processing", "serializer", "parser", + "microservice", "php 8" + ], + "autoload": { + "psr-4": { + "Diffhead\\PHP\\DataEnrichmentKit\\": "src/", + "Diffhead\\PHP\\DataEnrichmentKit\\Tests\\": "tests/" + } + }, + "require": { + "php": "^8.1", + "psr/http-message": "^1.0", + "nyholm/psr7": "^1.8.2" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "scripts": { + "test": "phpunit" + }, + "minimum-stability": "stable", + "authors": [ + { + "name": "Viktor S.", + "email": "thinlineseverywhere@gmail.com" + } + ] +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..766ef84 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1882 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "dfd8faef8d4f9e2504e2ebe7dbbb75f2", + "packages": [ + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + } + ], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + }, + "time": "2025-10-21T19:32:17+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.6.1", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.3.7" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.4.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-09-24T13:44:41+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.4.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9253ec75a672e39fcc9d85bdb61448215b8162c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9253ec75a672e39fcc9d85bdb61448215b8162c7", + "reference": "9253ec75a672e39fcc9d85bdb61448215b8162c7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.4.0", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.3", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.4-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.4" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-11-21T07:39:11+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2025-09-14T09:36:45+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dc904b4bb3ab070865fa4068cd84f3da8b945148", + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-20T11:27:00+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-08-12T14:11:56+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:16:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-29T11:29:25+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:57:12+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..82cf31e --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,27 @@ + + + + + tests + + + + + + src + + + diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 0000000..01f2af2 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,54 @@ +target($entity, $field); + + return $builder; + } + + public function item(string $key, string $alias = ''): static + { + $this->items[] = new Item($key, $alias); + + return $this; + } + + public function target(string $entity, string $field): static + { + $this->target = new Target($entity, $field); + + return $this; + } + + public function build(): Request + { + if (is_null($this->target)) { + throw new TargetIsNull(); + } + + $items = new ItemsBag(); + + foreach ($this->items as $item) { + $items->push($item); + } + + return new Request($items, $this->target); + } +} diff --git a/src/Enricher.php b/src/Enricher.php new file mode 100644 index 0000000..bcde31b --- /dev/null +++ b/src/Enricher.php @@ -0,0 +1,20 @@ +enrichment->enrich($data, $requests); + } +} diff --git a/src/Exception/InvalidRequest.php b/src/Exception/InvalidRequest.php new file mode 100644 index 0000000..01bbf13 --- /dev/null +++ b/src/Exception/InvalidRequest.php @@ -0,0 +1,17 @@ + + */ + public function getByFieldValues(string $field, array $values): iterable; +} diff --git a/src/Interface/Serializer.php b/src/Interface/Serializer.php new file mode 100644 index 0000000..1785cf8 --- /dev/null +++ b/src/Interface/Serializer.php @@ -0,0 +1,12 @@ +count() === 0) { + return $message; + } + + $header = $header->value; + + foreach ($requests as $request) { + $request = $this->serializer->toString($request); + $message = $message->withAddedHeader($header, $request); + } + + return $message; + } + + /** + * @param \Psr\Http\Message\MessageInterface $message + * @param \BackedEnum $header + * + * @return \Diffhead\PHP\DataEnrichmentKit\Storage\Requests + */ + public function getRequests(MessageInterface $message, BackedEnum $header): Requests + { + $header = $header->value; + $requests = $message->getHeader($header); + + $container = new Requests(); + + foreach ($requests as $request) { + $container->append($this->parser->parse($request)); + } + + return $container; + } + + public function getPayload(MessageInterface $message): array + { + $payload = json_decode($message->getBody()->getContents(), true); + + if (! is_array($payload)) { + throw new PayloadIsNotJson(); + } + + return $payload; + } + + public function setPayload(MessageInterface $message, array $payload): MessageInterface + { + return $message->withBody( + Stream::create(json_encode($payload)) + ); + } +} diff --git a/src/Object/Item.php b/src/Object/Item.php new file mode 100644 index 0000000..ac1ee79 --- /dev/null +++ b/src/Object/Item.php @@ -0,0 +1,23 @@ +key; + } + + public function alias(): string + { + return $this->alias; + } +} diff --git a/src/Object/ItemsBag.php b/src/Object/ItemsBag.php new file mode 100644 index 0000000..12cfba2 --- /dev/null +++ b/src/Object/ItemsBag.php @@ -0,0 +1,29 @@ +items[] = $item; + + return $this; + } + + /** + * @return \Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } +} diff --git a/src/Object/Request.php b/src/Object/Request.php new file mode 100644 index 0000000..b73f089 --- /dev/null +++ b/src/Object/Request.php @@ -0,0 +1,23 @@ +items; + } + + public function target(): Target + { + return $this->target; + } +} diff --git a/src/Object/Target.php b/src/Object/Target.php new file mode 100644 index 0000000..7088991 --- /dev/null +++ b/src/Object/Target.php @@ -0,0 +1,23 @@ +entity; + } + + public function field(): string + { + return $this->field; + } +} diff --git a/src/Service/Enrichment.php b/src/Service/Enrichment.php new file mode 100644 index 0000000..e89280b --- /dev/null +++ b/src/Service/Enrichment.php @@ -0,0 +1,153 @@ +enrichSingle($data, $request); + } + + return $data; + } + + private function enrichSingle(array $data, Request $request): array + { + $targetFieldValues = []; + $target = $request->target(); + + $items = $request->items(); + + $targetFieldValues = $this->getFieldValuesByItems($data, $items); + + $targets = $this->getTargetsByFieldValue( + $target->entity(), + $target->field(), + $targetFieldValues + ); + + return $this->enrichUsingTargets($data, $items, $targets); + } + + private function getFieldValuesByItems(array $data, ItemsBag $items): array + { + $targetFieldValues = []; + + /** + * @var \Diffhead\PHP\DataEnrichmentKit\Object\Item $item + */ + foreach ($items as $item) { + $valueOrValues = Arr::get($item->key(), $data); + + if (is_array($valueOrValues)) { + $targetFieldValues = array_merge($targetFieldValues, $valueOrValues); + } else { + $targetFieldValues[] = $valueOrValues; + } + } + + return array_unique($targetFieldValues); + } + + private function getTargetsByFieldValue(string $entity, string $field, array $values): array + { + $targets = $this->repositories->get($entity) + ->getByFieldValues($field, $values); + + $targetsHaveArrayAccess = is_array($targets) || $targets instanceof ArrayAccess; + $targetsByFieldValue = []; + + foreach ($targets as $index => $target) { + $fieldValue = $target[$field]; + $targetsByFieldValue[$fieldValue] = $target; + + if ($targetsHaveArrayAccess) { + unset($targets[$index]); + } + } + + return $targetsByFieldValue; + } + + private function enrichUsingTargets(array $data, ItemsBag $items, array $targets): array + { + foreach ($items as $item) { + $data = $this->enrichByItem($data, $item, $targets); + } + + return $data; + } + + private function enrichByItem(array $data, Item $item, array &$targets): array + { + $keyFirstItemIsWildcard = strpos($item->key(), '*.') === 0; + + if (array_is_list($data) && $keyFirstItemIsWildcard) { + /** + * If passed *.user_id key as example then + * we need to remove first wildcard part + */ + $parts = explode('.', $item->key()); + $nextKey = implode('.', array_slice($parts, 1)); + + $item = new Item($nextKey, $item->alias()); + + foreach ($data as $index => $entry) { + $data[$index] = $this->enrichByItem($entry, $item, $targets); + } + + return $data; + } + + $keyIsWildcard = is_numeric(strpos($item->key(), '*.')); + + if ($keyIsWildcard) { + $parts = explode('.', $item->key()); + $first = $parts[0]; + + $temporary = Arr::get($first, $data, []); + + $nextKey = implode('.', array_slice($parts, 1)); + $nextItem = new Item($nextKey, $item->alias()); + + $data[$first] = $this->enrichByItem($temporary, $nextItem, $targets); + } else { + $value = Arr::get($item->key(), $data); + $parts = explode('.', $item->key()); + + $alias = $item->alias() + ? $item->alias() + : $parts[count($parts) - 1]; + + $parts[count($parts) - 1] = $alias; + $nextKey = implode('.', $parts); + + $data = Arr::set($nextKey, $data, $targets[$value] ?? null); + } + + return $data; + } +} diff --git a/src/Service/Parser.php b/src/Service/Parser.php new file mode 100644 index 0000000..bcc784e --- /dev/null +++ b/src/Service/Parser.php @@ -0,0 +1,57 @@ +push( + new Item($referenceWithAlias[0], $referenceWithAlias[1] ?? '') + ); + } + + return new Request($items, $target); + } +} diff --git a/src/Service/Serializer.php b/src/Service/Serializer.php new file mode 100644 index 0000000..9f4eb4f --- /dev/null +++ b/src/Service/Serializer.php @@ -0,0 +1,40 @@ +target()->entity(), + $request->target()->field(), + ]; + + $target = implode(',', $targetParts); + + $itemsParts = []; + + /** + * @var \Diffhead\PHP\DataEnrichmentKit\Object\Item $item + */ + foreach ($request->items() as $item) { + $itemParts = [$item->key()]; + + if ($alias = $item->alias()) { + $itemParts[] = $alias; + } + + $itemsParts[] = implode('+', $itemParts); + } + + $items = implode(',', $itemsParts); + + return sprintf('%s@%s', $items, $target); + } +} diff --git a/src/Storage/Repositories.php b/src/Storage/Repositories.php new file mode 100644 index 0000000..a57ae75 --- /dev/null +++ b/src/Storage/Repositories.php @@ -0,0 +1,45 @@ + $map + * + * @throws \InvalidArgumentException + */ + public function __construct( + private array $map = [] + ) { + foreach ($this->map as $target => $repository) { + if (! $repository instanceof Repository) { + throw new InvalidArgumentException( + 'Map should contains only Repository instances as values' + ); + } + } + } + + public function set(string $target, Repository $repository): static + { + $this->map[$target] = $repository; + + return $this; + } + + public function get(string $target): Repository + { + if (! isset($this->map[$target])) { + throw new RepositoryNotFound($target); + } + + return $this->map[$target]; + } +} diff --git a/src/Storage/Requests.php b/src/Storage/Requests.php new file mode 100644 index 0000000..137f088 --- /dev/null +++ b/src/Storage/Requests.php @@ -0,0 +1,54 @@ + $requests + * + * @throws \InvalidArgumentException + */ + public function __construct( + private array $requests = [] + ) { + foreach ($this->requests as $request) { + if (! $request instanceof Request) { + throw new InvalidArgumentException( + 'Requests should be an array of Request instances' + ); + } + + $this->count++; + } + } + + public function append(Request $request): void + { + $this->count++; + $this->requests[] = $request; + } + + public function count(): int + { + return $this->count; + } + + /** + * @return \Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->requests); + } +} diff --git a/src/Utility/Arr.php b/src/Utility/Arr.php new file mode 100644 index 0000000..a7f7521 --- /dev/null +++ b/src/Utility/Arr.php @@ -0,0 +1,174 @@ + + */ + public static function get(string $key, array $data, mixed $default = null): mixed + { + $parts = explode('.', $key); + + $temporary = $data; + + foreach ($parts as $index => $part) { + if ($part === '*') { + $values = []; + + foreach ($temporary as $item) { + if (! is_array($item)) { + continue; + } + + $key = implode('.', array_slice($parts, $index + 1)); + $extracted = self::get((string) $key, $item, $default); + + if (is_array($extracted)) { + $values = array_merge($values, $extracted); + } else if ($extracted === $default) { + continue; + } else { + $values[] = $extracted; + } + } + + return $values; + } + + $isNotArray = ! is_array($temporary); + $isNotExisting = $isNotArray || ! array_key_exists($part, $temporary); + + if ($isNotExisting) { + return $default; + } + + $temporary = $temporary[$part]; + } + + return $temporary; + } + + /** + * Test array having value inside using key dot notation is supported. + * + * If passed strict as true then will return true if any of the wildcard + * paths exist else will return true only if all wildcard paths exist. + * + * @param string $key + * @param array $data + * @param bool $strict + * + * @return bool + */ + public static function has(string $key, array $data, bool $strict = false): bool + { + $parts = explode('.', $key); + $any = ! $strict; + + $temporary = $data; + + foreach ($parts as $index => $part) { + if ($part === '*') { + if (! is_array($temporary)) { + return false; + } + + $test = null; + + foreach ($temporary as $item) { + $key = implode('.', array_slice($parts, $index + 1)); + $has = self::has($key, $item, $strict); + + if ($strict) { + $test = $has && (is_null($test) ? true : $test); + } + + if ($any && $has) { + return true; + } + } + + return is_null($test) ? false : $test; + } + + $isNotArray = ! is_array($temporary); + $isNotExisting = $isNotArray || ! array_key_exists($part, $temporary); + + if ($isNotExisting) { + return false; + } + + $temporary = $temporary[$part]; + } + + return true; + } + + /** + * Set value inside array using key dot notation support + * Also supported wildcard keys as "users.*.name" and alike + * + * @param string $key Includes dot notation support + * @param array $data + * @param mixed $value + * + * @return array + */ + public static function set(string $key, array $data, mixed $value): array + { + $parts = explode('.', $key); + $last = array_pop($parts); + + $temporary = &$data; + + foreach ($parts as $index => $part) { + if ($part === '*') { + $isNotArray = ! is_array($temporary); + $isEmptyArray = is_array($temporary) && ! count($temporary); + + if ($isNotArray || $isEmptyArray) { + $temporary[] = []; + } + + foreach ($temporary as $i => $item) { + $actuallyParts = array_slice($parts, $index + 1); + $actuallyParts[] = $last; + + $key = implode('.', $actuallyParts); + + $temporary[$i] = self::set($key, $item, $value); + } + + return $data; + } + + $isNotExisting = ! array_key_exists($part, $temporary); + $isNotArray = $isNotExisting || ! is_array($temporary[$part]); + + if ($isNotExisting && $isNotArray) { + $temporary[$part] = []; + } + + $temporary = &$temporary[$part]; + } + + $temporary[$last] = $value; + + return $data; + } +} diff --git a/tests/BuilderTest.php b/tests/BuilderTest.php new file mode 100644 index 0000000..77e217d --- /dev/null +++ b/tests/BuilderTest.php @@ -0,0 +1,57 @@ +assertInstanceOf(Builder::class, $builder); + } + + public function testItemsInsideTheRequest(): void + { + $builder = new Builder(); + $builder->item('key', 'alias'); + + $request = $builder->target('entity', 'field')->build(); + + $this->assertCount(1, $request->items()->getIterator()); + $this->assertEquals('key', $request->items()->getIterator()[0]->key()); + $this->assertEquals('alias', $request->items()->getIterator()[0]->alias()); + } + + public function testTargetInsideTheRequest(): void + { + $builder = new Builder(); + $builder->target('entity', 'field'); + + $request = $builder->build(); + + $this->assertEquals('entity', $request->target()->entity()); + $this->assertEquals('field', $request->target()->field()); + } + + public function testBuildThrowsExceptionWhenTargetIsNull(): void + { + $this->expectException(TargetIsNull::class); + + $builder = new Builder(); + $builder->build(); + } +} diff --git a/tests/EnricherTest.php b/tests/EnricherTest.php new file mode 100644 index 0000000..457e49f --- /dev/null +++ b/tests/EnricherTest.php @@ -0,0 +1,116 @@ + 1, + 'content' => 'Post by user 1' + ], + [ + 'user_id' => 2, + 'content' => 'Post by user 2' + ], + [ + 'user_id' => 3, + 'content' => 'Post by user 3' + ], + ]; + + $enrichment = new Enrichment($this->getRepositories()); + $enricher = new Enricher($enrichment); + + $items = new ItemsBag(); + $items->push(new Item('*.user_id', 'user')); + + $target = new Target('user', 'id'); + + $requests = new Requests([ + new Request($items, $target) + ]); + + $enriched = $enricher->enrich($posts, $requests); + + $this->assertSame( + [ + 'user_id' => 1, + 'content' => 'Post by user 1', + 'user' => [ + 'id' => 1, + 'name' => 'Blank.1', + ], + ], + $enriched[0] + ); + + $this->assertSame( + [ + 'user_id' => 2, + 'content' => 'Post by user 2', + 'user' => [ + 'id' => 2, + 'name' => 'Blank.2', + ], + ], + $enriched[1] + ); + + $this->assertSame( + [ + 'user_id' => 3, + 'content' => 'Post by user 3', + 'user' => [ + 'id' => 3, + 'name' => 'Blank.3', + ], + ], + $enriched[2] + ); + } + + private function getRepositories(): Repositories + { + return new Repositories([ + 'user' => new EnricherTestUserRepository(), + ]); + } +} + +class EnricherTestUserRepository implements Repository +{ + public function getByFieldValues(string $field, array $values): iterable + { + return [ + $this->getBlankWithId(1), + $this->getBlankWithId(2), + $this->getBlankWithId(3), + ]; + } + + private function getBlankWithId(int $id): array + { + return [ + 'id' => $id, + 'name' => sprintf('Blank.%d', $id) + ]; + } +} diff --git a/tests/MessageTest.php b/tests/MessageTest.php new file mode 100644 index 0000000..d961a79 --- /dev/null +++ b/tests/MessageTest.php @@ -0,0 +1,127 @@ +createResponse(); + + $itemsBag = new ItemsBag(); + $itemsBag->push(new Item('key1', 'alias1')); + + $target = new Target('entity', 'field'); + + $requests = new Requests([ + new Request($itemsBag, $target) + ]); + + $header = Header::XEnrichmentRequest; + $result = $message->setRequests($psrMessage, $header, $requests); + + $this->assertTrue($result->hasHeader($header->value)); + $this->assertNotEmpty($result->getHeader($header->value)); + } + + public function testGetRequests(): void + { + $serializer = new Serializer(); + $parser = new Parser(); + $message = new Message($serializer, $parser); + + $header = Header::XEnrichmentRequest; + + $psrFactory = new Psr17Factory(); + $psrMessage = $psrFactory->createResponse() + ->withHeader($header->value, 'key1+alias1@entity,field'); + + $requests = $message->getRequests($psrMessage, $header); + + $this->assertInstanceOf(Requests::class, $requests); + $this->assertCount(1, $requests); + } + + public function testGetPayload(): void + { + $serializer = new Serializer(); + $parser = new Parser(); + $message = new Message($serializer, $parser); + + $psrFactory = new Psr17Factory(); + $psrMessage = $psrFactory->createResponse() + ->withBody(Stream::create('{"key":"value"}')); + + $payload = $message->getPayload($psrMessage); + + $this->assertIsArray($payload); + $this->assertSame(['key' => 'value'], $payload); + } + + public function testGetPayloadThrowsExceptionOnNonJson(): void + { + $this->expectException(\JsonException::class); + + $serializer = new Serializer(); + $parser = new Parser(); + $message = new Message($serializer, $parser); + + $psrFactory = new Psr17Factory(); + $psrMessage = $psrFactory->createResponse() + ->withBody(Stream::create('Invalid JSON')); + + $this->expectException(PayloadIsNotJson::class); + + $message->getPayload($psrMessage); + } + + public function testSetPayload(): void + { + $serializer = new Serializer(); + $parser = new Parser(); + $message = new Message($serializer, $parser); + + $psrFactory = new Psr17Factory(); + $psrMessage = $psrFactory->createResponse(); + + $payload = ['key' => 'value']; + $payloadString = json_encode($payload, JSON_THROW_ON_ERROR); + + $result = $message->setPayload($psrMessage, $payload); + + $this->assertEquals($payloadString, (string) $result->getBody()); + + $payloadNew = ['newKey' => 'newValue']; + $payloadNewString = json_encode($payloadNew, JSON_THROW_ON_ERROR); + + $resultNew = $message->setPayload($result, $payloadNew); + + $this->assertEquals($payloadNewString, (string) $resultNew->getBody()); + } +} diff --git a/tests/Object/ItemTest.php b/tests/Object/ItemTest.php new file mode 100644 index 0000000..6e45f96 --- /dev/null +++ b/tests/Object/ItemTest.php @@ -0,0 +1,24 @@ +assertEquals('key', $item->key()); + $this->assertEquals('alias', $item->alias()); + } +} diff --git a/tests/Object/ItemsBagTest.php b/tests/Object/ItemsBagTest.php new file mode 100644 index 0000000..ff48d5a --- /dev/null +++ b/tests/Object/ItemsBagTest.php @@ -0,0 +1,28 @@ +push($item); + + $this->assertCount(1, iterator_to_array($itemsBag->getIterator())); + $this->assertSame($item, iterator_to_array($itemsBag->getIterator())[0]); + } +} diff --git a/tests/Object/RequestTest.php b/tests/Object/RequestTest.php new file mode 100644 index 0000000..58c03bc --- /dev/null +++ b/tests/Object/RequestTest.php @@ -0,0 +1,28 @@ +assertSame($itemsBag, $request->items()); + $this->assertSame($target, $request->target()); + } +} diff --git a/tests/Object/TargetTest.php b/tests/Object/TargetTest.php new file mode 100644 index 0000000..4d4b87b --- /dev/null +++ b/tests/Object/TargetTest.php @@ -0,0 +1,24 @@ +assertEquals('entity', $target->entity()); + $this->assertEquals('field', $target->field()); + } +} diff --git a/tests/Service/EnrichmentTest.php b/tests/Service/EnrichmentTest.php new file mode 100644 index 0000000..bcdb57c --- /dev/null +++ b/tests/Service/EnrichmentTest.php @@ -0,0 +1,403 @@ +assertInstanceOf( + EnrichmentInterface::class, + new Enrichment($this->getRepositories()) + ); + } + + public function testEnrichmentJsonObjectWithArraysInside(): void + { + $payload = [ + "data" => [ + [ + 'user_id' => 1, + 'content' => 'Post by user 1' + ], + [ + 'user_id' => 2, + 'content' => 'Post by user 2' + ], + ] + ]; + + $items = new ItemsBag(); + $items->push(new Item('data.*.user_id', 'user')); + + $target = new Target('user', 'id'); + + $requests = new Requests(); + $requests->append(new Request($items, $target)); + + $enrichment = new Enrichment($this->getRepositories()); + $enriched = $enrichment->enrich($payload, $requests); + + $this->assertSame( + [ + 'user_id' => 1, + 'content' => 'Post by user 1', + 'user' => [ + 'id' => 1, + 'name' => 'Blank.1', + ], + ], + $enriched['data'][0] + ); + + $this->assertSame( + [ + 'user_id' => 2, + 'content' => 'Post by user 2', + 'user' => [ + 'id' => 2, + 'name' => 'Blank.2', + ], + ], + $enriched['data'][1] + ); + } + + public function testEnrichmentJsonObject(): void + { + $payload = [ + 'user_id' => 1, + 'content' => 'Post by user 1' + ]; + + $items = new ItemsBag(); + $items->push(new Item('user_id', 'user')); + + $target = new Target('user', 'id'); + + $requests = new Requests(); + $requests->append(new Request($items, $target)); + + $enrichment = new Enrichment($this->getRepositories()); + $enriched = $enrichment->enrich($payload, $requests); + + $this->assertSame( + [ + 'user_id' => 1, + 'content' => 'Post by user 1', + 'user' => [ + 'id' => 1, + 'name' => 'Blank.1', + ], + ], + $enriched + ); + } + + public function testEnrichArrayItemsUsingProperlyRequest(): void + { + $posts = [ + [ + 'user_id' => 1, + 'content' => 'Post by user 1' + ], + [ + 'user_id' => 2, + 'content' => 'Post by user 2' + ], + [ + 'user_id' => 3, + 'content' => 'Post by user 3' + ], + ]; + + $items = new ItemsBag(); + $items->push(new Item('*.user_id', 'user')); + + $target = new Target('user', 'id'); + + $requests = new Requests(); + $requests->append(new Request($items, $target)); + + $enrichment = new Enrichment($this->getRepositories()); + $enriched = $enrichment->enrich($posts, $requests); + + $this->assertSame( + [ + 'user_id' => 1, + 'content' => 'Post by user 1', + 'user' => [ + 'id' => 1, + 'name' => 'Blank.1', + ], + ], + $enriched[0] + ); + + $this->assertSame( + [ + 'user_id' => 2, + 'content' => 'Post by user 2', + 'user' => [ + 'id' => 2, + 'name' => 'Blank.2', + ], + ], + $enriched[1] + ); + + $this->assertSame( + [ + 'user_id' => 3, + 'content' => 'Post by user 3', + 'user' => [ + 'id' => 3, + 'name' => 'Blank.3', + ], + ], + $enriched[2] + ); + } + + public function tesEnrichArrayItemsWithoutExistingTargets(): void + { + $posts = [ + [ + 'user_id' => 10, + 'content' => 'Post by user 10' + ], + [ + 'user_id' => 20, + 'content' => 'Post by user 20' + ], + ]; + + $items = new ItemsBag(); + $items->push(new Item('*.user_id', 'user')); + + $target = new Target('user', 'id'); + + $requests = new Requests(); + $requests->append(new Request($items, $target)); + + $enrichment = new Enrichment($this->getRepositories()); + $enriched = $enrichment->enrich($posts, $requests); + + $this->assertSame( + [ + 'user_id' => 10, + 'content' => 'Post by user 10', + 'user' => null, + ], + $enriched[0] + ); + + $this->assertSame( + [ + 'user_id' => 20, + 'content' => 'Post by user 20', + 'user' => null, + ], + $enriched[1] + ); + } + + public function testEnrichArrayUsingNotWildcardKey(): void + { + $posts = [ + [ + 'user_id' => 1, + 'content' => 'Post by user 1' + ], + ]; + + $items = new ItemsBag(); + $items->push( + new Item('data.invalid_path', 'user') + ); + + $target = new Target('user', 'id'); + + $requests = new Requests(); + $requests->append(new Request($items, $target)); + + $enrichment = new Enrichment($this->getRepositories()); + $enriched = $enrichment->enrich($posts, $requests); + + $this->assertSame( + [ + [ + 'user_id' => 1, + 'content' => 'Post by user 1', + ], + 'data' => [ + 'user' => null + ] + ], + $enriched + ); + } + + public function testEnrichNestedArrayItems(): void + { + $categoriesGroups = [ + [ + [ + 'category_id' => 1, + 'name' => 'Category 1', + 'creator_id' => 1 + ], + [ + 'category_id' => 2, + 'name' => 'Category 2', + 'creator_id' => 3 + ], + ] + ]; + + $items = new ItemsBag(); + $items->push(new Item('*.*.creator_id', 'creator')); + + $target = new Target('user', 'id'); + + $requests = new Requests(); + $requests->append(new Request($items, $target)); + + $enrichment = new Enrichment($this->getRepositories()); + $enriched = $enrichment->enrich($categoriesGroups, $requests); + + $this->assertSame( + [ + [ + [ + 'category_id' => 1, + 'name' => 'Category 1', + 'creator_id' => 1, + 'creator' => [ + 'id' => 1, + 'name' => 'Blank.1', + ], + ], + [ + 'category_id' => 2, + 'name' => 'Category 2', + 'creator_id' => 3, + 'creator' => [ + 'id' => 3, + 'name' => 'Blank.3', + ], + ], + ] + ], + $enriched + ); + } + + public function testProperlyWorkingWithRepositoriesReturnNonArrayAccessObject(): void + { + $posts = [ + [ + 'user_id' => 1, + 'content' => 'Post by user 1' + ], + ]; + + $items = new ItemsBag(); + $items->push(new Item('*.user_id', 'user')); + + $target = new Target('user', 'id'); + + $requests = new Requests(); + $requests->append(new Request($items, $target)); + + $enrichment = new Enrichment($this->getRepositories(true)); + $enriched = $enrichment->enrich($posts, $requests); + + $this->assertSame( + [ + 'user_id' => 1, + 'content' => 'Post by user 1', + 'user' => [ + 'id' => 1, + 'name' => 'Blank.1', + ], + ], + $enriched[0] + ); + } + + private function getRepositories(bool $repoUsingGenerator = false): Repositories + { + return new Repositories([ + 'user' => $repoUsingGenerator + ? new EnrichmentTestUserRepositoryUsingGenerator() + : new EnrichmentTestUserRepository() + ]); + } +} + +class EnrichmentTestUserRepository implements Repository +{ + public function getByFieldValues(string $field, array $values): iterable + { + return array_filter( + [ + $this->getBlankWithId(1), + $this->getBlankWithId(2), + $this->getBlankWithId(3), + ], + fn ($item) => in_array($item[$field], $values, true) + ); + } + + private function getBlankWithId(int $id): array + { + return [ + 'id' => $id, + 'name' => sprintf('Blank.%d', $id) + ]; + } +} + +class EnrichmentTestUserRepositoryUsingGenerator implements Repository +{ + public function getByFieldValues(string $field, array $values): iterable + { + $items = [ + $this->getBlankWithId(1), + $this->getBlankWithId(2), + $this->getBlankWithId(3), + ]; + + foreach ($items as $item) { + if (in_array($item[$field], $values, true)) { + yield $item; + } + } + } + + private function getBlankWithId(int $id): array + { + return [ + 'id' => $id, + 'name' => sprintf('Blank.%d', $id) + ]; + } +} diff --git a/tests/Service/ParserTest.php b/tests/Service/ParserTest.php new file mode 100644 index 0000000..6937b96 --- /dev/null +++ b/tests/Service/ParserTest.php @@ -0,0 +1,66 @@ +assertInstanceOf(ParserInterface::class, new Parser()); + } + + public function testParseValidInput(): void + { + $parser = new Parser(); + $input = 'key1+alias1,key2+alias2@entity,field'; + + $request = $parser->parse($input); + + $this->assertInstanceOf(Request::class, $request); + $this->assertEquals('entity', $request->target()->entity()); + $this->assertEquals('field', $request->target()->field()); + + $items = iterator_to_array($request->items()->getIterator()); + $this->assertCount(2, $items); + $this->assertEquals('key1', $items[0]->key()); + $this->assertEquals('alias1', $items[0]->alias()); + $this->assertEquals('key2', $items[1]->key()); + $this->assertEquals('alias2', $items[1]->alias()); + } + + public function testParseThrowsExceptionOnEmptyTarget(): void + { + $this->expectException(InvalidRequest::class); + + $parser = new Parser(); + $parser->parse('key1+alias1,key2+alias2@'); + } + + public function testParseThrowsExceptionOnInvalidTarget(): void + { + $this->expectException(InvalidRequest::class); + + $parser = new Parser(); + $parser->parse('key1+alias1,key2+alias2@entity'); + } + + public function testParseThrowsExceptionOnEmptyInput(): void + { + $this->expectException(InvalidRequest::class); + + $parser = new Parser(); + $parser->parse(''); + } +} diff --git a/tests/Service/SerializerTest.php b/tests/Service/SerializerTest.php new file mode 100644 index 0000000..f722818 --- /dev/null +++ b/tests/Service/SerializerTest.php @@ -0,0 +1,55 @@ +assertInstanceOf(SerializerInterface::class, new Serializer()); + } + + public function testToString(): void + { + $itemsBag = new ItemsBag(); + $itemsBag->push(new Item('key1', 'alias1')); + $itemsBag->push(new Item('key2', 'alias2')); + + $target = new Target('entity', 'field'); + $request = new Request($itemsBag, $target); + + $serializer = new Serializer(); + $result = $serializer->toString($request); + + $this->assertEquals('key1+alias1,key2+alias2@entity,field', $result); + } + + public function testToStringWithEmptyAlias(): void + { + $itemsBag = new ItemsBag(); + $itemsBag->push(new Item('key1')); + $itemsBag->push(new Item('key2', 'alias2')); + + $target = new Target('entity', 'field'); + $request = new Request($itemsBag, $target); + + $serializer = new Serializer(); + $result = $serializer->toString($request); + + $this->assertEquals('key1,key2+alias2@entity,field', $result); + } +} diff --git a/tests/Storage/RepositoriesTest.php b/tests/Storage/RepositoriesTest.php new file mode 100644 index 0000000..efd7ebe --- /dev/null +++ b/tests/Storage/RepositoriesTest.php @@ -0,0 +1,44 @@ + $field, 'value' => $values[0]], + ]; + } + }; + + $repositories->set('target', $mockRepository); + + $retrievedRepository = $repositories->get('target'); + $this->assertSame($mockRepository, $retrievedRepository); + } + + public function testGetThrowsExceptionWhenRepositoryNotFound(): void + { + $this->expectException(RepositoryNotFound::class); + + $repositories = new Repositories(); + $repositories->get('nonexistent_target'); + } +} diff --git a/tests/Storage/RequestsTest.php b/tests/Storage/RequestsTest.php new file mode 100644 index 0000000..153e972 --- /dev/null +++ b/tests/Storage/RequestsTest.php @@ -0,0 +1,45 @@ +assertEquals(0, $requests->count()); + + $request = new Request(new ItemsBag(), new Target('entity', 'field')); + $requests->append($request); + + $this->assertEquals(1, $requests->count()); + } + + public function testRequestsAreSame(): void + { + $request1 = new Request(new ItemsBag(), new Target('entity', 'field')); + $request2 = new Request(new ItemsBag(), new Target('entity2', 'field2')); + + $requests = new Requests([$request1, $request2]); + + $requestsArray = iterator_to_array($requests); + + $this->assertCount(2, $requestsArray); + $this->assertSame($request1, $requestsArray[0]); + $this->assertSame($request2, $requestsArray[1]); + } +} diff --git a/tests/Utility/ArrTest.php b/tests/Utility/ArrTest.php new file mode 100644 index 0000000..39e6523 --- /dev/null +++ b/tests/Utility/ArrTest.php @@ -0,0 +1,322 @@ + [ + 'id' => 1, + 'name' => 'John Doe', + 'address' => [ + 'city' => 'New York', + 'zip' => '10001', + ], + ], + 'posts' => [ + ['id' => 101, 'title' => 'First Post'], + ['id' => 102, 'title' => 'Second Post'], + ], + ]; + + $this->assertEquals(1, Arr::get('user.id', $array)); + $this->assertEquals('John Doe', Arr::get('user.name', $array)); + $this->assertEquals('New York', Arr::get('user.address.city', $array)); + $this->assertEquals('10001', Arr::get('user.address.zip', $array)); + + $this->assertEquals(101, Arr::get('posts.0.id', $array)); + $this->assertEquals('First Post', Arr::get('posts.0.title', $array)); + + $this->assertEquals(102, Arr::get('posts.1.id', $array)); + $this->assertEquals('Second Post', Arr::get('posts.1.title', $array)); + } + + public function testGetExistingItemsFromNestedArray(): void + { + $array = [ + [ + 'orders' => [ + ['id' => 201, 'amount' => 150], + ['id' => 202, 'amount' => 200], + ] + ], + [ + 'orders' => [ + ['id' => 203, 'amount' => 250], + ['id' => 204, 'amount' => 300], + ] + ] + ]; + + $this->assertEquals(201, Arr::get('0.orders.0.id', $array)); + $this->assertEquals(200, Arr::get('0.orders.1.amount', $array)); + $this->assertEquals(203, Arr::get('1.orders.0.id', $array)); + $this->assertEquals(300, Arr::get('1.orders.1.amount', $array)); + } + + public function testGetMultipleExistingItems(): void + { + $array = [ + 'users' => [ + ['name' => 'John Doe'], + ['name' => 'Jane Smith'], + ['name' => 'Alice Johnson'], + ], + 'posts' => [ + 'categories' => [ + [ + 'name' => 'Tech', + 'posts' => [ + ['title' => 'Latest Tech Trends'], + ['title' => 'AI Innovations'] + ] + ], + [ + 'name' => 'Health', + 'posts' => [ + ['title' => 'Wellness Tips'], + ['title' => 'Nutrition Advice'] + ] + ] + ] + ], + ]; + + $this->assertSame( + [ + 'John Doe', + 'Jane Smith', + 'Alice Johnson' + ], + Arr::get('users.*.name', $array) + ); + + $this->assertSame( + [ + 'Latest Tech Trends', + 'AI Innovations', + 'Wellness Tips', + 'Nutrition Advice' + ], + Arr::get('posts.categories.*.posts.*.title', $array) + ); + } + + public function testGetMultipleExistingItemsInsideNestedArray(): void + { + $array = [ + [ + 'orders' => [ + ['id' => 201, 'amount' => 150], + ['id' => 202, 'amount' => 200], + ] + ], + [ + 'orders' => [ + ['id' => 203, 'amount' => 250], + ['id' => 204, 'amount' => 300], + ] + ] + ]; + + $this->assertSame( + [ + 150, + 200, + 250, + 300 + ], + Arr::get('*.orders.*.amount', $array) + ); + } + + public function testGetNonExistingItems(): void + { + $array = [ + 'user' => [ + 'id' => 1, + 'name' => 'John Doe', + ], + ]; + + $this->assertNull(Arr::get('user.age', $array)); + $this->assertEquals('Unknown', Arr::get('user.age', $array, 'Unknown')); + $this->assertNull(Arr::get('user.address.city', $array)); + $this->assertEquals('N/A', Arr::get('user.address.city', $array, 'N/A')); + $this->assertNull(Arr::get('posts.0.id', $array)); + } + + public function testGetMultipleNonExistingItems(): void + { + $array = [ + 'users' => [ + ['name' => 'John Doe'], + ['name' => 'Jane Smith'], + ], + ]; + + $this->assertSame([], Arr::get('users.*.age', $array)); + $this->assertSame([], Arr::get('users.*.address.*.city', $array)); + } + + public function testSetValuesAtExistingPaths(): void + { + $origin = [ + 'user' => [ + 'id' => 1, + 'name' => 'John Doe', + ], + ]; + + $updated = Arr::set('user.name', $origin, 'Jane Smith'); + $updated = Arr::set('user.id', $updated, 2); + + $this->assertEquals('Jane Smith', $updated['user']['name']); + $this->assertEquals(2, $updated['user']['id']); + } + + public function testSetMultipleValuesAtExistingPaths(): void + { + $origin = [ + 'users' => [ + ['name' => 'John Doe'], + ['name' => 'Jane Smith'], + ], + ]; + + $updated = Arr::set('users.*.age', $origin, 29); + $updated = Arr::set('users.*.source', $updated, 'facebook'); + + $this->assertEquals(29, $updated['users'][0]['age']); + $this->assertEquals(29, $updated['users'][1]['age']); + + $this->assertEquals('facebook', $updated['users'][0]['source']); + $this->assertEquals('facebook', $updated['users'][1]['source']); + } + + public function testSetValuesAtNonExistingPaths(): void + { + $origin = [ + 'user' => [ + 'id' => 1, + 'name' => 'John Doe', + ], + ]; + + $updated = Arr::set('user.address.city', $origin, 'New York'); + $updated = Arr::set('user.address.zip', $updated, '10001'); + $updated = Arr::set('posts.0.id', $updated, 101); + $updated = Arr::set('posts.0.title', $updated, 'First Post'); + + $this->assertEquals('New York', $updated['user']['address']['city']); + $this->assertEquals('10001', $updated['user']['address']['zip']); + $this->assertEquals(101, $updated['posts'][0]['id']); + $this->assertEquals('First Post', $updated['posts'][0]['title']); + } + + public function testSetMultipleValuesAtNonExistingPaths(): void + { + $origin = [ + 'users' => [], + ]; + + $updated = Arr::set('users.*.name', $origin, 'John Doe'); + $updated = Arr::set('users.*.age', $updated, 30); + + $this->assertCount(1, $updated['users']); + $this->assertEquals('John Doe', $updated['users'][0]['name']); + $this->assertEquals(30, $updated['users'][0]['age']); + } + + public function testHavingValueAtExistingPath(): void + { + $array = [ + 'user' => [ + 'id' => 1, + 'name' => 'John Doe', + 'address' => [ + 'city' => 'New York', + 'zip' => '10001', + ], + ], + ]; + + $this->assertTrue(Arr::has('user.id', $array)); + $this->assertTrue(Arr::has('user.name', $array)); + $this->assertTrue(Arr::has('user.address.city', $array)); + $this->assertTrue(Arr::has('user.address.zip', $array)); + } + + public function testHavingValueAtExistingPathUsingWildcardNonStrictMode(): void + { + $array = [ + 'users' => [ + ['age' => 25], + ['name' => 'Jane Smith'], + [ + 'name' => 'Alice Johnson', + 'phones' => [ + ['type' => 'mobile', 'number' => '123-456-7890'], + ['type' => 'home', 'number' => '098-765-4321'], + ], + ], + ], + ]; + + $this->assertTrue(Arr::has('users.*.name', $array)); + $this->assertTrue(Arr::has('users.*.phones.*.number', $array)); + } + + public function testHavingValueAtExistingPathUsingWildcardStrictMode(): void + { + $array = [ + 'users' => [ + ['age' => 25], + ['name' => 'Alice Brandon'], + ], + 'users_with_phones' => [ + [ + 'name' => 'Bob Brown', + 'phones' => [ + ['type' => 'mobile', 'number' => '555-555-5555'], + ], + ], + [ + 'name' => 'Carol White', + 'phones' => [ + ['type' => 'home', 'number' => '444-444-4444'], + ], + ] + ] + ]; + + $this->assertFalse(Arr::has('users.*.name', $array, true)); + $this->assertTrue(Arr::has('users_with_phones.*.phones.*.number', $array, true)); + } + + public function testHavingValueAtNonExistingPathUsingWildcard(): void + { + $array = [ + 'users' => [ + ['name' => 'John Doe'], + ['name' => 'Jane Smith'], + ], + ]; + + $this->assertFalse(Arr::has('users.*.age', $array)); + $this->assertFalse(Arr::has('users.*.address.city', $array)); + $this->assertFalse(Arr::has('users.*.phones.*.type', $array)); + } +}