tilleuls/ovh-bundle
Symfony bundle integrating the OVH API SDK, providing configured services and dependency injection for calling OVH endpoints from PHP applications. Simplifies API client setup and authentication within Symfony.
Reality check first: tilleuls/ovh-bundle is a Symfony bundle, not a Laravel package. It has no service provider, no facade, and its composer.json targets Symfony 3/4/5 and PHP 7.x (last release 2020-08-11). For a Laravel app the practical move is to consume the underlying SDK — ovh/ovh — and copy the bundle's configuration shape. The bundle itself is worth reading as documentation, not installing.
Where to look first in the repo:
DependencyInjection/Configuration.php — shows the actual knobs: endpoint_name, application_key, application_secret, consumer_key.Resources/config/services.xml — shows there is essentially one useful object: a configured Ovh\Api instance.CoopTilleulsOvhBundle.php — trivial; nothing else is hooked.Minimal steps in Laravel:
composer require ovh/ovh
php artisan make:provider OvhServiceProvider
config/ovh.php:
return [
'endpoint' => env('OVH_ENDPOINT', 'ovh-eu'),
'application_key' => env('OVH_APPLICATION_KEY'),
'application_secret' => env('OVH_APPLICATION_SECRET'),
'consumer_key' => env('OVH_CONSUMER_KEY'),
];
app/Providers/OvhServiceProvider.php:
public function register(): void
{
$this->app->singleton(\Ovh\Api::class, function ($app) {
$c = $app['config']->get('ovh');
return new \Ovh\Api(
$c['application_key'],
$c['application_secret'],
$c['endpoint'], // ovh-eu | ovh-ca | kimsufi-eu | soyoustart-eu
$c['consumer_key']
);
});
}
First use case: verify credentials, then a real read.
$ovh = app(\Ovh\Api::class);
$me = $ovh->get('/me'); // sanity check
$rec = $ovh->get('/domain/zone/example.com/record'); // list DNS records
Wrap the SDK behind your own class. This is the single most valuable pattern. Symfony users inject Ovh\Api directly; that's fine there, but in Laravel you want an interface for testing and for adding retries/logging.
interface OvhClient
{
public function me(): array;
public function dnsRecords(string $zone): array;
public function createDnsRecord(string $zone, array $payload): array;
}
final class SdkOvhClient implements OvhClient
{
public function __construct(private \Ovh\Api $api) {}
public function me(): array { return $this->api->get('/me'); }
public function dnsRecords(string $zone): array
{
return $this->api->get("/domain/zone/{$zone}/record");
}
public function createDnsRecord(string $zone, array $payload): array
{
return $this->api->post("/domain/zone/{$zone}/record", $payload);
}
}
Bind OvhClient to SdkOvhClient in the provider, and inject OvhClient everywhere.
Credential bootstrap as an artisan command. OVH requires a consumer key obtained via a token request:
$ovh = new \Ovh\Api($appKey, $appSecret, 'ovh-eu');
$access = $ovh->requestCredentials([
['method' => 'GET', 'path' => '/domain/zone/*'],
['method' => 'POST', 'path' => '/domain/zone/*'],
], 'https://your-app.test/ovh/callback');
// $access['consumerKey'], $access['validationUrl']
Ship this as php artisan ovh:request-token and store the returned consumerKey out-of-band. Never build it at request time.
One client per account/tenant. The bundle assumes a single configuration tree, so multi-tenant OVH usage is awkward under it. In Laravel, register a factory and cache clients by tenant:
class OvhClientFactory
{
protected array $clients = [];
public function for(string $tenant): \Ovh\Api
{
return $this->clients[$tenant] ??= new \Ovh\Api(
config("tenants.$tenant.ovh.app_key"),
config("tenants.$tenant.ovh.app_secret"),
config("tenants.$tenant.ovh.endpoint"),
config("tenants.$tenant.ovh.consumer_key"),
);
}
}
Queue the writes. OVH calls (orders, DNS updates, email migrations) are slow and rate-limited. Put them in jobs and use ->timeout(60); the default Guzzle client in the SDK has no per-request timeout until you supply one.
Inject a Guzzle client when you need observability. Ovh\Api accepts a ClientInterface as one of its trailing constructor args (verify against your installed version). Use it to add logging middleware and a sane connect_timeout/timeout.
ovh/ovh directly; the bundle contributes zero runtime behaviour beyond wiring an Ovh\Api service and a coop_tilleuls_ovh config tree.Invalid signature or Timestamp expired. The SDK caches the OVH server time, so drift after boot still bites. Run chrony/ntpd on the host./domain/zone/* cannot call /me or /email/*. Mint narrow keys per feature and per tenant; keep them in .env or a secret manager, never in a config file tracked by git (the bundle's YAML config is easy to leak).config:cache freezes credentials. Since config/ovh.php reads env(), changing .env requires php artisan config:clear. Rotating keys without redeploying cache is a common self-inflicted 403.\Ovh\Exceptions\NotCredentialException (bad/missing key), \Ovh\Exceptions\InvalidParameterException (malformed payload — 400, no retry), \Ovh\Exceptions\ApiException (everything else; inspect getResponse() for body and status). Don't blanket-catch \Throwable and retry — OVH POSTs like POST /order/* are not idempotent.X-Ovh-* headers but no generic idempotency key).Ovh\Api is a concrete class with no interface, and the bundle does not ship fakes. Bind OvhClient to a fake in tests; never bind a mock \Ovh\Api unless you're prepared to stub all the constructor side effects.https://eu.api.ovh.com/console (or ca.api.ovh.com) lets you replay calls with the same credentials and paths. Confirm the call works there before blaming your code.coop_tilleuls_ovh configuration section, and the OVH service definition in Resources/config/services.xml — which you can override via a compiler pass or by aliasing Ovh\Api in your own services.yaml. There is no event dispatching, no named-client support, and no factory — anything beyond one account means writing your own wiring regardless.ovh-eu, ovh-ca, kimsufi-eu, soyoustart-eu (and their -ca variants) are the recognised values. Passing an URL or a bare eu silently produces confusing 4xx errors./domain/zone/{zone}/record returns arrays with id, fieldType, subDomain, target, ttl. Normalise to DTOs in your wrapper so your app isn't coupled to OVH's JSON.POST /domain/zone/{zone}/refresh (and POST .../refresh on the zone after deletes) for changes to go live. This trips up nearly every first integration, and the bundle gives no hint that it's needed.How can I help you explore Laravel packages today?