はじめに
2023年のLaracon USにて、マルチプラットフォーム(Linux, macOS, Windows)のデスクトップアプリをLaravelにて開発できるNativePHPが紹介されました。
現在はまだアルファリリースで本番運用には適していないとのことですが、個人的に今後注目したい技術です。
ちょうどFilamentを使ってLaravelアプリを開発していたこともあり、「よっしゃ!いっちょFilament + NativePHPでデスクトップアプリでも開発してみるか!」と思い触ったところ、思わぬところでエラーに出くわしたしたので解決までの道のりを残しておきます。
もし、同じようにFilamentとNativePHPで何か開発されている方がいれば、何かの一助になれば幸いです。
結論
先に結論。
Filamentはブラウザ用に開発されているのに対してNativePHPは裏側がElectronでありNode.jsで動作するため差異が発生しエラーが起きていた
ということのようです。
開発環境
PHP 8.3(Laravel Herd利用)
MacBook Pro(macOS Sonoma 14.3;M1 Proチップ)
npm 10.2.4(Node.js v20.11.1)
Laravel new ○○(アプリ名)でベーシックなLaravelがインストールされており、Filamentも導入済み、かつログインできるユーザーも作成済みであることを前提とします。
今回はlaravel new nativephp
にてインストールしました。
記事投稿時点でNativePHPはSQLiteのみサポートしているのでSQLiteを使うように設定してください。
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=nativephp
# DB_USERNAME=root
# DB_PASSWORD=
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('nativephp.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
なお、認証機能はFilamentに任せるのでbreezeなどのスターターキットは入れません。
Filamentの導入は別記事でも投稿しているのですが、Filament v1で作成しています。
現在はv3がリリースされているため下記のドキュメントに従ってインストールしてください。
ではやっていきましょう。
道のり
NativePHPの導入
まずはNativePHPを導入します。ドキュメントに記載の通り下記コマンドを実行します。
以降、コマンド実行は原則Laravelがインストールされているルートディレクトリ(本記事では/Users/xxx/Herd/nativephp
)にて実行します。
> composer require nativephp/electron
~省略~
No security vulnerability advisories found.
Using version ^0.5.0 for nativephp/electron
続けてphp artisan native:install
を実行します。
> php artisan native:install
Publishing NativePHP Service Provider...
┌ Would you like to install the NativePHP NPM dependencies? ───┐
│ Yes │
└──────────────────────────────────────────────────────────────┘
~省略~
┌ Would you like to start the NativePHP development server ────┐
│ No │
└──────────────────────────────────────────────────────────────┘
NativePHP scaffolding installed successfully.
NativePHPは内部でNPMを利用しているためYesを選択してnpmパッケージをインストールします。
最後に開発用サーバーを起動するか聞かれますが、一旦Noを選択しておきます。
これでNativePHPが導入できました。
開発サーバーの起動
改めてphp artisan native:serve
を実行して開発用サーバーを起動します。
問題なければ内部で色々処理が走ってデスクトップアプリが起動します。
> php artisan native:serve
Starting NativePHP dev server…
~省略~
You may migrate manually by running: php artisan native:migrate
PHP Server started on port: 8100
・・・ん? 起動しない?
そうなんです、起動しないんです。いや正確には起動はしますが画面が表示されません。
省略したログを見てみるとFatal Errorを起こしています。(他のエラーは割愛)
stdout: '\n' +
'Fatal error: Composer detected issues in your platform: Your Composer dependencies require a PHP version ">= 8.2.0". You are running 8.1.17. in /Users/xxx/Herd/nativephp/vendor/composer/platform_check.php on line 24\n',
stderr: ''
いやいやPHP8.3使ってるのにYou are running 8.1.17てなんやねん。
と、なりますが実はNativePHPは内部でPHPバイナリを保持しているのでそれを利用しようとします。内部バイナリのバージョンは8.1であり、Laravel11は8.2からのサポートなのでエラーが出ているようです。
これは公式のリポジトリでもissueとして上がっています。
このエラーを解決するため、Laravel Herd内部のPHPバイナリを利用します。ここでは最新の8.3を利用しました。下記コマンドでPHPバイナリをNativePHPライブラリ内部にコピーします。
> cp /Users/xxx/Library/Application\ Support/Herd/bin/php83 vendor/nativephp/php-bin/bin/mac/arm64/php
> cp /Users/xxx/Library/Application\ Support/Herd/bin/php83 vendor/nativephp/php-bin/bin/mac/x86/php
コピーできました。これで再度php artisan native:serve
します。
> php artisan native:serve
~省略~
PHP Server started on port: 8100
(node:28924) electron: NSVisualEffectMaterialAppearanceBased has been deprecated and removed as of macOS 10.15.
(Use `Electron --trace-warnings ...` to show where the warning was created)
他にもエラーはいくつか出ていますが問題なければデスクトップアプリが起動します。スゴイ!!
ただ、このままだと画面が小さいですしFilamentのログイン画面が表示されないのでNativeAppServivePrivider.php
にて設定を変更します。
class NativeAppServiceProvider implements ProvidesPhpIni
{
/**
* Executed once the native application has been booted.
* Use this method to open windows, register global shortcuts, etc.
*/
public function boot(): void
{
Window::open()
->width(1200)
->height(960)
->showDevTools( false )
->route( 'filament.admin.auth.login' );
}
bootメソッド内のopen()にメソッドチェインして縦と横のサイズを変更しました。
showDevTool()にはfalseを渡して開発者ツールを一旦非表示にしておきます。(F12で表示することができます)
route()では起動時に表示する画面を指定することができます。
変更して保存すると反映されます。
いいですね!
早速、Filamentユーザーでログインしてみましょう!
ログインできました!
・・・ん? NativePHPくん、右上はみ出てない?
そうです、ブラウザで閲覧したときは非表示になっているメニューがはみ出てるんです!
エラーを確認する
ようやくここからが本題です。長かった。
おもむろにF12で開発者ツールを開いてみると、何やらエラーが起きています。
エラー箇所をクリックしてみるとsupport.js内部で「Uncaught TypeError: Cannot read properties of undefined (reading 'from')」が起きていました。
とりあえずエラってる箇所で唯一情報がありそうなJS_MD5_NO_BUFFER_FROM
でググってみるとjs-md5というライブラリがヒットしました。下の行にもmd5の記載があるのでこれっぽい。
・・・ん? Filamentの依存ライブラリでエラー吐いてる...ってこと!? 泣いちゃう。
support.jsを追う
泣いちゃいましたが、もう少し深掘りをしてみます。
support.jsが本番用にビルドされているのでこのままだと処理が追えません。なので、難読化されていないsupport.jsファイルを生成することにしました。
Filamentの公式リポジトリからCloneしてビルドする
ここからはLaravelをインストールしたディレクトリではなく別ディレクトリで作業します。
> git@github.com:filamentphp/filament.git
これでFilamentがクローンできたのでsupport.jsがどう生成されるのか見ていきます。
support.jsの元になるファイルはpackages/support/resources/js/index.js
にあります。
ただ、トランスパイル前のファイルなのでビルドする必要があります。
Filamentのpackage.jsonをみると開発用のリソースビルドはbuild.jsで行われているようです。
{
"name": "filament",
"type": "module",
"scripts": {
"dev:js": "node bin/build.js --dev",
import * as esbuild from 'esbuild'
~省略~
const corePackages = ['forms', 'notifications', 'panels', 'support', 'tables']
corePackages.forEach((packageName) => {
compile({
...defaultOptions,
platform: 'browser',
entryPoints: [`./packages/${packageName}/resources/js/index.js`],
outfile: `./packages/${packageName}/dist/index.js`,
})
})
build.js内では諸々ビルド用の設定が書かれていますが、上記の部分がキモになっています。
コアパッケージをそれぞれforEachで回してコンパイルしているよう。
とりあえずsupport.jsだけビルドできれば良いのでforms, notifications, panels, tablesは削除、コアパッケージのコンパイル以降は全てコメントアウトしてビルドしてみます。
// const corePackages = ['forms', 'notifications', 'panels', 'support', 'tables']
const corePackages = ['support']
corePackages.forEach((packageName) => {
compile({
...defaultOptions,
platform: 'browser',
entryPoints: [`./packages/${packageName}/resources/js/index.js`],
outfile: `./packages/${packageName}/dist/index.js`,
})
})
// compile({
// ...defaultOptions,
// platform: 'browser',
// entryPoints: [`./node_modules/async-alpine/dist/async-alpine.script.js`],
// outfile: `./packages/support/dist/async-alpine.js`,
// })
~省略~
クローンしたFilamentのルートディレクトリで、npm install
と npm run dev:js
を実行します。
> npm install
> npm run dev:js
> dev:js
> node bin/build.js --dev
Build started at 14:47:56: ./packages/support/dist/index.js
Build finished at 14:47:56: ./packages/support/dist/index.js
ビルドが終わったらCtrl + Cで止めます。
/packages/support/dist/index.js
をみてみると難読化されていない開発用のsupport.jsが手に入ります
これをFilament内部のsupport.jsと差し替えます。
- Laravelのルートプロジェクトに戻って、元々使われていた
public/js/filament/support/support.js
をpublic/js/filament/support/__support.js
にリネームしてバックアップ - 先ほど生成したindex.jsを
public/js/filament/support/
にコピーしてsupport.jsにリネーム
こうすることで難読化されていないJavaScriptファイルを開発者ツールで確認できるようになります。
早速、php artisan native:serve
でアプリを起動しましょう。
すると、コンソールの内容が変わりmd5.js内部にてエラーが起きていることが分かるようになりました。やったね!
実際にエラーが起きている180行目にブレイクポイントを置いて、Ctrl + Rで画面リロードしてみます。
178行目のBufferがundefinedであることが分かります。
その後、180行目でBufferがundefinedなのに.fromでアクセスしようとしています。
だから
Uncaught TypeError: Cannot read properties of undefined (reading 'from')
が発生していたんですね。ようやくエラーの原因が分かりました!
なるほど、なるほど🧐
・
・
・
・・・ん? じゃあなんでブラウザではエラーが起きないの?
そうです、ブラウザではエラーが起きずNativePHP利用時にエラーが起きるんです!
なんでブラウザではエラーが起きないのか
理解するための鍵がmd5.jsのもう少し上の方にあります。
20行目あたりに下記の記述があり、このスクリプトが動作しているのがNode.jsなのかそうでないかを判定して処理を変えています。
var WINDOW = typeof window === 'object';
var root = WINDOW ? window : {};
if (root.JS_MD5_NO_WINDOW) {
WINDOW = false;
}
var NODE_JS = !root.JS_MD5_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node;
if (NODE_JS) {
root = global;
} else if (WEB_WORKER) {
root = self;
}
このNODE_JS
という変数がその下の158行目あたりの判定に使われています。このケースだとNODE_JSにはNode.jsバージョンの文字列が入っています。
NODE_JSがtrueの場合、メソッドをNode.js用にラップしているようです。
var createMethod = function () {
var method = createOutputMethod('hex');
if (NODE_JS) { // <- ここ!
method = nodeWrap(method);
}
なるほど?🧐
さて、唐突ですがここでFilamentのビルド設定に戻ってみます。
const corePackages = ['support']
corePackages.forEach((packageName) => {
compile({
...defaultOptions,
platform: 'browser', // ・・・ん?
entryPoints: [`./packages/${packageName}/resources/js/index.js`],
outfile: `./packages/${packageName}/dist/index.js`,
})
})
・・・ん? ブラウザ用のビルドだこれ!
そうです、Filamentのリソースはブラウザを対象にビルドされているんです! (そりゃそう)
ΩΩΩ<な、なんだってー!?
何がいけなかったのか
そもそもFilamentはLaravel用のパッケージであり、ブラウザでの利用を想定されて開発されています。
ブラウザ用にコンパイルされたJavaScripファイルをNativePHPが読み込みますが、裏側はElectorn(Node.js)で動作するので相違が発生してエラーが起きていた、というわけですね。これが最初に述べた結論です。
エラーの解決
ブラウザ用のビルドがダメならNode.js用にビルドしてみたらどうだ、と言うことで
const corePackages = ['support']
corePackages.forEach((packageName) => {
compile({
...defaultOptions,
platform: 'node', // Node.js用に変換する
entryPoints: [`./packages/${packageName}/resources/js/index.js`],
outfile: `./packages/${packageName}/dist/index.js`,
})
})
こうして生成したsupport.jsを差し替えて試してみると今回のエラーは発生しなくなりましたが、代わりにRangeError: Maximum call stack size exceeded
が発生するようになり、どこかで循環参照や無限ループが発生するようになってしまいました。これを解決するのには骨が折れそう(というか無理そう)なので、なんとかエラーを解消しつつ使えないか調査します。
エラーを解決しつつ使えるようにするために
仕方がないのでエラー箇所(具体的には106行目あたり)をコメントアウトします。
つまり、Node.jsで動いているけどnodeWrapはさせないようにします。
var createMethod = function() {
var method = createOutputMethod("hex");
// if (NODE_JS) {
// method = nodeWrap(method);
// }
method.create = function() {
return new Md5();
};
こうすることでコンソールでのエラーは発生しなくなり、右上のメニューもちゃんと非表示(クリックで表示)されるようになります。
・・・ん? コメントアウトして大丈夫なの?
コメントアウトして本当に大丈夫なのか
結論、一部の機能を利用しない限り問題にはならないのではないか、という見解です。
理由は下記です。
support.js内で代入されたwindow.jsMd5ですが、現在のversion 3.xではunsaved-data-changes-alert.blade.php
でのみでしか利用されていないようです。
import AlpineFloatingUI from '@awcodes/alpine-floating-ui'
import AlpineLazyLoadAssets from 'alpine-lazy-load-assets'
// import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock'
import { md5 } from 'js-md5'
~省略~
window.jsMd5 = md5
@php
use Filament\Support\Facades\FilamentView;
@endphp
@if ($this->hasUnsavedDataChangesAlert() && (! FilamentView::hasSpaMode()))
@script
<script>
window.addEventListener('beforeunload', (event) => {
if (
window.jsMd5(
JSON.stringify($wire.data).replace(/\\/g, ''),
) === $wire.savedDataHash ||
$wire?.__instance?.effects?.redirect
) {
return
}
event.preventDefault()
event.returnValue = true
})
</script>
@endscript
@endif
Unsaved data change alertとはFilamentが提供しているpanelの追加機能で、panelにunsavedChangesAlerts()を生やすことでフィールドに値が入力されている状態で画面遷移しようとするとアラート(ブラウザのalert)を出してくれる機能です。
この機能を使わない限りはjsMd5が呼ばれることはないので、コメントアウトしても大丈夫なのではないか、という見解に至りました。
さいごに
最後までご覧いただきありがとうございました!
今回はライブラリ内部まで深ぼってエラーの原因まで辿り着いた道のりを記載しました。この記事から何か新しい発見があるようでしたら冥利に尽きます。
今回はライブラリを直接書き換えましたが、もしかしたらjs-md5を別のmd5ライブラリに差し替えることでも対応できるかもしれません。
まだアルファリリースではありますがNativePHPはLaravelに慣れていれば手軽にマルチプラットフォームのデスクトップアプリが開発できるので一度触ってみてはいかがでしょうか。
以上、HappyなLaravelライフを🎉