graham-campbell/cloudflare
A Laravel/PHP integration for the Cloudflare API that provides a service provider and facade for managing zones, DNS records, caching, and other Cloudflare resources from your application through a clean, expressive interface.
Install and wire up.
composer require graham-campbell/cloudflare
On Laravel 5.5+ the CloudflareServiceProvider and Cloudflare facade are auto-discovered. On older apps add them manually in config/app.php:
GrahamCampbell\Cloudflare\CloudflareServiceProvider::class,
// aliases
'Cloudflare' => GrahamCampbell\Cloudflare\Facades\Cloudflare::class,
Publish the config and add credentials.
php artisan vendor:publish --provider="GrahamCampbell\Cloudflare\CloudflareServiceProvider"
In .env, use either the legacy global key + email pair or a scoped API token:
[email protected]
CLOUDFLARE_KEY=your-global-api-key
# OR, preferred:
CLOUDFLARE_TOKEN=your-scoped-api-token
Look here first. Open config/cloudflare.php — that's the whole public surface of the package. The facade resolves to the underlying cloudflare/sdk adapter, so all real documentation lives in the cloudflare/sdk README and the Cloudflare\API\Endpoints\* classes, not in this wrapper. When in doubt, read the endpoint class you're calling.
First useful call (tinker). List zones, resolve one by name, then read its DNS records:
use GrahamCampbell\Cloudflare\Facades\Cloudflare;
Cloudflare::zones()->listZones('example.com');
$zoneId = Cloudflare::zones()->getZoneID('example.com');
Cloudflare::dns()->listRecords($zoneId);
That listZones → getZoneID → dns() sequence is the backbone of almost every real workflow.
Prefer constructor injection over the facade in services. The container binds the SDK adapter as a singleton, so this is clean and mockable:
use Cloudflare\API\Adapter\Adapter;
class CloudflareDns
{
public function __construct(private Adapter $cloudflare) {}
public function records(string $zone): array
{
$zoneId = $this->cloudflare->zones()->getZoneID($zone);
return $this->cloudflare->dns()->listRecords($zoneId);
}
}
Use the Cloudflare facade only in Artisan commands, tinker, and controllers where injection adds noise.
Cache zone IDs. getZoneID($name) is an extra API round-trip on every call. Resolve once, cache per zone name (config or cache store), and invalidate on zone changes. This alone removes a large chunk of your rate-limit pressure.
Wrap the SDK in your own thin domain service. The SDK methods are chatty — addRecord($zoneId, $type, $name, $content, $ttl, $proxied). A DnsRecord value object plus upsertRecord() / ensureRecord() helpers keeps call sites readable and gives you a single place to add logging and retries.
Reconcile, don't blind-write. For sync-style workflows (deploy pipelines, dynamic DNS, multi-tenant provisioning), list existing records, diff against the desired set, then addRecord / updateRecord / deleteRecord only what changed. Cloudflare is happy to accept duplicates — your code has to prevent them.
Deploy-time and scheduled use cases.
php artisan cloudflare:sync-dns) invoked in CI after a deploy.$schedule->command('cloudflare:ddns')->everyFiveMinutes()).Multi-account setups. The package wires one adapter from config. For a second account, construct a GuzzleAdapter yourself and bind it under a named key; don't fight the singleton.
Testing. Swap the binding:
$this->mock(Adapter::class, function ($mock) {
$mock->shouldReceive('zones->getZoneID')->andReturn('abc123');
});
Facade-level testing (Cloudflare::shouldReceive(...)) works too. If you want HTTP-level fakes, pass a Guzzle MockHandler through the options key in config/cloudflare.php — Http::fake() does not intercept this client, since the SDK owns its own Guzzle instance.
The facade is the adapter, not a manager. Cloudflare::dns(), Cloudflare::zones(), Cloudflare::user(), Cloudflare::zoneSettings() are endpoint factories on the SDK adapter. IDE autocompletion is only as good as the cloudflare/sdk version you installed; keep it in require-dev for stubs if needed.
Two auth modes, pick one. Global key + email and scoped API token behave differently (token permissions are per-resource). If a call returns 403 while others work, it's almost always a token-scope issue, not a package bug. Leaving both configured is a good way to get confusing, intermittent auth behavior — set only the pair you intend.
getZoneID() returns false/null on miss. Every downstream call will then fail with an opaque API error. Always guard:
$zoneId = Cloudflare::zones()->getZoneID('example.com');
throw_unless($zoneId, new \RuntimeException('Zone not found'));
Pagination is manual. listZones() and listRecords() cap results per page. If you're auditing a large zone, loop the page parameter (or pass a large per_page) and check the result_info returned. Silent truncation is the most common "why didn't my record get updated" cause.
TTL 1 means "automatic", not one second. And proxied: true only makes sense on A, AAAA, and CNAME records — the API rejects it elsewhere.
Rate limits are real. Cloudflare's v4 API is roughly 1,200 requests / 5 minutes per user. A naive reconcile loop across many zones will hit it. Batch, cache zone IDs, and back off on 429s (Cloudflare\API\Exceptions\APIException carries the response).
Config caching footgun. After changing .env, run php artisan config:cache (or config:clear) — the adapter singleton reads config once at boot.
Error handling. SDK exceptions (CloudflareException and subclasses) bubble straight through the package. Catch them at your service boundary, log the Cloudflare error code, and don't let a DNS sync failure crash a deploy without a clear message.
Extension points. To intercept requests, custom-timeout, or log, either add Guzzle options via the config options array or bind your own Adapter implementation in a service provider ahead of the package's binding. Subclassing GuzzleAdapter is the lowest-friction option.
Project health caveat. This wrapper is thin and low-activity. Pin the version, and if it lags behind Laravel releases, the escape hatch is easy: it's ~50 lines of service provider. You can drop it and instantiate GuzzleAdapter directly in your own provider with no changes to calling code that already injects Cloudflare\API\Adapter\Adapter.
How can I help you explore Laravel packages today?