tzurbaev/laravel-forge-api
A Laravel package that wraps the Laravel Forge API in a clean, fluent interface, letting you manage servers, sites, deployments, workers, and other Forge resources directly from your PHP applications.
This is a thin Laravel service-provider wrapper around the Forge HTTP API. It was built for Laravel 5.x–8.x and has been archived since Jan 2021 — treat it as a starting point, not a foundation. Before you commit, check whether laravel/forge-sdk (the community SDK that superseded the abandoned wrappers) or a few Guzzle calls do the job directly.
Minimal install:
composer require tzurbaev/laravel-forge-api
php artisan vendor:publish --provider="Tzurbaev\Forge\ForgeServiceProvider"
Add your Forge API token (Forge → Profile → API tokens) to .env:
FORGE_API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The provider is auto-discovered on Laravel 5.5+. On older versions, register Tzurbaev\Forge\ForgeServiceProvider in config/app.php and add the Forge alias under aliases.
Where to look first, in this order:
README.md — thin, but lists the resource managers and the facade name.config/forge.php — the only config surface: API token, base URI, and any version flag. Everything else is hard-coded in managers.src/ForgeServiceProvider.php — shows the container binding and which contract/class you actually inject.src/Contracts/ and src/Managers/ (or src/Models/) — this is the real API. Each Forge resource (servers, sites, databases, SSL, workers, jobs, deployments) has a corresponding manager class whose public methods map 1:1 to Forge endpoints. Read these method signatures rather than trusting the README.First use case — a read-only smoke test that proves the token and base URI work:
use Tzurbaev\Forge\Contracts\Forge; // or the facade alias
$forge = app(Forge::class);
dd($forge->servers()->list()); // verify method name against src/
Once that returns your servers, you can drill into a single server (find($id)) and its sites. Do not start writing automation until this call succeeds — 90% of "the package is broken" reports are a bad base URI or a token missing scope.
Resolve through the container, never construct managers yourself. The facade and the contract both point at the same singleton. Use the contract in constructors so you can swap it in tests:
final class ForgeServerProvisioner
{
public function __construct(private \Tzurbaev\Forge\Contracts\Forge $forge) {}
public function ensureServer(string $name, string $region, string $size): array
{
$existing = collect($this->forge->servers()->list())
->firstWhere('name', $name);
return $existing ?? $this->forge->servers()->create([
'name' => $name, 'region' => $region, 'size' => $size,
]);
}
}
Wrap the SDK in your own service class. Because the package is archived, the moment Forge changes a field name the SDK breaks and you have nowhere to send the PR. A single ForgeClient wrapper gives you one place to patch or replace. This is the single most valuable pattern for this package.
Manager-per-resource chaining is the idiomatic style. Typical day-to-day flows you'll write:
$server = $forge->servers()->find($serverId);
$forge->sites()->create($serverId, [
'domain' => 'example.com',
'project_type' => 'php',
'directory' => '/public',
]);
$forge->sites()->deploy($serverId, $siteId); // fire-and-forget
$forge->sites()->deploymentStatus($serverId, $siteId); // poll separately
Deployment orchestration belongs in queued jobs. Server creation and site provisioning routinely take 30–90 seconds; the HTTP client will time out inside a web request. Put each step in a job, log the Forge deployment_id/server_id, and poll status on a backoff() schedule rather than blocking.
Cache reads aggressively. servers()->list() and sites()->list() hit a rate-limited API. Cache::remember("forge.servers", 60, fn () => $forge->servers()->list()) in a dashboard view is trivial and removes most 429s.
Multi-tenant / multi-account. Forge tokens are per-user, and the package binds one token via config. If you manage several Forge accounts, don't try to mutate config at runtime — build a small factory that news up the underlying client per token (or app()->forgetInstance(Forge::class) between calls in a job, which is hacky but works). This is a case where wrapping the SDK pays for itself immediately.
Testing. The package predates Http::fake() (it uses Guzzle directly), so the reliable approach is to bind a fake implementation of the contract in your test case:
$this->app?->instance(\Tzurbaev\Forge\Contracts\Forge::class, new FakeForge());
Alternatively, pass a Guzzle client with a MockHandler if the manager constructor exposes one — check src/ for the injection point.
composer.json and budget for a migration. Check laravel/forge-sdk first — it's the actively maintained path and has a near-identical mental model.config:cache will freeze your token. If FORGE_API_TOKEN is set in .env but config is cached and the token is read via env() inside the package provider (common in older packages), you'll get nulls silently. Always read through config('forge.api_token') in your own code and re-run php artisan config:clear after edits.https://forge.laravel.com/api/v1. If the package defaults to an older path, every call 404s. Override the URI in config/forge.php (or override the binding) rather than editing vendor/.deploy() returns immediately; a green HTTP 200 does not mean the deployment succeeded. Always poll deploymentStatus() or use the Forge webhook and record the deployment ID.src/Exceptions/ before writing your catch blocks.sites()->updateEnv($serverId, $siteId, $content) overwrites the whole file. Read, append, write — never blindly push a single var.Server/Site objects returned are value objects built from JSON arrays; there is no lazy loading, no relations, no ->save(). Access nested data as arrays or via the small model class properties — inspect src/Models/ for what's actually hydrated.page= yourself.$timeout.HandlerStack::push) or temporarily Log::debug() the request/response in your wrapper. There's no built-in debug mode.How can I help you explore Laravel packages today?