zjkiza/http-response-validator

A Symfony bundle for HTTP responses validating using a simple Result monad and handler chains.

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

Type:symfony-bundle

pkg:composer/zjkiza/http-response-validator

v0.7.1 2025-12-11 14:56 UTC

This package is auto-updated.

Last update: 2025-12-11 18:40:50 UTC


README

0o# Http Response Validator Bundle

A Symfony bundle for HTTP responses validating using a simple Result monad and handler chains.

The main idea: the input is ResponseInterface (eg from symfony/http-client), then through a series of handlers (pipelines) the status code is validated, the content is logged, JSON is extracted and the structure is checked. Each handler returns a `Result', so the chain breaks on the first error and throws an exception with a unique message ID.

Characteristics

  • Declarative stacking steps over Result::success(...)->bind(...)
  • Built-in ready-to-use handlers:
    • ZJKiza\HttpResponseValidator\Handler\HttpResponseLoggerHandler – validates the expected status, logs the response, and masks sensitive keys
    • ZJKiza\HttpResponseValidator\Handler\ExtractResponseJsonHandler – decodes the JSON body (associatively or as an object)
    • ZJKiza\HttpResponseValidator\Handler\ArrayStructureValidateExactHandler – validates the structure of the response using strict/exact key and type checking
    • ZJKiza\HttpResponseValidator\Handler\ArrayStructureValidateInternalHandler – validates the structure using internal/relaxed rules for key existence and potential type checking
  • Simple extension: add your own handler and register it with a single service tag
  • Clear error messages with Message ID=<hex> for easy tracking in logs

Installation

Add "zjkiza/http-response-validator" to your composer.json file.

composer require zjkiza/http-response-validator

Symfony integration

Bundle wires up all classes together and provides method to easily setup.

  1. Register bundle within your configuration (i.e: bundles.php).
<?php

declare(strict_types=1);

return [
    // ...
    ZJKiza\HttpResponseValidator\ZJKizaHttpResponseValidatorBundle::class => ['all' => true],
];
  1. Bundle automatically registers services and factory class for handlers via service tag zjkiza.http_response_validate.handler_factory.

Quick start

Example with symfony/http-client and built-in handlers:

use Symfony\Component\HttpClient\HttpClient;
use ZJKiza\HttpResponseValidator\Monad\Result;
use ZJKiza\HttpResponseValidator\Contract\HandlerFactoryInterface;
use ZJKiza\HttpResponseValidator\Handler\HttpResponseLoggerHandler;
use ZJKiza\HttpResponseValidator\Handler\ExtractResponseJsonHandler;
use ZJKiza\HttpResponseValidator\Handler\ArrayStructureValidateExactHandler;
use ZJKiza\HttpResponseValidator\Handler\ArrayStructureValidateInternalHandler;

// ... in the service/controller with DI you get a handler factory
public function __construct(private HandlerFactoryInterface $handlerFactory) {}

public function fetch(): array
{
    $client = HttpClient::create();
    $response = $client->request('GET', 'https://example.com/api/user');

    // We arrange pipeline steps; getOrThrow() will throw an exception on error
    $data = Result::success($response)
        ->bind($this->handlerFactory->create(HttpResponseLoggerHandler::class)
            ->setExpectedStatus(200)
            ->addSensitiveKeys(['password', 'token']))
        ->bind($this->handlerFactory->create(ExtractResponseJsonHandler::class)
            ->setAssociative(true))
        ->bind($this->handlerFactory->create(ArrayStructureValidateInternalHandler::class)
            ->setKeys(['id', 'email']))
        ->getOrThrow();

    // $data is now an associative array with the required keys
    return $data;
}

If a step fails, an exception will be thrown with a message containing a unique `Message ID=...', and the error will be logged via the PSR‑3 logger.

Built-in Handlers

All handlers implement ZJKiza\HttpResponseValidator\Contract\HandlerInterface and expose a fluent API for configuration.

  • HttpResponseLoggerHandler

    • What it does: Validates the expected HTTP status, logs the response, and masks the values for the defined keys in the body
    • Essential methods:
      • setExpectedStatus(int $status): self
      • addSensitiveKeys(string[] $keys): self
  • ExtractResponseJsonHandler

    • What it does: calls $response->getContent(false), decodes the JSON, and returns the result as a string or object
    • Essential methods:
      • setAssociative(bool $assoc = true): self – when true returns an associative array; when false is an object
  • ArrayStructureValidateExactHandler

    • What it does: Validates array structure with exact match: same keys as expected, possible deep (nested), optional type checks
    • Essential methods:
      • setKeys(array $structure): self – associative or indexed array describing expected structure
      • setIgnoreNulls(bool $ignoreNulls = false): self – ignore null values if set
      • setCheckTypes(bool $checkTypes = false): self – enable type checking when structure contains types
  • ArrayStructureValidateInternalHandler

    • What it does: Validates array structure with more internally permissive (flexible) rules, suitable for partial checks.
    • Essential methods:
      • setKeys(array $structure): self
      • setIgnoreNulls(bool $ignoreNulls = false): self
      • setCheckTypes(bool $checkTypes = false): self

