NativePHP Tutorial: Building a Mac MenuBar application
Published on by Shane D Rosenthal
NativePHP, created by Marcel Pociot at BeyondCode, allows us Laravel Devs to leverage ALL the working knowledge we already have with Laravel to build native Mac, Windows, and Linux applications.
I recently saw Christopher Rumpel working on an app that let's you store the timezones of your friends so you can see what time it is at a glance.
Follow along with me as we put together a Mac MenuBar application to know the local time of each member of your team.
Wait - how does NativePHP even work??
NativePHP allows you to choose from two different popular technologies to use under the hood, Electron, and Tauri. They both allow you to "Build cross-platform desktop apps with JavaScript, HTML, and CSS". This is kinda like sorcery if you think about it - web technologies to build out a 'native' application. NativePHP provides a simple API with a familiar (Laravel) way to build out applications in either of these underlying technologies. For this example, I will be demonstrating the Electron wrapper.
NativePHP Installation and Hello World
In a fresh Laravel application:
laravel new team-time
Let's start with installing the package:
composer require nativephp/electron
Run the installer:
php artisan native:installWould you like to install the NativePHP NPM dependencies? - Select 'yes'Would you like to start the NativePHP development server? - Select 'no'
I want you to manually start the application so you get used to doing it this way:
php artisan native:serve
After a moment you should see a native desktop application spin up displaying the default Laravel homepage, hello there!
Show me the code!
Sure, but settle down a bit, everything will be revealed shortly. Navigate over to App\Providers\NativeAppServiceProvider.php
. Here you can see some of the NativePHP API stubbed out for you. For this example, we are not going to use this code though. Go ahead and clear out everything in the boot
method and replace it with the following:
<?php namespace App\Providers; use Native\Laravel\Facades\MenuBar; class NativeAppServiceProvider{ public function boot(): void { Menubar::create(); }}
Since NativePHP does hot reloading we should see the Window
close and a Menubar
icon appear at the top of your computer. Clicking on it will reveal the same default Laravel home page.
Nice! Let's build something cool!
Behind the scenes, I am installing TailwindCSS per their docs, Laravel Livewire 3 (dicey, I know but it's my drug of choice), Blade Heroicons and then adding our TeamMember model, migration and factory with the following command:
php artisan make:model TeamMember -mf
NOTE: I am keeping `npm run dev` running for hot reloading of the ui.
Migration:
public function up(): void{ Schema::create('team_members', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('timezone'); $table->timestamps(); });}
Factory
public function definition(): array{ return [ 'name' => $this->faker->name, 'timezone' => $this->faker->randomElement(timezone_identifiers_list()) ];}
Then updating my App\Database\seeders\DatabaseSeeder.php
to:
public function run(): void{ \App\Models\TeamMember::factory(10)->create();}
And running php artisan migrate
and php artisan db:seed
.
NOTE: The application inside of NativePHP does NOT have access to the database defined in your `.env`. From my experience, it can be useful to seed your database locally and debug in the browser or by using Spatie/Ray.
Let's Create Our Livewire Classes and Views
php artisan livewire:make TeamMember/Indexphp artisan livewire:make TeamMember/Createphp artisan livewire:make TeamMember/Update
Then update our web.php
to the following:
Route::get('/', \App\Livewire\TeamMember\Index::class)->name('index');Route::get('/team-members/create', \App\Livewire\TeamMember\Create::class)->name('create');Route::get('/team-members/{teamMember}/edit', \App\Livewire\TeamMember\Update::class)->name('edit');
And create an app.blade.php
inside of resources/views/components/layouts
with the following html :
<!DOCTYPE html><html lang="{{ str_replace('_', '-', app()->getLocale()) }}"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel</title> @vite('resources/css/app.css')</head><body class="antialiased bg-gray-900 text-gray-100"><div class="max-w-md mx-auto px-4 py-6"> {{$slot}}</div></body></html>
Listing Our Teammates
Inside the App\Livewire\TeamMember\Index
class we need to fetch all of the team members to display them, additionally, we should offer a link to create a new team member and offer update and delete buttons for existing team members.
Class:
<?php namespace App\Livewire\TeamMember; use App\Models\TeamMember;use Livewire\Component; class Index extends Component{ public function deleteMember(TeamMember $member) { $member->delete(); } public function render() { $team = TeamMember::get(); return view('livewire.team-member.index', compact('team')); }}
View:
<div> <div class="flex items-center justify-between mb-10"> <h1 class="text-xl font-bold">My Team</h1> <a href="{{route('create')}}" type="button" class="rounded-full bg-pink-600 px-2 py-1 text-xs font-bold text-white shadow hover:bg-pink-500">Add Team Mate</a> </div> <div wire:poll> @foreach($team as $member) <div wire:key="{{ $member->id }}" class="my-2 flex items-center justify-between"> <div> <p class="text-xs font-bold text-sky-500">{{$member->name}}</p> <p class="text-lg">{{now()->tz($member->timezone)->format('h:i:s A')}} <span class="text-xs text-gray-500">- {{$member->timezone}}</span></p> </div> <div class="flex items-center"> <a href="{{route('edit', ['team-member' => $member])}}"> <span class="sr-only">Edit</span> <x-heroicon-m-pencil class="w-5 h-5 mr-3 hover:text-pink-500 transition-all duration-300" /> </a> <button wire:click="deleteMember({{$member}})"> <x-heroicon-m-trash class="w-5 h-5 mr-3 hover:text-red-600 transition-all duration-300" /> </button> </div> </div> @endforeach </div></div>
If you have seeded your database locally then previewing this in the browser should look like this:
In the native app it should look like this since we don't have any data there yet (make sure to run npm run build
then php artisan native:serve
). NativePHP uses a local SQLite database behind the scenes, we don't need any additional setup or configuration for it.
Now let's handle the Create
operations, so we can see this in the native app too.
Class:
<?php namespace App\Livewire\TeamMember; use App\Models\TeamMember;use Livewire\Attributes\Rule;use Livewire\Component; class Create extends Component{ #[Rule(['required', 'string', 'min:3'])] public string $name; #[Rule(['required', 'string', 'min:3'])] public string $timezone; public function createMember() { TeamMember::create($this->validate()); $this->redirectRoute('index'); } public function render() { return view('livewire.team-member.create'); }}
View:
<div> <div class="flex items-center justify-between mb-10"> <h1 class="text-xl font-bold">Add Team Member</h1> <a href="{{route('index')}}" type="button" class="rounded-full bg-pink-600 px-2 py-1 text-xs font-bold text-white shadow hover:bg-pink-500 flex items-center"> Go Back </a> </div> <form wire:submit="createMember"> <div> <label for="name" class="block text-sm font-medium leading-6 text-gray-100">What is your team member's name?</label> <div class="mt-2"> <input type="text" wire:model="name" id="name" class="block w-full rounded-md border-0 py-1.5 text-gray-400 shadow-sm bg-gray-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-pink-600 sm:text-sm sm:leading-6" placeholder="Sarthak"> @error('name') <div class="mt-1 text-red-500 text-sm">{{ $message }}</div> @enderror </div> </div> <div class="mt-6"> <label for="timezone" class="block text-sm font-medium leading-6 text-gray-100">What is your team member's timezone</label> <select id="timezone" wire:model="timezone" class="mt-2 block w-full rounded-md border-0 py-1.5 text-gray-400 shadow-sm bg-gray-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-pink-600 sm:text-sm sm:leading-6"> @foreach(timezone_identifiers_list() as $timezone) <option wire:key="{{ $timezone }}">{{$timezone}}</option> @endforeach </select> @error('timezone') <div class="mt-1 text-red-500 text-sm">{{ $message }}</div> @enderror </div> <button type="submit" class="mt-6 rounded bg-pink-600 px-2 py-1 font-bold text-white shadow hover:bg-pink-500 w-full">Add Team Mate </button> </form></div>
Now we're cookin'! But it looks like I set Sarthak to the wrong timezone, let's set up our Edit class and view and put this puppy to sleep.
Class:
<?php namespace App\Livewire\TeamMember; use App\Models\TeamMember;use Livewire\Component;use Livewire\Features\SupportValidation\Rule; class Update extends Component{ public TeamMember $teamMember; #[Rule(['required','min:3', 'string'])] public $name; #[Rule(['required','string'])] public $timezone; public function mount(TeamMember $teamMember) { $this->teamMember = $teamMember; $this->name = $teamMember->name; $this->timezone = $teamMember->timezone; } public function saveMember() { $this->teamMember->update([ 'name' => $this->name, 'timezone' => $this->timezone ]); $this->redirectRoute('index'); } public function render() { return view('livewire.team-member.update'); }}
View:
<div> <div class="flex items-center justify-between mb-10"> <h1 class="text-xl font-bold">Update Team Member</h1> <a href="{{route('index')}}" type="button" class="rounded-full bg-pink-600 px-2 py-1 text-xs font-bold text-white shadow hover:bg-pink-500 flex items-center"> Go Back </a> </div> <form wire:submit="saveMember"> <div> <label for="name" class="block text-sm font-medium leading-6 text-gray-100">Name</label> <div class="mt-2"> <input type="text" wire:model.blur="name" id="name" class="block w-full rounded-md border-0 py-1.5 text-gray-200 shadow-sm bg-gray-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-pink-600 sm:text-sm sm:leading-6" placeholder="Sarthak"> @error('name') <div class="mt-1 text-red-500 text-sm">{{ $message }}</div> @enderror </div> </div> <div class="mt-6"> <label for="timezone" class="block text-sm font-medium leading-6 text-gray-100">Timezone</label> <select id="timezone" wire:model="timezone" class="mt-2 block w-full rounded-md border-0 py-1.5 text-gray-200 shadow-sm bg-gray-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-pink-600 sm:text-sm sm:leading-6"> @foreach(timezone_identifiers_list() as $timezone) <option {{$teamMember->timezone === $timezone ? 'selected' : ''}}>{{$timezone}}</option> @endforeach </select> @error('timezone') <div class="mt-1 text-red-500 text-sm">{{ $message }}</div> @enderror </div> <button type="submit" class="mt-6 rounded bg-pink-600 px-2 py-1 font-bold text-white shadow hover:bg-pink-500 w-full">Add Team Mate </button> </form></div>
Wrapping It Up
Now that the app is working and looking the way we want it, let's do just a couple more things before building it out. First, let's update the MenuBar icons. I created 2 images, one is a 22x22 png and the other is a 44x44 png. By suffixing the name of these files with the word Template
we get some nice functionality. On a Mac, NativePHP will convert these images to a white icon with transparency so that it matches the color scheme of the native menu bar.
The two images are named:
menuBarIconTemplate.pngmenuBarIconTemplate@2x.png
By adding these icons to the storage/app
directory, and then updating our NativeAppServiceProvider
boot method to:
public function boot(): void{ Menubar::create()->icon(storage_path('app/menuBarIconTemplate.png'));;}
On the next serve, we should see the icon update in our menu bar.
Last, let's add some items to our .env
file to tell NativePHP some details about our app:
NATIVEPHP_APP_NAME="TeamTime"NATIVEPHP_APP_VERSION="1.0.0"NATIVEPHP_APP_ID="com.teamtime.desktop"NATIVEPHP_DEEPLINK_SCHEME="teamtime"NATIVEPHP_APP_AUTHOR="Shane D Rosenthal"NATIVEPHP_UPDATER_ENABLED=false
Build your NativePHP app
php artisan native:build
Running this command will package everything up that we need to build the app locally and give us a native file ('.dmg', '.exe', etc.). Once complete, the files will be placed in your project's root/dist
directory and you can distribute the app as you see fit.
As of the time of this writing, the php artisan native:build function works, however when I open the .dmg locally it sort of 'hangs' and my menu bar application doesn't start. Again, NativePHP is still currently in an alpha
state and problems are expected, the BeyondCode team is hard at work fixing items like this and we should expect full functionality in the weeks or months to come.
Summary
Well, what did you think? Pretty awesome that we can build native apps with Laravel, right? I can think of a lot of use cases for such a feature and I can't wait to continue exploring and seeing Laravel get pushed to new heights. There are so many other items in the NativePHP docs that this app doesn't cover or go over, take a look yourself, get inspired, and build something awesome. #laravelforever!
I am a technology junkie, family man, community leader, pilot and musician. Since the mid 80's I have been taking things apart to see how they work and trying to put them back together again, sometimes in better condition than before. Along the way I met the love of my life, made a family, found purpose in leading and teaching others and recently became a pilot. I am passionate about flying, sharing the world with those around me and catching some live metal shows when I can.
Looking to the future I intend to own my own airplane, remain instrument rated, continue teaching and guiding our youth, grow my YouTube channel and followers and share my passions to influence whoever I can.