google/cloud-vision
The official PHP client library for Google Cloud Vision, making it easy to add image understanding to PHP apps. Detect labels, objects, faces, and landmarks; run OCR; and apply SafeSearch moderation through a simple, idiomatic API maintained by Google.
google/cloud-vision is the official PHP client for the Google Cloud Vision API — it is not a Laravel package. Expect to write a thin wrapper yourself; there is no config file, facade, or service provider to publish.
Install and authenticate:
composer require google/cloud-vision
Create a service account in GCP with the Cloud Vision API User role, download the JSON key, and store the path (not the contents) in .env:
GOOGLE_APPLICATION_CREDENTIALS=/var/www/app/storage/keys/gcp-vision.json
If you cannot use the ambient env var (e.g. multi-tenant apps), pass credentials explicitly:
$client = new ImageAnnotatorClient([
'credentials' => storage_path('keys/gcp-vision.json'),
]);
Enable the API in the console first — a perfectly valid key still throws PERMISSION_DENIED / SERVICE_DISABLED until you do.
Where to look first: the Google\Cloud\Vision\V1 namespace in vendor/google/cloud-vision/src/V1/, specifically ImageAnnotatorClient.php (every RPC), Feature/Type.php (the enum of detections), and Image.php (how to supply bytes vs. URIs). Ignore Google\Cloud\Vision\VisionClient and the image()/annotation() helpers — they are the legacy high-level API and are not what the generated client examples assume.
First use case — label detection on an uploaded file:
use Google\Cloud\Vision\V1\Client\ImageAnnotatorClient;
use Google\Cloud\Vision\V1\{BatchAnnotateImagesRequest, AnnotateImageRequest, Image, Feature};
$client = app(ImageAnnotatorClient::class);
$request = new AnnotateImageRequest([
'image' => new Image(['content' => file_get_contents($path)]),
'features' => [new Feature(['type' => Feature\Type::LABEL_DETECTION, 'max_results' => 10])],
]);
$response = $client->annotateImage($request)->getLabelAnnotations();
foreach ($response as $label) {
echo $label->getDescription(), ' ', $label->getScore(), PHP_EOL;
}
Bind the client as a singleton. The client holds a gRPC/HTTP channel and is expensive to construct. Register it in a VisionServiceProvider:
$this->app->singleton(ImageAnnotatorClient::class, fn () => new ImageAnnotatorClient([
'credentials' => config('services.vision.credentials'),
]));
Put the path in config/services.php rather than calling env() at runtime — env() returns null once config is cached.
Wrap it in your own service class. Never let controllers touch the generated client directly. A VisionService with methods like labels(string $path): array, ocr(string $path): string, safeSearch(string $path): array gives you one place to swap transports, add caching, and normalize the protobuf getters into plain arrays you can json_encode into a model column.
Batch instead of looping. batchAnnotateImages() accepts up to 16 images per call and is dramatically cheaper in latency than 16 sequential annotateImage() calls. Build an array of AnnotateImageRequest objects and read back $response->getResponses(), which is index-aligned with your input:
$batchResponse = $client->batchAnnotateImages(new BatchAnnotateImagesRequest([
'requests' => $requests,
]));
foreach ($batchResponse->getResponses() as $i => $r) {
if ($r->hasError()) { /* per-image failure, see Gotchas */ }
$labels[$i] = array_map(fn ($l) => $l->getDescription(), iterator_to_array($r->getLabelAnnotations()));
}
Push heavy work to the queue. Vision calls are 200–2000 ms and occasionally much worse. A ProcessImage job on a dedicated queue keeps uploads snappy and gives you $tries, backoff, and failed() for free. Store the image in S3/GCS first, then dispatch with the path or gs:// URI.
Prefer gs:// URIs for anything sizeable. Image::fromUri('gs://bucket/key.jpg') avoids base64-inflating the payload by ~33% and sidesteps the request-size ceiling. Same for output: asyncBatchAnnotateFiles() writes OCR results for PDFs/TIFFs to a GCS bucket — you never get the text back in the response, you must read the JSON from GCS afterward.
Combine features in one request. DOCUMENT_TEXT_DETECTION + SAFE_SEARCH_DETECTION + LABEL_DETECTION on a single image costs one round trip. Use max_results on every feature; the default is 10 but FACE_DETECTION and WEB_DETECTION can return a lot of noise.
Cache aggressively by content hash. Vision output for identical bytes is deterministic enough to cache forever. Cache::remember('vision:labels:'.hash_file('xxh128', $path), now()->addYear(), ...) will pay for itself immediately given the free tier is only 1,000 units per feature per month.
AnnotateImageResponse->getError() (corrupt image, INVALID_ARGUMENT, quota). Always check $r->getError() per response inside a batch. Only transport/auth failures throw ApiException.Feature\Type::TEXT_DETECTION etc. are plain strings in Feature\Type, not a PHP enum — typos fail at the API, not at compile time. Feature\Type::DOCUMENT_TEXT_DETECTION returns a TextAnnotation (with getPages(), structure, and blocks), while TEXT_DETECTION returns flat EntityAnnotations. Pick deliberately; DOCUMENT_TEXT_DETECTION is priced higher.null/empty, not defaults. getFullTextAnnotation() is null when the feature wasn't requested or nothing was found. getTextAnnotations() includes one entry per word plus the whole-document entry at index 0 — [0]->getDescription() is the usual "give me the text" answer. Always null-guard before iterating.ext-grpc is not installed, the client silently falls back to REST. REST has a hard ~10 MB request limit and different error shapes; gRPC gives faster cold starts. Force it with ['transport' => 'rest'] if you see gRPC channel errors (UNAVAILABLE: failed to connect to all addresses) in a container without proper networking.$client->annotateImage($request, ['timeoutMillis' => 30000]) or set retrySettings in the client constructor. A queued job with $timeout = 60 and a 30 s RPC timeout is a reasonable pairing.credentials, then GOOGLE_APPLICATION_CREDENTIALS, then gcloud ADC, then the metadata server. In Docker you will often hit the metadata server attempt and wait ~2 s before failing — a common source of slow cold starts. Set the env var explicitly to avoid the probe.max_results is per feature, not per request. Setting 50 on LABEL_DETECTION for every image will noticeably inflate response payloads and serialization time; the client parses every protobuf field.final-ish and hard to mock. Rather than Mockery on the generated class, bind a VisionService interface in the container and fake that; reserve one small integration test against a fixture image for the real client path.vision_units counter or log line per call so a misbehaving upload path becomes visible before the invoice does.ImageAnnotatorClient; its constructor signature and generated internals are not stable across minor versions.How can I help you explore Laravel packages today?