AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Openui Forge Php

skill-othmanadi-openui-forge-openui-forge-php · by OthmanAdi

OpenUI generative UI with a PHP (Laravel 13.x) backend. Forwards OpenAI's SSE stream verbatim via response()->stream().

No reviews yet
0 installs
31 views
0.0% view→install

Install

$ agentstack add skill-othmanadi-openui-forge-openui-forge-php

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-othmanadi-openui-forge-openui-forge-php)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Openui Forge Php? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

OpenUI Forge — PHP

Build generative UI apps with a React frontend + Laravel backend. Streams the OpenAI API's native SSE response straight through response()->stream().

Activation Triggers

  • "openui php", "openui laravel", "openui php backend"
  • "generative ui php", "laravel streaming ui backend"

Prerequisites

  • Node.js >= 22 (24 LTS recommended) + React >= 18.3.1 (19+ recommended) (frontend)
  • PHP >= 8.3 + Laravel 13.x (backend; Laravel 13 requires PHP 8.3 minimum and supports 8.3 through 8.5)
  • Composer; guzzlehttp/guzzle ships with Laravel and backs the Http facade (no extra dependency to call OpenAI)
  • OPENAI_API_KEY environment variable set

Quick Start

  1. Create the React frontend and install OpenUI deps:
npm install @openuidev/react-ui @openuidev/react-headless @openuidev/react-lang lucide-react zod
  1. Generate the system prompt:
npx @openuidev/cli generate ./src/lib/library.ts --out backend/storage/app/system-prompt.txt
  1. Create the Laravel backend (see Full Code below). On a fresh app, enable API routes once with php artisan install:api.
  2. Run: php artisan serve on :8000, frontend on :3000

Full Code

Backend: composer.json (require block)

{
    "require": {
        "php": "^8.3",
        "laravel/framework": "^13.0"
    }
}

> Laravel bundles guzzlehttp/guzzle, so the Http facade can call OpenAI with no extra package. The OpenAI SSE passthrough below needs nothing beyond the framework.

Backend: routes/api.php

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',
        ]);
    }
}

Backend: 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.

 [
        'key'      => env('OPENAI_API_KEY'),
        'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com/v1'),
        'model'    => env('OPENAI_MODEL', 'gpt-5.5'),
    ],
];

Backend: config/cors.php (publish once with php artisan config:publish cors)

 ['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 HandleCors middleware reads this config and answers the OPTIONS preflight automatically, so the controller never hand-writes Access-Control-* headers.

Frontend: 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 (
    
  );
}

> The Laravel backend forwards OpenAI's SSE stream verbatim through response()->stream(), reading the Guzzle PSR-7 body in chunks and calling flush() after each one, so the client sees tokens as they arrive. Pair it with openAIAdapter() on the frontend. openAIReadableStreamAdapter() is for NDJSON (no data: 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 ` sentinel, which rewrites the bytes and breaks openAIAdapter(). Plain response()->stream() keeps OpenAI's data: {chunk} frames and the literal data: [DONE] intact. > > An official-style OpenAI PHP client exists (openai-php/client, requires PHP 8.2+) with a createStreamed() helper as an alternative to calling the HTTP endpoint directly. This skill keeps the bundled Guzzle-backed Http facade as the dependency-free default. A pure ext-curl + CURLOPT_WRITEFUNCTION variant (return the chunk's byte count from the callback, echo+flush() inside it) is shown in templates/handler-php.php.template`.

System Prompt Generation

npx @openuidev/cli generate ./src/lib/library.ts --out backend/storage/app/system-prompt.txt

Validation Checklist

  • [ ] system-prompt.txt exists at storage/app/system-prompt.txt in the Laravel backend
  • [ ] OPENAI_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 exists
  • [ ] config/cors.php allowed_origins lists the frontend origin (not *)
  • [ ] Response streams SSE directly from OpenAI API (passthrough via response()->stream())
  • [ ] X-Accel-Buffering: no header is set so nginx/FastCGI does not buffer the stream
  • [ ] Frontend apiUrl points to http://localhost:8000/api/chat
  • [ ] Frontend uses streamProtocol={openAIAdapter()} and openAIMessageFormat
  • [ ] componentLibrary={openuiChatLibrary} prop passed to FullScreen
  • [ ] CSS import in root layout (@openuidev/react-ui/components.css)

Error Patterns

| 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) |

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.