Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"psr/simple-cache": "^2.0.0 || ^3.0.0",
"symfony/type-info": "^7.3.0 || ^8.0.0"
},
"suggest": {
"symfony/stopwatch": "To trace the hydrator with the StopwatchTracer of the TracingExtension"
},
"require-dev": {
"infection/infection": "^0.32.4",
"patchlevel/coding-standard": "^1.3.0",
Expand All @@ -38,6 +41,7 @@
"phpstan/phpstan-phpunit": "^2.0.15",
"phpunit/phpunit": "^11.5.53",
"symfony/cache": "^6.4.0 || ^7.0.0 || ^8.0.0",
"symfony/stopwatch": "^6.4.0 || ^7.0.0 || ^8.0.0",
"symfony/var-dumper": "^5.4.29 || ^6.4.0 || ^7.0.0 || ^8.0.0",
"wnx/commonmark-markdown-renderer": "^1.6"
},
Expand Down
68 changes: 67 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use Symfony\Component\Cache\Adapter\FilesystemAdapter;
$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->setCache(new FilesystemAdapter())
->build();
->buildHydrator();
```
:::note
Internally the builder wraps the metadata factory in a `Psr6MetadataFactory` or
Expand Down
2 changes: 1 addition & 1 deletion docs/cryptography.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ $cipherKeyStore = new InMemoryCipherKeyStore();
$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($cipherKeyStore)))
->build();
->buildHydrator();
```
## DataSubjectId

Expand Down
46 changes: 43 additions & 3 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ use Patchlevel\Hydrator\StackHydratorBuilder;
$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new LifecycleExtension())
->build();
->buildHydrator();
```
## Built-in extensions

The library ships with four extensions out of the box:
The library ships with five extensions out of the box:

| Extension | Purpose |
| --- | --- |
| `CoreExtension` | The default behaviour, the `TransformMiddleware` and the `BuiltInGuesser`. |
| `LifecycleExtension` | [Lifecycle hooks](lifecycle-hooks.md), run code before and after the extract and hydrate process. |
| `CryptographyExtension` | [Cryptography](cryptography.md), encrypt and decrypt sensitive data with crypto-shredding. |
| `UpcastExtension` | [Upcasting](upcasting.md), reshape outdated stored data while it is hydrated. |
| `TracingExtension` | [Tracing](tracing.md), measure every hydrate and extract call. |

## Middleware

Expand Down Expand Up @@ -152,6 +153,45 @@ extras) can be [cached](caching.md), everything you store in `extras` must be
serializable.
:::

## Decorators

A decorator wraps the whole hydrator instead of a single step inside the
stack. Use it for things which concern the call as a whole, like
[tracing](tracing.md) or logging. A decorator implements `HydratorDecorator`
and returns a new `Hydrator` around the given one. It also receives the
innermost `StackHydrator`, in case it needs its metadata.

```php
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\HydratorDecorator;
use Patchlevel\Hydrator\StackHydrator;

final class AuditDecorator implements HydratorDecorator
{
public function __construct(
private readonly AuditLog $auditLog,
) {
}

public function decorate(Hydrator $hydrator, StackHydrator $stack): Hydrator
{
return new AuditHydrator($hydrator, $this->auditLog);
}
}
```
```php
$builder->addDecorator(new AuditDecorator($auditLog));
```
Decorators also accept a priority, a decorator with a higher priority is
wrapped around the ones with a lower priority. The default is `0`, the
decorator of the [tracing](tracing.md) extension has priority `32`, so it also
measures the decorators below it.

:::warning
Decorators are only applied by `buildHydrator()`. The deprecated `build()`
throws a `DecoratorsNotApplied` exception as soon as a decorator is registered.
:::

## Writing your own extension

An extension implements the `Extension` interface and configures the builder.
Expand All @@ -175,4 +215,4 @@ final class AuditExtension implements Extension
* [How to run code before extract and after hydrate](lifecycle-hooks.md)
* [How to encrypt sensitive data](cryptography.md)
* [How to reshape outdated stored data](upcasting.md)
* [How to cache the metadata](caching.md)
* [How to trace the hydrator](tracing.md)
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->build();
->buildHydrator();
```
:::note
The builder is also the place to add [extensions](extensions.md), custom
Expand Down
4 changes: 2 additions & 2 deletions docs/guesser.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use Patchlevel\Hydrator\StackHydratorBuilder;
$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->addGuesser(new NameGuesser())
->build();
->buildHydrator();
```
:::note
The guessers are queried in order of their priority, and the first match wins.
Expand All @@ -66,7 +66,7 @@ $hydrator = (new StackHydratorBuilder())
Name::class => NameNormalizer::class,
Email::class => EmailNormalizer::class,
]))
->build();
->buildHydrator();
```
:::note
The `MappedGuesser` instantiates the normalizer class without arguments, so the
Expand Down
9 changes: 8 additions & 1 deletion docs/hydrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->build();
->buildHydrator();
```
:::note
`buildHydrator()` returns a `Hydrator`. Extensions can wrap the
`StackHydrator` in [decorators](extensions.md#decorators), so type hint
against the `Hydrator` interface. The older `build()` is deprecated, it only
works as long as no decorator is registered.
:::

If you don't need any extensions, you can also instantiate the `StackHydrator`
directly, it defaults to the same middleware and guesser:

Expand Down
1 change: 1 addition & 0 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ And if customization is required, it can be done easily using attributes.
* [Lifecycle hooks](lifecycle-hooks.md) before extracting and after hydrating.
* Encrypt and decrypt sensitive data with the [cryptography](cryptography.md) extension (crypto-shredding).
* [Upcast](upcasting.md) outdated stored data while it is hydrated.
* [Trace](tracing.md) every hydrate and extract call, for example in the Symfony profiler.
* [Cache](caching.md) the metadata with any PSR-6 or PSR-16 cache.

## Installation
Expand Down
2 changes: 1 addition & 1 deletion docs/lazy.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use Patchlevel\Hydrator\StackHydratorBuilder;
$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->enableDefaultLazy()
->build();
->buildHydrator();
```
Single classes can then opt out again with the attribute:

