원클릭으로
openui-forge-php
OpenUI generative UI with a PHP (Laravel 13.x) backend. Forwards OpenAI's SSE stream verbatim via response()->stream().
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
OpenUI generative UI with a PHP (Laravel 13.x) backend. Forwards OpenAI's SSE stream verbatim via response()->stream().
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
OpenUI generative UI with Anthropic Claude SDK backend. Stream conversion to OpenAI NDJSON format.
OpenUI generative UI with C# ASP.NET Core Minimal API backend. Direct OpenAI API SSE streaming via HttpClient on .NET 10.
OpenUI generative UI with Elixir Phoenix backend. Chunked SSE streaming via Plug.Conn and Req.
OpenUI generative UI with Go (net/http) backend. Direct OpenAI API streaming via HTTP.
OpenUI generative UI with a Java Spring Boot (WebFlux) backend. Streams the OpenAI API directly via WebClient as SSE.
OpenUI generative UI with LangChain/LangGraph backend. Supports ChatOpenAI and ChatAnthropic.
| name | openui-forge-php |
| description | OpenUI generative UI with a PHP (Laravel 13.x) backend. Forwards OpenAI's SSE stream verbatim via response()->stream(). |
| version | 1.2.0 |
| author | OthmanAdi |
Build generative UI apps with a React frontend + Laravel backend. Streams the OpenAI API's native SSE response straight through response()->stream().
guzzlehttp/guzzle ships with Laravel and backs the Http facade (no extra dependency to call OpenAI)OPENAI_API_KEY environment variable setnpm install @openuidev/react-ui @openuidev/react-headless @openuidev/react-lang lucide-react zod
npx @openuidev/cli generate ./src/lib/library.ts --out backend/storage/app/system-prompt.txt
php artisan install:api.php artisan serve on :8000, frontend on :3000composer.json (require block){
"require": {
"php": "^8.3",
"laravel/framework": "^13.0"
}
}
Laravel bundles
guzzlehttp/guzzle, so theHttpfacade can call OpenAI with no extra package. The OpenAI SSE passthrough below needs nothing beyond the framework.
routes/api.php<?php
use App\Http\Controllers\ChatController;
use Illuminate\Support\Facades\Route;
// `php artisan install:api` creates this file and prefixes it with /api,
// so this route is reachable at POST /api/chat.
Route::post('/chat', [ChatController::class, 'chat']);
app/Http/Controllers/ChatController.php<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ChatController extends Controller
{
// Loaded once per worker process, then reused across requests.
private static ?string $systemPrompt = null;
private function systemPrompt(): string
{
if (self::$systemPrompt === null) {
$path = storage_path('app/system-prompt.txt');
if (! is_file($path)) {
abort(500, 'system-prompt.txt not found at ' . $path);
}
self::$systemPrompt = (string) file_get_contents($path);
}
return self::$systemPrompt;
}
public function chat(Request $request): StreamedResponse
{
// Read config(), never env(), in a controller: after `php artisan
// config:cache` (standard in production) env() returns null outside
// config files. See the config/services.php block below.
$apiKey = config('services.openai.key');
if (! $apiKey) {
abort(500, 'OPENAI_API_KEY not set');
}
$incoming = $request->validate([
'messages' => 'required|array|min:1',
'messages.*.role' => 'required|string',
'messages.*.content' => 'required|string',
])['messages'];
// Prepend the system prompt; never trust a client-sent system message.
$messages = array_merge(
[['role' => 'system', 'content' => $this->systemPrompt()]],
array_map(
fn (array $m) => ['role' => $m['role'], 'content' => $m['content']],
$incoming,
),
);
$baseUrl = rtrim(config('services.openai.base_url'), '/');
$model = config('services.openai.model');
// stream => true returns the Guzzle PSR-7 response with its body still
// on the wire, so we read it chunk-by-chunk instead of buffering the
// whole completion in memory.
$upstream = Http::withToken($apiKey)
->withOptions(['stream' => true])
->acceptJson()
->post("{$baseUrl}/chat/completions", [
'model' => $model,
'stream' => true,
'messages' => $messages,
]);
if ($upstream->failed()) {
abort($upstream->status(), 'OpenAI request failed: ' . $upstream->body());
}
$body = $upstream->toPsrResponse()->getBody();
return response()->stream(function () use ($body): void {
// Forward OpenAI's SSE bytes verbatim. OpenAI already emits
// `data: {chunk}\n\n` frames plus a final `data: [DONE]`, which is
// exactly what openAIAdapter() parses, so no re-framing is needed.
while (! $body->eof()) {
$chunk = $body->read(8192);
if ($chunk === '') {
usleep(1000); // avoid a busy-wait if the stream momentarily has no data
continue;
}
echo $chunk;
if (ob_get_level() > 0) {
@ob_flush();
}
flush();
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
]);
}
}
config/services.php (add an openai entry)Read provider settings via config(), not env(), in the controller: after php artisan config:cache (standard in production) env() returns null outside config files. env() is only safe inside config files like this one.
<?php
return [
// ...existing services...
'openai' => [
'key' => env('OPENAI_API_KEY'),
'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com/v1'),
'model' => env('OPENAI_MODEL', 'gpt-5.5'),
],
];
config/cors.php (publish once with php artisan config:publish cors)<?php
return [
'paths' => ['api/*'],
'allowed_methods' => ['POST', 'OPTIONS'],
// Pin the frontend origin, not a wildcard.
'allowed_origins' => [env('FRONTEND_ORIGIN', 'http://localhost:3000')],
'allowed_headers' => ['Content-Type', 'Authorization'],
'exposed_headers' => [],
'max_age' => 0,
// Leave false unless you send cookies; with credentials a wildcard is illegal.
'supports_credentials' => false,
];
Laravel's built-in
HandleCorsmiddleware reads this config and answers theOPTIONSpreflight automatically, so the controller never hand-writesAccess-Control-*headers.
app/chat/page.tsx"use client";
import { FullScreen } from "@openuidev/react-ui";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import {
openAIAdapter,
openAIMessageFormat,
} from "@openuidev/react-headless";
export default function ChatPage() {
return (
<FullScreen
componentLibrary={openuiChatLibrary}
streamProtocol={openAIAdapter()}
messageFormat={openAIMessageFormat}
apiUrl="http://localhost:8000/api/chat"
/>
);
}
The Laravel backend forwards OpenAI's SSE stream verbatim through
response()->stream(), reading the Guzzle PSR-7 body in chunks and callingflush()after each one, so the client sees tokens as they arrive. Pair it withopenAIAdapter()on the frontend.openAIReadableStreamAdapter()is for NDJSON (nodata:prefix) and will silently produce no output here.Do not reach for Laravel's
response()->eventStream()here: it wraps each yield as a named SSE event and appends a</stream>sentinel, which rewrites the bytes and breaksopenAIAdapter(). Plainresponse()->stream()keeps OpenAI'sdata: {chunk}frames and the literaldata: [DONE]intact.An official-style OpenAI PHP client exists (
openai-php/client, requires PHP 8.2+) with acreateStreamed()helper as an alternative to calling the HTTP endpoint directly. This skill keeps the bundled Guzzle-backedHttpfacade as the dependency-free default. A pureext-curl+CURLOPT_WRITEFUNCTIONvariant (return the chunk's byte count from the callback,echo+flush()inside it) is shown intemplates/handler-php.php.template.
npx @openuidev/cli generate ./src/lib/library.ts --out backend/storage/app/system-prompt.txt
system-prompt.txt exists at storage/app/system-prompt.txt in the Laravel backendOPENAI_API_KEY is set in .env (and OPENAI_BASE_URL / OPENAI_MODEL if overriding)php artisan install:api has been run so POST /api/chat existsconfig/cors.php allowed_origins lists the frontend origin (not *)response()->stream())X-Accel-Buffering: no header is set so nginx/FastCGI does not buffer the streamapiUrl points to http://localhost:8000/api/chatstreamProtocol={openAIAdapter()} and openAIMessageFormatcomponentLibrary={openuiChatLibrary} prop passed to FullScreen@openuidev/react-ui/components.css)| Error | Cause | Fix |
|---|---|---|
| CORS blocked | Origin mismatch | Set allowed_origins in config/cors.php to the frontend origin |
404 on /api/chat | API routes not installed | Run php artisan install:api and confirm routes/api.php exists |
system-prompt.txt not found | File missing from storage/app | Run the CLI generate command into storage/app/system-prompt.txt |
500 OPENAI_API_KEY not set | Env var missing, or read via env() after config:cache | Set OPENAI_API_KEY in .env and read it through config('services.openai.key') (cache-safe) |
Upstream error surfaced via abort() | OpenAI key invalid or model wrong | Check OPENAI_API_KEY, OPENAI_MODEL, OPENAI_BASE_URL |
| Response arrives all at once (no streaming) | Output buffered by server | Keep X-Accel-Buffering: no; ensure flush() runs and no full-page output buffer wraps it |
| Empty response | Wrong adapter | Use openAIAdapter() (SSE), not openAIReadableStreamAdapter() (NDJSON) |