makermill/hydratype-tools

Build-time discovery tools for MakerMill HydraType.

Maintainers

Package info

github.com/MakerMill/HydraType-tools

Homepage

Issues

pkg:composer/makermill/hydratype-tools

Transparency log

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

1.0.0 2026-07-28 15:53 UTC

This package is auto-updated.

Last update: 2026-07-28 15:57:12 UTC


README

HydraType Tools discovers statically visible MakerMill\HydraType\HydraType hydration calls in a PHP codebase and writes their root class names to a deterministic PHP manifest.

It is intended for build and deployment pipelines. PHP Parser and the CLI's development dependencies remain in this separate package and never enter HydraType's runtime dependency graph.

Installation

The package requires PHP 8.2 or newer.

composer require --dev makermill/hydratype-tools:^1.0

Quick start

Pass one or more PHP files or source directories and an output file:

vendor/bin/hydratype discover src --output=var/cache/hydratype-classes.php

Directories are scanned recursively for files with a .php extension. Directory symlinks are not followed. Explicit file arguments are scanned regardless of their extension. Input files are sorted before scanning, and duplicate input files are scanned once.

The generated manifest contains sorted, unique class strings:

<?php

declare(strict_types=1);

return [
    'App\\Domain\\Invoice',
    'App\\Domain\\Order',
    'App\\Domain\\User',
];

There are no timestamps, source paths, or environment-specific values in the output. If the contents have not changed, the existing file is left untouched. Changed manifests are written through a temporary file and renamed into place.

For example, bin/warm-hydratype.php can pass the discovered roots to HydraType's explicit production warm-up API:

use MakerMill\HydraType\Configuration;
use MakerMill\HydraType\HydratorCache;

$projectRoot = dirname(__DIR__);

$rootClasses = require $projectRoot . '/var/cache/hydratype-classes.php';

$configuration = new Configuration(
    hydratorDirectory: $projectRoot . '/var/cache/hydratype',
);

(new HydratorCache($configuration))->warm(...$rootClasses);

HydratorCache::warm() compiles the complete reachable graph, including nested hydration dependencies. The manifest therefore only contains roots that appear at application call sites.

Deployment requirement: HydraType Tools is a development dependency. Run discovery and warm the cache while development dependencies are installed, then carry both the generated manifest and compiled cache into the production artifact.

Deployment order

HydraType Tools is installed with --dev. It is unavailable when Composer dependencies are installed with --no-dev and is removed if an existing installation is pruned that way. Discovery must therefore run during a build stage in which development dependencies are available.

A typical in-place build looks like this:

composer install
vendor/bin/hydratype discover src --output=var/cache/hydratype-classes.php
php bin/warm-hydratype.php
composer install --no-dev

The deployment pipeline should follow this order:

  1. Make HydraType Tools available in the build environment.
  2. Run discovery to create the manifest.
  3. Pass that manifest to HydratorCache::warm() to compile the hydrator cache.
  4. Remove or omit development dependencies according to the application's packaging strategy.
  5. Build the production artifact with both the generated manifest and compiled cache included.
  6. Deploy the artifact and configure HydraType to use the carried cache in read-only mode.

This works with an in-place composer install --no-dev, a multi-stage container build, or separate dependency and artifact assembly steps. The exact source paths, output directories, strictness options, and warm-up entry point belong to the consuming application.

The generated files must live outside vendor, because Composer may replace or remove anything below that directory. The production application must use the same HydraType configuration that was used during warm-up:

use MakerMill\HydraType\CacheMode;
use MakerMill\HydraType\Configuration;
use MakerMill\HydraType\HydraType;

$hydra = new HydraType(new Configuration(
    hydratorDirectory: __DIR__ . '/var/cache/hydratype',
    cacheMode: CacheMode::ReadOnly,
));

Do not run discovery after composer install --no-dev: at that point the CLI has intentionally been removed. The manifest and compiled cache are build artifacts and must be preserved when the production artifact is assembled.

Dynamic targets

A recognized HydraType call with a target that cannot be resolved produces a warning:

