paloma/client-bundle
Paloma Client Bundle is a PHP client integration for connecting your application to Paloma services. It provides simple configuration and client wrappers to reduce boilerplate when working with Paloma APIs in Laravel/PHP projects.
A 2.3.0 tag now exists, which means the package is not frozen after all — but calibrate before celebrating: the entire release is one added field (company on Customer). That is the signal of a package on life support, not an actively maintained one. Treat it as low-activity, single-maintainer code rather than dead: the maintenance ceiling has lifted slightly, the security posture has not. There is still one maintainer, one test suite of unknown coverage, and a dependency graph pinned to a PHP 7 / Symfony 3.4–4.x era.
The steps below assume one of two situations: you are maintaining an existing app that already depends on the bundle, or you have been handed it by a team that pointed at the 2.3.0 tag as evidence it is fine. For a greenfield project, skip to Gotchas and use symfony/http-client directly — the new tag does not change that advice.
1. Confirm compatibility before requiring it
composer show paloma/client-bundle --all # check the version constraints
Compare the require block against your PHP and Symfony versions. A 2019-era bundle will almost certainly cap symfony/* at ^3.4|^4.0 and PHP at ^7.1. 2.3.0 added a field; it almost certainly did not widen those constraints, so PHP 8 / Symfony 6 remains a hard install block. Pin explicitly:
composer require paloma/client-bundle:^2.3
2. Install and register
Symfony 3.4 (app/AppKernel.php) or Symfony 4 with config/bundles.php:
// config/bundles.php
return [
// ...
Paloma\ClientBundle\PalomaClientBundle::class => ['all' => true],
];
3. Read the bundle's own config schema first — do not trust the README
Documentation for a package this quiet is almost always stale, and a one-line changelog tells you nothing about how to wire it. The authoritative source is:
vendor/paloma/client-bundle/DependencyInjection/Configuration.php — the real config keys, defaults and required nodes.vendor/paloma/client-bundle/Resources/config/services.xml (or .yml) — the service ids and aliases you can autowire.vendor/paloma/client-bundle/DependencyInjection/PalomaClientExtension.php — how the config is mapped onto the service arguments.vendor/paloma/client-bundle/Tests/ — the only working usage example you have.4. Minimal configuration
# config/packages/paloma_client.yaml
paloma_client:
base_uri: '%env(PALOMA_BASE_URI)%'
timeout: 5
# verify the exact key names in Configuration.php before copying these
PALOMA_BASE_URI=https://paloma.internal.example.com
5. First use case: smoke-test the client in a console command
Wire it into a throwaway command before touching any real feature. This tells you in thirty seconds what the client's response type actually is (array, PSR-7 ResponseInterface, or a custom DTO), which the docs rarely state clearly.
// src/Command/PalomaSmokeTestCommand.php
protected function configure(): void
{
$this->setName('app:paloma:smoke')->addArgument('path');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// replace with the id found in services.xml
$client = $this->getContainer()->get('paloma.client');
dump($client->get($input->getArgument('path'))); // learn the return shape
return 0;
}
If the endpoint you smoke-test returns a Customer, dump it too — 2.3.0 changed that class, and you want to see how company materialises (populated, null, or absent from the array) before you build on it.
The single most valuable pattern is unchanged, and 2.3.0 is a good reminder of why: a released change to a vendor DTO should be a one-line diff in one adapter, not a grep across forty call sites. Introduce one interface your application owns and let the bundle implement it behind that interface.
namespace App\Paloma;
interface PalomaGateway
{
public function fetch(string $path, array $query = []): array;
public function send(string $path, array $payload): void;
}
final class BundlePalomaGateway implements PalomaGateway
{
public function __construct(private object $client) {} // the bundle's service
public function fetch(string $path, array $query = []): array
{
try {
return $this->client->get($path, ['query' => $query])->toArray();
} catch (\Throwable $e) {
throw new PalomaUnavailable($path, previous: $e);
}
}
public function send(string $path, array $payload): void
{
$this->client->post($path, ['json' => $payload]);
}
}
Register it with an alias so autowiring resolves the interface:
services:
Paloma\ClientBundle\Client\PalomaClient: '@paloma.client' # adjust to real id
App\Paloma\PalomaGateway:
class: App\Paloma\BundlePalomaGateway
App\Paloma\BundlePalomaGateway:
arguments: ['@paloma.client']
Customer::company at the boundary2.3.0 adds a company field to Customer. Depending on how the bundle hydrates, that shows up as one of three things: a new constructor argument, a mutable property defaulting to null, or a key that only appears when the API returns it. Do not let any of those leak into your domain. Hydrate into your own read model inside the gateway and default the field explicitly:
final class CustomerView
{
public function __construct(
public readonly string $id,
public readonly string $name,
public readonly ?string $company, // new in 2.3.0; may be null on older payloads
) {}
}
// inside BundlePalomaGateway
private function toView(array $row): CustomerView
{
return new CustomerView(
id: $row['id'],
name: $row['name'],
company: $row['company'] ?? null, // never assume the upstream always sends it
);
}
This is the whole point of the adapter: when 2.4.0 adds vatNumber, you change one mapping function.
Retries and circuit breaking live above the client, not inside it. Nothing in the 2.3.0 changelog suggests a retry strategy was added, so decorate your gateway:
final class RetryingPalomaGateway implements PalomaGateway
{
public function __construct(
private PalomaGateway $inner,
private int $attempts = 3,
) {}
public function fetch(string $path, array $query = []): array
{
for ($try = 1; ; $try++) {
try {
return $this->inner->fetch($path, $query);
} catch (PalomaUnavailable $e) {
if ($try >= $this->attempts) {
throw $e;
}
usleep(100_000 * 2 ** $try);
}
}
}
}
Logging deserves its own Monolog channel so you can silence a noisy upstream without muting the whole app:
monolog:
channels: ['paloma']
services:
App\Paloma\BundlePalomaGateway:
arguments:
- '@paloma.client'
- '@monolog.logger.paloma'
Old Guzzle-based clients ship MockHandler. That is your only sane unit-testing route — do not hit the network in tests. Note that a Customer fixture built against pre-2.3.0 shapes will now be missing company; make sure your fixtures include it, or your ?? null path goes untested.
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
$mock = new MockHandler([
new Response(200, [], json_encode([
['id' => 'c1', 'name' => 'Acme', 'company' => 'Acme Holdings'],
])),
]);
$client = new \Paloma\ClientBundle\Client\PalomaClient([
'handler' => HandlerStack::create($mock),
]);
$gateway = new BundlePalomaGateway($client);
self::assertSame('Acme Holdings', $gateway->fetch('/customers')['items'][0]['company'] ?? null);
If the client does not accept a handler stack, bypass the bundle in tests and mock your PalomaGateway interface at the boundary instead — which is the pattern you should prefer anyway.
2.3.0 is a maintenance signal, not a health signal. One added field after years of silence says a single consumer needed a single column. It does not mean CVEs are being triaged, PHP 8 constraints are being widened, or Symfony 5+ is on the roadmap. Keep the migration ticket in the backlog; just remove the word "archived" from it.
Check whether Customer::company was inserted or appended. This is the one real compatibility risk in this release. If Customer takes positional constructor arguments and company went in the middle, then any code doing new Customer($id, $name, ...) — including your own test fixtures and any hydrator — silently shifts arguments. Read vendor/paloma/client-bundle/Model/Customer.php (or Entity/) and diff the constructor. Appended with a default is safe; anything else needs a changelog entry you probably did not get.
New fields change outbound payloads. If you serialise Customer back to Paloma or forward it to another service, toArray() / jsonSerialize() now emits company. Downstream consumers with strict schemas (additionalProperties: false in JSON Schema, strict Doctrine hydration, Avro/Protobuf) will start rejecting payloads that used to pass. Grep for the DTO before deploying.
The default verify behaviour is unknown and probably wrong for your context. Old Guzzle-based clients often default to verify => true against a private CA it does not know about, producing an SSL certificate problem that looks like a network fault. Conversely, someone may have set verify => false in dev and never reverted it. Check Configuration.php for the key and pin it explicitly per environment.
Config key names will not match the README. The Configuration.php tree node names are the contract. If your YAML silently does nothing, Symfony ignored an unrecognised key without error — run bin/console debug:config paloma_client to see what actually loaded.
Service ids may not be aliased for autowiring. If debug:autowiring paloma returns nothing, add the alias yourself in services.yaml rather than injecting by string id everywhere.
Private services in Symfony 4. If the bundle's services are declared public: false and you rely on $container->get('paloma.client') in a controller, it will throw. Move to constructor injection.
No timeout by default means hung workers. Set an explicit timeout and, if supported, a separate connect_timeout. Under PHP-FPM a missing timeout turns one slow upstream into a fleet-wide 502 storm.
Debugging. Enable framework.profiler in dev and look for a data collector (check Resources/config/ for a DataCollector tag); if present you get full request/response in the web toolbar. If not, dump() inside your gateway decorator, never inside vendor code.
Extension points remain limited and undocumented. You get constructor options via bundle config and service decoration in your own container. Do not subclass the bundle's client or its DTOs — 2.3.0 just demonstrated that those classes move without notice, and a subclass pins you to whatever shape existed when you wrote it.
Migration path. The escape hatch is symfony/http-client, which is PSR-18 compatible and maintained:
$response = $this->http->request('GET', $this->baseUri . $path, [
'query' => $query,
'timeout' => 5,
])->toArray();
Because your application only talks to PalomaGateway, this is a new class plus one container alias — no call-site changes, and the company field is handled in the mapping function you already rewrote. Do it as soon as the bundle's PHP constraint blocks an upgrade; it will, and the 2.3.0 tag does not change that timeline.
How can I help you explore Laravel packages today?