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

Symfony Advent Calendar 2020 day8の記事です。(2ヶ月も大遅刻!)

Symfony World初日の基調講演とそれに続くセッションで発表されたSymfonyUXについて、使ってみた方は既にいると思いますが、

  • SymfonyUXとはなにか
  • なぜSymfonyUXが作られたのか

については当日の講演を聞いていない方には十分に伝わってないと思われたので、大遅刻ですが「使ってみた」だけでなくなにか・なぜ作られたのかについてもまとめた記事を書こうと思い立ちました。 (SymfonyWorld参加者はビデオを何度でもリプレイすることができるので、特典を利用して10回ぐらい基調講演を聞き返しました)

SymfonyUXとはなにか?

SymfonyのWebアプリケーションのビューにJavaScriptを組み込むことができる仕組みです。 「SymfonyForm, TwigのサーバーサイドレンダリングのDX(デベロッパー体験)を損なわずに、ネイティブアプリのような体験を統合」できるのを目指して作られています。

SymfonyUXで何ができるか?

SymfonyUXを使ってみたプロジェクトをこちらに用意しました。 https://github.com/77web/symfony-ux-example

composer require してSymfonyUXのコンポーネントを取り込むと、Flexがpackage.jsonを書き換え、SymfonyUXのJavaScriptを必要な依存としてフロントエンドに認識させます。 この状態で yarn install すると、EncoreがSymfonyUXのJavaScriptを含めた状態でwebpackでビルドします。 あとは、所定のtwig関数を書き込むだけで、フロントエンドの部品が組み込まれます。

つまり、composer requireしてPHPとtwigのコードを書くだけで、SymfonyUXのJavaScriptを利用したフロントエンド部品が利用できるのです。

SymfonyUXはどういう仕組みで動いているか?

SymfonyUXはStimulusというJavaScriptフレームワークを利用して作られています。 https://stimulusjs.org/
StimulusはもともとRuby On Railsと連携することを念頭に作られたフロントエンドフレームワークで、 Router for nodes DOMノードへのルーティングだけを提供するものということでした。(AngularのようなMVVMではないですね)

SymfonyUXが提供するのは、所定のHTMLを吐き出すTwigの関数
https://github.com/77web/symfony-ux-example/blob/main/templates/chart/index.html.twig#L6
と、それに合わせたJavaScriptファイルです。JavaScriptファイルは、Encoreを通じてWebpackでビルドされたものを読み込むことができます。
https://github.com/77web/symfony-ux-example/blob/main/templates/chart/index.html.twig#L10

※ SymfonyUXはStimulusを通じて各フロントエンドフレームワークとも連携できるそうです。(私のフロントエンド力が低すぎてサンプル作れませんでした…)

なぜSymfonyUXがつくられたのか?

昔、symfony1の頃にはprototype.jsと連携する機能がありましたが、Symfony2〜Symfony4の時代は、asseticやEncoreはあったものの、フロントエンドと連携する機能はコアとしては提供されていませんでした。サードパーティのバンドルという形で連携を試みた開発者もいますし、フロントエンドのコードはSymfonyアプリケーションに含めずにSPAなフロントエンドをまるでモバイルアプリのように独立させたものとAPIだけを通じて連携する道を取った開発者もいました。(カルテット開発部は途中まで前者で、ここ1年半ほどは後者でした) Fabien曰く、Symfony2の時代(約10年前)から今まで、フロントエンドはAngular, React, Vueのような人気のあるフレームワークが複数あって、どれと連携するか決めきれない状態だったから、ということです。

さて、Symfonyは HttpKernelInterface に10年間更新がないのが自慢だそうです(Fabien談)。 なぜ10年間更新無しで良いのかを考えたとき、

  • 標準
  • シンプル

という特徴があるから、ということが導き出されました。
そこから、Symfonyにフロントエンド統合の機能をつけるために必要な要素として下記が出てきます。

  • フロントエンドのコードも同じプロジェクト(レポジトリ)の中で管理したい
  • できるだけ複雑なことをせずに、ユーザー体験を向上させたい
    • しかしコード量は少なくしたい
  • 他の開発者のコードを再利用したい
  • 標準に寄せる、標準(ブラウザのhistory, url, クッキー, セッション)を再発明しない
  • 特定のフロントエンドフレームワークに依存しない
  • 自動テストしやすさを損なわない

これらを実現する方法として、Stimulusを使って、ピュアな(Vanilla)JavaScriptのライブラリと連携する機能を提供することにしたそうです。

感想

