Version 1.0.0

This commit is contained in:
diffhead
2025-11-22 12:10:44 +04:00
committed by Viktor Smagin
commit 3f19a38412
14 changed files with 6722 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.

220
README.md Normal file
View File

@@ -0,0 +1,220 @@
# Laravel Data Enrichment
Laravel Data Enrichment helps you enrich and augment data across your Laravel applications. It offers a flexible, extensible workflow to collect enrichment requests and resolve them against repositories, whether you work with arrays, HTTP messages, or custom data sources.
## Features
- **Multiple sources:** Enrich array data, HTTP requests, and responses.
- **Middleware support:** Pin enrichment requests to controller responses automatically.
- **Extensible managers:** Plug in custom logic via interfaces and repositories.
- **Simple facades:** Convenient API via `ArrayEnrichment` and `HttpEnrichment`.
## Additional Info
- Core library: [diffhead/php-data-enrichment-kit](https://github.com/diffhead/php-data-enrichment-kit)
- This package provides Laravel integration (service provider, facades, middleware, and configuration) on top of the core kit.
## Installation
Install via Composer:
```bash
composer require diffhead/laravel-data-enrichment
```
Publish the configuration (if needed):
```bash
php artisan vendor:publish --provider="Diffhead\PHP\LaravelDataEnrichment\ServiceProvider"
```
## Configuration
The configuration file `config/enrichment.php` controls how enrichment works. Customize it for your application.
- **Repositories:** On the receiver side, create repositories implementing `\Diffhead\PHP\DataEnrichmentKit\Interface\Repository` and list them under `enrichment.repositories`.
- **Bindings:** Optionally map repository interfaces to implementations via `enrichment.bindings` for auto-resolution.
- **Custom logic:** Override the enrichment workflow by providing your implementations for:
- `\Diffhead\PHP\DataEnrichmentKit\Interface\Parser` — parse raw requests
- `\Diffhead\PHP\DataEnrichmentKit\Interface\Serializer` — serialize request objects
- `\Diffhead\PHP\DataEnrichmentKit\Interface\Enrichment` — core enrichment business logic
## Usage
### Array Enrichment
Use the `ArrayEnrichment` facade to enrich plain PHP arrays:
```php
use Diffhead\LaravelDataEnrichment\Facade\ArrayEnrichment;
$books = [
[
'title' => 'Magic Things: How To',
'author_id' => 353,
],
];
ArrayEnrichment::addRequest('user', 'id', [
['key' => '*.author_id', 'alias' => 'author'],
]);
$enrichedBooks = ArrayEnrichment::enrich($books);
/**
* Result:
* [
* [
* 'title' => 'Magic Things: How To',
* 'author_id' => 353,
* 'author' => [
* 'id' => 353,
* 'name' => 'John Doe',
* 'email' => 'john-doe@mysite.com'
* ]
* ],
* ]
*/
print_r($enrichedBooks);
/**
* When done, clear the request storage and add new
* requests for the next data object to enrich.
*/
ArrayEnrichment::clearRequests();
```
### HTTP Enrichment
Use the `HttpEnrichment` facade to enrich HTTP requests or responses:
```php
use Diffhead\LaravelDataEnrichment\Facade\HttpEnrichment;
enum Header: string
{
case XEnrich = 'X-Enrich';
}
$psrFactory = new PsrHttpFactory();
/**
* @var \Symfony\Component\HttpFoundation\Response $response
* @var \Psr\Http\Message\MessageInterface $psrMessage
*/
$psrMessage = $psrFactory->createResponse($response);
/**
* Optionally set the header used for enrichment.
* Default: \Diffhead\LaravelDataEnrichment\Header::XEnrichRequest
* Accepts any BackedEnum.
* Note: if you change the default on the client, also change it on the server.
*/
HttpEnrichment::useHeader(Header::XEnrich);
/**
* Add enrichment requests to storage.
*/
HttpEnrichment::addRequest('user', 'id', [
['key' => 'data.*.assigned.*.user_id', 'alias' => 'user'],
['key' => 'data.*.created_by', 'alias' => 'creator'],
]);
/**
* Client-side step: attach requests before passing the message downstream.
*/
$psrMessagePrepared = HttpEnrichment::setRequests($psrMessage);
/**
* Example (gateway side): enrich a response.
* This method parses and enriches the JSON payload in the PSR message.
* Note: call HttpEnrichment::useHeader again if you changed it earlier.
*/
$psrMessageEnriched = HttpEnrichment::enrichMessage($psrMessagePrepared);
```
### Middleware
Use the `enrichment.pin-requests` middleware to automatically pin added requests to the controller response:
```php
use Diffhead\LaravelDataEnrichment\Facade\HttpEnrichment;
Route::middleware(['enrichment.pin-requests'])->group(
function () {
Route::get('/posts', function () {
HttpEnrichment::addRequest('user', 'id', [
['key' => 'data.*.owner_id', 'alias' => 'owner'],
]);
return response()->json([
'data' => [
['id' => 1, 'title' => 'Cats Dev', 'owner_id' => 1],
['id' => 2, 'title' => "Sponge Bob's Bio", 'owner_id' => 2],
],
]);
});
}
);
```
## Repository Examples
Below is an example of a repository interface and its Laravel Eloquent implementation used for enrichment. The repository searches users by a field with multiple values and returns an iterable cursor.
#### Interface
```php
namespace App\Shared\Service\User;
use Diffhead\PHP\DataEnrichmentKit\Interface\Repository;
interface SearchByFieldValuesInterface extends Repository
{
/**
* @param string $field
* @param array $values
*
* @return iterable<int,\App\Models\User\User>
*/
public function getByFieldValues(string $field, array $values): iterable;
}
```
#### Implementation
```php
namespace App\Shared\Service\User;
use App\Models\User\User;
class SearchByFieldValues implements SearchByFieldValuesInterface
{
/**
* @param string $field
* @param array<int,mixed> $values
*
* @return iterable<int,\App\Models\User\User>
*/
public function getByFieldValues(string $field, array $values): iterable
{
return User::query()->whereIn($field, $values)->cursor();
}
}
```
#### Registration
`config/enrichment.php`
```php
return [
/** ... */
'bindings' => [
\App\Shared\Service\User\SearchByFieldValuesInterface::class =>
\App\Shared\Service\User\SearchByFieldValues::class
],
'repositories' => [
'user' => \App\Shared\Service\User\SearchByFieldValuesInterface::class
],
/** ... */
];
```

43
composer.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "diffhead/laravel-data-enrichment",
"description": "Data enrichment library based on DataEnrichmentKit. A suitable components for micro services written using laravel framework.",
"type": "library",
"license": "MIT",
"version": "1.0.0",
"keywords": [
"laravel", "data enrichment", "facade", "psr7", "http",
"pipeline", "message processing", "middleware", "microservice"
],
"autoload": {
"psr-4": {
"Diffhead\\PHP\\LaravelDataEnrichment\\": "src/",
"Diffhead\\PHP\\LaravelDataEnrichment\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Diffhead\\PHP\\LaravelDataEnrichment\\ServiceProvider"
]
}
},
"require": {
"php": "^8.1",
"laravel/framework": "^10 || ^11.0 || ^12.0",
"diffhead/php-data-enrichment-kit": "^1.0.0",
"symfony/psr-http-message-bridge": "^7.3"
},
"scripts": {
"test": "phpunit",
"post-install-cmd": [
"php artisan vendor:publish --provider=\"Diffhead\\PHP\\LaravelDataEnrichment\\ServiceProvider\""
]
},
"minimum-stability": "stable",
"authors": [
{
"name": "Viktor S.",
"email": "thinlineseverywhere@gmail.com"
}
]
}

