flapjacklabs/chargify-bundle
Symfony bundle for integrating Chargify billing into your application, providing services and configuration to manage subscriptions, customers, and Chargify API interactions with less boilerplate.
This is a Symfony 2.x bundle, not a Laravel package. There is no service provider, no facade, and no config/chargify.php — so the realistic "day one" move for a Laravel developer is to treat it as a reference implementation for wiring the Chargify API client, not as a dependency you install.
Where to look first (read the repo, don't require it):
DependencyInjection/Configuration.php — the exact config tree (subdomain, api_key, shared_secret, environment/test_mode flags). This is your config schema.Resources/config/services.yml — the service IDs and constructor arguments. This tells you what the bundle actually wrapped (usually a Chargify\Client / ChargifyAPI instance plus a thin wrapper service).DependencyInjection/FJLChargifyExtension.php — how config is turned into client construction. This is the 20 lines you will rewrite as a Laravel service provider.composer.json — the underlying Chargify PHP SDK version it pins. That vendor library is the only part with real residual value.Minimal reimplementation in Laravel (the practical equivalent of the bundle):
// composer require chargify/chargify-php (or use Guzzle directly)
// config/services.php
'chargify' => [
'subdomain' => env('CHARGIFY_SUBDOMAIN'),
'api_key' => env('CHARGIFY_API_KEY'),
'shared_secret' => env('CHARGIFY_SHARED_SECRET'),
'test_mode' => env('CHARGIFY_TEST_MODE', false),
],
// app/Providers/ChargifyServiceProvider.php
public function register(): void
{
$this->app->singleton(ChargifyClient::class, fn () => new ChargifyClient(
config('services.chargify.subdomain'),
config('services.chargify.api_key'),
));
}
First use case: read a subscription by ID and create a customer + subscription, behind a small ChargifyService class so you never touch the SDK directly from a controller. That single abstraction is the thing the bundle gave Symfony users — recreate it once and you're done.
Thin service wrapper, never call the SDK from controllers. The bundle's whole job was injecting a configured client into services. Do the same: a ChargifyService (or several focused ones: Subscriptions, Customers, Products) that owns error translation and logging.
class ChargifyService
{
public function __construct(private ChargifyClient $client) {}
public function subscription(int $id): Subscription
{
try {
return $this->client->subscriptions->read($id);
} catch (ChargifyException $e) {
throw new ChargifyApiException($e->getMessage(), $e->getCode(), $e);
}
}
}
Webhook-first state. Chargify is the source of truth. Mirror only what you need locally (chargify_subscription_id, state, product_id, current_period_ends_at) and update it from webhooks. Keep a chargify_events table keyed on Chargify's event/subscription IDs so replayed deliveries are no-ops.
Route::post('/webhooks/chargify', ChargifyWebhookController::class)
->middleware('verify.chargify.signature');
Verify X-Chargify-Webhook-Signature — HMAC-SHA256 of the raw body using the shared secret, compared with hash_equals() against the hex digest. Symfony bundle config exposes the shared secret for exactly this; in Laravel it belongs in config/services.php.
Queue the handler. Dispatch an event-specific job (SubscriptionStateChanged, PaymentFailed, SubscriptionCanceled) so a slow handler doesn't time out the webhook and trigger Chargify retries. Match on the event string, not the HTTP verb.
Interface the client for tests. Bind ChargifyClientInterface in the container and swap in a fake in tests; the SDK is Guzzle-based, so Http::fake() doesn't apply — you either fake at the interface boundary or point the client at a local stub subdomain.
Multi-tenant / multi-subdomain. The bundle assumes one client per container, which breaks the moment you serve several Chargify sites. Register the client as a factory keyed by subdomain rather than a singleton:
$this->app->bind(ChargifyClient::class, fn ($app, $params) => new ChargifyClient(...));
Reconciliation job. A nightly chargify:reconcile command that pulls subscriptions updated since the last run catches dropped webhooks. Cheap insurance; the bundle had no equivalent, so it's on you.
AppKernel, use Symfony\Component\DependencyInjection for config, and depend on Symfony 2.x-era contracts. Pulling it in means dragging in a dead Symfony stack for zero Laravel benefit. Copy the config schema and the client wiring; leave the bundle.price_in_cents as dollars. Convert at the boundary and pick one canonical unit for your DB.{"errors": [...]} with a 422. Wrap them into a domain exception with the array intact — you will need those messages for user-facing billing errors.$request->all() re-encodes JSON and breaks the HMAC. Use $request->getContent(). Also guard against replays: store the event ID and reject duplicates, and don't trust ordering — a subscription.canceled can arrive before the subscription.created you're expecting.test=true parameter. Keep separate env vars (CHARGIFY_SANDBOX_*) rather than a boolean flag threaded through the client.->all() style call silently truncates or hammers the API. Always paginate explicitly and back off on 429.parameters.yml in the repo; in Laravel use env vars plus config:cache, and never log services.chargify.api_key.How can I help you explore Laravel packages today?