src/Importer.php:24: dynamic HydraType target in hydrate(): class name could not be resolved through local assignments and direct callable arguments

Examples whose origins or declarations are not visible to discovery include:

$runtimeClass = $_GET['class'];

$hydra->hydrate($runtimeClass, $data);
$hydra->hydrate(ExternalTargets::TARGET_CLASS, $data);
$hydra->hydrate(__NAMESPACE__ . '\\User', $data);
$hydra->hydrate(static::class, $data);
$hydra->hydrate(...$runtimeArguments);

Variables and callable parameters are reported as dynamic only when their origins cannot be resolved completely. If a callable is invoked once with User::class and once with a runtime value, User is included in the manifest and the runtime origin still produces a warning.

Warnings do not prevent the known portion of the manifest from being written by default. Production pipelines can make them fatal:

vendor/bin/hydratype discover src \
    --output=var/cache/hydratype-classes.php \
    --fail-on-dynamic

With --fail-on-dynamic, the command exits with status 2 and does not create or replace the manifest.

Known dynamic classes can be added explicitly:

vendor/bin/hydratype discover src \
    --output=var/cache/hydratype-classes.php \
    --include='App\Domain\User' \
    --include='App\Domain\Order'

--include is repeatable. Manual includes supplement discovery but do not suppress dynamic warnings or make --fail-on-dynamic pass: the scanner cannot prove that the supplied list covers every runtime value.

Discovery examples

The scanner recognizes hydrate(), hydrateMany(), and hydrator() calls when the receiver is syntactically known to be MakerMill\HydraType\HydraType.

The snippets below are independent; imports and application data unrelated to discovery are omitted.

Direct calls and receivers

use MakerMill\HydraType\HydraType;

$hydra = new HydraType();
$hydra->hydrate(User::class, $data);

$mapper = $hydra;
$mapper->hydrator(Invoice::class);

(new HydraType())->hydrateMany(Order::class, $rows);

function import(HydraType $hydra, array $data): Invoice
{
    return $hydra->hydrate(Invoice::class, $data);
}

final class Importer
{
    public function __construct(private HydraType $hydra)
    {
    }

    public function import(array $data): User
    {
        return $this->hydra->hydrate(User::class, $data);
    }
}

// Discovers:
// - Invoice
// - Order
// - User

Function wrappers

Class targets can flow through local assignments and directly resolved functions and methods, including wrapper chains across the scanned files:

use MakerMill\HydraType\HydraType;

function hydrateRequest(string $className, array $data): object
{
    $hydra = new HydraType();
    $target = $className;

    return $hydra->hydrate($target, $data);
}

function post(string $url, string $requestClass): void
{
    hydrateRequest($requestClass, requestData());
}

post('/users', User::class);

// Discovers User.

This discovers User without requiring the routing function to know anything about the discovery tool. Positional and named arguments, parameter defaults, imported function aliases, global-function fallback, and multiple call sites are supported.

Method wrappers

Instance methods are resolved when their receiver is statically tied to the declaring class:

final class Rest
{
    public function __construct(private HydraType $hydra)
    {
    }

    public function post(string $url, string $className): void
    {
        $this->hydra->hydrate($className, requestData());
    }
}

$rest = new Rest(new HydraType());
$router = $rest;

$router->post('/users', User::class);
(new Rest(new HydraType()))->post('/orders', Order::class);

// Discovers:
// - Order
// - User

Supported method receivers are a direct new Rest() expression, a local variable assigned from it, a local alias of that variable, $this for a method in the current class, and a direct static class reference. The supplied source paths form the analysis boundary; only direct calls visible within those files can contribute parameter values.

Constructor wrappers

Arguments passed during direct construction flow into the declared constructor:

final class Endpoint
{
    public function __construct(string $className)
    {
        (new HydraType())->hydrator($className);
    }
}

new Endpoint(User::class);
new Endpoint(className: Order::class);

// Discovers:
// - Order
// - User

Unpacked arguments

Statically shaped argument arrays retain positional and named argument meaning:

