rokka/client
Official PHP client for Rokka, the image processing and delivery service. Upload, store, and manage images, generate dynamic URLs with stack options, and integrate Rokka into PHP or Laravel apps through a simple, PSR-compatible API.
Rokka is an image-processing CDN. The client is a thin SDK over its REST API: you push source images in, define stacks (named pipelines of operations), and get back URLs that render on the fly.
composer require rokka/client
As of 1.22.0 the package requires PHP ^8.2 and guzzlehttp/guzzle ^7.4.5 / guzzlehttp/psr7 ^2.4.5. PHP 8.0/8.1 and Guzzle 6 are no longer supported, so check your composer.json platform constraint and any Laravel version still pinned to 8.1 before upgrading — this is the only hard breaking change in the release, and it will surface as a resolver error, not a runtime one.
Grab an API key from the Rokka dashboard — it is bound to an organization, and the org name is usually embedded in the key (myorg-a1b2c3…), which is why most methods also take an $org argument.
Where to look first
Rokka\Client\Factory (or Client::create() in newer majors) — the entry point. Factory::getUserClient() gives you the org/stack/image-management surface; older releases also expose Factory::getImageClient() for read-only rendering work.Rokka\Client\Core\* DTOs (Stack, Image, SourceImage, SourceImageAlias, Operation) — the client returns typed objects, not arrays, so getHash(), getShortHash(), getUrl() are your accessors.listStackOptions() (new in 1.22.0) instead of guessing from docs.First use case: upload → stack → URL
use Rokka\Client\Factory;
$client = Factory::getUserClient(getenv('ROKKA_API_KEY'));
$org = 'myorg';
// 1. Upload the original
$source = $client->uploadSourceImage(
fopen(storage_path('app/avatar.jpg'), 'r'),
'avatar.jpg',
$org
);
$hash = $source->getHash(); // full SHA-1 style hash
$short = $source->getShortHash(); // what you usually store in the DB
// 2. Define how it should be rendered (idempotent PUT)
$client->createStack($org, 'avatar', [
'operations' => [
['type' => 'resize', 'width' => 200, 'height' => 200, 'mode' => 'fill'],
['type' => 'autoformat'],
],
]);
// 3. Build the URL
$url = "https://{$org}.rokka.io/avatar/{$short}.jpg";
That URL is now cacheable, resizable-on-demand, and can be overridden per-request with query options (?width=400).
Bind it as a singleton in Laravel. Configure once in config/services.php, resolve through the container, never new it in controllers:
$this->app->singleton(\Rokka\Client\Client::class, fn () => Factory::getUserClient(
config('services.rokka.key'),
null,
['client' => app(\Psr\Http\Client\ClientInterface::class)] // injectable transport
));
Treat stacks as code, not clicks. Keep stack definitions in config/rokka/stacks.php and push them in a deploy/artisan command. Because createStack is effectively an upsert, re-running it is safe — but only if the stack definition is byte-identical, so keep the array shape stable. In 1.22.0+ you can validate the definitions against listStackOptions() in CI before they ever reach the API, which turns a typo'd operation name into a failing test instead of a broken deploy.
Store hashes, never URLs. Persist short_hash plus a stack name; construct the render URL at read time via a small helper. This is what makes swapping jpg → webp or changing a crop a config change rather than a migration. If you want stable, human-meaningful keys instead of a hash column, use source image aliases (setSourceImageAlias / getSourceImageAlias / deleteSourceImageAlias, new in 1.22.0) — an alias maps to a source image and reads as a friendlier URL segment, but it is an extra API object to keep in sync, so hashes are still the simpler default.
Build URLs through the SDK helpers, not string concatenation:
$client->getStackUrl($hash, 'avatar', 'jpg', ['org' => $org, 'width' => 400]);
Rokka\Client\UriHelper and TemplateHelper are the lighter-weight options for Blade/Twig views where you don't want the full client in scope — they only build URLs, no network. That matters for signing: UriHelper::signUrl() signs locally, while the new signUrlOnServer() (POST /utils/{org}/sign_url) is a network round-trip. Use the latter only when the signing secret must not be in the app's environment at all.
Overriding per render. Stack operations are the defaults; query parameters win. Use this for a srcset loop (?width=320, ?width=640, …) instead of defining a stack per breakpoint.
Integration with the upload flow. Rokka needs the bytes, so UploadedFile → fopen($file->getRealPath(), 'r') → uploadSourceImage(). Do this in a queued job; the round trip is tens to hundreds of milliseconds and you don't want it on the request path for user avatars.
Housekeeping / ops methods. The 1.22.0 additions round out the admin surface nicely and are all worth wrapping in small artisan commands rather than calling ad hoc:
setSourceImageName — rename a source image without re-uploading (good for tidying a media library or matching a renamed model).getUserMetadata / getUserMetadataField — read back user metadata; handy for reconciling what the app stored vs. what Rokka holds.getBilling — pull monthly usage for an org; useful for a dashboard or a cost-anomaly alert when staging starts burning production quota.setOrganizationOptions — replace all org options in one request; complements the existing single-option setter, so prefer it when applying a full options array from config (same "byte-identical or don't bother" discipline as stacks).downloadSourceImagesAsZip — bulk export, capped at 300 source images per call. Chunk your IDs; it is not a "download everything" button.Errors. The client throws Rokka\Client\ClientException (and PSR-18 transport exceptions underneath). Catch it, inspect the response body — Rokka returns a JSON { "error": { "code", "message" } } that the exception message often flattens badly. Enable the debug option in the constructor to attach request/response to the exception during development.
guzzlehttp/guzzle ^6 pinned by another package, the upgrade fails at resolve time. Upgrade PHP first, then the client.getImageClient / getUserClient, array-ish returns) and the Jane-generated v2 (Client::create, typed DTOs, $client->getStacks()->create(...)) differ enough that most Stack Overflow snippets won't compile. Pin the major in composer.json and read the changelog before upgrading.getHash() returns the long hash used in API calls; URLs take getShortHash(). Mixing them gives a 404 that looks like a permissions problem.copySourceImage now uses documented POST /sourceimages/{org}/{hash}/copy instead of the legacy COPY verb, and getSourceImagesWithBinaryHash uses GET /sourceimages/{org}/binaryhash/{binaryHash} instead of ?binaryHash=. Signatures and behaviour are unchanged, but if you log/trace request verbs or allow-list methods in an outbound proxy, expect POST/GET where you previously saw COPY. Nothing in your calling code needs to change.deleteSourceImageCache (single image) and deleteStackCache (stack, or * for the entire organization) are new, billable, and irreversible. Never expose them on a route a user can hit directly, and treat '*' as a manual, break-glass operation behind an explicit confirmation flag.getStack) before debugging the render, then purge deliberately with deleteStackCache.keepHash is a footgun with a safety rail. Passing $options['keepHash' => true] to setDynamicMetadata / deleteDynamicMetadata keeps the image hash stable — great for late metadata fixes — but every downstream cache (CloudFront, memcached, browser) keeps serving stale renders until you purge manually. It is mutually exclusive with deletePrevious; passing both throws a LogicException.Accept. If you get a GuzzleHttp\Psr7\Stream where you expected an object, check the header you sent.file_get_contents() a 20 MB photo — pass a stream resource. The client forwards it straight to the transport.deleteSourceImage — the source is gone, but previously rendered URLs can still be served from cache until TTL. Plan for that if you rely on deletion for moderation, and reach for deleteSourceImageCache (if you're entitled to it) rather than waiting out the TTL.signUrlOnServer, which is one more endpoint that can 429.tests/ImageApiTest.php) for the new and migrated endpoints that you can crib from.autoformat pays for itself — it negotiates WebP/AVIF from Accept and saves meaningful bytes; add it to every stack rather than special-casing formats.How can I help you explore Laravel packages today?