Expand Down
2 changes: 1 addition & 1 deletion docs/lifecycle-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use Patchlevel\Hydrator\StackHydratorBuilder;
$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new LifecycleExtension())
->build();
->buildHydrator();
```
## Hooks

Expand Down
3 changes: 2 additions & 1 deletion docs/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"subEntries": [
{ "title": "Lifecycle Hooks", "file": "lifecycle-hooks.md" },
{ "title": "Cryptography", "file": "cryptography.md" },
{ "title": "Upcasting", "file": "upcasting.md" }
{ "title": "Upcasting", "file": "upcasting.md" },
{ "title": "Tracing", "file": "tracing.md" }
]
},
{
Expand Down
90 changes: 90 additions & 0 deletions docs/tracing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Tracing

The tracing extension measures every `hydrate` and `extract` call on the
hydrator. It wraps the hydrator in a [decorator](extensions.md#decorators) and
passes each call to a `Tracer`, which decides what to record: an event in the
Symfony profiler, a log entry or a span in your observability tool.

## Setup

Register the `TracingExtension` on the builder and pass it a `Tracer`. The
library ships with the `StopwatchTracer`, which records every call as an event
of the Symfony Stopwatch. It needs the `symfony/stopwatch` package.

```bash
composer require symfony/stopwatch
```
```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Extension\Tracing\StopwatchTracer;
use Patchlevel\Hydrator\Extension\Tracing\TracingExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;
use Symfony\Component\Stopwatch\Stopwatch;

$stopwatch = new Stopwatch();

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new TracingExtension(new StopwatchTracer($stopwatch)))
->buildHydrator();
```
Each call shows up as an event named after the operation and the class, for
example `hydrate App\Profile\Event\ProfileCreated`, in the category
`hydrator`. You can pass another category as the second argument of the
`StopwatchTracer`.

:::warning
The decorators are only applied by `buildHydrator()`. The deprecated `build()`
throws a `DecoratorsNotApplied` exception as soon as an extension registers a
decorator, like the `TracingExtension` does.
:::

## Write your own tracer

A tracer implements the `Tracer` interface. It receives the operation, the
class and a callback which runs the actual call. The tracer has to call it,
return its result unchanged and let exceptions pass.

```php
use Patchlevel\Hydrator\Extension\Tracing\Operation;
use Patchlevel\Hydrator\Extension\Tracing\Tracer;
use Psr\Log\LoggerInterface;

final class SlowCallTracer implements Tracer
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly float $thresholdInMs = 5.0,
) {
}

public function trace(Operation $operation, string $class, Closure $callback): mixed
{
$start = hrtime(true);

try {
return $callback();
} finally {
$duration = (hrtime(true) - $start) / 1_000_000;

if ($duration >= $this->thresholdInMs) {
$this->logger->warning('Slow {operation} of {class} took {duration} ms', [
'operation' => $operation->value,
'class' => $class,
'duration' => $duration,
]);
}
}
}
}
```
:::note
Nested objects are traced as well. Their trace runs inside the trace of their
parent, so the parent's time includes the time of its nested objects.
:::

## Learn more

* [How to write your own decorator](extensions.md#decorators)
* [How to create the hydrator](hydrator.md)
* [How to cache the metadata](caching.md)
2 changes: 1 addition & 1 deletion docs/upcasting.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ $hydrator = (new StackHydratorBuilder())
),
],
))
->build();
->buildHydrator();
```
:::note
Upcasting only runs during [hydration](hydrator.md). Extraction always writes
Expand Down
15 changes: 15 additions & 0 deletions src/DecoratorsNotApplied.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

use RuntimeException;

final class DecoratorsNotApplied extends RuntimeException implements HydratorException
{
public function __construct()
{
parent::__construct('Decorators are registered but build() can not apply them, use buildHydrator() instead.');
}
}
11 changes: 11 additions & 0 deletions src/Extension/Tracing/Operation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Extension\Tracing;

enum Operation: string
{
case Hydrate = 'hydrate';
case Extract = 'extract';
}
Loading
Loading