developersharif / php-gui
Cross-platform GUI development package for PHP using Tcl/Tk
Requires
- php: >=8.1
- ext-ffi: *
This package is auto-updated.
Last update: 2026-04-15 15:38:32 UTC
README
PHP GUI
Build native desktop apps with PHP — no Electron, no web server, no compromises.
demo-video.mp4
PHP GUI gives you two ways to build desktop applications from the same PHP codebase:
| Mode | Best for | Engine |
|---|---|---|
| Native Widgets | System-style UIs — forms, dialogs, tools | Tcl/Tk via FFI |
| WebView | Modern UIs with HTML/CSS/JS (like Tauri, but PHP) | WebKitGTK / WKWebView / WebView2 |
Both modes work on Linux, macOS, and Windows with zero system dependencies on Linux — libraries are bundled.
Requirements
| Minimum | |
|---|---|
| PHP | 8.1+ |
| Extension | ext-ffi enabled (ffi.enable=true in php.ini) |
| Composer | any recent version |
Linux — no extra packages needed (Tcl/Tk is bundled).
macOS — no extra packages needed.
Windows — no extra packages needed.
Enable FFI if not already on:
; php.ini extension=ffi ffi.enable=true
Installation
composer require developersharif/php-gui
That's it. No system Tcl/Tk install, no native build steps.
Quick Start
Create app.php:
<?php require_once __DIR__ . '/vendor/autoload.php'; use PhpGui\Application; use PhpGui\Widget\Window; use PhpGui\Widget\Label; use PhpGui\Widget\Button; $app = new Application(); $window = new Window(['title' => 'Hello PHP GUI', 'width' => 400, 'height' => 250]); $label = new Label($window->getId(), ['text' => 'Hello, World!']); $label->pack(['pady' => 20]); $button = new Button($window->getId(), [ 'text' => 'Click Me', 'command' => fn() => $label->setText('You clicked it!'), ]); $button->pack(); $app->run();
php app.php
A native window opens immediately. No compilation, no manifest files, no packaging step.
Native Widgets
Native widgets render as real OS controls using Tcl/Tk under the hood. The PHP API is simple and consistent across all platforms.
Available Widgets
| Widget | Description | Docs |
|---|---|---|
Window |
Main application window | → |
TopLevel |
Secondary window / dialog launcher | → |
Label |
Static or dynamic text display | → |
Button |
Clickable button with callback | → |
Input / Entry |
Single-line text field | → |
Checkbutton |
Checkbox with on/off state | → |
Combobox |
Dropdown selection | → |
Frame |
Container for grouping widgets | → |
Menu |
Menu bar with submenus and commands | → |
Menubutton |
Standalone menu button | → |
Canvas |
Drawing surface for shapes and images | → |
Message |
Multi-line text display | → |
Image |
Display images inside windows | — |
Layout
Every widget supports three layout managers. Mix them freely within a window.
// Pack — flow layout (simplest) $widget->pack(['side' => 'top', 'pady' => 10, 'fill' => 'x']); // Grid — row/column table $label->grid(['row' => 0, 'column' => 0, 'sticky' => 'w']); $input->grid(['row' => 0, 'column' => 1]); // Place — absolute position $badge->place(['x' => 20, 'y' => 20]);
Styling
Pass Tcl/Tk options directly in the constructor array or update them at runtime:
$button = new Button($window->getId(), [ 'text' => 'Save', 'bg' => '#4CAF50', 'fg' => 'white', 'font' => 'Helvetica 14 bold', 'relief' => 'raised', 'padx' => 12, 'pady' => 6, ]); // Update at runtime $button->setBackground('#2196F3'); $label->setText('Saved!');
Common options: bg, fg, font, relief (flat raised sunken groove ridge), padx, pady, width, height, cursor.
Dialogs
TopLevel provides native system dialogs — no extra packages:
// File picker $file = TopLevel::getOpenFile(); // Directory picker $dir = TopLevel::chooseDirectory(); // Color picker $color = TopLevel::chooseColor(); // Message box — returns 'ok', 'cancel', 'yes', 'no' $result = TopLevel::messageBox('Are you sure?', 'yesno');
Menus
$menu = new Menu($window->getId(), ['type' => 'main']); $fileMenu = $menu->addSubmenu('File'); $fileMenu->addCommand('New', fn() => newFile()); $fileMenu->addCommand('Open', fn() => openFile()); $fileMenu->addSeparator(); $fileMenu->addCommand('Exit', fn() => exit(), ['foreground' => 'red']); $editMenu = $menu->addSubmenu('Edit'); $editMenu->addCommand('Copy', fn() => copy()); $editMenu->addCommand('Paste', fn() => paste());
Complete Example
<?php require_once __DIR__ . '/vendor/autoload.php'; use PhpGui\Application; use PhpGui\Widget\{Window, Label, Button, Input, Menu, TopLevel}; $app = new Application(); $window = new Window(['title' => 'Demo', 'width' => 500, 'height' => 400]); // Menu bar $menu = new Menu($window->getId(), ['type' => 'main']); $fileMenu = $menu->addSubmenu('File'); $fileMenu->addCommand('Open', function () use (&$status) { $file = TopLevel::getOpenFile(); if ($file) $status->setText("Opened: " . basename($file)); }); $fileMenu->addSeparator(); $fileMenu->addCommand('Exit', fn() => exit()); // Input + button $input = new Input($window->getId(), ['text' => 'Type something...']); $input->pack(['pady' => 10, 'padx' => 20, 'fill' => 'x']); $status = new Label($window->getId(), ['text' => 'Ready', 'fg' => '#666']); $status->pack(['pady' => 5]); $btn = new Button($window->getId(), [ 'text' => 'Submit', 'bg' => '#2196F3', 'fg' => 'white', 'command' => function () use ($input, $status) { $status->setText('You typed: ' . $input->getValue()); }, ]); $btn->pack(['pady' => 10]); $app->run();
WebView Mode
WebView lets you build the UI with HTML, CSS, and JavaScript while keeping all your business logic in PHP. Think of it as Tauri for PHP.
<?php require_once __DIR__ . '/vendor/autoload.php'; use PhpGui\Application; use PhpGui\Widget\WebView; $app = new Application(); $wv = new WebView(['title' => 'My App', 'width' => 900, 'height' => 600]); $wv->setHtml('<h1 style="font-family:sans-serif">Hello from PHP + HTML!</h1>'); $wv->onClose(fn() => $app->quit()); $app->addWebView($wv); $app->run();
PHP ↔ JavaScript Bridge
JS → PHP — call PHP functions from the browser:
// PHP: register a handler $wv->bind('getUser', function (string $reqId, string $args) use ($wv): void { $id = json_decode($args, true)[0]; $user = getUserFromDatabase($id); $wv->returnValue($reqId, 0, json_encode($user)); });
// JavaScript: call it like a local function const user = await invoke('getUser', 42); console.log(user.name);
PHP → JS — push events to the frontend:
// PHP: emit an event $wv->emit('orderUpdated', ['id' => 99, 'status' => 'shipped']);
// JavaScript: listen for it onPhpEvent('orderUpdated', (order) => { document.getElementById('status').textContent = order.status; });
Serving a Frontend App
Load a built frontend (React, Vue, Svelte, Vanilla — anything) directly from disk. No HTTP server, no open ports, no firewall prompts:
$wv->serveFromDisk(__DIR__ . '/frontend/dist');
| Platform | Mechanism | URL |
|---|---|---|
| Linux | phpgui:// custom URI scheme |
phpgui://app/index.html |
| Windows | WebView2 virtual hostname | https://phpgui.localhost/ |
| macOS | loadFileURL:allowingReadAccess: |
file:///path/to/dist/ |
Vite Dev + Production in One Line
serveVite() auto-detects whether the dev server is running:
// In dev: hot-reloads via the Vite dev server (HMR works) // In prod: loads dist/ from disk — no server needed $wv->serveVite(__DIR__ . '/frontend/dist');
Recommended vite.config.js for cross-platform builds:
export default { base: './', // required for macOS file:// serving build: { outDir: 'dist' }, }
Bypass CORS — Transparent Fetch Proxy
Cross-origin API calls fail from phpgui:// / file:// origins. One call routes all fetch() requests through PHP:
$wv->enableFetchProxy(); // add this before serveFromDisk() / serveVite()
// Works identically on all platforms, no changes to your frontend code const data = await fetch('https://api.example.com/data').then(r => r.json());
Full Vite App Example
<?php require_once __DIR__ . '/vendor/autoload.php'; use PhpGui\Application; use PhpGui\Widget\WebView; $app = new Application(); $wv = new WebView(['title' => 'My Vite App', 'width' => 1024, 'height' => 768]); $wv->enableFetchProxy(); $wv->serveVite(__DIR__ . '/frontend/dist'); // Expose a PHP function to JavaScript $wv->bind('readFile', function (string $reqId, string $args) use ($wv): void { $path = json_decode($args, true)[0]; $content = is_file($path) ? file_get_contents($path) : null; $wv->returnValue($reqId, 0, json_encode($content)); }); $wv->onClose(fn() => $app->quit()); $app->addWebView($wv); $app->run();
See the full WebView documentation →
Platform Support
| Platform | Native Widgets | WebView | Notes |
|---|---|---|---|
| Linux (x86-64) | ✅ | ✅ | Tcl/Tk bundled. WebView needs libwebkit2gtk-4.1-dev |
| Linux (ARM64) | ✅ | — | Tcl/Tk bundled |
| macOS | ✅ | ✅ | No extra dependencies |
| Windows | ✅ | ✅ | No extra dependencies |
Linux WebView dependency:
sudo apt install libwebkit2gtk-4.1-dev # Debian / Ubuntu sudo dnf install webkit2gtk4.1-devel # Fedora / RHEL
Documentation
| Guide | |
|---|---|
| Getting Started | FFI setup, first app, layout, events |
| Architecture | How the FFI bridge and event loop work |
| WebView | Full WebView API reference |
Widget reference: Window · Button · Label · Input · Entry · Checkbutton · Combobox · Frame · Canvas · Menu · Menubutton · TopLevel · Message