iboldurev/api-ai-php
A PHP client for the API.AI (Dialogflow) natural language service, letting you add intent detection, entity recognition, and conversational agents to PHP or Laravel apps through a simple, expressive API.
This is a thin, unmaintained (last release Oct 2017) PHP wrapper around the Dialogflow/API.AI v1 HTTP API — not the v2 gRPC/REST API. Before anything else, confirm the endpoint it targets still works for your agent; if you are on Dialogflow ES v2 or CX, this package will not talk to your project.
Install and verify the source
composer require iboldurev/api-ai-php
ls vendor/iboldurev/api-ai-php/src
The repo is not linked from Packagist metadata you have, so read composer.json and src/ first. The classes you'll actually touch are the client (HTTP + auth), a query/message object (text, session, contexts, language), and a response wrapper around the JSON body.
Wire up the token
Store the agent's client access token (Settings → Agent → API keys in the legacy console) in Laravel config, never inline:
// config/services.php
'dialogflow' => [
'token' => env('DIALOGFLOW_CLIENT_TOKEN'),
],
// .env
DIALOGFLOW_CLIENT_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxx
First call
use IBoldurev\ApiAi\Client;
use IBoldurev\ApiAi\Query;
$client = new Client(config('services.dialogflow.token'));
$query = new Query('Book a table for two tomorrow at 7pm');
$query->setSessionId('chat-'.auth()->id()); // stable per conversation
$query->setLang('en');
$response = $client->query($query);
// The wrapper exposes the decoded "result" object; treat it as an array
// so you are not coupled to the DTO's getters.
$result = $response->getResult();
$reply = $result['fulfillment']['speech'] ?? '';
$intent = $result['metadata']['intentName'] ?? null;
$params = $result['parameters'] ?? [];
First real use case: a form/chat widget where free-text input is classified into intents and slot parameters (date, time, partySize) are extracted, then handed to your own domain code. Don't use it for the response text alone — the value is the structured parameters and contexts.
Bind it as a singleton, not a per-request new. The client holds a Guzzle instance; recreating it per call wastes connections and makes middleware-based logging harder.
// AppServiceProvider
$this->app->singleton(Client::class, fn () =>
new Client(config('services.dialogflow.token'))
);
Session identity is the whole state machine. Dialogflow keeps context and follow-up state per sessionId. Use one ID per conversation (user + channel + thread), persisted — not uniqid() per request, or every follow-up question starts from zero.
$sessionId = "web:{$userId}:{$conversationId}";
Treat contexts as your slot-filling loop. When a required parameter is missing, Dialogflow returns an outputContext (e.g. awaiting_size) plus a prompt. Your loop is: send text → read actionIncomplete → if incomplete, keep the same session and just send the next user message. Don't reimplement slot filling in PHP; let the agent drive it.
$result = $response->getResult();
if (! empty($result['actionIncomplete'])) {
return $result['fulfillment']['speech']; // ask the next question
}
return $this->dispatch($result['metadata']['intentName'], $result['parameters']);
Route intents to handlers, not to if chains.
protected array $handlers = [
'book.table' => BookTableIntent::class,
'order.status' => OrderStatusIntent::class,
'smalltalk' => SmalltalkIntent::class,
];
Each handler gets (array $parameters, array $contexts) and returns a reply string. This keeps the SDK at the edge and your domain logic testable without HTTP.
Clear contexts to reset. If a user abandons a booking flow, the next message will still be inside awaiting_size. Send an explicit reset — either a /reset style query or an empty/null contexts payload with a fresh sessionId. Rotating the session ID is the cheap, reliable reset.
Map SDK/Guzzle failures to user-facing behaviour. Wrap calls and distinguish transport from semantic failure:
try {
$response = $client->query($query);
} catch (\GuzzleHttp\Exception\ConnectException $e) {
// timeout / DNS — retry once, then fall back to "try again"
} catch (\Throwable $e) {
Log::warning('dialogflow.query_failed', ['session' => $sessionId, 'e' => $e->getMessage()]);
return $this->fallbackReply();
}
A chatbot must never 500 on the user; degrade to a canned reply and log.
Log the raw payload, always. Classification problems are almost never PHP bugs — they're the agent missing training phrases. Persist query text → intentName → confidence → parameters (a JSON column or a log channel) so you can hand real misclassifications back to whoever owns the agent.
Async where latency doesn't matter. For analytics-style enrichment (tagging a conversation, sentiment, intent auditing) push the query into a queued job. Keep synchronous calls only for the request/response path.
Testing without the network. Inject a Guzzle client with a MockHandler (or bind a fake Client) so intent-handler tests assert on fixture payloads:
$mock = new MockHandler([new Response(200, [], json_encode($fixture))]);
$client = new Client('test-token', new \GuzzleHttp\Client(['handler' => HandlerStack::create($mock)]));
api.api.ai/v1/query and the whole v1 surface were retired; Dialogflow v2 requires a service-account JSON, OAuth2 access tokens, and a projects/{project}/agent/sessions/{session} path. If your target is a current Dialogflow agent, this package is a reference implementation at best — plan on google/cloud-dialogflow or a small hand-rolled v2 client. Validate this first; it determines whether the rest of this assessment matters.composer why-not guzzlehttp/guzzle before blaming your own code. Isolating the dependency in a small internal service is cheaper than fighting it.sessionId defaults can silently create fresh state. If the query object doesn't carry a session ID, every call may look like a new conversation — follow-ups never resolve, contexts never persist. Always set it explicitly.result.fulfillment.speech, result.parameters, result.contexts, result.metadata.intentName, result.score, result.actionIncomplete. Reaching for a custom getter that doesn't exist wastes time; read the array and log one real payload early.null, not absent. $params['date'] ?? null will be null both when unset and when the agent returned an empty slot. Check actionIncomplete and your required-slot list rather than testing nulls.timezone parameter) or relative phrases ("tomorrow") resolve against the agent's default timezone — a classic off-by-one-day bug. Same for lang: default it from the request, not the app config.Middleware::tap or Middleware::log to the handler stack to see exact request/response bodies; the SDK's exceptions often don't include the response body, and Dialogflow's error JSON is where the actual message lives.DialogflowService and never expose SDK types above it, (b) add Guzzle middleware for auth refresh/logging, (c) fork the package — for a ~10-file 2017 SDK, vendoring the source into app/Services/Dialogflow and deleting the Composer dependency is often the lowest-risk move, since no upstream fixes are coming.How can I help you explore Laravel packages today?