graham-campbell/cloudflare-api
A Laravel/PHP client for the Cloudflare API, providing a simple, expressive wrapper for managing zones, DNS records, caching, and other Cloudflare resources through a clean, framework-friendly interface.
Reality check before line one: this is a retired package living under a GrahamDeprecated namespace, last shipped as 0.7.0-alpha whose headline change was "upgraded to Laravel 5.0." Day-to-day you will not be leveraging it so much as operating it inside an existing Laravel 5.0–5.4 app. Everything below assumes that constraint.
1. Pin, don't range. There are no patch releases coming, so a floating constraint is a liability:
// composer.json
"minimum-stability": "alpha",
"prefer-stable": true,
"require": {
"graham-campbell/cloudflare-api": "0.7.0-alpha"
}
If swallowing minimum-stability: alpha for the whole graph is unacceptable, use a path or VCS repository pointing at a frozen commit instead.
2. Register manually. Laravel 5.0–5.4 has no package auto-discovery:
// config/app.php
'providers' => [
GrahamCampbell\CloudFlareApi\CloudFlareApiServiceProvider::class,
],
'aliases' => [
'CloudFlareApi' => GrahamCampbell\CloudFlareApi\Facades\CloudFlareApi::class,
],
php artisan vendor:publish --provider="GrahamCampbell\CloudFlareApi\CloudFlareApiServiceProvider"
3. Fill in the connection config (published as config/cloudflare-api.php): a default connection name plus a connections map of email + key. Multi-connection is the whole point of the underlying graham-campbell/manager — set up staging and production entries from day one rather than retrofitting later.
4. Look at three files first, in this order:
connection($name = null), which is the only surface you should touch.CloudFlareApiManager) — resolves a name to a configured Cloudflare client. This is the seam.5. First real use case: a credential smoke test, not a DNS write. Run this in tinker against a scratch zone before wiring anything:
$manager = CloudFlareApi::connection();
$zones = new \Cloudflare\API\Endpoints\Zones($manager->getClient());
foreach ($zones->listZones()->result as $zone) {
dump($zone->id, $zone->name);
}
If that returns zones, auth and connection resolution work. If it throws, you know it's credentials or connection name — and you haven't mutated anything. Turn this into an artisan cloudflare:ping command so it's repeatable in every environment.
Never call the facade from a controller or a model. One adapter class per concern, and the SDK endpoint injected — this is the single seam you'll swap when you migrate to cloudflare/sdk:
class CloudflareDnsService
{
public function __construct(private \Cloudflare\API\Endpoints\DNS $dns) {}
public function upsert(string $zoneId, string $type, string $name, string $content, bool $proxied = true): void
{
$existing = collect($this->dns->listRecords($zoneId, $name, null, $type)->result);
$payload = ['type' => $type, 'name' => $name, 'content' => $content, 'ttl' => 1, 'proxied' => $proxied];
$existing->isNotEmpty()
? $this->dns->updateRecord($zoneId, $existing->first()->id, $payload)
: $this->dns->addRecord($zoneId, $type, $name, $content, 1, $proxied);
}
}
Bind the endpoint classes in your own service provider so nothing downstream knows the manager exists:
$this->app->bind(\Cloudflare\API\Endpoints\DNS::class, fn ($app) =>
new \Cloudflare\API\Endpoints\DNS($app['cloudflare-api']->connection()->getClient())
);
Cache the zone IDs. Zone lookups are a network call you don't need on every request. Resolve domain → zone ID once, store it (config, DB column, or cache key), and refresh only on a miss. This removes most of your API traffic.
Queue cache purges. The bread-and-butter daily flow — save a page, purge its zone — must never be inline:
class PurgeCloudflareCache implements ShouldQueue
{
public function __construct(private string $zoneId) {}
public function handle(\Cloudflare\API\Endpoints\Zones $zones): void
{
$response = $zones->cachePurgeEverything($this->zoneId);
if (! $response->success) {
logger()->error('CF purge failed', ['errors' => $response->errors]);
$this->release(60);
}
}
}
Fire it from an Eloquent observer (saved on your page/post model), not from the controller.
Custom-domain onboarding as an explicit state machine. This is the highest-value flow the package nominally serves, and also where its lack of maintenance bites hardest. Model it as states — pending → zone_created → nameservers_set → active → record_created → ssl_set — storing the current state on the tenant record. Each transition is a queued job that is safe to retry. Do not write it as a linear method; a partial failure at step 3 must be resumable, not restartable.
Multi-tenant connection switching. If each customer has their own Cloudflare account, resolve the connection name per tenant at runtime and pass it explicitly:
$client = CloudFlareApi::connection($tenant->cloudflare_connection)->getClient();
Keep a tenant→connection mapping table; don't rely on the config default.
Testing. Mock the manager and the endpoint bindings in the container and assert your adapter's payloads — never hit Cloudflare in the suite. Keep exactly one contract test behind @group cloudflare-live and an env flag, pointed at a scratch zone. That test is your only early warning that the abandoned wrapper has drifted from Cloudflare's response shapes.
Ops command. Add cloudflare:records {zone} and cloudflare:purge {zone} artisan commands early. When someone reports a stale page at 6pm, you want a one-liner, not a tinker session.
success: false with HTTP 200. Cloudflare wraps errors in the body. ->success, ->errors, ->messages must be checked on every call — the SDK will happily hand you a transport-level success that means "record rejected." Silent DNS failures during onboarding are the classic symptom.listZones() / listRecords() return a page, not everything. Your upsert will create duplicates if the existing record was on page 2. Loop on result_info->page / total_pages, or pass an explicit per_page, before concluding a record is absent.status is pending until nameservers propagate — minutes to hours. Poll with backoff; the "create zone then immediately add a proxied A record and set SSL Full" sequence fails on brand-new zones and produces confusing errors.CLOUDFLARE_KEY won't take effect until php artisan config:cache runs again. Put it in the deploy script, or key rotation silently becomes auth failures at 2am.-alpha version string fails stable constraints. composer update in any sibling package can refuse the graph. Pin exactly; don't try to widen it.Provider class references across app, config, and any published config before touching composer. Assume a full namespace rewrite and config republish.X-Auth-Email / X-Auth-Key. There is no evidence of scoped API Token (Authorization: Bearer) support. If your security posture requires scoped tokens, this is where you stop rather than where you patch — don't build a shim around abandoned auth plumbing.$client->getConfig('headers') during staging setup. It instantly separates "wrong credentials" from "wrong connection resolved," which is otherwise ten minutes of staring at a 403.ttl => 1 means "automatic," and proxied => true is what actually turns the orange cloud on. Both are easy to leave out of a payload and both are the difference between "we set up DNS" and "the site loads."foreach over 500 DNS records in an unthrottled job will start returning 429s that look like random failures. Batch, sleep, and respect the limit.\Cloudflare\API\Endpoints\* classes. Do not subclass CloudFlareApiManager — nothing is merging upstream and you'll own the divergence forever.email + global key. Ensure neither ends up in Laravel's exception context or your queue payloads.How can I help you explore Laravel packages today?