Version 1.0.0

This commit is contained in:
diffhead
2025-11-16 01:51:25 +04:00
committed by Viktor Smagin
commit 8cb7f400fd
43 changed files with 4462 additions and 0 deletions

22
.editorconfig Normal file
View File

@@ -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

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
vendor/
.phpunit.cache/

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright 2025 Viktor S. <thinlineseverywhere@gmail.com>
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.

230
README.md Normal file
View File

@@ -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<int,\Diffhead\PHP\DataEnrichmentKit\Interface\Entity>
*/
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.

36
composer.json Normal file
View File

@@ -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"
}
]
}

1882
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

27
phpunit.xml Normal file
View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
displayDetailsOnPhpunitDeprecations="true"
failOnPhpunitDeprecation="true"
failOnRisky="true"
failOnWarning="true"
testdox="true"
colors="true">
<testsuites>
<testsuite name="Unit">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>

54
src/Builder.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit;
use Diffhead\PHP\DataEnrichmentKit\Exception\TargetIsNull;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Object\Target;
class Builder
{
private array $items = [];
private ?Target $target = null;
public static function withTarget(string $entity, string $field): static
{
$builder = new static();
$builder->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);
}
}

20
src/Enricher.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit;
use Diffhead\PHP\DataEnrichmentKit\Interface\Enrichment;
use Diffhead\PHP\DataEnrichmentKit\Storage\Requests;
class Enricher
{
public function __construct(
private Enrichment $enrichment,
) {}
public function enrich(array $data, Requests $requests): array
{
return $this->enrichment->enrich($data, $requests);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Exception;
use RuntimeException;
class InvalidRequest extends RuntimeException
{
public function __construct(string $value)
{
return parent::__construct(
sprintf('Invalid enrichment request: "%s".', $value)
);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Exception;
use RuntimeException;
class PayloadIsNotJson extends RuntimeException {}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Exception;
use RuntimeException;
class RepositoryNotFound extends RuntimeException
{
public function __construct(string $target)
{
return parent::__construct(
sprintf('Repository not found for target "%s".', $target)
);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Exception;
use LogicException;
class TargetIsNull extends LogicException {}

10
src/Header.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit;
enum Header: string
{
case XEnrichmentRequest = 'X-Enrichment-Request';
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Interface;
use Diffhead\PHP\DataEnrichmentKit\Storage\Requests;
interface Enrichment
{
public function enrich(array $data, Requests $requests): array;
}

10
src/Interface/Entity.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Interface;
use ArrayAccess;
use JsonSerializable;
interface Entity extends ArrayAccess, JsonSerializable {}

12
src/Interface/Parser.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Interface;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
interface Parser
{
public function parse(string $value): Request;
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Interface;
interface Repository
{
/**
* @param string $field
* @param array $values
*
* @return iterable<int,\Diffhead\PHP\DataEnrichmentKit\Interface\Entity>
*/
public function getByFieldValues(string $field, array $values): iterable;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Interface;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
interface Serializer
{
public function toString(Request $request): string;
}

85
src/Message.php Normal file
View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit;
use BackedEnum;
use Diffhead\PHP\DataEnrichmentKit\Exception\PayloadIsNotJson;
use Diffhead\PHP\DataEnrichmentKit\Interface\Parser;
use Diffhead\PHP\DataEnrichmentKit\Interface\Serializer;
use Diffhead\PHP\DataEnrichmentKit\Storage\Requests;
use Nyholm\Psr7\Stream;
use Psr\Http\Message\MessageInterface;
class Message
{
public function __construct(
private Serializer $serializer,
private Parser $parser,
) {}
/**
* @param \Psr\Http\Message\MessageInterface $message
* @param \BackedEnum $header
* @param \Diffhead\PHP\DataEnrichmentKit\Storage\Requests $requests
*
* @return \Psr\Http\Message\MessageInterface
*/
public function setRequests(
MessageInterface $message,
BackedEnum $header,
Requests $requests
): MessageInterface {
if ($requests->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))
);
}
}

23
src/Object/Item.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Object;
class Item
{
public function __construct(
private string $key,
private string $alias = ''
) {}
public function key(): string
{
return $this->key;
}
public function alias(): string
{
return $this->alias;
}
}

29
src/Object/ItemsBag.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Object;
use ArrayIterator;
use IteratorAggregate;
use Traversable;
class ItemsBag implements IteratorAggregate
{
private array $items = [];
public function push(Item $item): static
{
$this->items[] = $item;
return $this;
}
/**
* @return \Traversable<int,\Diffhead\PHP\DataEnrichmentKit\Object\Item>
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}
}

23
src/Object/Request.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Object;
class Request
{
public function __construct(
private ItemsBag $items,
private Target $target
) {}
public function items(): ItemsBag
{
return $this->items;
}
public function target(): Target
{
return $this->target;
}
}

23
src/Object/Target.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Object;
class Target
{
public function __construct(
private string $entity,
private string $field
) {}
public function entity(): string
{
return $this->entity;
}
public function field(): string
{
return $this->field;
}
}

153
src/Service/Enrichment.php Normal file
View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Service;
use ArrayAccess;
use Diffhead\PHP\DataEnrichmentKit\Interface\Enrichment as EnrichmentInterface;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Storage\Repositories;
use Diffhead\PHP\DataEnrichmentKit\Storage\Requests;
use Diffhead\PHP\DataEnrichmentKit\Utility\Arr;
class Enrichment implements EnrichmentInterface
{
public function __construct(
private Repositories $repositories,
) {}
/**
* @param array $payload
* @param \Diffhead\PHP\DataEnrichmentKit\Storage\Requests $requests
*
* @return array
*/
public function enrich(array $data, Requests $requests): array
{
foreach ($requests as $request) {
$data = $this->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;
}
}

57
src/Service/Parser.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Service;
use Diffhead\PHP\DataEnrichmentKit\Exception\InvalidRequest;
use Diffhead\PHP\DataEnrichmentKit\Interface\Parser as ParserInterface;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Object\Target;
class Parser implements ParserInterface
{
/**
* @param string $value
*
* @return \Diffhead\PHP\DataEnrichmentKit\Object\Request
*
* @throws \Diffhead\PHP\DataEnrichmentKit\Exception\InvalidRequest
*/
public function parse(string $value): Request
{
$referenceWithTarget = explode('@', $value);
if (! isset($referenceWithTarget[1])) {
throw new InvalidRequest($value);
}
$entityWithField = explode(',', $referenceWithTarget[1]);
if (! isset($entityWithField[0]) || ! isset($entityWithField[1])) {
throw new InvalidRequest($value);
}
$entity = $entityWithField[0];
$field = $entityWithField[1];
$target = new Target($entity, $field);
$references = $referenceWithTarget[0];
$references = explode(',', $references);
$items = new ItemsBag();
foreach ($references as $reference) {
$referenceWithAlias = explode('+', $reference);
$items->push(
new Item($referenceWithAlias[0], $referenceWithAlias[1] ?? '')
);
}
return new Request($items, $target);
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Service;
use Diffhead\PHP\DataEnrichmentKit\Interface\Serializer as SerializerInterface;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
class Serializer implements SerializerInterface
{
public function toString(Request $request): string
{
$targetParts = [
$request->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);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Storage;
use Diffhead\PHP\DataEnrichmentKit\Exception\RepositoryNotFound;
use Diffhead\PHP\DataEnrichmentKit\Interface\Repository;
use InvalidArgumentException;
class Repositories
{
/**
* @param array<string,\Diffhead\PHP\DataEnrichmentKit\Interface\Repository> $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];
}
}

54
src/Storage/Requests.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Storage;
use ArrayIterator;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;
class Requests implements IteratorAggregate
{
private int $count = 0;
/**
* @param array<int,\Diffhead\PHP\DataEnrichmentKit\Object\Request> $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<int,\Diffhead\PHP\DataEnrichmentKit\Object\Request>
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->requests);
}
}

174
src/Utility/Arr.php Normal file
View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Utility;
class Arr
{
/**
* Get value inside array using key dot notation is supported
* also supported wildcard keys as "users.*.name" and alike.
*
* Returns empty array when passed wildcard key but no values found.
* This mode will never return the default value and values strictly
* equal to default are skipping.
*
* @param string $key
* @param array $data
* @param mixed $default
*
* @return mixed|array<int,mixed>
*/
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;
}
}

57
tests/BuilderTest.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests;
use Diffhead\PHP\DataEnrichmentKit\Builder;
use Diffhead\PHP\DataEnrichmentKit\Exception\TargetIsNull;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Builder::class)]
#[CoversMethod(Builder::class, 'withTarget')]
#[CoversMethod(Builder::class, 'item')]
#[CoversMethod(Builder::class, 'target')]
#[CoversMethod(Builder::class, 'build')]
class BuilderTest extends TestCase
{
public function testBuildingWithTarget(): void
{
$builder = Builder::withTarget('entity', 'field');
$this->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();
}
}

116
tests/EnricherTest.php Normal file
View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests;
use Diffhead\PHP\DataEnrichmentKit\Enricher;
use Diffhead\PHP\DataEnrichmentKit\Interface\Repository;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Object\Target;
use Diffhead\PHP\DataEnrichmentKit\Service\Enrichment;
use Diffhead\PHP\DataEnrichmentKit\Storage\Repositories;
use Diffhead\PHP\DataEnrichmentKit\Storage\Requests;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(Enricher::class)]
class EnricherTest extends TestCase
{
public function testEnrich(): 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'
],
];
$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)
];
}
}

127
tests/MessageTest.php Normal file
View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests;
use Diffhead\PHP\DataEnrichmentKit\Exception\PayloadIsNotJson;
use Diffhead\PHP\DataEnrichmentKit\Header;
use Diffhead\PHP\DataEnrichmentKit\Message;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Object\Target;
use Diffhead\PHP\DataEnrichmentKit\Service\Parser;
use Diffhead\PHP\DataEnrichmentKit\Service\Serializer;
use Diffhead\PHP\DataEnrichmentKit\Storage\Requests;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Stream;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Message::class)]
#[CoversMethod(Message::class, 'setRequests')]
#[CoversMethod(Message::class, 'getRequests')]
class MessageTest extends TestCase
{
public function testSetRequests(): void
{
$serializer = new Serializer();
$parser = new Parser();
$message = new Message($serializer, $parser);
$psrFactory = new Psr17Factory();
$psrMessage = $psrFactory->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());
}
}

