> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tapapi.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# PHP / Laravel

> PHP cURL 和 Laravel HTTP Client 接入 Tapapi

PHP / Laravel 用户建议直接走 HTTP REST。Tapapi 的文本和图片主线接口都是 JSON 请求，不需要专门的 PHP SDK。

<Warning>以下示例默认运行在 PHP 后端、Laravel Controller、Laravel Job、WordPress 后端插件或队列 worker 中。不要把 Tapapi API Key 放进浏览器前端、移动端包、公开仓库或客户端配置。</Warning>

## 环境变量

```bash theme={null}
TAPAPI_API_KEY=sk-xxx
TAPAPI_BASE_URL=https://tapapi.ai/v1
```

Laravel 项目把它们放到 `.env`，普通 PHP 项目可以通过服务器环境变量或配置文件读取。

Laravel 的 `config/services.php` 可以这样配置：

```php theme={null}
'tapapi' => [
    'key' => env('TAPAPI_API_KEY'),
    'base_url' => env('TAPAPI_BASE_URL', 'https://tapapi.ai/v1'),
],
```

## 原生 PHP 请求 Helper

```php theme={null}
<?php

function tapapi_json_request(string $path, array $payload, int $timeout = 120): array
{
    $apiKey = getenv('TAPAPI_API_KEY');
    $baseUrl = rtrim(getenv('TAPAPI_BASE_URL') ?: 'https://tapapi.ai/v1', '/');

    $ch = curl_init($baseUrl . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HEADER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $apiKey,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
        CURLOPT_TIMEOUT => $timeout,
    ]);

    $raw = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $curlError = curl_error($ch);
    curl_close($ch);

    if ($raw === false || $curlError) {
        throw new RuntimeException('Tapapi request failed: ' . $curlError);
    }

    $headers = substr($raw, 0, $headerSize);
    $body = substr($raw, $headerSize);
    $requestId = null;

    if (preg_match('/^X-Oneapi-Request-Id:\s*(.+)$/mi', $headers, $matches)) {
        $requestId = trim($matches[1]);
    }

    $data = json_decode($body, true);

    if (!is_array($data)) {
        throw new RuntimeException('Tapapi returned invalid JSON: ' . $body);
    }

    if ($status >= 400) {
        $error = $data['error'] ?? [];
        $details = [
            'status' => $status,
            'request_id' => $requestId,
            'type' => $error['type'] ?? null,
            'code' => $error['code'] ?? ($data['code'] ?? null),
            'message' => $error['message'] ?? ($data['message'] ?? $body),
        ];

        throw new RuntimeException('Tapapi error: ' . json_encode($details, JSON_UNESCAPED_UNICODE));
    }

    return [
        'status' => $status,
        'request_id' => $requestId,
        'data' => $data,
    ];
}
```

排障时至少记录 HTTP 状态码、`X-Oneapi-Request-Id`、`error.type`、`error.code` 和 `error.message`。任务/视频类接口的错误也可能返回顶层 `code`、`message`、`data`，不要只保存一段错误文本。

## 原生 PHP 文本请求

```php theme={null}
<?php

$result = tapapi_json_request('/chat/completions', [
    'model' => 'gpt-5.4',
    'messages' => [
        ['role' => 'user', 'content' => '用一句话解释 Tapapi 是什么'],
    ],
]);

echo $result['data']['choices'][0]['message']['content'] ?? '';
```

## 原生 PHP Responses 请求

`/v1/chat/completions` 是 PHP 接入的默认主线。需要 Responses API 时，先确认模型详情和返回字段：

```php theme={null}
<?php

$result = tapapi_json_request('/responses', [
    'model' => 'gpt-5.4',
    'input' => '用一句话解释 Tapapi 是什么',
]);

echo $result['data']['output_text'] ?? '';
```

Responses 的返回结构和 Chat Completions 不完全一样，不要共用同一套解析代码。

## 原生 PHP 图片生成

```php theme={null}
<?php

$result = tapapi_json_request('/images/generations', [
    'model' => 'nano-banana-pro',
    'prompt' => 'a clean product photo of a white ceramic mug',
    'n' => 1,
    'size' => '1024x1024',
    'response_format' => 'url',
], 180);

if (empty($result['data']['data'])) {
    throw new RuntimeException('Tapapi returned empty image data');
}

$item = $result['data']['data'][0];
$image = $item['url'] ?? $item['b64_json'] ?? null;

if (!$image) {
    throw new RuntimeException('Tapapi returned image data without url or b64_json');
}

echo $image;
```

图片返回可能是 `url` 或 `b64_json`。生产环境建议把图片转存到自己的对象存储，不要长期依赖临时 URL。批量生成时还要记录请求 `n`、实际返回数量和成功转存数量；如果响应包含 `metadata.tapapi_partial`，按实际返回张数处理。

## 保存图片结果

