NativePHP で簡単なメモ帳アプリつくってみた

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

【皆さんへのお礼: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!?」と思われたかもしれませんが、NativePHPLaravelのパッケージとして提供されています。

ということで、今回は専用に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.1nginxを使っていますがご自身の環境に合わせて適宜インストールするパッケージを変更してください。

データベースの設定をする

.env内のDB_CONNECTIONsqliteへ変更し、さらに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, // 👈 ここを追加しました
    ]);
}

// 省略

では、この状態でDBSQLite)を初期化してみましょう。
以下のコマンドを実行してください。

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 にまで対応させることができる

つまり、専用の新しい技術を学ぶ必要がないため、学習コストがほとんど無く、さらにクロスプラットフォームに対応しているので誰にでも配布することができるというスグレモノな訳です。

もしこういったご希望をお持ちでしたら、いつでもお気軽にお問い合わせください。

お待ちしております。😊✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、記念すべき500記事目(本当はストックがあるので501記事目なんですけどね…😅)はNativePHPをお届けしました。

まだまだアルファ版なので、今後どうなるかはわかりませんがここまでの完成度ならきっと正式版まで行くと思いますし、Electrontauriに変わるデスクトップアプリ開発の新しい道になる可能性もあると思います。

ぜひ皆さんも楽しんでやってみてくださいね。

ではでは〜❗

「神様に嫌われているのか、
神社に行ったあとは
必ず嫌なことが起こります…なぜ😭」

このエントリーをはてなブックマークに追加       follow us in feedly