SEO Metadata
SEO Title Options
- PHP and AI: Integrating ChatGPT & Claude APIs Into Your
- PHP APIs: Practical 2026 Guide
- APIs Playbook: PHP APIs
Meta Description Options
- Learn PHP APIs with a practical APIs framework, expert mistakes, implementation steps, examples, FAQ, and schema-ready guidance.
- Practical tutorial for calling LLM APIs from PHP - streaming responses, function calling, context management, and cost optimization.
URL Slug
php-ai-integrating-chatgpt-claude-apis-php-application
Focus Keyword
PHP APIs
Additional LSI Keywords
- APIs
- PHP
- OpenAI
- ChatGPT
- Claude
- AI
- PHP and AI: Integrating ChatGPT & Claude APIs Into Your PHP Application
- production checklist
- implementation guide
- best practices
- architecture decisions
- testing strategy
Table of Contents
- Article overview
- What PHP APIs means
- Why it matters now
- Implementation framework
- Practical comparison
- Expert workflow
- Common mistakes
- Media and link plan
- Original technical deep dive
- FAQ
- Structured data
- Conclusion
Article overview
PHP APIs is the kind of topic that looks simple until it reaches production. Teams usually discover the real cost late: unclear boundaries, weak defaults, hidden maintenance work, and decisions that seemed harmless when the codebase was small.
The problem gets worse when the article, tutorial, or implementation guide only explains the happy path. This guide closes that gap with a practical framework, a comparison table, common mistakes, and a deep technical section you can use while planning real work.
Keep reading for the non-obvious part: the safest implementation is rarely the most impressive-looking one. It is the one your team can debug, test, document, and evolve without turning every future change into archaeology.
Key Takeaways
- PHP APIs should be evaluated as a production decision, not only as a syntax or tooling choice.
- The best implementation keeps responsibilities visible, with clear ownership, tests, documentation, and rollback paths.
- Search visibility improves when practical depth, structured answers, and expert examples live on the same page.
[IMAGE: A mobile-first technical article layout showing the main concept, decision table, implementation checklist, and FAQ blocks. Alt: PHP APIs expert guide for APIs]
What PHP APIs means
PHP APIs means applying apis knowledge to a concrete engineering decision, then turning that decision into reliable code, documentation, and operational behavior. In practice, it combines the topic's core concepts with trade-off analysis, implementation boundaries, testing strategy, and maintenance discipline.
This is the definition worth optimizing for featured snippets because it avoids hype. It tells the reader what the topic does and what a professional implementation must include.
Why it matters now
The technical web is more crowded than it was a few years ago. Thin tutorials can still get indexed, but they rarely earn trust from senior developers, buyers, AI answer systems, or teams that need production guidance.
For apis topics, the strongest content now has three layers:
- a clear answer for fast scanning
- a practical framework for implementation
- expert context that explains what breaks later
That same structure helps search engines understand the page. It also helps readers decide whether the advice fits their project.
Implementation framework
Use this framework before adopting the approach described in this article.
- Define the user problem and the production risk.
- Identify the smallest reliable implementation boundary.
- Keep configuration, secrets, and environment-specific behavior outside the article's core logic.
- Add tests for the behavior that would hurt if it regressed.
- Document the trade-off, not only the final code.
- Measure the result with logs, metrics, or user-facing outcomes.
- Revisit the decision after real usage exposes edge cases.
The sequence is deliberately conservative. It keeps the work grounded in outcomes instead of novelty.
[IMAGE: A seven-step implementation framework with discovery, boundary design, configuration, tests, documentation, measurement, and iteration. Alt: PHP APIs implementation framework]
Practical comparison
| Decision area | Strong approach | Weak approach | Why it matters |
|---|---|---|---|
| Scope | Solve one clear problem | Mix unrelated concerns | Focus improves testing and search intent |
| Architecture | Put logic in explicit classes or documented boundaries | Hide behavior in templates or incidental callbacks | Future changes stay easier to review |
| Data flow | Pass prepared data into the view or endpoint | Query or compute in presentation code | Reduces regressions and performance surprises |
| Testing | Cover the risky behavior directly | Test only the happy path | Catches production failures earlier |
| Documentation | Explain trade-offs and limits | Repeat generic definitions | Builds E-E-A-T and reader trust |
| Operations | Track logs, metrics, and rollback steps | Ship without measurement | Makes the decision reversible |
This table is intentionally practical. It gives a reviewer something to check before the implementation becomes expensive to change.
Expert workflow
Expert tip: "Treat PHP APIs as a system boundary. If the next developer cannot find where the decision lives, how it is tested, and when it should be avoided, the implementation is not finished."
A useful workflow is simple:
- Start with the smallest working example.
- Add the constraints that exist in your real project.
- Remove anything that only demonstrates cleverness.
- Write down the failure modes.
- Add links to related decisions so future readers can navigate the topic cluster.
That last point matters for both humans and search systems. A single article can answer a question; a cluster proves authority.
Common mistakes
Mistake 1: Copying a pattern without its context
A pattern that works in a small demo can fail in a real application. The missing context is usually data volume, team experience, deployment process, security requirements, or observability.
Before copying the pattern, ask what assumption made it safe in the original example.
Mistake 2: Putting business logic in the wrong layer
This is the fastest way to make future debugging expensive. In Laravel, PHP, and server-rendered websites, presentation should receive prepared data, not discover rules on its own.
Keep decision logic in models, actions, services, policies, requests, jobs, or documented helpers where it can be tested directly.
Mistake 3: Optimizing for novelty instead of maintainability
Newer tools and language features can be valuable. They can also hide simple behavior behind unfamiliar syntax.
Use the option that makes the next production incident easier to understand.
Mistake 4: Publishing without a measurement plan
If the article describes a performance, SEO, security, or architecture improvement, define how success will be checked. Logs, tests, crawl diagnostics, analytics, and user behavior are all stronger than assumptions.
[IMAGE: A common-mistakes board with context loss, wrong layer, novelty bias, and missing measurement highlighted. Alt: PHP APIs common mistakes]
Media and link plan
Image placeholders
- [IMAGE: A concept diagram for PHP APIs with input, decision boundary, implementation, tests, and production feedback. Alt: PHP APIs concept diagram]
- [IMAGE: A mobile screenshot-style checklist for PHP and AI: Integrating ChatGPT & Claude APIs Into Your PHP Application. Alt: PHP APIs mobile checklist]
- [IMAGE: A comparison table visualization for strong versus weak implementation choices. Alt: PHP APIs comparison table]
Video placeholder
[VIDEO: Insert a 5-8 minute YouTube walkthrough that demonstrates the main decision, the implementation boundary, the test strategy, and the production caveats for PHP APIs.]
Trustworthy outbound links
- PHP manual - use this as the trust reference for language-level reference.
- Google Search quality guidance - use this as the trust reference for people-first content and E-E-A-T alignment.
Internal linking opportunities
- Internal guide: Building Webhooks in PHP: Payload Validation - use this when readers need a related APIs follow-up.
- Internal guide: Building a PHP SDK: HTTP Client Abstraction - use this when readers need a related APIs follow-up.
Original Technical Deep Dive
Version note
This article is dated March 25, 2025 because it belongs to the editorial timeline of this blog series.
The examples were reviewed on May 7, 2026. OpenAI and Anthropic APIs move quickly, so keep model names in configuration and verify current provider docs before shipping a production integration.
The short version
Do not wire an AI provider directly into controllers.
Use a small boundary:
- One application interface:
AiClient. - One implementation for OpenAI's Responses API.
- One implementation for Anthropic's Messages API.
- One tool executor with an allowlist.
- One place to handle retries, timeouts, request IDs, token usage, and provider errors.
The important differences:
| Concern | OpenAI | Claude |
|---|---|---|
| Main endpoint for new text/tool apps | POST /v1/responses | POST /v1/messages |
| Authentication | Authorization: Bearer ... | x-api-key: ... plus anthropic-version |
| Streaming format | Server-sent events with response.output_text.delta events | Server-sent events with content_block_delta text deltas |
| Tool calling | Model returns function_call items; app sends function_call_output items back | Claude returns tool_use blocks; app sends tool_result blocks back |
| Conversation state | Manual history, previous_response_id, or Conversations API | Stateless Messages API; send conversation history each request |
| Prompt caching | Automatic for supported long prompts; optimize stable prefixes | Explicit cache_control breakpoints for reusable prefixes |
Keep both providers behind your own code. Product code should ask for a response, not know which JSON shape each vendor currently uses.
Environment configuration
Use server-side environment variables:
AI_PROVIDER=openai
OPENAI_API_KEY=sk-...
OPENAI_MODEL=your-openai-model
ANTHROPIC_API_KEY=sk-ant-...
ANTHROPIC_MODEL=your-claude-model
ANTHROPIC_VERSION=2023-06-01
Never expose these keys in browser JavaScript, mobile app bundles, or logs.
A minimal config object:
declare(strict_types=1);
final readonly class AiConfig
{
public function __construct(
public string $provider,
public string $openAiApiKey,
public string $openAiModel,
public string $anthropicApiKey,
public string $anthropicModel,
public string $anthropicVersion = '2023-06-01',
) {}
public static function fromEnvironment(): self
{
return new self(
provider: self::required('AI_PROVIDER'),
openAiApiKey: self::required('OPENAI_API_KEY'),
openAiModel: self::required('OPENAI_MODEL'),
anthropicApiKey: self::required('ANTHROPIC_API_KEY'),
anthropicModel: self::required('ANTHROPIC_MODEL'),
anthropicVersion: getenv('ANTHROPIC_VERSION') ?: '2023-06-01',
);
}
private static function required(string $key): string
{
$value = getenv($key);
if (! is_string($value) || trim($value) === '') {
throw new RuntimeException("Missing required environment variable: {$key}");
}
return $value;
}
}
Make the model required instead of hardcoding a default. That forces deployment config to make a conscious provider and cost choice.
A small interface
Application code should depend on this:
declare(strict_types=1);
interface AiClient
{
/**
* @param list<AiMessage> $messages
*/
public function complete(array $messages): AiResponse;
/**
* @param list<AiMessage> $messages
* @param callable(string): void $onText
*/
public function stream(array $messages, callable $onText): void;
}
final readonly class AiMessage
{
public function __construct(
public string $role,
public string $content,
) {}
}
final readonly class AiResponse
{
/**
* @param array<string, mixed> $usage
*/
public function __construct(
public string $text,
public array $usage = [],
public ?string $providerRequestId = null,
) {}
}
Now a controller or job can stay boring:
declare(strict_types=1);
final readonly class SupportReplyService
{
public function __construct(private AiClient $ai) {}
public function draftReply(string $customerMessage): string
{
$response = $this->ai->complete([
new AiMessage('system', 'Write concise support replies. Do not invent policy.'),
new AiMessage('user', $customerMessage),
]);
return $response->text;
}
}
Provider details stay outside this service.
Shared HTTP client
Use Guzzle, Symfony HTTP Client, Laravel HTTP client, Saloon, or PSR-18 in a real app. The examples below use cURL so the protocol details are visible.
declare(strict_types=1);
final class JsonHttpClient
{
/**
* @param array<string, string> $headers
* @param array<string, mixed> $payload
* @return array{body: array<string, mixed>, headers: array<string, string>, status: int}
*/
public function postJson(string $url, array $headers, array $payload, int $timeoutSeconds = 60): array
{
$curl = curl_init($url);
$responseHeaders = [];
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $this->formatHeaders($headers + ['Content-Type' => 'application/json']),
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADERFUNCTION => static function ($curl, string $line) use (&$responseHeaders): int {
$length = strlen($line);
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return $length;
},
CURLOPT_TIMEOUT => $timeoutSeconds,
]);
$raw = curl_exec($curl);
if ($raw === false) {
$error = curl_error($curl);
curl_close($curl);
throw new RuntimeException("AI HTTP request failed: {$error}");
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$body = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
if (! is_array($body)) {
throw new RuntimeException('AI provider returned an invalid JSON body.');
}
if ($status >= 400) {
$message = $body['error']['message'] ?? $body['error']['type'] ?? 'Unknown AI provider error';
throw new RuntimeException("AI provider returned HTTP {$status}: {$message}");
}
return [
'body' => $body,
'headers' => $responseHeaders,
'status' => $status,
];
}
/**
* @param array<string, string> $headers
* @return list<string>
*/
private function formatHeaders(array $headers): array
{
$formatted = [];
foreach ($headers as $name => $value) {
$formatted[] = "{$name}: {$value}";
}
return $formatted;
}
}
Production additions:
- Retry only safe transient errors.
- Add a hard timeout.
- Log provider request IDs.
- Track status code, model, input tokens, output tokens, and cached tokens.
- Do not log prompts unless your privacy policy and customer contract allow it.
[IMAGE: Supporting visual 1 for PHP and AI: Integrating ChatGPT & Claude APIs Into Your PHP Application, showing PHP APIs decisions, examples, and PHP, OpenAI, ChatGPT. Alt: PHP APIs php-ai-integrating-chatgpt-claude-apis-php-application visual 1]
[IMAGE: Supporting visual 1 for PHP and AI: Integrating ChatGPT & Claude APIs Into Your PHP Application, showing PHP APIs decisions, examples, and PHP, OpenAI, ChatGPT. Alt: PHP APIs php-ai-integrating-chatgpt-claude-apis-php-application visual 1]
OpenAI Responses API client
OpenAI recommends the Responses API for new projects. It supports text generation, multimodal inputs, stateful interactions, and tools.
declare(strict_types=1);
final readonly class OpenAiResponsesClient implements AiClient
{
public function __construct(
private JsonHttpClient $http,
private string $apiKey,
private string $model,
) {}
public function complete(array $messages): AiResponse
{
$payload = [
'model' => $this->model,
'input' => $this->toOpenAiInput($messages),
'store' => false,
];
$result = $this->http->postJson(
url: 'https://api.openai.com/v1/responses',
headers: [
'Authorization' => "Bearer {$this->apiKey}",
'X-Client-Request-Id' => bin2hex(random_bytes(16)),
],
payload: $payload,
);
return new AiResponse(
text: $this->extractText($result['body']),
usage: $result['body']['usage'] ?? [],
providerRequestId: $result['headers']['x-request-id'] ?? null,
);
}
public function stream(array $messages, callable $onText): void
{
$payload = [
'model' => $this->model,
'input' => $this->toOpenAiInput($messages),
'stream' => true,
'store' => false,
];
(new SseClient())->post(
url: 'https://api.openai.com/v1/responses',
headers: [
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
],
payload: $payload,
onEvent: static function (string $event, array $data) use ($onText): void {
if (($data['type'] ?? null) === 'response.output_text.delta') {
$delta = $data['delta'] ?? '';
if (is_string($delta) && $delta !== '') {
$onText($delta);
}
}
if (($data['type'] ?? null) === 'error') {
$message = $data['error']['message'] ?? 'OpenAI streaming error';
throw new RuntimeException((string) $message);
}
},
);
}
/**
* @param list<AiMessage> $messages
* @return list<array{role: string, content: string}>
*/
private function toOpenAiInput(array $messages): array
{
return array_map(
static fn (AiMessage $message): array => [
'role' => $message->role,
'content' => $message->content,
],
$messages,
);
}
/**
* @param array<string, mixed> $body
*/
private function extractText(array $body): string
{
if (isset($body['output_text']) && is_string($body['output_text'])) {
return $body['output_text'];
}
$text = '';
foreach (($body['output'] ?? []) as $item) {
foreach (($item['content'] ?? []) as $content) {
if (($content['type'] ?? null) === 'output_text' && is_string($content['text'] ?? null)) {
$text .= $content['text'];
}
}
}
return $text;
}
}
store: false disables default response storage for this request. Use provider-managed conversation state only when your product needs it and your retention policy allows it.
Claude Messages API client
Claude's Messages API is stateless. You send the current conversation history on each request.
declare(strict_types=1);
final readonly class ClaudeMessagesClient implements AiClient
{
public function __construct(
private JsonHttpClient $http,
private string $apiKey,
private string $model,
private string $anthropicVersion,
) {}
public function complete(array $messages): AiResponse
{
[$system, $conversation] = $this->splitSystemMessages($messages);
$payload = [
'model' => $this->model,
'max_tokens' => 1024,
'messages' => $conversation,
];
if ($system !== '') {
$payload['system'] = $system;
}
$result = $this->http->postJson(
url: 'https://api.anthropic.com/v1/messages',
headers: [
'x-api-key' => $this->apiKey,
'anthropic-version' => $this->anthropicVersion,
],
payload: $payload,
);
return new AiResponse(
text: $this->extractText($result['body']),
usage: $result['body']['usage'] ?? [],
providerRequestId: $result['headers']['request-id'] ?? null,
);
}
public function stream(array $messages, callable $onText): void
{
[$system, $conversation] = $this->splitSystemMessages($messages);
$payload = [
'model' => $this->model,
'max_tokens' => 1024,
'stream' => true,
'messages' => $conversation,
];
if ($system !== '') {
$payload['system'] = $system;
}
(new SseClient())->post(
url: 'https://api.anthropic.com/v1/messages',
headers: [
'x-api-key' => $this->apiKey,
'anthropic-version' => $this->anthropicVersion,
'Content-Type' => 'application/json',
],
payload: $payload,
onEvent: static function (string $event, array $data) use ($onText): void {
if (($data['type'] ?? null) === 'content_block_delta') {
$delta = $data['delta'] ?? [];
if (($delta['type'] ?? null) === 'text_delta' && is_string($delta['text'] ?? null)) {
$onText($delta['text']);
}
}
if (($data['type'] ?? null) === 'error') {
$message = $data['error']['message'] ?? 'Claude streaming error';
throw new RuntimeException((string) $message);
}
},
);
}
/**
* @param list<AiMessage> $messages
* @return array{0: string, 1: list<array{role: string, content: string}>}
*/
private function splitSystemMessages(array $messages): array
{
$system = [];
$conversation = [];
foreach ($messages as $message) {
if ($message->role === 'system') {
$system[] = $message->content;
continue;
}
$conversation[] = [
'role' => $message->role,
'content' => $message->content,
];
}
return [implode("\n\n", $system), $conversation];
}
/**
* @param array<string, mixed> $body
*/
private function extractText(array $body): string
{
$text = '';
foreach (($body['content'] ?? []) as $block) {
if (($block['type'] ?? null) === 'text' && is_string($block['text'] ?? null)) {
$text .= $block['text'];
}
}
return $text;
}
}
Claude does not use a "system" role in the messages array. Put system instructions in the top-level system field.
Streaming with server-sent events
Both APIs stream as server-sent events. The event names differ, but the wire format is similar.
A minimal SSE client:
declare(strict_types=1);
final class SseClient
{
/**
* @param array<string, string> $headers
* @param array<string, mixed> $payload
* @param callable(string, array<string, mixed>): void $onEvent
*/
public function post(string $url, array $headers, array $payload, callable $onEvent): void
{
$buffer = '';
$event = 'message';
$dataLines = [];
$flush = static function () use (&$event, &$dataLines, $onEvent): void {
if ($dataLines === []) {
return;
}
$rawData = implode("\n", $dataLines);
$dataLines = [];
if ($rawData === '[DONE]') {
return;
}
$decoded = json_decode($rawData, true, flags: JSON_THROW_ON_ERROR);
if (is_array($decoded)) {
$onEvent($event, $decoded);
}
$event = 'message';
};
$curl = curl_init($url);
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $this->formatHeaders($headers),
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_WRITEFUNCTION => static function ($curl, string $chunk) use (&$buffer, &$event, &$dataLines, $flush): int {
$buffer .= $chunk;
while (($position = strpos($buffer, "\n")) !== false) {
$line = rtrim(substr($buffer, 0, $position), "\r");
$buffer = substr($buffer, $position + 1);
if ($line === '') {
$flush();
continue;
}
if (str_starts_with($line, 'event:')) {
$event = trim(substr($line, 6));
continue;
}
if (str_starts_with($line, 'data:')) {
$dataLines[] = ltrim(substr($line, 5));
}
}
return strlen($chunk);
},
CURLOPT_TIMEOUT => 120,
]);
$ok = curl_exec($curl);
if ($ok === false) {
$error = curl_error($curl);
curl_close($curl);
throw new RuntimeException("AI stream failed: {$error}");
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($status >= 400) {
throw new RuntimeException("AI stream returned HTTP {$status}");
}
}
/**
* @param array<string, string> $headers
* @return list<string>
*/
private function formatHeaders(array $headers): array
{
$formatted = [];
foreach ($headers as $name => $value) {
$formatted[] = "{$name}: {$value}";
}
return $formatted;
}
}
In a web app, stream to the browser from your server:
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
$ai->stream($messages, static function (string $delta): void {
echo 'data: '.json_encode(['text' => $delta], JSON_THROW_ON_ERROR)."\n\n";
flush();
});
Do moderation and policy checks before streaming when possible. Streaming partial output is harder to inspect than a complete response.
Function calling with OpenAI
Function calling is not magic. The model asks to call a tool. Your application decides whether to run it.
Define tools:
$tools = [
[
'type' => 'function',
'name' => 'lookup_order',
'description' => 'Look up an order by public order number.',
'parameters' => [
'type' => 'object',
'properties' => [
'order_number' => [
'type' => 'string',
'description' => 'The customer-facing order number.',
],
],
'required' => ['order_number'],
'additionalProperties' => false,
],
'strict' => true,
],
];
Call the model:
$first = $http->postJson(
'https://api.openai.com/v1/responses',
['Authorization' => "Bearer {$openAiKey}"],
[
'model' => $openAiModel,
'input' => [
[
'role' => 'user',
'content' => 'Where is order A10045?',
],
],
'tools' => $tools,
'tool_choice' => 'auto',
'store' => false,
],
)['body'];
Execute only allowlisted calls:
$toolOutputs = [];
foreach (($first['output'] ?? []) as $item) {
if (($item['type'] ?? null) !== 'function_call') {
continue;
}
$name = $item['name'] ?? '';
$arguments = json_decode((string) ($item['arguments'] ?? '{}'), true, flags: JSON_THROW_ON_ERROR);
if ($name !== 'lookup_order') {
throw new RuntimeException("Unsupported tool call: {$name}");
}
$order = $orderRepository->findPublicSummary((string) $arguments['order_number']);
$toolOutputs[] = [
'type' => 'function_call_output',
'call_id' => $item['call_id'],
'output' => json_encode($order, JSON_THROW_ON_ERROR),
];
}
Send the tool result back:
$secondInput = [
...($first['output'] ?? []),
...$toolOutputs,
];
$final = $http->postJson(
'https://api.openai.com/v1/responses',
['Authorization' => "Bearer {$openAiKey}"],
[
'model' => $openAiModel,
'input' => $secondInput,
'tools' => $tools,
'store' => false,
],
)['body'];
Security rules:
- Never let the model choose arbitrary PHP functions.
- Never execute SQL, shell commands, or HTTP requests from raw model text.
- Validate tool arguments with normal application validation.
- Enforce authorization before returning private data to a tool result.
- Make write tools idempotent where possible.
Tool use with Claude
Claude's client-side tool flow is similar, but the JSON shape is different.
Tool definition:
$tools = [
[
'name' => 'lookup_order',
'description' => 'Look up an order by public order number.',
'input_schema' => [
'type' => 'object',
'properties' => [
'order_number' => [
'type' => 'string',
],
],
'required' => ['order_number'],
],
],
];
First request:
$messages = [
[
'role' => 'user',
'content' => 'Where is order A10045?',
],
];
$first = $http->postJson(
'https://api.anthropic.com/v1/messages',
[
'x-api-key' => $anthropicKey,
'anthropic-version' => '2023-06-01',
],
[
'model' => $claudeModel,
'max_tokens' => 1024,
'tools' => $tools,
'messages' => $messages,
],
)['body'];
Handle tool_use blocks:
$toolResults = [];
foreach (($first['content'] ?? []) as $block) {
if (($block['type'] ?? null) !== 'tool_use') {
continue;
}
if (($block['name'] ?? '') !== 'lookup_order') {
throw new RuntimeException('Unsupported Claude tool call.');
}
$input = $block['input'] ?? [];
$order = $orderRepository->findPublicSummary((string) $input['order_number']);
$toolResults[] = [
'type' => 'tool_result',
'tool_use_id' => $block['id'],
'content' => json_encode($order, JSON_THROW_ON_ERROR),
];
}
Second request:
$messages[] = [
'role' => 'assistant',
'content' => $first['content'],
];
$messages[] = [
'role' => 'user',
'content' => $toolResults,
];
$final = $http->postJson(
'https://api.anthropic.com/v1/messages',
[
'x-api-key' => $anthropicKey,
'anthropic-version' => '2023-06-01',
],
[
'model' => $claudeModel,
'max_tokens' => 1024,
'tools' => $tools,
'messages' => $messages,
],
)['body'];
Do not normalize OpenAI and Claude tool payloads too early. Normalize at your application boundary, but preserve provider payloads inside provider clients so you can follow each provider's protocol correctly.
Context management
Context is not memory. It is input you pay to send.
For OpenAI:
- You can manually send prior messages.
- You can chain turns with
previous_response_id. - You can use Conversations API when you need durable provider-managed state.
- Even with
previous_response_id, previous input tokens in the chain are billed as input tokens. - Response objects are stored by default; set
store: falsewhen provider-side storage is not needed.
[IMAGE: Supporting visual 2 for PHP and AI: Integrating ChatGPT & Claude APIs Into Your PHP Application, showing PHP APIs decisions, examples, and PHP, OpenAI, ChatGPT. Alt: PHP APIs php-ai-integrating-chatgpt-claude-apis-php-application visual 2]
For Claude:
- The Messages API is stateless.
- Send prior turns in
messages. - Use the top-level
systemparameter for system instructions. - Count tokens before large requests with
/v1/messages/count_tokens. - Summarize or trim old turns when the conversation gets too large.
[IMAGE: Supporting visual 2 for PHP and AI: Integrating ChatGPT & Claude APIs Into Your PHP Application, showing PHP APIs decisions, examples, and PHP, OpenAI, ChatGPT. Alt: PHP APIs php-ai-integrating-chatgpt-claude-apis-php-application visual 2]
A simple application-managed conversation table:
ai_conversations
id
user_id
provider
model
summary
created_at
updated_at
ai_messages
id
conversation_id
role
content
token_estimate
created_at
Before each call:
- Load the system prompt.
- Add the conversation summary.
- Add recent messages.
- Add the new user message.
- Estimate tokens.
- If too large, summarize older messages and retry.
Do not store raw prompts forever by accident. Put retention rules on these tables.
Prompt caching and cost control
Cost control starts with fewer tokens and fewer calls.
Use these rules for both providers:
- Put stable instructions first.
- Put tool definitions before volatile user context.
- Put user-specific or request-specific data at the end.
- Keep large reference context stable across turns when possible.
- Log input tokens, output tokens, and cached token fields.
- Route simple tasks to smaller models when quality is sufficient.
- Use batch APIs for offline bulk work.
OpenAI prompt caching is automatic on supported long prompts. You can improve hit rate by keeping an exact stable prefix and using prompt_cache_key consistently for related requests:
$payload = [
'model' => $openAiModel,
'instructions' => $staticInstructions,
'input' => $dynamicUserQuestion,
'prompt_cache_key' => 'support-assistant-v4',
'store' => false,
];
Claude prompt caching is explicit. Mark the end of reusable content with cache_control:
$payload = [
'model' => $claudeModel,
'max_tokens' => 1024,
'system' => [
[
'type' => 'text',
'text' => $longStablePolicyManual,
'cache_control' => [
'type' => 'ephemeral',
'ttl' => '5m',
],
],
],
'messages' => [
[
'role' => 'user',
'content' => 'Draft a reply for ticket A10045.',
],
],
];
Do not cache content that changes every request. A timestamp in the cached prefix can destroy the cache hit rate.
Provider selection
Select the provider at composition time:
declare(strict_types=1);
final readonly class AiClientFactory
{
public function __construct(
private JsonHttpClient $http,
private AiConfig $config,
) {}
public function make(): AiClient
{
return match ($this->config->provider) {
'openai' => new OpenAiResponsesClient(
http: $this->http,
apiKey: $this->config->openAiApiKey,
model: $this->config->openAiModel,
),
'anthropic' => new ClaudeMessagesClient(
http: $this->http,
apiKey: $this->config->anthropicApiKey,
model: $this->config->anthropicModel,
anthropicVersion: $this->config->anthropicVersion,
),
default => throw new InvalidArgumentException("Unsupported AI provider: {$this->config->provider}"),
};
}
}
Keep provider switching operationally controlled. A fallback from one model family to another can change answers, tool behavior, latency, safety behavior, and cost.
Testing
Do not run live LLM calls in normal unit tests.
Test the boundary:
declare(strict_types=1);
final class FakeAiClient implements AiClient
{
public function __construct(private string $reply = 'Test reply') {}
public function complete(array $messages): AiResponse
{
return new AiResponse($this->reply);
}
public function stream(array $messages, callable $onText): void
{
$onText($this->reply);
}
}
Integration tests should use recorded fixtures or a separate provider test suite:
- One test proves OpenAI request mapping.
- One test proves Claude request mapping.
- One test proves tool allowlisting.
- One test proves malformed provider responses fail cleanly.
- One scheduled smoke test can hit real APIs with a tiny prompt.
[IMAGE: Supporting visual 3 for PHP and AI: Integrating ChatGPT & Claude APIs Into Your PHP Application, showing PHP APIs decisions, examples, and PHP, OpenAI, ChatGPT. Alt: PHP APIs php-ai-integrating-chatgpt-claude-apis-php-application visual 3]
Production checklist
- API keys stay server-side.
- Provider, model, and timeout are configurable.
- User prompts are validated and length-limited.
- Request IDs are logged.
- Token usage is logged per customer, feature, provider, and model.
- Tool calls use an allowlist.
- Tool arguments go through normal validation and authorization.
- Streaming endpoints disable buffering and handle disconnects.
- Long-running calls run in jobs when the user does not need an immediate response.
- Retry logic handles rate limits and transient 5xx errors with backoff.
- Prompt and response retention are documented.
- High-cost features have per-user or per-account quotas.
- Provider SDK or raw HTTP client versions are tracked in dependency updates.
[IMAGE: Supporting visual 3 for PHP and AI: Integrating ChatGPT & Claude APIs Into Your PHP Application, showing PHP APIs decisions, examples, and PHP, OpenAI, ChatGPT. Alt: PHP APIs php-ai-integrating-chatgpt-claude-apis-php-application visual 3]
Common mistakes
Do not call LLM APIs from Blade, Twig, or frontend JavaScript.
Do not concatenate untrusted user text into tool instructions and then execute arbitrary operations.
Do not assume OpenAI and Claude have the same conversation state model.
Do not stream unreviewed model output into sensitive workflows where partial output can cause harm.
Do not send entire database records when a small public summary is enough.
Do not let "fallback provider" mean "silently change model behavior in production."
Do not use model-generated JSON without schema validation.
FAQ
What is PHP APIs?
PHP APIs is a practical apis topic that should be evaluated through implementation scope, production risk, testing, documentation, and long-term maintainability.
When should a team use PHP APIs?
Use PHP APIs when it solves a real project constraint, improves clarity, or reduces operational risk. Avoid it when it only adds novelty or hides behavior from future maintainers.
What is the biggest risk with PHP APIs?
The biggest risk is copying a pattern without its context. Production systems need clear boundaries, rollback options, tests, and observability before a technique becomes dependable.
How do you test PHP APIs?
Test the smallest unit that owns the behavior, then add integration coverage for the path users or systems actually rely on. Include failure cases, configuration differences, and regression checks.
How does PHP APIs affect SEO and AI search visibility?
It improves visibility when the article gives a direct answer, expert context, structured headings, internal links, trustworthy references, and FAQ content that matches the visible page.
Conclusion
PHP APIs is worth doing when the implementation improves clarity, reliability, or delivery speed. It is not worth doing when it hides ownership, increases operational risk, or makes the system harder to explain.
Use the framework above as a review checklist. Then connect this topic to the rest of the project documentation so readers can move from concept to implementation without losing context.