ozznest/api-images
ozznest/api-images is a lightweight Laravel/PHP package for integrating image handling into API-driven applications. It offers a simple, developer-friendly setup for adding image support to existing projects with minimal configuration.
Start by auditing the source before installing — with zero stars and no dependents there is no community feedback to rely on, so confirm the directive signatures yourself (look for src/Directives/ and src/ServiceProvider.php first; those two files tell you the entire public surface).
Installation assumes a Laravel app already running Lighthouse PHP:
composer require ozznest/api-images
php artisan vendor:publish --tag=api-images-config
The service provider typically auto-registers directives via Lighthouse's directive autoloader. Confirm in config/lighthouse.php that the package's namespace is present, or add the directives manually under 'namespaces':
'namespaces' => [
'App\\GraphQL\\Directives',
'Ozznest\\ApiImages\\Directives',
],
Your first use case is a single model field. Attach the directive to an Image type and let the package resolve the URL server-side:
type Post {
id: ID!
title: String!
image: Image @image(width: 1200, height: 630, fit: "crop")
}
type Image {
url: String!
width: Int
height: Int
}
Query it from the client:
query {
post(id: 1) {
image { url width height }
}
}
Verify the returned URL renders in a browser. If it 404s, the package's storage disk in config/api-images.php does not match the disk your model writes to — that is the single most common first-run failure.
Directive-per-variant. The idiomatic pattern is to declare each rendition as its own field, so responses are explicit and cacheable rather than exposing a runtime variant resolver:
type Post {
thumbnail: Image @image(width: 200, height: 200, fit: "crop")
hero: Image @image(width: 1920, fit: "max")
ogImage: Image @image(width: 1200, height: 630, fit: "crop", quality: 90)
}
Disk separation. Keep original uploads on a private disk and rendered derivatives on a public or CDN-backed disk. Configure this split in config/api-images.php (disk, cache_disk, path) rather than hardcoding paths in resolvers — it is the main knob for swapping local dev storage for S3 in production.
Model integration. Expose the underlying file via an Eloquent accessor and let the directive handle transformation. Keep the model unaware of GraphQL:
public function getImagePathAttribute(): ?string
{
return $this->image_path; // relative path on the configured disk
}
Cache-aware resolution. For list queries, warm the derived images at write time (an observer on saved) so the GraphQL layer only ever emits pre-rendered URLs. On-demand generation inside a directive is fine for a single record but will blow up latency on posts(first: 50).
CDN prefix. Bind the public URL through the config's url_prefix rather than storing absolute URLs in the database — otherwise a domain change requires a backfill migration.
Audit before adopting. Zero stars, zero dependents, no reported issues. Treat the directive API as unstable, pin an exact version in composer.json, and keep a fallback resolver that emits plain storage URLs so you can drop the package without rewriting your schema.
Lighthouse version coupling. Lighthouse has shipped breaking directive-API changes across majors. Check the package's composer.json constraint against your installed Lighthouse major; a mismatch usually surfaces as Directive "image" not found or a reflection error inside DirectiveLocator.
Image driver dependencies. If the package wraps intervention/image, you need the GD or Imagick extension compiled in. Docker images frequently lack Imagick — php -m | grep -i imagick before you debug anything else.
N+1 on list queries. A directive that transforms lazily will fire one job per row. Use @hasMany/batched resolvers, or pre-generate. Watch the query log — the package will not warn you.
Signed URL pitfalls. If derivatives are served from a private disk via temporary URLs, APP_URL and the storage disk's url must be correct or every image 403s in staging. Also verify TTL: an expiry shorter than your client's cache TTL produces intermittent broken images that are painful to reproduce.
Storage facade mismatch. The directive resolves paths through whatever disk the package config names, not the disk your upload code used. Test with Storage::fake('public') in feature tests and assert the URL is generated and that the file exists — asserting only the URL hides disk misconfiguration.
Testing. Lighthouse makes directive testing straightforward: assert the schema field returns a non-null url, then separately assert the file was written. Keep the two assertions apart so a transformation failure and a disk failure are distinguishable.
Extension point. If the package exposes a transformer or processor binding in its service provider, that is the supported hook for custom resizing/filters. Prefer it over subclassing directives — subclasses break whenever the vendor updates the base class, and with no maintainer activity you should assume future upstream churn is unlikely to preserve inheritance chains.
How can I help you explore Laravel packages today?