Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changelog/bundled-consumer-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: added
type: minor
---

Bundle the fast-forward-http-client consumer skill with executable examples, optional installation guidance and Composer discovery metadata.
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# fast-forward-http-client maintenance

This repository owns `fast-forward/http-client`. Read Composer metadata, source and
tests before changing public integration. Preserve unrelated work. Run
`composer install`, `composer validate --strict` and `composer dev-tools`.
Keep merge, release and deployment decisions with the maintainer. Ordinary
source/resources share this root contract; consumer procedures live in SKILL.md.

## Bundled consumer skill

Read [the consumer skill](resources/skills/fast-forward-http-client/SKILL.md) when changing application integration
examples, consumer guides or the skill. Keep its version contract, references
and executable examples aligned with the supported public API. The canonical
distributable source is `resources/skills`; it needs no installed copy or host
alias. Repository maintenance continues to follow this AGENTS.md.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,12 @@ This package is open-source software licensed under the

Contributions, issues, and pull requests are welcome at
[php-fast-forward/http-client](https://github.com/php-fast-forward/http-client).

## Consumer agent skill

The package includes [fast-forward-http-client](resources/skills/fast-forward-http-client/SKILL.md) for application integration.
It supports `fast-forward/http-client:^1.2`; inspect the installed version.
See [skill installation and lifecycle](resources/skills/fast-forward-http-client/references/installation.md) for
optional project synchronization with `llm/skills ^1.15`, guarded manual
project/global copies and a pinned Skills CLI alternative. Shipping files does
not install tooling or activate the skill in an agent session.
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
},
"require-dev": {
"fast-forward/dev-tools": "dev-main",
"llm/skills": "^1.15"
"nyholm/psr7": "^1.8"
},
"suggest": {
"llm/skills": "Optionally synchronize the bundled consumer skill in your project."
},
"minimum-stability": "stable",
"autoload": {
Expand Down Expand Up @@ -56,6 +59,9 @@
},
"grumphp": {
"config-default-path": "vendor/fast-forward/dev-tools/grumphp.yml"
},
"skills": {
"source": "resources/skills"
}
},
"scripts": {
Expand Down
19 changes: 19 additions & 0 deletions resources/skills/fast-forward-http-client/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2025 Felipe Sayão Lobato Abreu

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.
110 changes: 110 additions & 0 deletions resources/skills/fast-forward-http-client/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
name: fast-forward-http-client
description: Provide Fast Forward's Symfony-backed PSR-18 client, supply its required PSR-17 factories, and test success and transport failures without a network.
license: MIT; see LICENSE
compatibility: Supports fast-forward/http-client ^1.2 and PHP ^8.3. Requires Composer and declared extensions; inspect the installed version. Earlier releases are outside this contract.
metadata:
author: Fast Forward
api-baseline: fast-forward/http-client ^1.2
---

# Wire a PSR-18 HTTP client

Use this skill for an application consuming `fast-forward/http-client:^1.2`.
Read its instructions, manifest/lockfile and installed source before editing.
Run `composer show fast-forward/http-client`. For versions outside this contract,
inspect matching documentation; do not silently upgrade the application.

## First working integration

Requires PHP ^8.3, Composer's application autoloader and this package's runtime
dependencies. The direct example also needs `nyholm/psr7:^1.8`.
With authority to install the dependency:

```sh
composer require 'fast-forward/http-client:^1.2' 'nyholm/psr7:^1.8'
```

Create `app.php` in the application root alongside composer.json/vendor:

```php
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use FastForward\Config\ArrayConfig;
use FastForward\Container\ContainerInterface;
use FastForward\Container\ServiceProvider\ArrayServiceProvider;
use FastForward\Http\Client\ServiceProvider\HttpClientServiceProvider;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpClient\Exception\TransportException;
use function FastForward\Container\container;

$config = new ArrayConfig([ContainerInterface::class => [HttpClientServiceProvider::class]]);
$factory = new Psr17Factory();
$makeContainer = static function (MockHttpClient $transport) use ($config, $factory): ContainerInterface {
return container(
new ArrayServiceProvider([
HttpClient::class => static fn(): MockHttpClient => $transport,
ResponseFactoryInterface::class => static fn(): Psr17Factory => $factory,
StreamFactoryInterface::class => static fn(): Psr17Factory => $factory,
]),
$config,
);
};
$services = $makeContainer(new MockHttpClient(new MockResponse('{"ready":true}', ['http_code' => 200])));
$client = $services->get(ClientInterface::class);
$response = $client->sendRequest($factory->createRequest('GET', 'https://example.invalid/health'));
if ($response->getStatusCode() !== 200 || (string) $response->getBody() !== '{"ready":true}') {
throw new RuntimeException('Client response differs.');
}
$failedClient = $makeContainer(new MockHttpClient(static function (): never {
throw new TransportException('offline fixture');
}))->get(ClientInterface::class);

echo "integration ready\n";
```

Run `php app.php`. It throws on a wrong result; success prints
`integration ready`. It uses no production data and starts no server.

## Wiring and failure boundaries

The primary product is `FastForward\Http\Client\ServiceProvider\HttpClientServiceProvider`. Place that class in ArrayConfig under `FastForward\Container\ContainerInterface::class`, pass the config to `FastForward\Container\container($config)`, and request `Psr\Http\Client\ClientInterface`. It uses Symfony HttpClient::create and Psr18Client. A provider-only application may pass the provider class directly.

This package requires PSR-17 interfaces but does not ship a concrete message implementation or factory provider. In the application install `fast-forward/http-factory:^1.2` and register `FastForward\Http\Message\Factory\ServiceProvider\HttpMessageFactoryServiceProvider::class` before the client provider. Alternatively supply ResponseFactoryInterface and StreamFactoryInterface through your own ArrayServiceProvider. The direct example needs `nyholm/psr7:^1.8` explicitly; MockHttpClient/MockResponse come from the required Symfony component, not a test-only package.

Put custom factories/transport in an earlier ArrayServiceProvider; first successfully resolving source wins. Provider services are cached. A missing PSR-17 factory can make get(ClientInterface::class) fail even if has() is true. The helper constructs the composed container; your application passes the resolved client to services that send requests.

Read and validate application configuration before constructing a custom Symfony transport; this provider has no invented timeout/environment config key. For custom timeout/base URI/auth choose HttpClient::create($options) in an explicit factory. PSR-18 HTTP 4xx/5xx responses are returned normally; catch ClientExceptionInterface for client/transport failures. Keep credentials in the application's secret mechanism and avoid real network calls in tests.

The helper tries aggregate sources in registration order. A PSR container or
not-found exception from one source permits fallback to another source; other
throwables propagate. Source priority is therefore the first successful resolution,
not a guarantee that an earlier broken factory terminates lookup. Nested provider
list errors during helper composition are caught; if services are absent, inspect
the raw configuration and resolve the provider directly to diagnose the failure.

## Verify integration

Run the [complete example](examples/integration.php) against the application's
autoloader in a disposable process. It checks success and negative cases:

```sh
php /path/to/fast-forward-http-client/examples/integration.php ./vendor/autoload.php
```

Expected final marker: `fast-forward-http-client: ok`. Keep application-specific regression
tests in its existing PHPUnit suite, with each fixture class in its same-named
file. File copying, host discovery and completed agent tasks are different checks.
For optional copying/synchronization load
[installation and lifecycle](references/installation.md).
49 changes: 49 additions & 0 deletions resources/skills/fast-forward-http-client/examples/integration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

require $argv[1] ?? dirname(__DIR__, 4) . '/vendor/autoload.php';

use FastForward\Config\ArrayConfig;
use FastForward\Container\ContainerInterface;
use FastForward\Container\ServiceProvider\ArrayServiceProvider;
use FastForward\Http\Client\ServiceProvider\HttpClientServiceProvider;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpClient\Exception\TransportException;
use function FastForward\Container\container;

$config = new ArrayConfig([ContainerInterface::class => [HttpClientServiceProvider::class]]);
$factory = new Psr17Factory();
$makeContainer = static function (MockHttpClient $transport) use ($config, $factory): ContainerInterface {
return container(
new ArrayServiceProvider([
HttpClient::class => static fn(): MockHttpClient => $transport,
ResponseFactoryInterface::class => static fn(): Psr17Factory => $factory,
StreamFactoryInterface::class => static fn(): Psr17Factory => $factory,
]),
$config,
);
};
$services = $makeContainer(new MockHttpClient(new MockResponse('{"ready":true}', ['http_code' => 200])));
$client = $services->get(ClientInterface::class);
$response = $client->sendRequest($factory->createRequest('GET', 'https://example.invalid/health'));
if ($response->getStatusCode() !== 200 || (string) $response->getBody() !== '{"ready":true}') {
throw new RuntimeException('Client response differs.');
}
$failedClient = $makeContainer(new MockHttpClient(static function (): never {
throw new TransportException('offline fixture');
}))->get(ClientInterface::class);
try {
$failedClient->sendRequest($factory->createRequest('GET', 'https://example.invalid/health'));
throw new RuntimeException('Transport failure unexpectedly succeeded.');
} catch (ClientExceptionInterface) {
}

echo "fast-forward-http-client: ok\n";
Loading
Loading