headoo/google-vision-api-bundle
Symfony bundle that integrates Google Cloud Vision into PHP applications, enabling image analysis for labels, text, faces, and safe-search detection via configurable services and simple dependency injection setup.
This is a Symfony bundle, not a Laravel package. There is no auto-discovery, no config/vision.php, no facade. Treat it as a library of thin wrappers around the Google Vision REST calls and expect to do the wiring yourself.
Step 1 — Evaluate before installing. Last release was March 2017 (4 stars, zero dependents). Before committing, run:
composer require headoo/google-vision-api-bundle --dry-run
Check the resolved google/* and guzzlehttp/guzzle versions against your Laravel version's constraints. Most 2017-era bundles pin Symfony 2.8/3.x components and an old google/apiclient, which will usually conflict with a modern Laravel skeleton. If it resolves at all, treat it as a "reference implementation" you may end up copying rather than a dependency you keep.
Step 2 — Know where to look. Once it's in vendor/, read these in order:
DependencyInjection/Configuration.php — the canonical list of config keys (api_key, credentials path, etc.). This is your source of truth, not the README.Services/ — the actual callable classes; note the exact public method names and whether they return arrays or objects.Resources/config/services.yml — the service IDs you'd have to replicate manually in a Laravel service provider.Step 3 — First use case. Label detection on an uploaded image is the fastest way to confirm the wiring works end to end:
// app/Services/VisionClient.php
namespace App\Services;
use Headoo\GoogleVisionApiBundle\Services\VisionApi; // verify actual FQCN in vendor/
class VisionClient
{
public function __construct(private VisionApi $vision) {}
public function labels(string $absolutePath): array
{
// confirm the real signature in Services/ — most methods take a path
// and return a nested array under 'labelAnnotations'
return $this->vision->labelDetection($absolutePath);
}
}
Bind it in AppServiceProvider::register() with the API key pulled from config('services.google.vision_key'), and you have a working call inside an hour. Everything after that is about how you use it, not about the bundle.
Adapter over the bundle, not the bundle directly. Never inject the bundle's service into controllers. Wrap it in one VisionClient class with methods that return your own DTOs/arrays. This costs ten minutes and means the day the bundle is abandoned (it already is) you swap the internals without touching call sites.
Config in config/services.php. Mirror what Configuration.php expected, but in Laravel style:
// config/services.php
'google_vision' => [
'key' => env('GOOGLE_VISION_KEY'),
'credentials' => env('GOOGLE_APPLICATION_CREDENTIALS'),
],
Prefer the service-account JSON path over a bare API key — an API key in .env is easy to leak and cannot be scoped per-project as cleanly.
Never call Vision in the request cycle. Every call costs money and 200–1500ms. The day-to-day pattern is:
Upload -> Controller stores file -> dispatch(AnalyzeImage::class) -> queue worker
-> VisionClient -> persist annotations -> UI reads from DB
A queued job with $tries = 3, backoff = [30, 120], and a failed() handler that marks the image analysis_failed is the whole integration. The request returns immediately with a pending status.
Read from Storage, not from URLs. Vision does not accept arbitrary public URLs — it takes local file contents, base64-encoded content, or gs:// URIs. For S3/Local disks:
$contents = Storage::disk('s3')->get($path);
$result = $this->vision->labelDetection($contents); // if the method accepts content
// or: Storage::disk('local')->path($path) if the method requires a filesystem path
Check which one your installed version supports; this is the single most common source of "it returns empty" bugs.
Batch features into one request. Vision bills and rate-limits per feature and per image. If you need labels + safe-search + text on the same image, request all three in one annotate call rather than three round trips. If the bundle's service only exposes single-feature helpers, drop to the underlying client for that one call.
Cache by content hash. For any image that can be re-analyzed (user re-opens a page, retry job fires twice), key results on sha1_file() and store in Redis for 30 days. This is the cheapest optimization you'll make — Vision pricing is per-request and identical images are common in e-commerce/classifieds workloads.
Feature-specific patterns worth knowing:
score >= 0.7 and cap at 10; raw responses return 20+ mostly-useless tags.DOCUMENT_TEXT_DETECTION for dense/scanned text, TEXT_DETECTION for sparse signage; they're different features and priced separately.adult, violence, racy); pick per-category cutoffs, don't use one global flag.Integration tip — one interface, two implementations. Define ImageAnalyzer interface, implement GoogleVisionAnalyzer and NullImageAnalyzer (returns empty annotations). Bind the real one in production and the null one in testing, and your feature tests never touch the network.
The package is effectively unmaintained. Released 2017-03, zero dependents, 4 stars. Assume no security fixes, no compatibility with modern google/cloud-vision, and no support for newer Vision features (object localization, crop hints v2, etc.). Budget for a rewrite or a direct dependency on google/cloud-vision within 12 months. The bundle is a starting point, not an architecture.
Symfony DI assumptions bite you. The bundle expects a kernel, a container, YAML service definitions, and its own configuration tree. If you install it, you'll spend more time reverse-engineering services.yml than you would writing a 60-line HTTP client against the REST endpoint. For Laravel specifically, the honest recommendation is: read the bundle's Services/ folder for the request/response shape, then implement it natively with google/cloud-vision and an Illuminate\Support\Facades\Http fallback.
Auth failures are silent-ish. The two errors you'll hit first:
403 PERMISSION_DENIED — the Cloud Vision API is not enabled on the project, or billing isn't attached. Enabling it in the console is a separate step from creating the service account.400 INVALID_ARGUMENT — the feature/request combination is malformed, often "too many features" or an unsupported image format.Both come back as an error object in the response body, not always as an exception. Always inspect for $response['error'] ?? null before parsing annotations, and log the raw payload at debug level for the first week.
Image constraints. Request body limit is 20 MB; base64 inflates by ~33%, so keep source images well under 15 MB. Images must be JPEG/PNG/GIF/BMP/WebP (no HEIC, no SVG, no PDF page 1 unless you use the Files API). Animated GIFs only analyze the first frame. Downscale to ~1024px on the long edge before sending — Vision's models don't benefit from larger inputs and you save upload time.
Quotas are per-project, per-feature. The free tier is 1000 units/month per feature type. A single image that requests labels + text + safe-search consumes three units. A batch import of 5,000 product images across three features will blow past free tier in one afternoon — set a RateLimiter::for('vision') and alert on monthly usage.
Response parsing is nested and verbose. Label results live at responses[0]['labelAnnotations'] as an array of {mid, description, score, topicality}. Text results nest under fullTextAnnotation.pages[].blocks[].paragraphs[].words[] and are painful to flatten — write one normalize() method, test it against saved fixtures, and never touch it again.
Testing. Save three or four real JSON responses into tests/fixtures/vision/ and assert your normalize() logic against them. Never assert against the live API in CI — flaky, slow, and billable. Use Http::fake() if you go the native route.
Extension points (if you keep the bundle): override the service definition in your Symfony side config, decorate the service to add logging/caching, or fork the bundle into app/Support/Vision/. Forking is usually the right answer for a 4-star, 8-year-old bundle — you inherit the working code and control the dependency surface.
Debugging checklist, in order: (1) does curl against vision.googleapis.com/v1/images:annotate with the same key work? (2) is the image path absolute and readable by the PHP user? (3) is the underlying client version actually the one the bundle was written against (composer show google/apiclient)? (4) are you reading responses[0] and not annotations? Nearly every "it's broken" report lands on one of those four.
How can I help you explore Laravel packages today?