24
tests/Object/ItemTest.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Interface;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Item::class)]
#[CoversMethod(Item::class, 'key')]
#[CoversMethod(Item::class, 'alias')]
class ItemTest extends TestCase
{
public function testProperlyInitialized(): void
{
$item = new Item('key', 'alias');
$this->assertEquals('key', $item->key());
$this->assertEquals('alias', $item->alias());
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests\Object;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(ItemsBag::class)]
#[CoversMethod(ItemsBag::class, 'push')]
#[CoversMethod(ItemsBag::class, 'getIterator')]
class ItemsBagTest extends TestCase
{
public function testProperlyInitialized(): void
{
$itemsBag = new ItemsBag();
$item = new Item('key', 'alias');
$itemsBag->push($item);
$this->assertCount(1, iterator_to_array($itemsBag->getIterator()));
$this->assertSame($item, iterator_to_array($itemsBag->getIterator())[0]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests\Object;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Object\Target;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Request::class)]
#[CoversMethod(Request::class, 'items')]
#[CoversMethod(Request::class, 'target')]
class RequestTest extends TestCase
{
public function testProperlyInitialized(): void
{
$itemsBag = new ItemsBag();
$target = new Target('entity', 'field');
$request = new Request($itemsBag, $target);
$this->assertSame($itemsBag, $request->items());
$this->assertSame($target, $request->target());
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests\Object;
use Diffhead\PHP\DataEnrichmentKit\Object\Target;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Target::class)]
#[CoversMethod(Target::class, 'entity')]
#[CoversMethod(Target::class, 'field')]
class TargetTest extends TestCase
{
public function testProperlyInitialized(): void
{
$target = new Target('entity', 'field');
$this->assertEquals('entity', $target->entity());
$this->assertEquals('field', $target->field());
}
}

View File

@@ -0,0 +1,403 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests\Service;
use Diffhead\PHP\DataEnrichmentKit\Interface\Enrichment as EnrichmentInterface;
use Diffhead\PHP\DataEnrichmentKit\Interface\Repository;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Object\Target;
use Diffhead\PHP\DataEnrichmentKit\Service\Enrichment;
use Diffhead\PHP\DataEnrichmentKit\Storage\Repositories;
use Diffhead\PHP\DataEnrichmentKit\Storage\Requests;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Enrichment::class)]
#[CoversMethod(Enrichment::class, 'enrich')]
class EnrichmentTest extends TestCase
{
public function testImplementsEnrichmentInterface(): void
{
$this->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)
];
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests\Service;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Service\Parser;
use Diffhead\PHP\DataEnrichmentKit\Exception\InvalidRequest;
use Diffhead\PHP\DataEnrichmentKit\Interface\Parser as ParserInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Parser::class)]
#[CoversMethod(Parser::class, 'parse')]
class ParserTest extends TestCase
{
public function testImplementsParserInterface(): void
{
$this->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('');
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests\Service;
use Diffhead\PHP\DataEnrichmentKit\Interface\Serializer as SerializerInterface;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Object\Target;
use Diffhead\PHP\DataEnrichmentKit\Service\Serializer;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Serializer::class)]
#[CoversMethod(Serializer::class, 'toString')]
class SerializerTest extends TestCase
{
public function testImplementsSerializerInterface(): void
{
$this->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);
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests\Storage;
use Diffhead\PHP\DataEnrichmentKit\Exception\RepositoryNotFound;
use Diffhead\PHP\DataEnrichmentKit\Interface\Repository;
use Diffhead\PHP\DataEnrichmentKit\Storage\Repositories;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Repositories::class)]
#[CoversMethod(Repositories::class, 'set')]
#[CoversMethod(Repositories::class, 'get')]
class RepositoriesTest extends TestCase
{
public function testSetAndGet(): void
{
$repositories = new Repositories();
$mockRepository = new class implements Repository {
public function getByFieldValues(string $field, array $values): array
{
return [
['field' => $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');
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests\Storage;
use Diffhead\PHP\DataEnrichmentKit\Object\ItemsBag;
use Diffhead\PHP\DataEnrichmentKit\Object\Request;
use Diffhead\PHP\DataEnrichmentKit\Object\Target;
use Diffhead\PHP\DataEnrichmentKit\Storage\Requests;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Requests::class)]
#[CoversMethod(Requests::class, 'append')]
#[CoversMethod(Requests::class, 'count')]
#[CoversMethod(Requests::class, 'requests')]
class RequestsTest extends TestCase
{
public function testAppendAndCount(): void
{
$requests = new Requests();
$this->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]);
}
}

322
tests/Utility/ArrTest.php Normal file
View File

@@ -0,0 +1,322 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\DataEnrichmentKit\Tests\Utility;
use Diffhead\PHP\DataEnrichmentKit\Utility\Arr;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Arr::class)]
#[CoversMethod(Arr::class, 'get')]
#[CoversMethod(Arr::class, 'set')]
class ArrTest extends TestCase
{
public function testGetExistingItems(): void
{
$array = [
'user' => [
'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));
}
}