Building desktop apps...with PHP? [eng]
Презентація доповіді
PHP is known for being a server-side language. But what if we could make use of PHP to build desktop applications that run on Windows, Linux and macOS? Let me introduce NativePHP — a framework that allows you to do just that.
Транскрипція доповіді
Hi, welcome to my talk called 'Writing Desktop Apps.' Since we're at PHP FW days, we're going to do it with PHP, of course. Now, some of you might initially think, 'Why PHP? Why should I write a desktop application with PHP?' I guess because we can, and that's cool. I love to explore the weirder things with PHP, so bear with me. I think you'll love it, and it's a great way to write a desktop application. My name is Marcel Potiot. I am the CTO at BeyondCode. Feel free to follow me on Twitter, where I tweet mostly about Laravel and PHP-related topics. At BeyondCode, we create software tools for developers, for people like myself, people like you who use Laravel, PHP, AlpineJS, Tailwind, Livewire, all the cool stuff.
The very first application that I created at BeyondCode as a software app is Tinkerwell. If you don't know, Tinkerwell is sort of like a playground, a scratchpad for PHP where you can just run any kind of PHP code within the scope of your local or remote PHP applications or even no scope at all if you just want to run some PHP code. This is not what the app actually looks like; this is the very first screenshot that I found of the app while I was building version one. Version one of Tinkerwell was written entirely in Swift because, at the time, I just wanted to scratch my own itch. I just wanted to use Tinkerwell for myself, and I guess I also just wanted to find an excuse to learn Swift some more. So this one was written in Swift while I built it.
As soon as we released it, a lot of people reached out and said, 'Well, Tinkerwell looks awesome. I want to use it, but I'm on Linux or I'm on Windows.' So we had to find some ways to add support for other operating systems. Of course, my first approach was, 'Well, since we have this native app, I'm just going to rewrite this thing two more times for Windows and for Linux, all natively.' I even started doing this for Windows, and, well, as you can imagine, I quickly realized that this is not a really smart idea because we would need to rewrite every new feature for all operating systems. We would have to do the maintenance for all operating systems with a very small team at BeyondCode, so it didn't really make a lot of sense.
Luckily, I also quickly realized this, so I started looking for alternatives. The two biggest alternatives that we have are Tauri and Electron. Tauri and Electron allow you to write your code base using a single code base and then use this across multiple operating systems. With Tauri, you write your desktop applications in a combination of Rust and JavaScript, and Electron is JavaScript only. There are also a lot of pros and cons between using Tauri or Electron. Tauri, for example, is a lot smaller than an Electron application and uses less memory because it doesn't ship an entire browser, while Electron basically bundles and ships Chromium along with their applications.
For me, the choice between one or the other was kind of easy because I don't really know Rust, and back at the time, the documentation for Tauri, they were just leading up towards a 1.0 release; the documentation wasn't really that good, so I decided to go with Electron instead. Now, before we go and take a look at any PHP code and any implementation and how this actually works with PHP, I want to give you a quick overview of how the Electron workflow or how the Electron architecture looks like so that we can then see how we could actually add PHP into the mix.
This is what a typical launch of an Electron application looks like. For example, when you start up VS Code or Slack or Tinkerwell, this is what happens. We have our Electron application, and as soon as it starts, it basically starts two processes. The first one is called the main process, or also sometimes referred to as a background process. This process can communicate with JavaScript with the native UI of your operating system. You can create windows, you can change the dock icon, you can add something to the menu bar or the menu itself and stuff like that. You can also make use of Node.js modules in Electron because of that, and you can access system APIs on the operating system.
So, for example, Touch ID. And then we have the renderer process which, as the name suggests, takes care of actually rendering your application. So this basically is the Chromium instance that displays your application. And then if you want to communicate from this renderer to the main process, there's a lot of inter-process communication involved, and you have to do a lot of back and forth. But that's like the main idea for an Electron application. Now, how can we add PHP to this mix? Let's just, for a second, ignore the fact that we need to somehow ship PHP to the user and control it. So if we just ignore that, the setup is relatively simple. We can just add PHP to the user. Again, the Electron application starts, and we start a main process. That's just the way it works; we can't avoid that. But now, this time, our main process starts two individual things.
So, first of all, it's going to start a PHP process, and this PHP process basically runs php artisan serve or php -S like the built-in PHP web server. And now we have PHP serving a local server. So, we can start a PHP server, and our renderer process can now simply use this locally served PHP app and display it. Now, if we want to communicate with PHP and our main process, we can just add a small HTTP API into our main process and then use that for the communication layer, which is really easy and convenient. So that's like the main bird's eye view of how we can work with PHP. Now let's just add a small HTTP API into our main process and see how we can make this work.
But, as I said, we still have this problem of PHP in there. Like, we need to control PHP because with this setup, every user of your desktop application needs to have PHP installed in order to just open the application. This means that we need to control the PHP version that our users have. If you write the app for 8.2 and your users only have 8.0 or 8.1, the app won't start. We need to make sure that they have all the correct extensions installed and properly configured. We need to make sure that they have the right PHP ini settings.
So there are a lot of moving parts that could go wrong with this approach if we don't have full control of PHP. How can we solve this? Well, we just ship PHP along with our application. The way we do this with native PHP is using ASP. So we're gonna do a little bit of a demo. We're gonna use a single binary file that contains PHP and all of its dependencies, all of its extensions bundled into this one binary. The binary, as of right now with all the extensions that we selected, is roughly 21 megabytes large, which isn't too bad, I think. And well, the extensions that we picked are the ones that you need to just start a default Laravel application so that we can have everything just packed in to get started with a Laravel app.
And by using this approach, we basically solve all of these issues because we can control the PHP version, we control the PHP extensions, and we can control the PHP ini settings of your application. All right. And this is what we did with something that we called native PHP. It is a Laravel app, a Laravel package that allows you to make use of this setup of using Electron or also Tauri soon to write your desktop applications with PHP. Now enough of the slides, let's just jump into PHPStorm and show you some of the APIs and how they work. So the first thing you want to do if you want to get started with native PHP is you want to actually install native PHP. As I said, it's just a composer package. So all you need to do is composer require native PHP, and then you can choose between different flavors if you want to call them that on how you want these apps to work. So you can choose between Electron and Tauri.
Now, under the hood, we try to abstract all of the API calls. So you don't have to worry about using if it's actually Rust or if it's JavaScript. But if at some points your application gets a bit more complex and you actually want to dig into these rules, you can do that. So you can say if it's actually Rust or JavaScript code basis, it might be better to make the decision upfront. So you can say composer require native PHP Electron, and this is going to install the actual Electron JavaScript dependencies that we need. And it's going to download this statically compiled PHP binary for you. Once the package is installed, you can run PHP artisan native install to actually install all the other NPM dependencies. This is going to copy the service provider for you that we're going to need. And a config file is going to be published. Once that's done, your desktop application is basically ready to go. So all you can do after that is you can run PHP artisan native serve.
And just like the traditional PHP artisan serve, this is going to take my application and serve it up in a desktop app. As you can see here, we now have a little window. It says FW days, we have a dock icon in here, and this is now serving my Laravel app. One important note is that this does not use my local PHP. It's not using my PHP that I've installed using Homebrew or, if you have Homebrew, Nginx. This is the built-in PHP static binary that native PHP uses. Let's see what we can do with this.
As I said, native PHP is going to publish a service provider, and it's called the NativeAppServiceProvider. It works kind of differently than a default Laravel service provider because, as you can see here, it doesn't extend from Laravel's own service provider. What we do in here is we basically bootstrap everything that your desktop application needs. This is when it first starts. This could be opening windows, configuring global shortcuts, configuring the menu of your app, all this stuff. By default, this is just the window open call. All of this does is what we see here. It opens up a new window, a new desktop window. It sets the title of this window to our application name that we have in our environment file. And it also opens up the default home route, the homepage of our Laravel application. So let's change some things in this API.
The first thing I want to do, especially for this talk, is I want this window to be always on top. Let's save this. We have hot reloading for the service provider. So all the changes that you do in the service provider will automatically be hot reloaded, and our application restarts. Now our window is always on top, so I can just focus PhpStorm, and our window stays on top, which is great for this demo. We can change a lot of things using the PHP API and control the behavior of this desktop application. For example, we can say that we want to open a different route. Let's say that we want to open a dialog route. Let's save this already. There we go. We can also change the default width and height. So we can say that the default width is 500, and the default height should be 300. Let's save this. Now it's wider. As you can see, we can still restart. We can resize the window and make it just super small. Let's change this as well. So we can say that we want to have a minimum width. The minimum width should be, let's say 300, and the minimum height should be 200. Let's save this. Hot reloading kicks in, and now we can only resize our window with the given constraints that we have in place. Perfect.
One thing that maybe you noticed is that every time hot reloading kicks in, our application, first of all, has the default width and default height. If I resize it, it doesn't really remember it. It's always like in the center of the screen. From a user experience point, I just want it to, if I just manually make it small and move it, for example, to a corner, the next time I open my app, I want this to be a little bit smaller. I want this window to be at the exact same spot. Now with native PHP, all we can say is, we can say, remember the state of this window. Let's save this. Now let's make it small, move it here, and trigger hot reloading, for example, by changing the title to "Hi FW days," save it. That's it. Now native PHP remembered the state of my application. It remembered that I moved it to this position with a given width and height; our title changes, but the position and the width and height of our window didn't. There's a ton of more stuff that you can do with these windows. You can change if it should be closeable or minimizable or full screen; you can even change if it should have a title screen at all; you can make it sort of half translucent, this default Mac OS blur style, a ton of stuff on windows, but I want to show you some more APIs. I'm going to move on to the next thing.
As I said, this NativeAppServiceProvider is basically what bootstraps your app and provides the entry points of your application; in this case, this is a window, but you can also have applications that provide a menu bar icon as we have up here. To do this with native PHP, you can say that we want to create a menu bar; that's all we need to do. Save this; we still have our window, and now we also have this icon up here, this native PHP menu bar icon. By default, if you use a menu bar, the dock icon will be hidden. So if you just want to create an application that only has the menu bar, you don't really want the dock icon to show. In our case, we want to show the dock icon, so we just add the should show dock icon method to it. Save, and now we still have our dock icon, but we also have this icon at the top.
Now, how does a menu bar app actually work? It's similar to a traditional window. Every time we click on this icon in here, this is going to open up a window, basically, underneath the icon. And then we can change this behavior pretty much the same way as we can do it with a window. So we can say that we want to create the menu bar, and when we click on this, we want to open up the menu bar route. It should have a height of, let's say, 300, and a width of 270, something like that. Let's save it. Click on the icon, and now we have a traditional Laravel route. This is just a get route that we open up using blade templates in the 6. For example, you don't need to do use view or live wire; you can use it; it makes your life easier, but you can also just go for blade if you want to do that. Like this, you can just easily configure what you want to show when a user clicks on this menu bar icon. These are the two main ways to enter your desktop application using the menu bar or a window.
Now, the next thing I want to show you is we usually need to open more than one window. For example, let's imagine that we have some settings. I want to open the settings in my application, and this should open up a new window. How can we make this work with native PHP? Well, first of all, we need to register a way to open up these settings. Usually, this is inside of the menu that we have in our application. So let's add this in our app as well.
So, we can say that we want to create a new menu. The first entry of this menu should be a sub-menu called FW days, and this sub-menu has a menu with the label 'hi FW days'. We have a separator, and we have the option to quit our application. This way, we can build up our menu for our application. Once we're done with this, we could just register it, save this. Hot. Reloading. And we can see that it kicks in. Now, if we take a look at the top, you can see that we now have this FW days menu. If we click on this, it says 'hi FW days'. That's our label. We have this separator, and we have the option to quit our application. Cool. Now, all of the other default menu items are gone, like the file, edit, window, or view. You can easily just re-add them. So we can say that we want the file. We can say that we want the file menu and the edit menu, the view menu, and the window menu. So you don't have to build those menus yourself. You can just use the pre-default ones that every application basically has if you want to use them. Cool. So now we have them in place.
Now that we have this menu, how can we actually add something to this menu and then basically react to when a user clicks on an item? Well, we would do this in a more front-end JavaScript way using an event. So we can say that we want to add a new item to our menu, which is an event. The first argument is the event that gets dispatched when you click on this. So this would be a 'settings clicked' event. I already created this one. Then we have the title of our menu, which could be just 'preferences'. And last but not least, we can also specify a hotkey, which by default on a Mac OS would be command plus comma. Now, save it again. Let's open up our menu, and as you can see, we now have 'settings' in here with our shortcut. But when we click on this, nothing yet happens because we don't listen for this event just yet, but under the hood, native PHP dispatches this event, and this is just a default Laravel event so that we can listen for it and then add our logic.
So let's go to our event service provider class. And in here, in this boot method, let's listen for this event. So we can say event.listen, we want to listen for the 'settings click' event. And every time this event gets dispatched, what exactly do we want to do? We want to open a new window, so we can say window.open, and this window should be the settings route. And let's say the title of it is also 'settings'. One important thing is, because we have multiple windows, we should give this window an ID so that later on in our application, we can reference it. So we can say that this should be the 'settings window'. All right. Now, if I go to FW days to the menu, click on settings, our event gets dispatched, our service provider listens for this event, where we just open up a new window. That's it.
So it has the 'settings' title. And now we have this window open. One important thing, because we gave this window with a 'settings' title, the 'settings' ID right here, let me open that again. Whenever this event gets dispatched, we're now not going to create multiple settings windows because native PHP now knows that, or basically checks if a window with the ID 'settings' is already open. If that's the case, we're just going to focus it. So if I'm just pressing the hotkey, for example, we're just going to switch focus to our settings. Cool. All right. So with our settings window open, let's add the actual logic.
So what I want to do in the settings is we have this switch between the background color, and I want to change the color from white to green, and this should automatically change the color in this other window. And the next time I open up my settings, it should still say green. So it should be persistent. Let's see how we can make this work. If we go to our settings, this is a live wire component, a very simple common, we have a public property for the color. Whenever we mount the component, the default value of our color will be pulled from the settings facade, which is a class that native PHP provides. That makes it just easy for you to access and modify values in the settings, in a settings storage, basically. So we can say that the default color should be the color key from our settings.
If it doesn't exist, it should be white. Now, whenever we change the value in our dropdown, our updated color method gets called. This is just Livewire logic. And whenever we do this, we're just going to edit and change the setting using the settings facade. So we're just going to set the color to the new value. This means that when we open up our settings, now let's change this to green. This method gets called, and the green setting value will be stored in our settings facade. So the next time, let's close this window, reopen it, we pull the setting back from the setting storage. It's green. So now our select is pre-filled with the value green. All right. So persisting settings is already done. How can we now add then change this in real life? We can do this in a Livewire application. So let's do that. Let's open up this dialog component.
So the way that we would do this in a Livewire application is using something like Laravel Echo. So using web sockets, we would connect and listen for an event. So you would do something like echo: then have a channel and then have the actual event that you want to listen to. With native PHP, we can do this kind of the same way. So we can say that we want to listen for a native event. And native PHP, by default, dispatches an event every time a setting gets changed. So we want to listen for the 'setting changed' event. And every time the setting gets changed, we want to call this updatedSettings method like this. All right. So what does this updatedSettings method do? It basically retrieves the value and the key of the setting that was changed. In our case, it would be the color. And all we do is every time the color changes, we listen for this event, we go into this method because of Livewire, and then we change the color property in our Livewire component, which then Livewire takes to rerender and redraw the actual view that we show. With a bit of luck, if we now open up our settings, change the color from white to green, boom, it automatically in real time changes. In real time. In my main view and all of this with just PHP, with probably like in total, this is like 10 lines of code, the persistence and the event listening. So pretty cool stuff.
And there's a ton more that you can do with native PHP. So, for example, in this dialogue component that we have here, you can show file dialogues, so we can create a new dialogue. Then we can say that we want to only be able to select images using these extensions or PHP files using these, then we have a button label and we can change this. We can configure it to select multiple items. And once we open this, this is just going to open up a native file picker. As you can see here, our button has our custom label. We can only select between images and PHP files because we configured that in PHP itself. If I now select one or multiple files, these will be stored or basically returned and stored in this variable. And then we can just use this to display it in the UI, or you can read the files. You can save to these files. All of that is entirely possible with native PHP.
All right. Now this is basically just scratching the surface because there's so much more that native PHP can do for you, but I only have 30 minutes of time, but I still want to give you a quick overview of what else native PHP can do. So we have access to these file system dialogues, to open dialogues, to, like open files, to save files, to show errors. We have access to system APIs, like Touch ID, for example. So you can add something in the UI, add a button. And if the user presses the button, they need to unlock using Touch ID or using their password, we have full database support. So. So you can make use of just Eloquent models as usual. They get persisted in a SQLite database that lives outside of your application. So it's safe to update and we run migrations every time your users update their application. We have full support for queue jobs. So if you want to run something in the background, just push it to the queue. Native PHP just handles it automatically for you. We have full support for the scheduler. So if you want to run some tasks like a cron job. Automatically, as long as your native application is open, you just add it to this console kernel and schedule it there in Laravel.
It just works out of the box with native PHP. We have built-in auto-updater that allows you to update your application using S3, Digital Ocean, or GitHub. You can access the clipboard read content, right? Content from it. We have global shortcuts, system notifications, progress bars in the dock, which is really cool. You can customize context menus on right-click. We have deep linking between one app and your app. So we have a custom URL scheme. We have a ton of events. And again, this is not everything that native PHP has to offer. There's really a lot in there. Right now native PHP is Mac OS only. We already have, um, pull requests open for Linux and Windows support that just need to. Get verified and, um, and then merged in. So I really don't think this is going to take a long time before we add support for Linux and Windows as well. So if you want to check out native PHP, check out the documentation, check out the source code. It's entirely open source. Please go to native php.com, uh, where it can find the documentation of it and just reach out on GitHub on discord. Um, if you have any questions. Questions or feedback. I'm also now in this chat. So if you have any questions, just feel free to ask me, hang out and chat with me. And I thank you very much for your time. I hope you enjoyed it. And I hope that you take a look at native PHP so that we can build desktop apps with our favorite language. Thank you.