$arguments = [
    'className' => User::class,
    'data' => $data,
];

$alias = $arguments;
$hydra->hydrate(...$alias);

(new Rest(new HydraType()))->post(...[
    'url' => '/orders',
    'className' => Order::class,
]);

// Discovers:
// - Order
// - User

Directly resolved array parameters and variadic parameters can forward the complete pack:

function hydrateRequest(string $url, string $className): void
{
    (new HydraType())->hydrate($className, requestData());
}

function post(...$arguments): void
{
    hydrateRequest(...$arguments);
}

post('/users', User::class);

// Discovers User.

Variadic methods may instead consume literal positional or named offsets directly:

public function post(...$arguments): void
{
    $className = $arguments[1];
    $this->hydra->hydrate($className, requestData());
}

$className = $arguments['className'];

Flat destructuring is also supported:

[$url, $className] = $arguments;
['className' => $className] = $arguments;

Nested static array spreads are expanded. Unknown spreads, computed arrays, and packs modified after their initial assignment remain dynamic. Computed offsets such as $arguments[$key] are not resolved.

Closures and arrow functions

Known HydraType receivers and class targets retain their meaning when captured:

$hydra = new HydraType();
$className = User::class;

$callback = function (array $data) use ($hydra, $className): object {
    return $hydra->hydrate($className, $data);
};

$arrow = fn (array $data): object => $hydra->hydrate($className, $data);

// Discovers User.

Ternary and match targets

Finite result expressions contribute every possible class:

$className = $admin
    ? Admin::class
    : User::class;

$hydra->hydrate($className, $data);

$hydra->hydrate(
    match ($type) {
        'invoice' => Invoice::class,
        'order' => Order::class,
    },
    $data,
);

// Discovers:
// - Admin
// - Invoice
// - Order
// - User

Known and unresolved outcomes can coexist:

$className = match ($type) {
    'user' => User::class,
    default => $runtimeClass,
};

$hydra->hydrate($className, $data);

// Discovers User and reports one dynamic target.

Scanned class constants

Class constants may refer to supported targets or other scanned class constants:

final class RequestTypes
{
    public const USER = User::class;
    public const DEFAULT_REQUEST = self::USER;
}

$hydra->hydrate(RequestTypes::DEFAULT_REQUEST, $data);

// Discovers User.

Constant declarations and usages may be in different scanned files. Cycles are reported as dynamic rather than followed indefinitely.

Other supported forms include nullable and union-typed HydraType parameters, typed static properties, fully qualified and imported class names, self::class, parent::class, literal class strings, and the named className: argument.

Discovery uses PHP syntax only. It does not load the application's Composer autoloader, reflect classes, or require HydraType itself as a package dependency.

Boundaries

HydraType Tools is a focused syntax-discovery tool, not a general-purpose PHP analyzer such as PHPStan. It follows statically visible class values through straightforward local assignments and directly resolvable function, method, and constructor calls within the supplied source files.

It does not execute application code or resolve service containers, factories, arbitrary return values, dynamic dispatch, or runtime object state. Class-target values stored in object properties, computed or mutated arrays, traversables, calls such as array_merge(), or statement-level control flow may therefore remain unresolved. Unpacking is followed only while the argument pack retains a statically visible shape.

Dynamic targets are reported once the HydraType receiver has been recognized. Unsupported receiver forms cannot be diagnosed reliably. Dead code is scanned; runtime reachability is not analyzed.

Exit statuses

  • 0: manifest written successfully, possibly with non-fatal dynamic warnings
  • 1: invalid arguments, unreadable input, parse failure, or output failure
  • 2: dynamic target found with --fail-on-dynamic

Parse and strict-dynamic failures leave an existing manifest untouched.

Development

Install dependencies and run all checks:

composer install
composer check

The checks match the main HydraType project:

  • Pest
  • PHPStan level 10
  • PHPCS with the modified PSR-12 ruleset
  • GrumPHP

To verify against the minimum supported PHP version, build the supplied PHP 8.2 image and use the Docker Composer scripts:

docker build -t php82-cli:latest .
composer check:docker