tlamedia/grabzit-bundle
Symfony bundle integrating the GrabzIt screenshot and PDF API. Capture website screenshots, generate PDFs, and create other media from your PHP application through configurable services and simple dependency injection.
Read this first: tlamedia/grabzit-bundle is a Symfony bundle, not a Laravel package. It exists to (a) register the GrabzItClient service in Symfony's container and (b) wire your API key/secret from config. Underneath it is just grabzit/grabzit — the official PHP SDK. In a Laravel app the bundle's auto-wiring, config/packages/*.yaml, and bundles.php registration do not apply, so the practical path is to depend on the SDK directly and reproduce the (tiny) wiring yourself:
composer require grabzit/grabzit
Then in a service provider:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(\GrabzItClient::class, fn () => new \GrabzItClient(
config('services.grabzit.key'),
config('services.grabzit.secret')
));
}
Add GRABZIT_KEY / GRABZIT_SECRET to .env and config/services.php. Grab both from the GrabzIt dashboard (Account → API). Note that GrabzIt is a paid, credit-metered external API — you cannot test it without a real key.
First use case to validate end-to-end: capture a screenshot of a URL and save it to disk.
$grabzIt = app(\GrabzItClient::class);
$grabzIt->URLToImage('https://example.com');
$grabzIt->SaveTo(storage_path('app/shots/example.png'));
If that file exists and is a valid PNG, your wiring is correct. A zero-byte file or null return almost always means a bad key/secret or exhausted credits — check the dashboard before debugging code.
If you are genuinely in a Symfony app: composer require tlamedia/grabzit-bundle, enable TlaMedia\GrabzitBundle\GrabzitBundle in config/bundles.php, then set tlamedia_grabzit.key and tlamedia_grabzit.secret in config/packages/tlamedia_grabzit.yaml and inject GrabzItClient normally.
Synchronous (blocking) capture — fine for CLI/queue jobs, never in a request.
SaveTo($path) blocks until GrabzIt finishes, which can be several seconds. Use it inside a queued job, not a controller action:
class CaptureScreenshot implements ShouldQueue
{
public function __construct(public string $url, public string $path) {}
public function handle(\GrabzItClient $grabzIt): void
{
$grabzIt->URLToImage($this->url, ['width' => 1280, 'height' => 800]);
$grabzIt->SaveTo(storage_path("app/{$this->path}"));
}
}
Asynchronous (callback) capture — the pattern GrabzIt is actually designed around. Save($callbackUrl) fires the request and returns immediately; GrabzIt later POSTs the result back to your route. This is the only pattern that scales to user-facing features:
// dispatch
$grabzIt->URLToImage($url);
$grabzIt->Save(route('grabzit.callback', ['capture' => $capture->id]));
// routes/api.php — must be publicly reachable
Route::post('/grabzit/callback/{capture}', GrabzItCallbackController::class)
->withoutMiddleware(['csrf']);
In the callback controller, read GrabzIt's id and customId off the request, then re-hydrate a client and pull the result:
$grabzIt = app(\GrabzItClient::class);
$grabzIt->GetResult($request->input('id')); // writes to configured destination
Persist the GrabzIt id (and your own customId correlating to a model) at dispatch time so the callback can join back.
HTML/source capture variants. The same client exposes HTMLToPDF($html, $options), HTMLToImage, URLToPDF, URLToTable, and FileToPDF. Treat them uniformly: configure → Save() (async) or SaveTo() (sync). The configuration call and the save call are two separate steps — a common source of "nothing happened" bugs.
Laravel integration tips:
CaptureService rather than injecting \GrabzItClient all over — the fluent configure-then-save API is stateful on the client, so sharing a singleton across concurrent calls in one request is dangerous. Either resolve a fresh instance per capture (app()->make(...) with bind not singleton) or serialize.Storage::disk('s3') / local disk rather than in DB blobs. GrabzIt can save to S3 directly via SaveToCloud, but a local SaveTo + Storage::put gives you more control and easier retries.ScreenshotReady) from the callback controller so downstream logic (OCR, thumbnailing, notifications) is decoupled.grabzit/grabzit directly. The bundle adds ~two files of config glue and a hard Symfony dependency you don't want.Save() vs SaveTo() is the classic footgun. Save($url) triggers an async job and returns null; SaveTo($file) blocks and writes. Passing a filesystem path to Save() silently does nothing useful, and passing a URL to SaveTo() won't schedule a callback. Match the method to the intent.$grabzIt->URLToImage($url) only sets up the capture; it makes no network request. The network call is Save/SaveTo. Call them in the same scope; don't stash the configured client in a service property between requests.VerifyCsrfToken (Laravel 10+) and make it reachable from the public internet — localhost dev will not receive callbacks. Use ngrok/Expose while developing.null rather than exceptions. On any unexpected result, first check the GrabzIt dashboard's request log — it shows status (processing, failed, successful) per capture ID, which is faster than instrumenting your code.SaveTo in a try/catch, verify file_exists and filesize > 0, and throw your own exception so queued jobs actually retry. Consider a failed() handler that marks the capture record as errored.'width', 'height', 'format', 'delay', 'country', 'customId'). They are not validated client-side — a typo silently falls back to defaults. Keep a constants class or a typed DTO per capture type rather than scattering string keys.customId is your only safe correlation on callbacks; GrabzIt's own id is opaque. Set it at dispatch and read it back — it survives the async round-trip.CaptureService interface with a Laravel binding and a Symfony service alias, and let both resolve the same GrabzItClient — the bundle is just a service registration, replaceable in either direction.How can I help you explore Laravel packages today?