Direct Use: ArrayStructureExactValidation and ArrayStructureInternalValidation

Validation services can be used standalone without the handler pipeline. For example, to validate data against a strict structure:

use ZJKiza\HttpResponseValidator\Validator\ArrayStructureExactValidation;
use ZJKiza\HttpResponseValidator\Validator\Helper\ErrorCollector;

$validator = new ArrayStructureExactValidation(new ErrorCollector(), $ignoreNulls = false, $checkTypes = true);

$validator->validate($structure, $data);

if ($validator->getErrorCollector()->hasErrors()) {
    // Handle or inspect `$validator->getErrorCollector()->all()`
}

For internal/permissive validation:

use ZJKiza\HttpResponseValidator\Validator\ArrayStructureInternalValidation;
use ZJKiza\HttpResponseValidator\Validator\Helper\ErrorCollector;

$validator = new ArrayStructureInternalValidation(new ErrorCollector(), $ignoreNulls = false, $checkTypes = true);

$validator->validate($structure, $data);

if ($validator->getErrorCollector()->hasErrors()) {
    // Handle or inspect errors
}

Example (Handler Pipeline)

// ...
$data = Result::success($response)
    ->bind($this->handlerFactory->create(HttpResponseLoggerHandler::class)->setExpectedStatus(200))
    ->bind($this->handlerFactory->create(ExtractResponseJsonHandler::class)->setAssociative(true))
    ->bind($this->handlerFactory->create(ArrayStructureValidateExactHandler::class)
        ->setKeys($requiredStructure)
        ->setCheckTypes(true)
        ->setIgnoreNulls(false)
    )
    ->getOrThrow();

How to add your own Handler

  1. Create a class that implements HandlerInterface (recommendation: inherit from AbstractHandler and use the TagIndexMethod trait). Example: validating the format of an email field in a string.
<?php

declare(strict_types=1);

namespace App\HttpResponse\Handler;

use ZJKiza\HttpResponseValidator\Handler\AbstractHandler;
use ZJKiza\HttpResponseValidator\Handler\Factory\TagIndexMethod;
use ZJKiza\HttpResponseValidator\Contract\HandlerInterface;
use ZJKiza\HttpResponseValidator\Monad\Result;

/**
 * @implements HandlerInterface<array<string,mixed>, array<string,mixed>>
 */
final class ValidateEmailFormatHandler extends AbstractHandler implements HandlerInterface
{
    use TagIndexMethod; // automates the static index for factory registration

    public function __invoke(mixed $value): Result
    {
        if (!isset($value['email']) || !\filter_var($value['email'], FILTER_VALIDATE_EMAIL)) {
            return $this->fail('Email is not valid..');
        }

        return Result::success($value);
    }
}
  1. Register the service and be sure to add a tag zjkiza.http_response_validate.handler_factory:
# config/services.yaml
services:
  App\HttpResponse\Handler\ValidateEmailFormatHandler:
    tags:
      - { name: 'zjkiza.http_response_validate.handler_factory' }
  1. Use it in a chain across HandlerFactoryInterface:
$result = Result::success($response)
    ->bind($handlerFactory->create(HttpResponseLoggerHandler::class)->setExpectedStatus(200))
    ->bind($handlerFactory->create(ExtractResponseJsonHandler::class)->setAssociative(true))
    ->bind($handlerFactory->create(ValidateEmailFormatHandler::class))
    ->getOrThrow();

Note: if you don't want to use the TagIndexMethod, make sure your class implements the static getIndex(): string method that returns a unique index (most commonly FQCN).

Logging in and message ID

Errors are logged via the PSR‑3 logger. Messages are automatically prefixed with Message ID=<hex> (see helper ZJKiza\HttpResponseValidator\addIdInMessage()), which facilitates correlation between logs and client messages.

Development and tests (optional for contributors)

composer phpunit
composer phpstan
composer psalm
composer php-cs-fixer

License

MIT. View the `LICENSE' file in the repository.

What else needs to be done

[ ] Update readme

[+] When ValidateArrayKeysExistHandler encounters the first error in the keys, it does not immediately throw an exception, but collects all the missing keys and only then throws the exception.