```php theme={null}
<?php

function save_tapapi_image_item(array $item, string $path): void
{
    if (!empty($item['url'])) {
        $context = stream_context_create([
            'http' => ['timeout' => 60],
        ]);

        $bytes = file_get_contents($item['url'], false, $context);

        if ($bytes === false) {
            throw new RuntimeException('Image download failed');
        }

        file_put_contents($path, $bytes);
        return;
    }

    if (!empty($item['b64_json'])) {
        $bytes = base64_decode($item['b64_json'], true);

        if ($bytes === false) {
            throw new RuntimeException('Invalid b64_json image data');
        }

        file_put_contents($path, $bytes);
        return;
    }

    throw new RuntimeException('image item has no url or b64_json');
}

save_tapapi_image_item($item, __DIR__ . '/output.png');
```

如果 URL 下载失败，优先重试下载和转存，不要立刻重新生成图片。

## Laravel 文本请求

```php theme={null}
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;

function parseTapapiLaravelError(Response $response): array
{
    $body = $response->json() ?? [];
    $error = $body['error'] ?? [];

    return [
        'status' => $response->status(),
        'request_id' => $response->header('X-Oneapi-Request-Id'),
        'type' => $error['type'] ?? null,
        'code' => $error['code'] ?? ($body['code'] ?? null),
        'message' => $error['message'] ?? ($body['message'] ?? $response->body()),
    ];
}

$response = Http::withToken(config('services.tapapi.key'))
    ->timeout(120)
    ->post(config('services.tapapi.base_url') . '/chat/completions', [
        'model' => 'gpt-5.4',
        'messages' => [
            ['role' => 'user', 'content' => '用一句话解释 Tapapi 是什么'],
        ],
    ]);

if ($response->failed()) {
    throw new RuntimeException(
        json_encode(parseTapapiLaravelError($response), JSON_UNESCAPED_UNICODE)
    );
}

$text = $response->json('choices.0.message.content');
```

## Laravel Responses 请求

```php theme={null}
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.tapapi.key'))
    ->timeout(120)
    ->post(config('services.tapapi.base_url') . '/responses', [
        'model' => 'gpt-5.4',
        'input' => '用一句话解释 Tapapi 是什么',
    ]);

if ($response->failed()) {
    throw new RuntimeException(
        json_encode(parseTapapiLaravelError($response), JSON_UNESCAPED_UNICODE)
    );
}

$text = $response->json('output_text');
```

## Laravel 图片生成

```php theme={null}
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.tapapi.key'))
    ->timeout(180)
    ->post(config('services.tapapi.base_url') . '/images/generations', [
        'model' => 'nano-banana-pro',
        'prompt' => 'a clean product photo of a white ceramic mug',
        'n' => 1,
        'size' => '1024x1024',
        'response_format' => 'url',
    ]);

if ($response->failed()) {
    throw new RuntimeException(
        json_encode(parseTapapiLaravelError($response), JSON_UNESCAPED_UNICODE)
    );
}

$item = $response->json('data.0');

if (!$item) {
    throw new RuntimeException('Tapapi returned empty image data');
}

$image = $item['url'] ?? $item['b64_json'] ?? null;

if (!$image) {
    throw new RuntimeException('Tapapi returned image data without url or b64_json');
}
```

Laravel 项目可以用 `Storage` 把图片保存到 S3、R2、OSS、COS 或本地磁盘。前端只拿你自己的图片 URL，不要长期展示上游临时 URL。

## 视频 API

视频 API 当前不是默认全量开放能力。只有控制台可见模型、价格已确认、账号有权限时，才适合生产调用。

推荐流程：

```text theme={null}
POST /v1/videos
  -> GET /v1/videos/{task_id}
  -> completed 后 GET /v1/videos/{task_id}/content
```

视频请求字段会随模型变化，不要把某个模型的 `seconds`、`size`、`aspect_ratio`、参考图字段直接复用到所有视频模型。更多见 [视频 API](/video-generation/overview) 和 [任务与输出](/video-generation/tasks-and-output)。

## 生产建议

| 场景                 | 建议                                                      |
| ------------------ | ------------------------------------------------------- |
| WordPress / 传统 PHP | 后端保存 API Key，前端只调用自己的业务接口                               |
| Laravel Queue      | 批量任务放进 Queue / Job，限制 worker 并发                         |
| 重试                 | 只对 429、临时 5xx、网络中断做有限重试，并加随机抖动                          |
| 幂等                 | 每个业务任务保存自己的 `task_id`，避免重复点击或 worker 重启重复请求             |
| 日志                 | 保存接口、模型、HTTP 状态码、`request_id`、错误字段和最终用量                 |
| 图片保存               | 同时兼容 `url` 和 `b64_json`，成功后尽快转存                         |
| 超时                 | 文本建议 120s 左右，图片建议按业务设置 180s 左右；超时后先查状态和账单再补跑            |
| 视频 API             | 受控开放，先看 [视频 API](/video-generation/overview)，不要默认写进批量任务 |

## 下一步

| 场景                   | 文档                                                    |
| -------------------- | ----------------------------------------------------- |
| 底层 HTTP 规则           | [HTTP REST](/integrations/http-rest)                  |
| Python 完整示例          | [Python](/integrations/python)                        |
| Node.js / TypeScript | [Node.js / TypeScript](/integrations/node-typescript) |
| OpenAI SDK 迁移总览      | [OpenAI SDK 迁移](/integrations/openai-sdk)             |
| 错误与重试                | [错误码与重试](/errors)                                     |
| 生产上线检查               | [上线 Checklist](/production/launch-checklist)          |
