Build native desktop apps with Nette Framework and Electron.
A bridge between Nette and the NativePHP Electron runtime — the same concept as nativephp/laravel, but for Nette.
composer require jansuchanek/nattivephpAdd to your config/common.neon:
extensions:
nativephp: NativePHP\Nette\NativePhpExtension
nativephp:
app_id: com.myapp.desktop
app_name: My Nette App
version: 1.0.0
provider: App\NativePHP\MyAppProviderIn your RouterFactory:
// NativePHP API endpoints (Electron ↔ PHP communication)
$router->addRoute('_native/api/<action>', [
'presenter' => 'NativeApi',
'action' => 'default',
]);namespace App\Presentation\NativeApi;
use NativePHP\Nette\NativeAppProvider;
use Nette\Application\UI\Presenter;
use Nette\Http\IResponse;
class NativeApiPresenter extends Presenter
{
public function __construct(
private readonly NativeAppProvider $provider,
) {
parent::__construct();
}
public function startup(): void
{
parent::startup();
$secret = $this->getHttpRequest()->getHeader('X-NativePHP-Secret');
$expected = (string) getenv('NATIVEPHP_SECRET');
$action = $this->getAction();
if ($expected !== '' && $action !== 'cookie' && $secret !== $expected) {
$this->getHttpResponse()->setCode(IResponse::S403_Forbidden);
$this->sendJson(['error' => 'Invalid secret']);
}
}
public function actionBooted(): void
{
$this->provider->boot();
$this->sendJson(['success' => true]);
}
public function actionEvents(): void
{
$this->sendJson(['success' => true]);
}
public function actionCookie(): void
{
$secret = (string) getenv('NATIVEPHP_SECRET');
$this->getHttpResponse()->setCookie('_php_native', $secret, '365 days');
$this->sendJson(['success' => true]);
}
}namespace App\NativePHP;
use NativePHP\Nette\NativeAppProvider;
class MyAppProvider extends NativeAppProvider
{
public function boot(): void
{
$phpPort = (string)
($_SERVER['SERVER_PORT'] ??
getenv('NATIVEPHP_PHP_PORT') ?: '8000');
$this->window->open(
id: 'main',
url: "http://127.0.0.1:{$phpPort}/",
width: 1024,
height: 768,
title: 'My Nette App',
);
}
}Your Bootstrap.php needs to handle NativePHP environment:
public function __construct()
{
$this->rootDir = dirname(__DIR__);
$this->configurator = new Configurator;
// Writable temp dir in NativePHP bundle
$tempPath = getenv('NATIVEPHP_TEMP_PATH') ?: ($this->rootDir . '/temp');
if (!is_dir($tempPath)) {
@mkdir($tempPath, 0777, true);
}
$this->configurator->setTempDirectory($tempPath);
}
private function setupContainer(): void
{
// Dynamic storage path (writable in DMG context)
$storagePath = getenv('NATIVEPHP_STORAGE_PATH')
? (string) getenv('NATIVEPHP_STORAGE_PATH')
: ($this->rootDir . '/storage');
if (!is_dir($storagePath)) {
@mkdir($storagePath, 0777, true);
}
$this->configurator->addDynamicParameters([
'storagePath' => $storagePath,
]);
$this->configurator->addConfig($configDir . '/common.neon');
}The electron/ directory from nette-bridge contains the Electron shell.
# 1. Copy your Nette app into electron/resources/app/
cp -R app/ electron/resources/app/app/
cp -R config/ electron/resources/app/config/
cp -R vendor/ electron/resources/app/vendor/
# 2. Install Electron dependencies
cd electron && npm install
# 3. Build DMG (macOS ARM64)
npm run build:mac-arm64
# Output: electron/dist/MyApp-1.0.0-arm64.dmgnpm run build:mac-x86 # macOS Intel
npm run build:win-x64 # Windows
npm run build:linux-x64 # Linux AppImage + debPerfect for desktop apps — no external database server needed.
composer require nettrine/orm nettrine/dbal nettrine/cacheextensions:
dbal: Nettrine\DBAL\DI\DbalExtension
orm: Nettrine\ORM\DI\OrmExtension
orm.cache: Nettrine\Cache\DI\CacheExtension
dbal:
connections:
default:
driver: pdo_sqlite
path: %storagePath%/app.db # writable in both dev and DMG
orm:
managers:
default:
connection: default
proxyDir: %tempDir%/proxies
autoGenerateProxyClasses: true
mapping:
App:
type: attributes
directories: [%appDir%/Entity]
namespace: App\EntityImportant: Use
%storagePath%(not%appDir%) for the SQLite path. Inside a DMG, the app bundle is read-only. The%storagePath%parameter resolves to~/Library/Application Support/<app-name>/storage/on macOS.
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'contacts')]
class Contact
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string')]
private string $name;
// ... getters, constructor
}Add to your presenter's startup():
public function startup(): void
{
parent::startup();
// Creates tables on first access (idempotent, fast no-op when schema exists)
$schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->em);
$metadata = $this->em->getMetadataFactory()->getAllMetadata();
if (count($metadata) > 0) {
$schemaTool->updateSchema($metadata);
}
}All classes follow the same pattern and can be injected via DI. Each PHP class includes @see annotations referencing the Electron TypeScript source and Laravel equivalent for easy upstream sync.
| PHP Class | Electron TS Source | Laravel Facade | Key Methods |
|---|---|---|---|
Window |
api/window.ts |
Window |
open, close, resize, title, maximize, minimize, hide, show, reload |
Menu |
api/menu.ts |
Menu |
set |
MenuBar |
api/menuBar.ts |
MenuBar |
create, label, tooltip, icon, contextMenu, show, hide, resize |
ContextMenu |
api/contextMenu.ts |
ContextMenu |
set, remove |
Clipboard |
api/clipboard.ts |
Clipboard |
read, write, clear |
Notification |
api/notification.ts |
Notification |
send |
Dialog |
api/dialog.ts |
Dialog |
open, save |
Alert |
api/alert.ts |
Alert |
message, error |
Shell |
api/shell.ts |
Shell |
openExternal, showItemInFolder, trashItem |
Screen |
api/screen.ts |
Screen |
primaryDisplay, allDisplays, cursorPosition |
App |
api/app.ts |
App |
quit, relaunch, hide, show |
Dock |
api/dock.ts |
Dock |
menu, show, hide, icon, bounce, cancelBounce, getBadge, setBadge |
GlobalShortcut |
api/globalShortcut.ts |
GlobalShortcut |
register, unregister, isRegistered |
ProgressBar |
api/progressBar.ts |
ProgressBar |
update |
PowerMonitor |
api/powerMonitor.ts |
PowerMonitor |
getSystemIdleState, getSystemIdleTime, getCurrentThermalState, isOnBatteryPower |
ChildProcess |
api/childProcess.ts |
ChildProcess |
start, startPhp, stop, restart, get, all, message |
AutoUpdater |
api/autoUpdater.ts |
AutoUpdater |
checkForUpdates, downloadUpdate, quitAndInstall |
System |
api/system.ts |
System |
canPromptTouchID, promptTouchID, encrypt, decrypt, getPrinters, print, printToPdf, getTheme, setTheme |
Settings |
api/settings.ts |
Settings |
get, set, delete, clear |
Broadcasting |
api/broadcasting.ts |
Broadcasting |
send |
Debug |
api/debug.ts |
Debug |
log, info, warning, error |
Each PHP class has @see annotations linking to:
- Electron source:
electron-plugin/src/server/api/*.ts— the Express API routes - Laravel facade: corresponding NativePHP/Laravel class
When upstream NativePHP adds new endpoints:
- Check
electron-plugin/src/server/api.tsfor newhttpServer.use()mounts - Read the corresponding
.tsfile for endpoint signatures - Add matching methods to the PHP wrapper class
- Register in
NativePhpExtension::loadConfiguration()
use NativePHP\Nette\Window;
use NativePHP\Nette\Notification;
use NativePHP\Nette\Clipboard;
class MyPresenter extends Presenter
{
public function __construct(
private readonly Window $window,
private readonly Notification $notification,
private readonly Clipboard $clipboard,
) {}
public function handleNotify(): void
{
$this->notification->send('Hello', 'From Nette!');
}
public function handleCopy(): void
{
$this->clipboard->write('Copied from desktop app');
}
}Electron (Express API :4000) ←→ HTTP ←→ PHP (Nette :8100)
↑ ↑
NativePHP runtime Your Nette app
(window, menu, tray) (presenters, forms, DB)
- PHP → Electron:
Clientsends HTTP POST/GET to Express API (window, clipboard, etc.) - Electron → PHP: HTTP POST to
/_native/api/*(booted, events, cookie) - Environment: Electron sets
NATIVEPHP_API_URL,NATIVEPHP_SECRET,NATIVEPHP_STORAGE_PATH
Electron sends events to PHP via /_native/api/events. Use the EventDispatcher to handle them:
use NativePHP\Nette\Events\EventDispatcher;
use NativePHP\Nette\Events\Windows\WindowFocused;
use NativePHP\Nette\Events\MenuBar\MenuBarClicked;
class MyPresenter extends Presenter
{
public function __construct(
private readonly EventDispatcher $events,
) {}
public function startup(): void
{
parent::startup();
$this->events->on(WindowFocused::class, function ($event) {
// Handle window focused
});
}
}Available event namespaces:
| Namespace | Events |
|---|---|
Events\Windows |
WindowShown, WindowBlurred, WindowClosed, WindowFocused, WindowHidden, WindowMaximized, WindowMinimized, WindowResized |
Events\MenuBar |
MenuBarClicked, MenuBarCreated, MenuBarDoubleClicked, MenuBarDroppedFiles, MenuBarHidden, MenuBarRightClicked, MenuBarShown |
Events\PowerMonitor |
PowerStateChanged, ScreenLocked, ScreenUnlocked, Shutdown, SpeedLimitChanged, ThermalStateChanged, UserDidBecomeActive, UserDidResignActive |
Events\AutoUpdater |
CheckingForUpdate, DownloadProgress, Error, UpdateAvailable, UpdateCancelled, UpdateDownloaded, UpdateNotAvailable |
Events\ChildProcess |
ProcessSpawned, ProcessExited, MessageReceived, ErrorReceived, StartupError |
Events\Notifications |
NotificationClicked, NotificationActionClicked, NotificationClosed, NotificationReply |
php bin/console native:serve # Start Electron dev server
php bin/console native:build -p mac-arm64 # Build DMG (mac-arm64, mac-x86, win-x64, linux-x64)
php bin/console native:install # Scaffold electron/ directory
php bin/console native:config # Show NativePHP config for Electron
php bin/console native:php-ini # Show PHP ini settings for Electronvendor/bin/phpstan analyse # PHPStan (max level)
vendor/bin/rector --dry-run # Rector
vendor/bin/tester tests/ # TestsMIT