commit 8cb7f400fd86dba292bbc75127e33ea6f61bc37a Author: diffhead <82263657+diffhead@users.noreply.github.com> Date: Sun Nov 16 01:51:25 2025 +0400 Version 1.0.0 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)); + } +}