google/cloud-language
Official PHP client for Google Cloud Natural Language API, enabling sentiment analysis, entity recognition, syntax parsing, and content classification in Laravel/PHP apps via Composer and Google Cloud authentication.
Install and enable the API
composer require google/cloud-language
Then enable the Cloud Natural Language API in the target Google Cloud project and make credentials available via Application Default Credentials. Locally this usually means:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
On GCP (Cloud Run, GCE, GKE with Workload Identity) ADC is picked up automatically — do not ship key files.
Where to look first
src/V1/LanguageClient.php — the generated client you will use for most work (analyzeSentiment, analyzeEntities, analyzeEntitySentiment, analyzeSyntax, classifyText, annotateText).src/V2/LanguageClient.php — newer surface, including moderateText; check whether the features you need exist in V2 before standardizing on it.src/V1/Document.php, TextInput.php — request payload shape.src/V1/AnnotateTextRequest/Features.php — feature toggles for multi-feature calls.Google\Cloud\Language\LanguageClient; treat that as a legacy wrapper and prefer the versioned GAPIC client.First use case: sentiment on a support ticket
use Google\Cloud\Language\V1\AnalyzeSentimentRequest;
use Google\Cloud\Language\V1\Document;
use Google\Cloud\Language\V1\LanguageClient;
$client = new LanguageClient([
'credentials' => config('services.google.credentials'), // or omit for ADC
]);
$document = (new Document())
->setContent($ticket->body)
->setType(Document\Type::PLAIN_TEXT)
->setLanguageCode('en');
$response = $client->analyzeSentiment(
(new AnalyzeSentimentRequest())->setDocument($document)
);
$sentiment = $response->getDocumentSentiment();
return [
'score' => $sentiment->getScore(), // -1.0 .. 1.0
'magnitude' => $sentiment->getMagnitude(), // 0 .. +inf
];
Register the client as a singleton in a service provider — construction performs discovery/setup and is not free.
Bind the client, inject a domain service. Do not let protobuf request/response objects leak through your controllers. Wrap the client in an application service (SentimentAnalyzer, ContentTagger) that accepts strings/DTOs and returns arrays or your own value objects. This makes it trivial to swap in a fake for tests and to add caching or fallbacks.
// AppServiceProvider
$this->app->singleton(LanguageClient::class, fn () => new LanguageClient([
'credentials' => config('services.google_language.credentials'),
'transport' => extension_loaded('grpc') ? 'grpc' : 'rest',
]));
Use annotateText for multi-feature extraction. If you need sentiment + entities + syntax for the same text, one annotateText call with a Features object avoids three round trips. Billing is still per feature, but latency and connection overhead drop sharply.
use Google\Cloud\Language\V1\AnnotateTextRequest;
use Google\Cloud\Language\V1\AnnotateTextRequest\Features;
$features = (new Features())
->setExtractDocumentSentiment(true)
->setExtractEntities(true)
->setExtractSyntax(true)
->setExtractEntitySentiment(false)
->setClassifyText(true);
$response = $client->annotateText(
(new AnnotateTextRequest())->setDocument($document)->setFeatures($features)
);
Offload to queues. The client is synchronous. For review ingestion, ticket triage, comment moderation, or bulk backfills, push a job per document and run a small pool of workers. Apply a Laravel rate limiter or queue throttle middleware so a backfill does not burn your per-minute quota.
Persist the raw annotation, then derive. Store $response->serializeToJsonString() in a JSON column alongside your derived fields. You can re-derive sentiment thresholds, entity filters, or salience cutoffs later without re-billing the API. Version the payload with the model/feature set used.
Choose the cheapest sufficient feature. analyzeSentiment is the default for tone detection. analyzeEntities is good for tagging and search enrichment. analyzeEntitySentiment is noticeably more expensive — reserve it for cases where “the shipping was great but the product was broken” matters. classifyText returns a fixed taxonomy (or an AutoML model where configured), so it is not a general-purpose custom classifier.
Handle classification as a best-effort signal. classifyText needs a reasonable amount of prose (roughly 20+ words) and returns categories with confidence. Short strings, code, and product names produce empty or noisy results. Always have a fallback bucket for empty getCategories().
Normalize entity output. Entity type values are enum-like strings (PERSON, ORGANIZATION, LOCATION, EVENT, WORK_OF_ART, CONSUMER_GOOD, OTHER, plus numeric/address types). salience is relative importance within the document, not confidence. Deduplicate on name + type and keep the highest salience.
Language handling. Set languageCode when you know it. Auto-detection works but can misfire on short or mixed-language text; for multilingual apps, translate first or route to language-specific prompts/models rather than trusting detection on a three-word string.
Chunk long text deliberately. The API caps document size around 1 MB, and some features have tighter practical limits. Chunk on paragraph boundaries, send chunks, then aggregate sentiment by weighting score by magnitude or character count. Do not concatenate chunk scores naively.
Version and namespace confusion. V1\LanguageClient and V2\LanguageClient are distinct; so are their request/response classes. Copy-pasting snippets that mix V1 requests with a V2 client produces type errors that look like auth failures. Pick one version and import consistently. Check src/V1/Document/Type.php for the actual enum constant names in your installed version.
gRPC vs REST. The client prefers gRPC when ext-grpc is present. If your container lacks the extension, either install it or force 'transport' => 'rest'. REST is easier to debug through proxies and Private Service Connect, but higher per-call overhead. A gRPC/protobuf version mismatch usually surfaces as a fatal extension error at autoload time, not a clean API exception.
Sentiment semantics. Score is document-level polarity; magnitude is emotional volume. A long rant and a short praise can both score -0.9/+0.9, but the rant's magnitude will be much higher. Mixed content often yields a near-zero score with high magnitude — that is a signal of ambiguity, not neutrality. Do not compare scores across documents without considering length and magnitude.
Encoding and offsets. If you need token offsets to highlight text, set the request encoding type to UTF-8 and treat offsets as byte offsets. Slicing multibyte text with substr will corrupt accented characters and emoji. Use mb_* carefully or convert to code-point offsets. If you do not request an encoding, offsets may be unavailable or in different units.
Auth failures that are not auth failures. PERMISSION_DENIED often means the API is not enabled on the project, the service account lacks access, or the quota project is wrong — not necessarily a bad key. ApiException::getStatus() gives the canonical status string; log it plus the request's project and endpoint. When using user credentials or impersonation, set quotaProjectId so quota is charged correctly.
Retries and timeouts. These calls are read-only and safe to retry. Configure retrySettings on the client (or per call where supported) with bounded total timeouts, and catch ApiException for UNAVAILABLE, DEADLINE_EXCEEDED, and RESOURCE_EXHAUSTED. For RESOURCE_EXHAUSTED, back off with jitter rather than hammering; for INVALID_ARGUMENT, fix the payload — retrying will never help.
Testing. Never call the live API in CI. Wrap the client behind your own interface and bind a fake in tests. If you need to test the GAPIC layer itself, reuse the mock transport patterns from the package's generated tests (Google\ApiCore\Testing\*) rather than inventing HTTP stubs. Also assert on your own DTOs, not on protobuf object graphs.
Composer conflicts. This package pulls google/gax, google/common-protos, and google/cloud-core. In larger apps these are frequently pinned by other Google packages. Use composer why google/gax before bumping. Since releases are automated from the google-cloud-php monorepo, a high patch version does not imply a hand-written feature; check the changelog for actual API surface changes.
Logging and PII. Request bodies contain raw user text. Do not log full content or full responses by default — log document IDs, feature flags, lengths, latency, and status codes. If you need payload debugging, gate it behind an env flag and redact. Consider DLP or local redaction before sending sensitive text to any external API.
Billing surprises. Charges accrue per feature per 1,000-character unit. annotateText with four features enabled is billed as four feature calls, so a “single request” can be several times the cost of analyzeSentiment. Watch analyzeEntitySentiment and repeated classifyText on long documents, and cache aggressively for immutable content.
Debugging transport issues. For gRPC, run one request with GRPC_VERBOSITY=DEBUG GRPC_TRACE=all to see handshake and metadata problems; for REST, force 'transport' => 'rest' and inspect with a local proxy. apiEndpoint can be overridden for private endpoints and local stubs, which is useful when validating networking without touching production credentials.
How can I help you explore Laravel packages today?