Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cloudflare Bundle Laravel Package

gpenverne/cloudflare-bundle

A Symfony/PHP bundle that integrates the Cloudflare API into your app, making it easy to manage zones, DNS records, and cache purging from code. Useful for automating Cloudflare tasks in deployment, admin, or maintenance workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. gpenverne/cloudflare-bundle is a Symfony bundle (GPenverne\CloudflareBundle), not a Laravel package. There is no CloudflareServiceProvider, no publishable config, no facade, and Laravel's package discovery won't touch it. It exists to register cloudflare/sdk's Adapter (a Guzzle-backed HTTP client for api.cloudflare.com/client/v4) into a Symfony DI container. Last release was January 2020 and it has essentially no adoption.

For day-to-day Laravel work, the pragmatic path is to skip the bundle and depend on the SDK it wraps:

composer require cloudflare/sdk
// config/services.php
'cloudflare' => [
    'token' => env('CLOUDFLARE_API_TOKEN'),          // preferred: scoped API token
    'email' => env('CLOUDFLARE_EMAIL'),              // legacy global key auth
    'key'   => env('CLOUDFLARE_API_KEY'),
    'zone'  => env('CLOUDFLARE_ZONE_ID'),
],

If you genuinely want the bundle (e.g. you're porting a Symfony app and want identical wiring), install it and read these files first, in this order:

  1. DependencyInjection/Configuration.php — the real config keys (email/api_key/api_token, adapter choice). Names change across versions; don't trust blog posts.
  2. Resources/config/services.xml — the service IDs it registers (cloudflare.adapter, the GuzzleAdapter alias).
  3. GPenverneCloudflareBundle.php — confirms there's nothing beyond DI.

Because a Symfony bundle's Extension::load() expects a ContainerBuilder, pulling it into Laravel means instantiating the Symfony DI component manually — more ceremony than the four lines of bindings below.

First use case: resolve a zone name to a zone ID, then list DNS records. That's 90% of what people actually do with this API.

use Cloudflare\API\Adapter\GuzzleAdapter;
use Cloudflare\API\Auth\APIToken;
use Cloudflare\API\Auth\APIKey;
use Cloudflare\API\Endpoints\Zones;
use Cloudflare\API\Endpoints\DNS;
use GuzzleHttp\Client;

// app/Providers/CloudflareServiceProvider.php
public function register(): void
{
    $this->app->singleton(GuzzleAdapter::class, function () {
        $client = new Client([
            'base_uri' => 'https://api.cloudflare.com/client/v4/',
            'timeout'  => 10,
        ]);

        $auth = config('services.cloudflare.token')
            ? new APIToken(config('services.cloudflare.token'))
            : new APIKey(
                config('services.cloudflare.email'),
                config('services.cloudflare.key')
              );

        return new GuzzleAdapter($client, $auth);
    });

    $this->app->bind(Zones::class, fn ($app) => new Zones($app->make(GuzzleAdapter::class)));
    $this->app->bind(DNS::class,   fn ($app) => new DNS($app->make(GuzzleAdapter::class)));
}
$zoneId = app(Zones::class)->getZoneID('example.com');
$page   = app(DNS::class)->listRecords($zoneId, '', '', '', 1, 100);

foreach ($page->result as $record) {
    echo "{$record->type} {$record->name} -> {$record->content}\n";
}

Implementation Patterns

Wrap the endpoints in one small domain service. Don't sprinkle app(DNS::class) through controllers. One class, one seam to fake in tests:

namespace App\Services;

use Cloudflare\API\Endpoints\DNS;
use Cloudflare\API\Endpoints\Zones;
use Illuminate\Support\Facades\Cache;

class CloudflareDns
{
    public function __construct(
        private DNS $dns,
        private Zones $zones,
    ) {}

    public function zoneId(string $domain): string
    {
        return Cache::rememberForever("cf:zone:{$domain}", function () use ($domain) {
            $id = $this->zones->getZoneID($domain);
            if (! $id) {
                throw new \RuntimeException("No Cloudflare zone for {$domain}");
            }
            return $id;
        });
    }

    /** Create-or-update, so repeated deploys are idempotent. */
    public function upsertRecord(string $domain, string $type, string $name, string $content, int $ttl = 1, bool $proxied = false): void
    {
        $zoneId = $this->zoneId($domain);
        $existing = $this->dns->listRecords($zoneId, $name, '', $type, 1, 1);

        if ($existing->result) {
            $this->dns->updateRecord($zoneId, $existing->result[0]->id, $type, $name, $content, $ttl, $proxied);
            return;
        }

        $this->dns->addRecord($zoneId, $type, $name, $content, $ttl, $proxied);
    }

    public function deleteRecord(string $domain, string $recordId): void
    {
        $this->dns->deleteRecord($this->zoneId($domain), $recordId);
    }

    /** Iterate every page when you need the whole zone. */
    public function allRecords(string $domain): \Generator
    {
        $zoneId = $this->zoneId($domain);
        $page = 1;

        do {
            $response = $this->dns->listRecords($zoneId, '', '', '', $page, 100);
            yield from $response->result;
            $totalPages = $response->result_info->total_pages ?? 1;
        } while ($page++ < $totalPages);
    }
}

Drive it from queued jobs, not requests. DNS changes are slow, rate-limited, and failure-prone. A job with retries and backoff is the right shape:

class SyncDnsRecord implements ShouldQueue
{
    use InteractsWithQueue, Queueable;

    public int $tries = 5;
    public array $backoff = [10, 30, 60, 120];

    public function __construct(public string $domain, public array $record) {}

    public function handle(CloudflareDns $cf): void
    {
        $cf->upsertRecord($this->domain, ...$this->record);
    }
}

Artisan commands for ops workflows. A cloudflare:dns:sync {domain} command that reads a YAML/JSON manifest and diffs against allRecords() is far more useful than ad-hoc tinker calls, and it gives you --dry-run for free.

Cache purging after deploys. Same adapter, a different endpoint (Zone/Zones purgeCache). Wire it into your deploy pipeline rather than a controller:

app(\Cloudflare\API\Endpoints\Zone::class)
    ->purgeCache($zoneId, ['files' => ['https://example.com/app.css']]);

If you insist on the Symfony bundle: register it as a singleton by hand, bypassing the container extension — build the GuzzleAdapter from the bundle's config values yourself. You gain nothing over the provider above except a transitive dependency on old Symfony DI packages.

Dynamic DNS / edge config use case. For a self-hosted service, a scheduled command that compares the machine's public IP to the current A record and calls upsertRecord is the canonical "day two" use case. Wrap it in withoutOverlapping().

Gotchas and Tips

  • Staleness and pinned dependencies. The bundle's last release is 2020 and it pins an old cloudflare/sdk, which in turn pins old Guzzle. Composer will happily downgrade Guzzle and break other packages. Check composer why guzzlehttp/guzzle before committing. This alone is the strongest argument for depending on cloudflare/sdk directly and keeping it current.
  • Token vs. key. APIToken and APIKey are different classes with different constructor signatures. Passing a scoped API token to APIKey yields 400/403 with an unhelpful body. Prefer API tokens (scoped to Zone:DNS:Edit, Zone:Zone:Read) — global keys leak the whole account.
  • getZoneID returns false, not an exception, when the zone is absent or the token lacks access. Always check the return, as the wrapper above does.
  • Zone IDs are stable. Cache them forever (Cache::rememberForever); re-resolving per request is a wasted API call against a 1200-req/5-min budget.
  • Pagination is per-endpoint. listRecords defaults to 20 per page — a naive foreach over one call silently truncates big zones. Read result_info->total_pages. Some endpoints use page/per_page, others cursor-based — check the endpoint class.
  • TTL 1 means "automatic". Setting ttl to 0 or omitting it produces different results depending on endpoint version. proxied => true is only valid for A, AAAA, and CNAME, and proxied records ignore custom TTLs.
  • Errors are not exceptions by default. Responses carry success, errors[], messages[]. Enable Guzzle http_errors and wrap calls, or inspect ->success explicitly. Log the full errors array — Cloudflare's code values are the actually useful part.
  • No sandbox. Cloudflare has no test API. Unit-test with GuzzleHttp\Handler\MockHandler injected into the Client and assert on the request your code builds — that's the only reliable way to verify DNS payloads.
  • Swap in LoggingAdapter (from cloudflare/sdk) wrapping GuzzleAdapter in local/staging to get request/response logging without touching your services. That's the cleanest extension point the underlying SDK offers — the bundle adds none of its own.
  • Don't use this for cache purging on every request. Cloudflare purges are expensive and rate-limited; batch them per deploy.
  • Rate limits are per-account, shared with the dashboard. If a job bursts, humans in the dashboard start seeing 429s. Concurrency-limit your DNS jobs (->middleware(fn ($job) => $job->onQueue('dns')) with a single worker).
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console