ずっと以前、symfony1の頃に、prototype.jsと連携するビューのファンクション( https://symfony.com/legacy/doc/book/1_0/en/11-Ajax-Integration )を使っていたことを懐かしく思い出しました(Symfonyユーザー会の割と古いメンバーでも実際にこれを使ったことがある人は少ない気がします :grin:

SymfonyUXは、フロントエンドとバックエンドを分離して開発するのが当たり前の理想的なプロジェクトにいる方から見れば、古臭く、わざわざフロントエンドとバックエンドを密結合にしようとする時代遅れな試みに見えるかもしれません。 しかし、12月のPHPカンファレンスで私がSPAのAPIについて発表した際に、「最近ではフロントエンドとPHPは別の人・チームで開発するようになっています」と言ったら、「まだフロントエンドもPHPも同じ人が書いてます」というコメントが多数あり、意外と両者を分離しきれずに開発しているプロジェクトは多いようです。幸いにもカルテットにはフロントエンドチームがいて、協力しあってSPAで開発することができます! :v: そのようなプロジェクトで苦労しているPHPのエンジニアにとっては、SymfonyUXは十分に助けになるのではないでしょうか。(日本に限らず、Symfony WorldでSymfonyUXが発表されたその場に参加していた海外のPHP開発者からも歓迎のコメントが多数あったことから、意外とここで苦労しているPHPエンジニアは多いのかもしれません)


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

Symfony Advent Calendar 2020 24日目の記事です!
カルテット開発部では基本的にWebアプリケーションはSymfonyを使って開発していますが、実は「フレームワークはどうでもいい」と考えています。 「え?どういうこと?」という反応が予想されるので、内容と理由について説明します。

「フレームワークはどうでもいい」

私達が作りたいのは我々のモデリングしたドメイン(リスティング広告運用業務のドメイン)を体現したアプリケーションです。フレームワークのイケてる機能を使ったアプリケーションではありません。 モデリングしたドメインを適切に表現してユーザーに提供できるなら、フレームワークもプラットフォームも何でも良いのです。究極的にはエクセルマクロでも良いぐらいです。

現時点では「Webアプリケーションという形式に乗せたい」という要求があるため、WebアプリケーションのプラットフォームであるPHPを利用していて、WebアプリケーションのフレームワークであるSymfonyを選択しています。
Lisketも最初は社内向けのエクセルマクロから始まっていました。Webアプリケーションにすることで、社内の担当者間・チーム間での情報共有や引き継ぎがスムーズになります。SaaSとして社内のみならず社外にも価値を届けることができます。
なお、コロナウィルス感染症対策のためにカルテットの営業・運用部門でもリモートワークが実施された際、必然的に多拠点対応が必要になりましたが、Webアプリケーションになっていたことで担当者のマシンスペックやOSに左右されずに各種効率化ツールを提供し続けることができました。

フレームワーク選定基準

次に、PHPのWebアプリケーションフレームワークが多数ある中で、特にSymfonyを選んだ理由について。 最初に選んだから惰性で使い続けているわけではありません。日本Symfonyユーザー会のメンバーが在籍しているからSymfonyを使っているわけでもありません。
全面刷新するリニューアルのタイミングでもSymfonyを選び続けているのです。

  • 認証やセキュリティ・ルーティング、HTTPのリクエストを受け取ってレスポンスを返すのが面倒じゃない
  • 私達がモデリングしたピュアPHPによるドメインのコードを邪魔しないこと
  • 今のメンバーだけでなく将来に渡って、ピュアPHPによるドメインのコードを邪魔しない思考法を誘導してくれる

の3点を満たすPHPフレームワークがSymfony以外に見当たらないという理由です。

1つ目は、普通のWebアプリケーションフレームワークなら大抵は満たしますね。少なくともこの部分にバグがあったり、頭を使って工夫しないと実現できないようだと採用するのは難しいです。

2つ目は、クラスの命名規則やファイルの配置について、フレームワークからの強制がないかどうか、という点です。Webアプリケーションの構造の中にドメインのコードを埋め込みたいので、フレームワークの規約に合わせるためにドメインのコードの側を変えることを求められるようだとNGです。 Symfonyはautoloadしたものはほぼautowireできるうえに、Resolverに大量の同種interfaceをaddしたいときにもフレームワークのDIコンテナが(邪魔するのではなく)助けてくれます。
※どのように助けてくれるのか?は過去記事をご参照ください。 Symfonyで同種のクラスを大量に使うときDIで楽するテクニック(自作タグのススメ)

3つ目については、先日小さな勉強会でLaravelについて私が発表した こちら がちょうど反面教師となっています。
Laravelの提供する便利なresolve関数があることで、やろうと思えば resolve(Hoge::class) のようなコードをドメインのコード内に書いて、ドメインからフレームワークへの逆方向の依存を発生させることができてしまいます。ベテランばかりの今のメンバー構成だと良いのですが、今後新人さんが入ってきたときには、わざわざ「resolveを使わない」のようなローカルルールを覚えてもらわなければなりません。その点Symfonyでは、DependencyInjectionパターンの利用を原則として強制されるので安心です。
なお、Symfonyでも厳密に言えば ContainerAwareInterface を使ってドメインのコードにコンテナをinjectすることは可能です。が、普通に初心者向けドキュメントを読みながら開発してもたどり着かない方法なので、新人さんをいきなりタスクにアサインしたとしても不意に使われる心配はほぼありません。


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

BEAR.Sunday Advent Calendar 2020 21日目の記事です!(18日目から始まったので正確には4日目?)

Symfonyしか使ってないと思われがちな弊社ですが、主にマイクロサービス系のコマンドラインアプリケーションではDIにRay.Diを活用しています。 Ray.Diのダウンロード数を日々上げ続けているのは弊社かもしれません(主にCIで…汗)。

さて、導入から今まで、普通のInjectorしか使っていなかったのですが、初めて コンパイル 機能を使ってみようと思い立ちました。 以前は、実装を変更したときに本番サーバー上でキャッシュクリアする実装ないしオペレーションをしないといけないのは面倒だと思い、実行効率より開発効率優先で敢えてコンパイル機能を使わないという方針でやっていました。 しかし、インフラのECS化が進んできた中で、毎回新規にコンテナが起動してフレッシュなソースコードが使われるようになり、キャッシュクリアの手間を考えなくて良くなったため、ようやく実行効率を考えることができるようになりました。

早速使ってみることにします。Ray.Diの コンパイル 機能のドキュメントはこちらにあります。 https://github.com/ray-di/Ray.Di#performance-boost

実際のソースコード上の変更

今回の対象はECSのFargate上で動くコマンドラインアプリケーションです。 アプリケーションはキューからコマンドの引数を取ってコマンドを実行し続けるランナースクリプトと、そのランナーから起動される実際のコマンドを実行するスクリプトで構成されています。 ランナーのほうはそのコンテナ内で毎回1回だけ起動し、ランナーが死んだらコンテナも死ぬ仕組みなので、コンパイルしても旨味がないと思われます。そこで、「実際のコマンドを実行するスクリプト」のみを対象にInjector部分を変更してみることにしました。

BEFORE

<?php

// 略

$injector = new Injector(new FooModule());
$command = $injector->getInstance(FooCommand::class);

// run
$app = new Application();
$app->add($command);
$app->setDefaultCommand($command->getName(), true);

$app->run();

AFTER

<?php

// 略

$injector = new ScriptInjector(__DIR__.'/cache/', function(){ return new FooModule(); });
$command = $injector->getInstance(FooCommand::class);

// run
$app = new Application();
$app->add($command);
$app->setDefaultCommand($command->getName(), true);

$app->run();

ドキュメント通りではないのですが、ScriptInjectorのソースコード読んだ限りちゃんと「まだコンパイルされたものがなかったらコンパイル」という挙動があるようだったので、こうしてみました。

動作確認

ScriptInjectorにした状態で1回目の実行をすると、指定したキャッシュフォルダにキャッシュファイル(コンパイルされたDI設定のファイル)ができます。 DI内で $this->bind() したクラスやinterfaceの数だけファイルができていました。
※ 今回の対象が小さなスクリプトなので数は少ないです。

スクリーンショット_2020-12-21_13_14_58

キャッシュファイルの1つの中身を見てみると、こんな感じでした。

<?php

namespace Ray\Di\Compiler;

$instance = new \FooApp\DependencyInjection\LoggerProvider('/Users/h-hishida/projects/foo-app/src/DependencyInjection/../../var/foo.log');
$is_singleton = false;
return $instance->get();

ScriptInjectorに切り替えても、元のコマンドラインアプリケーションのスクリプト自体は正常に実行完了し、生のInjectorを使ったと時と変わらない成果物も生成できることが確認できました。

なお、私は疑い深いので、「こんなんでホントにコンパイルされたものが使われているのかな?」と不安になったので、キャッシュファイルの1つに echo 'Hello ScriptInjector'; と書き込んだ状態で実行してみました。 正常実行のログとともに Hello ScriptInjector が出力されたので、ちゃんと使われているようです :smile:

計測結果

time をつけてコマンドを実行する簡易計測です。

通常のInjector利用

7.46s user 0.08s system 99% cpu 7.559 total

ScriptInjector利用(1回目…コンパイル処理あり)

7.65s user 0.08s system 99% cpu 7.769 total

ScriptInjector利用(2回目…コンパイルされたものを利用できる)

7.33s user 0.06s system 99% cpu 7.403 total

スクリプトが小さすぎるせいか、あまり有意な差が出たようには見えませんね…。 しかし、通常、1つのコンテナが起動してから死ぬまで、数十〜数百回実行されるスクリプトなので、1秒未満の差でもチリツモで効果が出ることが予想されます。 (ECSで実行する前提なので、1個のコンテナの処理可能件数が増えたり、同じ物量のキューを流したとき、より短い時間で処理が終わることを期待しています)

まとめ

次々に新しいアプリケーションを開発する受託開発と異なり、自社サービス開発では1つのアプリケーションを数年に渡って保守し続けることになるため、意外と地味な作業が多くなります。 同じアプリケーションの機能改善やバグ修正を続ける中でも、新しい技術やライブラリの新しい機能を使ってみることで、技術的好奇心も満たしながら開発を進めることができます。

完全に技術的興味だけによる提案はNGなのですが、アプリケーションを正しい方向に改善する視点をもって提案するのがポイントだと思います!