6008
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
config/enrichment.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
return [
/**
* Data enrichment requests parser
*/
'parser' => \Diffhead\PHP\DataEnrichmentKit\Service\Parser::class,
/**
* Data enrichment requests serializer
*/
'serializer' => \Diffhead\PHP\DataEnrichmentKit\Service\Serializer::class,
/**
* Enrichment business logic
*/
'enrichment' => \Diffhead\PHP\DataEnrichmentKit\Service\Enrichment::class,
/**
* DI container bindings if you want to register specific implementations
* automatically.
*/
'bindings' => [
/**
* \App\Repository\Repository\UserRepositoryInterface::class =>
* \App\Repository\Repository\UserRepository::class,
*/
],
/**
* Repositories mapping where key is the target name which will be passed
* inside the request and value is the repository class name.
*
* The repository class will be resolved via DI container and it should
* implement \Diffhead\PHP\DataEnrichmentKit\Interface\Repository interface.
*/
'repositories' => [
/**
* 'user' => \App\Repository\Repository\UserRepository::class,
*/
],
];

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelDataEnrichment\Facade;
use Illuminate\Support\Facades\Facade;
/**
* @method static \Diffhead\PHP\LaravelDataEnrichment\Manager\ArrayManager cleanRequests()
* @method static \Diffhead\PHP\DataEnrichmentKit\Builder addRequest(string $target, string $field, array<int,array{key:string,alias:string}|\Diffhead\PHP\DataEnrichmentKit\Object\Item> $items)
* @method static array enrichData(array $data)
*/
class ArrayEnrichment extends Facade
{
public static function getFacadeAccessor(): string
{
return \Diffhead\PHP\LaravelDataEnrichment\Manager\ArrayManager::class;
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelDataEnrichment\Facade;
use Illuminate\Support\Facades\Facade;
/**
* @method static \Diffhead\PHP\LaravelDataEnrichment\Manager\HttpManager useHeader(\BackedEnum $header)
* @method static \Diffhead\PHP\LaravelDataEnrichment\Manager\HttpManager cleanRequests()
* @method static \Diffhead\PHP\DataEnrichmentKit\Builder addRequest(string $target, string $field, array<int,array{key:string,alias:string}|\Diffhead\PHP\DataEnrichmentKit\Object\Item> $items)
* @method static \Psr\Http\Message\MessageInterface setRequests(\Psr\Http\Message\MessageInterface $message)
* @method static \Psr\Http\Message\MessageInterface enrichMessage(\Psr\Http\Message\MessageInterface $message)
*/
class HttpEnrichment extends Facade
{
public static function getFacadeAccessor(): string
{
return \Diffhead\PHP\LaravelDataEnrichment\Manager\HttpManager::class;
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelDataEnrichment\Manager;
use Diffhead\PHP\DataEnrichmentKit\Builder;
use Diffhead\PHP\DataEnrichmentKit\Enricher;
use Diffhead\PHP\DataEnrichmentKit\Object\Item;
use Diffhead\PHP\DataEnrichmentKit\Storage\Requests;
use InvalidArgumentException;
abstract class AbstractManager
{
/**
* @var array<int,\Diffhead\PHP\DataEnrichmentKit\Builder>
*/
protected array $builders = [];
public function __construct(
protected Enricher $enricher,
) {}
/**
* @param string $target
* @param string $field
* @param array<int,array{key:string,alias:string}|\Diffhead\PHP\DataEnrichmentKit\Object\Item> $items
*
* @return \Diffhead\PHP\DataEnrichmentKit\Builder
*/
public function addRequest(string $target, string $field = 'id', array $items = []): Builder
{
$builder = Builder::withTarget($target, $field);
foreach ($items as $item) {
if (is_array($item)) {
$this->validateRequestItemAsArray($item);
} else {
$item = $this->objectRequestItemToArray($item);
}
$builder->item($item['key'], $item['alias']);
}
$this->builders[] = $builder;
return $builder;
}
public function cleanRequests(): static
{
$this->builders = [];
return $this;
}
protected function getRequests(): Requests
{
$requests = new Requests();
foreach ($this->builders as $builder) {
$requests->append($builder->build());
}
return $requests;
}
/**
* @param array $item
*
* @return void
*
* @throws \InvalidArgumentException
*/
private function validateRequestItemAsArray(array $item): void
{
$notExistingKey = ! array_key_exists('key', $item);
$notExistingAlias = ! array_key_exists('alias', $item);
if ($notExistingKey || $notExistingAlias) {
throw new InvalidArgumentException(
'Item as array should contain "key" and "alias" values.'
);
}
}
/**
* @param \Diffhead\PHP\DataEnrichmentKit\Object\Item $item
*
* @return array{key:string,alias:string}
*/
private function objectRequestItemToArray(Item $item): array
{
return [
'key' => $item->key(),
'alias' => $item->alias(),
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelDataEnrichment\Manager;
class ArrayManager extends AbstractManager
{
public function enrichData(array $data): array
{
return $this->enricher->enrich($data, $this->getRequests());
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelDataEnrichment\Manager;
use BackedEnum;
use Diffhead\PHP\DataEnrichmentKit\Enricher;
use Diffhead\PHP\DataEnrichmentKit\Header;
use Diffhead\PHP\DataEnrichmentKit\Message;
use Psr\Http\Message\MessageInterface;
class HttpManager extends AbstractManager
{
private BackedEnum $requestsHeader = Header::XEnrichmentRequest;
public function __construct(
protected Enricher $enricher,
private Message $message,
) {}
public function useHeader(BackedEnum $header): static
{
$this->requestsHeader = $header;
return $this;
}
public function setRequests(MessageInterface $message): MessageInterface
{
$header = $this->requestsHeader;
$requests = $this->getRequests();
return $this->message->setRequests($message, $header, $requests);
}
public function enrichMessage(MessageInterface $message): MessageInterface
{
$requests = $this->message->getRequests($message, $this->requestsHeader);
$payload = $this->message->getPayload($message);
$enriched = $this->enricher->enrich($payload, $requests);
return $this->message->setPayload($message, $enriched);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelDataEnrichment\Middleware;
use Closure;
use Diffhead\PHP\LaravelDataEnrichment\Facade\HttpEnrichment;
use Illuminate\Http\Request;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\Response;
class PinRequestsToResponse
{
public function __construct(
private PsrHttpFactory $psrFactory,
private HttpFoundationFactory $httpFactory,
) {}
public function handle(Request $request, Closure $next): Response
{
/**
* @var \Symfony\Component\HttpFoundation\Response $response
*/
$response = $next($request);
$message = $this->psrFactory->createResponse($response);
return $this->httpFactory->createResponse(
HttpEnrichment::setRequests($message)
);
}
}

137
src/ServiceProvider.php Normal file
View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\LaravelDataEnrichment;
use Diffhead\PHP\DataEnrichmentKit\Enricher;
use Diffhead\PHP\DataEnrichmentKit\Interface\Enrichment as EnrichmentInterface;
use Diffhead\PHP\DataEnrichmentKit\Interface\Parser as ParserInterface;
use Diffhead\PHP\DataEnrichmentKit\Interface\Serializer as SerializerInterface;
use Diffhead\PHP\DataEnrichmentKit\Message;
use Diffhead\PHP\DataEnrichmentKit\Service\Enrichment;
use Diffhead\PHP\DataEnrichmentKit\Service\Parser;
use Diffhead\PHP\DataEnrichmentKit\Service\Serializer;
use Diffhead\PHP\DataEnrichmentKit\Storage\Repositories;
use Diffhead\PHP\LaravelDataEnrichment\Manager\ArrayManager;
use Diffhead\PHP\LaravelDataEnrichment\Manager\HttpManager;
use Diffhead\PHP\LaravelDataEnrichment\Middleware\PinRequestsToResponse;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
class ServiceProvider extends LaravelServiceProvider
{
private array $middlewares = [
'enrichment.pin-requests' => PinRequestsToResponse::class,
];
public function register(): void
{
$this->registerBindings();
$this->registerServices();
$this->registerRepositories();
$this->registerFacadeManagers();
}
public function boot(Router $router): void
{
$this->registerConfigsPublishment();
foreach ($this->middlewares as $alias => $class) {
$router->aliasMiddleware($alias, $class);
}
}
private function registerBindings(): void
{
/**
* @var array<class-string,class-string> $bindings
*/
$bindings = config('enrichment.bindings', []);
foreach ($bindings as $abstract => $concrete) {
$this->app->bind($abstract, $concrete);
}
}
private function registerServices(): void
{
$this->app->bind(
EnrichmentInterface::class,
config('enrichment.enrichment', Enrichment::class)
);
$this->app->bind(
SerializerInterface::class,
config('enrichment.serializer', Serializer::class)
);
$this->app->bind(
ParserInterface::class,
config('enrichment.parser', Parser::class)
);
}
private function registerRepositories(): void
{
$this->app->singleton(
Repositories::class,
function (Application $application): Repositories {
$repositories = new Repositories();
/**
* @var array<string,string> $mapping
*/
$mapping = config('enrichment.repositories', []);
foreach ($mapping as $target => $class) {
/**
* @var \Diffhead\PHP\DataEnrichmentKit\Interface\Repository $repository
*/
$repository = $application->make($class);
$repositories->set($target, $repository);
}
return $repositories;
}
);
}
private function registerFacadeManagers(): void
{
$this->app->singleton(
HttpManager::class,
function (Application $application): HttpManager {
return new HttpManager(
$application->make(Enricher::class),
$application->make(Message::class)
);
}
);
$this->app->singleton(
ArrayManager::class,
function (Application $application): ArrayManager {
return new ArrayManager(
$application->make(Enricher::class)
);
}
);
}
private function registerConfigsPublishment(): void
{
$this->publishes(
[
$this->configPath('config/enrichment.php') => config_path('enrichment.php'),
],
'config'
);
}
private function configPath(string $path): string
{
return sprintf('%s/../%s', __DIR__, $path);
}
}