Native PHP: Building Full-Featured Desktop Apps with Laravel
Beyond the Browser: The Case for Native PHP
For years, PHP developers remained tethered to the browser. If you wanted to build a desktop application, you typically had to pivot to
The core problem with cross-platform development is the maintenance burden. Maintaining three separate codebases for different operating systems is a nightmare. While tools like
Prerequisites and Toolkit
Before you start, you need a basic understanding of the Laravel ecosystem. Native PHP isn't a replacement for Laravel; it’s an extension of it. You should be comfortable with Composer, Artisan commands, and basic frontend concepts.
Key tools used in this tutorial include:
- Native PHP Framework: The core package that orchestrates the desktop environment.
- Electron: The default rendering engine used to display the UI.
- Livewire: A full-stack framework for Laravel that makes building dynamic interfaces simple.
- SQLite: The portable database engine used for local data storage within the app.
Getting Started with Installation
Setting up your first project is a matter of a few commands. Unlike some starter kits that require a fresh installation, you can add Native PHP to any existing Laravel application. Start by requiring the package through Composer:
composer require nativephp/electron
Once the package is in your project, run the installation command to publish the necessary service providers and configuration files:
php artisan native:install
To launch your application in development mode, use the serve command. This will boot a native window and provide access to developer tools immediately.
php artisan native:serve
Controlling the Window and State
In a web app, the browser controls the window. In a desktop app, you are the pilot. Native PHP provides a Window facade to manage size, position, and behavior. A common requirement is ensuring your app remembers where the user last left it. You can achieve this with the rememberState() method.
Window::new()
->title('My Desktop App')
->width(800)
->height(600)
->rememberState();
You can also configure windows to stay "always on top" or define minimum dimensions to prevent the UI from breaking when resized. Because Native PHP uses a service provider model, these configurations feel like standard Laravel development.
Building Native Menus and Events
A true desktop experience requires a native menu bar. Native PHP allows you to define these menus using a fluent API. You can even hook into system-level events. For instance, if you want a "Preferences" menu item to trigger a Laravel event, you can register an EventItem.
Menu::new()
->submenu('App', Menu::new()
->event(SettingsClicked::class, 'Preferences', 'CmdOrCtrl+,')
->separator()
->quit()
)
->register();
When the user clicks "Preferences," Native PHP dispatches a standard Laravel event. You can then listen for this event in your EventServiceProvider to open a new settings window. This makes the boundary between the OS and your code almost invisible.
Real-Time Native Communication
one of the most impressive features is the ability to communicate between windows in real-time. By using the Native prefix in
// Inside a Livewire component
protected $listeners = [
'native:settings-updated' => 'refreshUI'
];
Syntax Notes and Best Practices
- Facades: Native PHP relies heavily on facades like
Window,MenuBar, andSettings. These are your primary interfaces for interacting with the OS. - Hotkeys: Use strings like
CmdOrCtrl+Sto ensure your shortcuts work across both Mac and Windows. - Storage: Since the app runs locally, use the
Settingsfacade for key-value pairs rather than a traditional remote database when possible.
Practical Tips and Gotchas
Distribution is the final hurdle. When you run php artisan native:build, the framework bundles your PHP source code into the app. Note: Currently, this does not obfuscate your PHP code. If you are shipping a closed-source product, be aware that your source files are technically accessible within the app package.
Additionally, Native PHP handles migrations automatically on startup. This ensures that every time you ship an update with a new database schema, the user's local SQLite database stays in sync without manual intervention. Always test your migration paths thoroughly, as rolling back on a user's machine is significantly harder than on a centralized server.
