
九保すこひです(フリーランスのITコンサルタント、エンジニア)
【皆さんへのお礼:500記事達成しました】
今回の記事でなんと500
記事になります。
前回の400
記事が「2021年10月18日公開」だったので、1年9ヶ月ほどで100
記事を書きました。
ひとえに皆さんが訪問してくださることがモチベーションにつながりました。
今後ともConsole dot Log
をよろしくお願いいたします。m(_ _)m
※ ちなみに一番最初の技術記事(第2回目の記事)はLarvel 5.4
を使ってました。時間が経つのは早いですね…
では、ここからは本題です。
この間情報の宝庫、X(旧ツイッター)を見ていると、ある面白そうなプロジェクトが紹介されていました。
それが・・・・・・
PHPでデスクトップ・アプリが作成できる「NativePHP」
です。
そこで
今回はNativePHP
を使って簡単なメモ帳アプリを作ってみることにしました。
ぜひ何かの参考になりましたら嬉しいです。
ご注意:
2023.08.10
現在、NativePHP
はアルファ版です。今後変更が加えられると思いますので、お気をつけください。
「500記事も書くと思ってなかったです
ホントに皆さん
ありがとう 」
開発環境: Laravel 10.x、PHP 8.1
目次 [非表示]
NativePHP をインストールする
では、まずはNativePHP
をインストールします。
なお、必要な環境は以下になります。
- PHP 8.1
- Laravel 10 以上
- NPM
- Linux or MacOS
参考ページ: Installation – NativePHP
ちなみに、ここにはwindows
が入っていないのですが、以下のビルドに関するページでは、「mac」「windows」「linux」と書いてあります。(なのでビルドはどれでもOK
なようです)
参考ページ: Building – NativePHP
また、以下のページによるとwindows
のサポートもなんとか改造すればいけるようですし、windows
向けは開発中とのことなので今のところ、「開発は mac と linux だけね。でもビルドは全部いけるよ」ということなのかもしれません。
参考ページ: NativePHP for Windows
Laravel をインストールする
「NativePHP なのに Laravel!?」と思われたかもしれませんが、NativePHP
はLaravel
のパッケージとして提供されています。
ということで、今回は専用にLaravel 10.x
をインストールし、そこに環境をつくっていくことにします。
では、以下のコマンドを実行してください。
composer create-project laravel/laravel native_php
※ プロジェクト名はnative_php
にしていますがお好みで変更してください
NativePHP パッケージをインストールする
続いて、NativePHP
の本体をインストールします。
以下のコマンドでフォルダ内へ移動し、パッケージをインストールしてください。
cd native_php
composer require nativephp/electron
これでNativePHP
本体がインストールされました!
インストーラーを実行する
次に、NativePHP
のインストーラーを実行します。
以下のコマンドを実行してください。
php artisan native:install
すると、以下のような表示になりますので、Yes
を選択しEnter
キーを押します。(npm
パッケージインストールする?と聞いています)
すると、(少し時間がかかるかもですが)npm
のパッケージがインストールされることになります。
そして、最後に「NativePHP の開発サーバー起動する??」と聞いてくるのでこれも「Yes」にしてEnter
キーを押してください。
すると、これだけで以下のウィンドウが表示されます。(スゴイですね!)
※ なお、コマンドで起動する場合は以下を使ってください。
php artisan native:serve
SQLite がつかえるようにする
今回はテキストファイルでDB
機能を持たせることができるSQLite
をインストールしておいてください。
なお、以下はUbuntu 22.04
のインストール方法です。(レアケースでゴメンナサイ)
以下のコマンドを実行します。
sudo apt update
sudo apt upgrade
sudo apt install sqlite3
sudo apt install php8.1-sqlite3
sudo systemctl restart nginx
※ なお、今回はphp 8.1
&nginx
を使っていますがご自身の環境に合わせて適宜インストールするパッケージを変更してください。
データベースの設定をする
.env
内のDB_CONNECTION
をsqlite
へ変更し、さらにDB_DATABASE
はコメントアウトしてください。(初期値のファイルパスを使うようにします)
.env
DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
#DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
また、コンフィグの中のsqlite
部分の参照ファイルをnativephp.sqlite
へ変更します。
config/database.php
// 省略
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DATABASE_URL'),
'database' => env('DB_DATABASE', database_path('nativephp.sqlite')), // database.sqlite から変更しました
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
// 省略
ウィンドウサイズを指定する
NativePHP
は起動時のウィンドウサイズを変更することができます。
せっかくなので、変更しておきましょう。
app/Providers/NativeAppServiceProvider.php
// 省略
public function boot(): void
{
Window::open()
->width(1200) //
以降を追加しました
->height(600);
}
// 省略
必要なファイルをつくる
では、ここからは通常のLaravel
と同じ作業で、メモ帳の機能をプログラムしていきます。
まずは必要なファイルを作成します。
以下のコマンドを実行してください。
php artisan make:model Memo -mcs
これで、「モデル」「マイグレーション」「コントローラー」「Seeder」の4ファイルが作成されました。
これらのファイルを変更していきましょう。
コントローラーを変更する
まずはコントローラーです。
以下のように変更してください。
app/Http/Controllers/MemoController.php
<?php
namespace App\Http\Controllers;
use App\Models\Memo;
use Illuminate\Http\Request;
class MemoController extends Controller
{
public function index()
{
return view('memo.index');
}
public function list()
{
return Memo::query()
->orderBy('id', 'desc')
->get();
}
public function save(Request $request)
{
// バリデーションは省略しています
$memo = Memo::findOrNew($request->id);
$memo->title = $request->title;
$memo->body = $request->body;
$result = $memo->save();
return ['result' => $result];
}
public function destroy(Memo $memo)
{
// バリデーションは省略しています
$result = $memo->delete();
return ['result' => $result];
}
}
内容としては、基本的なものばかりですのでシンプルかと思います。
Seeder ファイルを変更する
続いて、テストデータを自動で作成するためのファイルSeeder
です。
database/seeders/MemoSeeder.php
// 省略
public function run(): void
{
for($i = 1; $i <= 10; $i++) {
$memo = new \App\Models\Memo();
$memo->title = 'タイトル' . $i;
$memo->body = "テスト内容\nテスト内容\nテスト内容" . $i;
$memo->save();
}
}
// 省略
Seeder
は作成しただけでは有効になりませんので、Laravel
側へセットします。
// 省略
public function run(): void
{
$this->call([
MemoSeeder::class, //
ここを追加しました
]);
}
// 省略
では、この状態でDB
(SQLite
)を初期化してみましょう。
以下のコマンドを実行してください。
php artisan migrate:fresh --seed
すると、 実際のテーブルはこうなりました。(FireFox
のアドオンで表示してます)
ビューをつくる
次にビューを作ります。
今回必要になるのは、以下の2つです。
- レイアウト
- メモ帳管理ページ
では、まずはレイアウトです。
今回は「TailwindCSS」「Alpine.js」「Axios」をCDN
で呼び出して使います。
resources/views/layouts/app.blade.php
<html>
<head>
<title>NativePHP のメモ帳</title>
<script src="https://cdn.tailwindcss.com/3.3.3"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div class="p-5">
@yield('content')
</div>
@yield('script')
</body>
</html>
そして、メモ帳の部分です。
resources/views/memo/index.blade.php
@extends('layouts.app')
@section('content')
<div x-data="memoData">
<!-- index モード -->
<template x-if="isModeIndex">
<div>
<div class="mb-5">
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 rounded" @click="onCreate">
メモを追加する
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<template x-for="memo in memos">
<div class="relative max-w-xs w-full h-auto border-2 bg-gray-50 shadow-lg rounded-lg overflow-hidden">
<div class="px-4 py-2">
<h1 class="font-bold mb-2 truncate" x-text="memo.title"></h1>
<p class="text-gray-700 text-base mb-5 whitespace-pre-wrap" x-text="memo.body"></p>
</div>
<div class="absolute bottom-2 right-4 space-x-2">
<a href="#" class="text-blue-500 hover:text-blue-700 text-sm" @click.prevent="onEdit(memo)">変更</a>
<a href="#" class="text-red-500 hover:text-red-700 text-sm" @click="onDelete(memo)">削除</a>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- form モード -->
<template x-if="isModeForm">
<div>
<div class="mb-5">
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 rounded" @click="setMode('index')">
戻る
</button>
</div>
<div class="w-full max-w-xl mx-auto mt-10">
<!-- Title Input -->
<div class="mb-6">
<label for="title" class="block text-gray-700 text-sm font-bold mb-2">タイトル:</label>
<input
id="title"
type="text"
name="title"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
x-model="params.title">
</div>
<!-- Body Textarea -->
<div class="mb-6">
<label for="body" class="block text-gray-700 text-sm font-bold mb-2">本文:</label>
<textarea
id="body"
name="body"
rows="5"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
x-model="params.body"></textarea>
</div>
<div class="flex items-center justify-between">
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="button"
@click="onSave">
送信
</button>
</div>
</div>
</div>
</template>
</div>
@endsection
@section('script')
<script>
const memoData = {
mode: 'index', // or `form`
setMode(mode) {
this.mode = mode;
if(mode === 'form') {
this.$nextTick(() => {
document.getElementById('title').focus();
});
}
},
isModeIndex() {
return this.mode === 'index';
},
isModeForm() {
return this.mode === 'form'
},
// Index
params: {},
onCreate() {
this.params = {
title: '',
body: '',
};
this.setMode('form');
},
onEdit(memo) {
this.params = memo;
this.setMode('form');
},
// Form
onSave() {
if(! confirm('送信します。よろしいですか?')) return;
const url = '{{ route('memo.save') }}';
axios.post(url, this.params)
.then(response => {
if(response.data.result === true) {
this.setMode('index');
this.getMemos();
}
});
},
// Delete
onDelete(memo) {
if(! confirm('削除します。よろしいですか?')) return;
const url = '{{ route('memo.destroy', '') }}/'+ memo.id;
const params = {
_method: 'DELETE',
};
axios.post(url, params)
.then(response => {
if(response.data.result === true) {
this.getMemos();
}
});
},
memos: [],
getMemos() {
const url = '{{ route('memo.list') }}';
axios.get(url)
.then(response => {
this.memos = response.data;
})
},
init() {
this.getMemos();
}
};
</script>
@endsection
ちょっとコードが長いですが、一覧表示とフォームを切り替えられるようにして一体型のページにしています。
データの取得や保存、削除はMemoController
にアクセスすることになります。
ルートをつくる
そして、最期にルートです。
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\MemoController;
Route::get('/', function(){ return to_route('memo.index'); });
Route::prefix('memo')->controller(MemoController::class)->group(function(){
Route::get('/', [MemoController::class, 'index'])->name('memo.index');
Route::get('/list', [MemoController::class, 'list'])->name('memo.list');
Route::post('/save', [MemoController::class, 'save'])->name('memo.save');
Route::delete('/delete/{memo}', [MemoController::class, 'destroy'])->name('memo.destroy');
});
ちなみに、トップページにアクセスしたとき(デスクトップアプリを起動した時、自動的にメモ帳のindex
へリダイレクトするようになっています)
これで作業は完了です。
お疲れ様でした
テストしてみる
では、実際にテストしてみましょう。
以下のコマンドでアプリを起動します。
php artisan native:serve
うまく起動できるでしょうか・・・・・・
はい
うまくデスクトップアプリのウィンドウが表示されました。
では、「メモを追加する」ボタンをクリックしてみましょう。
どうなるでしょうか・・・・・・
はい
ちょっとわかりにくですけど、入力フォームが表示されました。
では、以下のように入力して送信してみましょう。
うまくいくでしょうか・・・・・・
はい
うまく保存され、表示されました。
成功です
では、せっかくデスクトップなのでビルドしてインストールできるパッケージに変換してみましょう。
以下のコマンドを使います。
php artisan native:build linux
※ なお、linux
の部分はmac
もしくはwindows
でもOK
です。
すると、少し時間はかかりましたが・・・・・・
はい
Linux
のパッケージインストールファイルの.deb
ファイルがdist
フォルダの中に作成されています。(ファイルサイズは、79 MB
でした)
では、これを使ってインストールしてみましょう。
すると・・・・・・
はい
デスクトップ上にアプリが登録されました。
もちろん動きも同じで、さらにDB
はまっさらな状態で起動することができました。
すべて成功です
企業様へのご提案
NativePHP
はまだアルファ版なので製品として利用するにはもう少し時間がかかる状態ではありますが、とても便利に感じました。
しかも以下のようなメリットもあります。
- Laravel(PHP)の技術を使うことができる
- VueやReactなど好きなフロントエンドの技術をそのまま使える
- コードをひとつ作れば mac や windows だけでなく linux にまで対応させることができる
つまり、専用の新しい技術を学ぶ必要がないため、学習コストがほとんど無く、さらにクロスプラットフォームに対応しているので誰にでも配布することができるというスグレモノな訳です。
もしこういったご希望をお持ちでしたら、いつでもお気軽にお問い合わせください。
お待ちしております。
おわりに
ということで、記念すべき500
記事目(本当はストックがあるので501
記事目なんですけどね…)は
NativePHP
をお届けしました。
まだまだアルファ版なので、今後どうなるかはわかりませんがここまでの完成度ならきっと正式版まで行くと思いますし、Electron
やtauri
に変わるデスクトップアプリ開発の新しい道になる可能性もあると思います。
ぜひ皆さんも楽しんでやってみてくださいね。
ではでは〜
「神様に嫌われているのか、
神社に行ったあとは
必ず嫌なことが起こります…なぜ」