NativePHP Tutorial: Building a Mac MenuBar application

Published on by

NativePHP Tutorial: Building a Mac MenuBar application image

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:install
Would 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/Index
php artisan livewire:make TeamMember/Create
php 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.png
menuBarIconTemplate@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!

Shane D Rosenthal photo

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.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Asymmetric Property Visibility in PHP 8.4 image

Asymmetric Property Visibility in PHP 8.4

Read article
Access Laravel Pulse Data as a JSON API image

Access Laravel Pulse Data as a JSON API

Read article
Laravel Forge adds Statamic Integration image

Laravel Forge adds Statamic Integration

Read article
Transform Data into Type-safe DTOs with this PHP Package image

Transform Data into Type-safe DTOs with this PHP Package

Read article
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article