PHPNomad host adapter for NativePHP. Lets a PHPNomad app run as the PHP guest inside an Electron desktop process, without Laravel.
Sits at the same architectural level as phpnomad/wordpress-integration — it binds NativePHP's runtime primitives (windows, dialogs, notifications, system access) to PHPNomad's strategy interfaces, so apps slot in as PHPNomad Initializers and never touch the Electron HTTP API directly.
This package is functionally complete and end-to-end verified against real Electron. It is being released as v0.1.x rather than v1.0.0 because of one outstanding upstream dependency:
The Electron-side @nativephp/electron-plugin is currently Laravel-coupled in three small places ('artisan' bin name, server.php router path, Laravel-specific bootstrap commands). A minimal three-env-var patch that opens those up for non-Laravel guests is awaiting review as NativePHP/electron#265. Until that lands, consumers apply the same patch locally via patch-package (recipe below — about two extra lines in your package.json).
When PR #265 merges and ships in a NativePHP release, the patch step goes away and this package will tag v1.0.0 without API changes.
- 17 fluent feature classes for the Electron API:
Notification,Window,Dialog,Clipboard,Shell,Settings,Screen,System,Dock,MenuBar,Menu,ContextMenu,PowerMonitor,GlobalShortcut,ProgressBar,Process,Alert - Typed event classes for Electron's outbound webhooks:
AppBooted,WindowFocused,WindowBlurred,WindowMinimized,WindowMaximized,WindowShown,WindowClosed,WindowResized,AppOpenedFromUrl,NotificationClicked,UnknownNativeEvent(catch-all). Translated from the raw JS event names byEventTranslator. - REST controllers for the inbound callback routes (
/_native/api/booted,/_native/api/events,/_native/api/cookie), wired throughphpnomad/fastroute-rest-integrationwith a secret-headerSecretGuardmiddleware. - Symfony Console commands (
native:config,native:php-ini,native:serve) registered throughphpnomad/symfony-console-integration. The Electron bootstrap invokes the first two to learn app metadata and PHP ini overrides. - Declarative
WindowManager— registerWindowDefinitionobjects once; the integration opens them at boot via theOpenWindowsOnBootlistener. - HTTP
Clientwith theX-NativePHP-Secretheader baked in, unified error type (NativePHPException), and PSR-7 compatible plumbing via Guzzle.
composer require phpnomad/nativephp-integrationThen register Initializer in your bootstrapper alongside the strategy bindings it composes with:
use PHPNomad\Loader\Bootstrapper;
use PHPNomad\Di\Container\Container;
use PHPNomad\NativePHP\Integration\Initializer as NativePHP;
use PHPNomad\Symfony\Component\EventDispatcherIntegration\Initializer as Events;
use PHPNomad\Symfony\Component\Console\Initializer as Console;
use PHPNomad\FastRoute\Component\RestInitializer as Rest;
$container = new Container();
(new Bootstrapper(
$container,
new YourBindings(), // logger + CurrentContextResolverStrategy + User
new YourConfig(), // ConfigStrategy with `nativephp.*` keys
new Events(),
new Console(),
new Rest(),
new NativePHP(),
new YourApp(), // your window defs, listeners, etc.
))->load();The Electron plugin needs a small patch until NativePHP/electron#265 merges. The patch is bundled at sandbox/electron/patches/@nativephp+electron-plugin+0.5.5.patch in this repo — copy it into your app's electron-host/patches/ directory and wire up patch-package in package.json:
{
"scripts": {
"postinstall": "patch-package"
},
"devDependencies": {
"patch-package": "^8.0.1"
}
}npm install will then automatically apply the patch every time. The patch is idempotent and survives reinstalls.
Then set these env vars before requiring @nativephp/electron-plugin in your Electron main process:
Object.assign(process.env, {
NATIVEPHP_PHP_BOOT_BIN: '/abs/path/to/your/bin/nomad',
NATIVEPHP_SERVER_SCRIPT: '/abs/path/to/your/public/index.php',
NATIVEPHP_SERVER_CWD: '/abs/path/to/your/public',
NATIVEPHP_SKIP_LARAVEL_SETUP: '1',
});
const nativePHP = require('@nativephp/electron-plugin');
nativePHP.bootstrap(app, icon, phpBinary, cert);A complete working example lives in sandbox/electron/main-full.js.
Type against the strategy interface — the Initializer binds the Electron-backed implementation by default, and consumers can rebind any of them (a mock for tests, a logger-backed notifier, an xclip-backed clipboard, etc.) without touching this package.
use PHPNomad\NativePHP\Integration\Interfaces\NotificationStrategy;
$container->get(NotificationStrategy::class)
->title('Hello')
->body('From a PHPNomad app')
->show();The seventeen strategy interfaces live in PHPNomad\NativePHP\Integration\Interfaces\ — NotificationStrategy, WindowStrategy, DialogStrategy, ClipboardStrategy, ShellStrategy, SettingsStrategy, ScreenStrategy, SystemStrategy, DockStrategy, MenuBarStrategy, MenuStrategy, ContextMenuStrategy, PowerMonitorStrategy, GlobalShortcutStrategy, ProgressBarStrategy, ProcessStrategy, AlertStrategy. The default Electron-backed implementations live in PHPNomad\NativePHP\Integration\Strategies\ with the same simple names.
use PHPNomad\NativePHP\Integration\Interfaces\NotificationStrategy;
// In your own Initializer's getClassDefinitions():
return [
YourLoggingNotification::class => NotificationStrategy::class,
];YourLoggingNotification implements NotificationStrategy and does whatever you want (logs to stdout, queues for batching, fans out to Slack — whatever). The rest of the integration uses the interface, so any consumer that calls $container->get(NotificationStrategy::class) resolves your implementation instead.
class MyHandler implements PHPNomad\Events\Interfaces\CanHandle
{
public function handle(PHPNomad\Events\Interfaces\Event $event): void
{
// $event is a PHPNomad\NativePHP\Integration\Events\WindowFocused
$windowId = $event->windowId();
}
}
// In your app initializer:
public function getListeners(): array
{
return [
\PHPNomad\NativePHP\Integration\Events\WindowFocused::class => [MyHandler::class],
];
}use PHPNomad\NativePHP\Integration\WindowManager;
use PHPNomad\NativePHP\Integration\DataObjects\WindowDefinition;
$container->get(WindowManager::class)->register(new WindowDefinition(
id: 'main',
url: 'http://127.0.0.1:8100/',
title: 'My App',
width: 1024,
height: 768,
));The integration opens registered windows automatically when the AppBooted event fires.
┌─────────────────────────────────────────────────┐
│ Electron main process (TypeScript) │
│ ├── @nativephp/electron-plugin (patched) │
│ │ ├── /api/* HTTP server │
│ │ └── spawn(php bin/nomad ...) │
│ └── BrowserWindow → loads http://localhost:N/ │
└────────────────┬────────────────────────────────┘
│ HTTP, X-NativePHP-Secret
┌────────────────▼────────────────────────────────┐
│ Your PHPNomad app (this integration) │
│ ├── bin/nomad (symfony-console-integration) │
│ │ ├── native:config │
│ │ ├── native:php-ini │
│ │ └── native:serve │
│ ├── public/index.php (fastroute) │
│ │ ├── /_native/api/booted → BootedController│
│ │ ├── /_native/api/events → EventsController│
│ │ └── /_native/api/cookie → CookieController│
│ └── Your app code │
│ ├── Feature classes (Notification, etc.) │
│ ├── Event listeners (AppBooted, ...) │
│ └── WindowManager registrations │
└─────────────────────────────────────────────────┘
The integration is the brace in the middle. Apps slot in as PHPNomad Initializers and never touch the Electron HTTP API directly.
composer test23 unit tests covering Feature wire format, event translation, the window manager, and the typed event API. Tests use a SpyClient so they run without a network or Electron — fast and deterministic.
- The upstream patch hasn't merged (PR #265 above). Once it does, the
patch-packagestep goes away and this package tagsv1.0.0. phpnomad/symfony-console-integrationrequiresdev-mainuntil a1.0.5release is tagged — the integration depends on a fix that's onmainbut not yet in a tagged release.- No app-starter scaffold yet. The sandbox under
sandbox/is the working reference; a properphpnomad/electron-app-starteris a planned follow-up. - No
nativephp/php-binwiring for portable PHP runtimes (needed for cross-platform distributable installers). - No auto-updater event surface (
electron-updateris in the plugin's deps but the integration doesn't expose its events as PHPNomad events yet).
cd sandbox/electron
npm install
./node_modules/.bin/electron .Electron boots, the plugin calls bin/nomad native:config to learn the app shape, spawns php -S pointed at sandbox/app/public/index.php, and opens a window on the served page. Click "Fire notification" in the window → a native toast appears. Focus the window → a different toast appears (the demo handler fires one back through the integration when it catches the WindowFocused event).
MIT License. See LICENSE.txt.