TECH PLAY

株式会社ラクス

株式会社ラクス の技術ブログ

927

こんにちは。 株式会社 ラク スで先行技術検証をしたり、ビジネス部門向けに技術情報を提供する取り組みを行っている「技術推進課」という部署に所属している鈴木( @moomooya )です。 ラク スの開発部ではこれまで社内で利用していなかった技術要素を自社の開発に適合するか検証し、ビジネス要求に対して迅速に応えられるようにそなえる 「技術推進プロジェクト」 というプロジェクトがあります。 このプロジェクトで「WEBアプリケーションのDockerコンテナ移行」にまつわる検証を行なったので、その報告を共有しようかと思います。 今回はコンテナ化そのものの話よりも、コンテナ化する際の環境や、対象のアプリケーション設計についてなど、周辺の話が多いです。 ちなみに中間報告時点で公開した記事はこちらになります。 tech-blog.rakus.co.jp 本検証での構成環境 既存のアプリケーション実行環境 アプリケーション概要 検証した環境 本検証で目指したこと、既存の課題 コンテナ化の際に検討および対応したこと 実行環境構築手順の整備 アプリケーション間の連携 システムコマンドの利用 CI Runnerの不足 ヘッドレスChromeを使ったE2Eテストでは--disable-dev-shm-usage 別リポジトリに格納されたE2Eテストコード アプリケーションサーバーとDBサーバーが分離していない前提のテストコード もしかしたらこんなことも必要かも ACME対応ローカルCA 使用しているgitコマンドパスとgitのオプション設定に注意 コンテナレジストリの用意 Dockerfileの管轄をどうするか まとめ 本検証での構成環境 既存のアプリケーション実行環境 Apache + PHP Postfix PostgreSQL これらは1台のWEBサーバーに相乗りする形でインストール アプリケーション概要 PHP で実装されたWEBアプリケーション メール関連アプリケーション 記録されている最古のコミットが2012年 開発開始自体は2001年ごろ=20年以上の長寿サービス 検証した環境 Apache + PHP + Postfix の相乗りコンテナ PostgreSQL コンテナ 今回はCIでテストさせたかったのでストレージもコンテナ内 Apache + PHP と Postfix をなんで相乗りさせているの? と思われるかもしれませんが理由は後述。 他にも Apache + PHP を php -fpm使って独立させないのか? という話も出ましたが、コンテナ化「+α」の部分であり、コンテナ化自体に必須ではないので今回はスコープ外としています。 まずはコンテナに乗せることが優先です。 本検証で目指したこと、既存の課題 先述の通り、コンテナ上で動作させることを最優先としています。いわゆるコンテナファースト的な設計への作り直しはスコープ外にしています 1 。 また、本番運用を視野に入れると考慮しなければならないことが増えてしまうので、今回はコンテナベースのCI、とりわけE2Eテストを可能にするまでです。「本番までコンテナに統一しないと意味ないのでは?」という声もありそうですが、既存の課題として以下のような物があり、開発環境のコンテナ化だけでも恩恵が充分あると判断しています 2 。 個人ごとに 仮想マシン ( VM )上に開発環境を作っているため環境差異によるトラブルが発生していた 新規参加メンバーの環境構築に手間がかかっていた 過去バージョン環境の再現が手間 チーム外メンバーがサポートする際に、動作環境の調達が面倒だった 最大の理由は1つ目ですね。ベースとなる VM イメージは共通のものを使用していますが、 VM は状態を保持してしまうので不意に差異が発生しがちです。こまめに再作成すればいいのかもしれませんが、面倒 3 なのでなかなかそうもいかず、という感じです。 コンテナ化の際に検討および対応したこと 実行環境構築手順の整備 最初に直面したのはOSインストールから始まる環境構築手順が見つからないことでした。 比較的最近開発され始めたサービスであれば大抵の場合は用意されていると思いますが、今回対象にしたアプリケーションではテンプレートとなる VM をコピーして構築する運用だったため、テンプレート VM を解析して手順作成を行うことになりました 4 。 コンテナ化する際には環境構築手順をDockerfileに書き起こす形で進めていくため、ゼロから構築することができる手順が必要です。 手順はアプリケーションの ソースコード レポジトリと一緒に管理しておく、少なくとも手順への参照情報が記載されているとコンテナ化にあたってスムーズに準備が進められるでしょう。手順はコード化されていると理想的ですが、 GitHub やGitLabなどには Wiki 機能もあるので、そこにまとめておいても良いと思います。 アプリケーション間の連携 1コンテナあたり1つの役割を持たせるのがコンテナとしてはセオリーだと思いますが、現状のアプリケーションは全ての機能が1つになっています。 このアプリケーションの分割を検討するにあたって、 PHP アプリケーションと ミドルウェア アプリケーションとの連携方法がコンテナを分割できるかどうかの分かれ目となりました。 このアプリケーションに含まれる主だった連携内容には以下のようなものがあります。 PHP と Postfix の連携 PHP から Postfix の設定ファイル更新や再読み込み Postfix からメール受信をトリガーにした PHP 実行 PHP と PostgreSQL の連携 PHP から PostgreSQL へのデータ入出力 これらのうち問題となったのは1つ目の PHP と Postfix の連携です。 これらは直接設定ファイルを編集していたり、 systemctl postfix reload といったコマンドで設定の再読み込みを行なっていたり、 php -f hogehoge.php といったコマンドで実行していたりしました。このようにOSを介した連携を行なっている部分は別コンテナにしづらく、HTTPなどの TCP/IP でやりとりするインタフェースを新たに実装する必要があります 5 。 一方で2つ目の PHP と PostgreSQL の連携は TCP/IP に則っているため、問題なく別コンテナにすることができました。コンテナイメージも PostgreSQLのオフィシャルイメージ を使うことができたので構築手順を簡略化できました。 システムコマンドの利用 既存のアプリケーションでは26種類260箇所のシステムコマンド呼び出しがありました。 もし、システムコマンドの呼び出し部分を置き換えるとすると改修に必要な 工数 の概算は以下のようになります。 修正コスト概算 1箇所あたりの改修コスト概算 2時間 影響調査: 1時間 修正:0.5時間 テスト:0.5時間 2時間 x 260箇所 = 520時間 = 3.25人月 互換性や依存関係などにより追加のコストが必要になる可能性あり これに対して、メリットとデメリットは以下のようになります。 得られるメリット 余計なコマンドがインストールされないためよりセキュアになる コンテナイメージが小さくなる 被るデメリット 3.25人月の修正コストがかかる 対応しなかった場合のデメリットは以下のようなものです。 コマンドがインストールされることでセキュリティ的に若干劣る 既存の環境では存在しており、使うコマンドだけインストールするため既存よりはセキュアになる コンテナイメージが大きくなる といっても Linux コマンド26種類の容量なのでそれほど大きな差にはならない 本来であればコンテナ内にインストールされるコマンドは最小限に抑えるべきでしょうが、修正コストと得られるメリット/デメリットを検討した結果、コンテナイメージをビルドする際に、必要なシステムコマンドをインストールする方法を選択しました。 CI Runnerの不足 CIにはGitLab CIを利用しましたが、Shared RunnerがなかったためCI Runnerを用意するところから準備しました。 CI Runner用のマシンリソースを用意できると良かったのですが、今回は検証期間中のみ使用するため各自のPCにCI Runnerのコンテナを起動し、Shared Runner(実際にはGroup Runner)として登録しました。 このあたりは普段のCI環境整備の一環として用意しておくと楽に進めることが出来ると思います。 ヘッドレス Chrome を使ったE2Eテストでは --disable-dev-shm-usage コンテナ環境では /dev/shm の容量が小さいため、デフォルトの状態だとクラッシュするようです。 この事象自体は chromium の Issueにも上がっている問題 で、回避策としては Chrome の起動オプションに --disable-dev-shm-usage を付与して /dev/shm の代わりに /tmp を使わせて回避するのが良いようです。 別 リポジトリ に格納されたE2Eテストコード 今回対象としたアプリケーションではE2Eテスト用のコードがメインのコードベースと別 リポジトリ で管理されていました。 今回、Docker Composeを利用していましたが、アプリケーションのDockerfileとテストコードの位置はそれぞれ 相対パス で指定する必要があります。今回は既存のルールとして特定のパスにそれぞれの リポジトリ を配置するルールだったので、 リポジトリ をまたいで 相対パス での指定が出来ました。 しかし実際には docker submodule などを利用して リポジトリ 内のパスとして参照できるようにするか、そもそも同一 リポジトリ 内で管理するようにしたほうが良いでしょう。 アプリケーションサーバ ーとDBサーバーが分離していない前提のテストコード アプリケーション本体はDBサーバー参照先も 環境変数 などで管理していることが多いと思いますが、テストコードはどちらもローカルホストである前提のコードが書かれていました 6 。 今回はDBサーバー参照先を地道に書き換えて対応しました。 もしかしたらこんなことも必要かも ACME 対応ローカルCA 弊社では各拠点ごとに外部ネットワークに接続する際、クライアント認証を行っています。そのためdnfコマンドやcomposerコマンドでパブリックなパッケージ レジストリ からダウンロードする際にはコンテナイメージ内にもクライアント証明書を持つ必要があります。 対処としてはコンテナイメージをビルドするタイミングでクライアント証明書を含める必要があります。しかも東京、大阪など拠点別に証明書は用意されているため、すべての証明書を含めないと手元で起動する際に通信出来ません。 しかし、クライアント証明書を含んだ状態のコンテナイメージをコンテナ レジストリ に格納するのはセキュリティ上好ましくありません。 今回はコンテナ レジストリ を使わなかった都合上、各検証メンバーの手元でコンテナビルドをしていたため必要にはなりませんでしたが、本格的に運用していくことを考えると、コンテナ起動時に Let's Encrypt のように自動で証明書を取得できるようなローカル 認証局 を用意するのが良さそうに思えます。 今回は検証スコープ外としましたが、 ACME という プロトコル に対応した 認証局 を用意することで実現できそうです。 使用しているgitコマンドパスとgitのオプション設定に注意 コンテナというよりもgitの話なのですが、今回の検証で Windows + WSL2環境でDockerを動かしていた環境がありました。この環境上でcloneしたコードで docker-compose build がエラーになるという問題が発生しました。 結論としてはgitのautocrlf設定が期待どおりに設定されていなくて、docker-compose.ymlの改行コードが変換されてしまっていた、という話だったのですがこれが起きた経緯が以下のようなものでした。 WSL2上のgitはautocrlf=falseとなっており、改行コードが変換されることはないはずだった PhpStormでgit cloneしていたが、PhpStormは Windows 上のgitを参照していた Windows 上のgitはデフォルト設定のautocrlf=trueでインストールされていた わかってしまえば単純なミスだったのですが、私自身は Windows 環境においてはWSL2上のシェルから コマンドライン でgitを使っていて盲点だったので記録しておきます。 コンテナ レジストリ の用意 弊社では一部を除いてコンテナを用いた開発を行われていないので社内から使えるプライベートなコンテナ レジストリ がありませんでした。 今回の検証中ではビルド→E2Eテスト、の間のコンテナイメージの受け渡しをマルチステージビルドで賄ったためなんとか使わずにすみましたが、実際の開発環境で運用する際にはコンテナイメージを共有したいのでコンテナ レジストリ が欲しくなると思います。 Dockerfileの管轄をどうするか アプリケーション開発チームとインフラチームが分かれている場合、Dockerfileやdocker-compose. yaml の更新をどちらのチームがどうやるかという問題が発生します。 ここでいうインフラチームは ミドルウェア 周りを担当しています。 両チームが同じファイルを更新する インフラチームが作成したコンテナイメージをベースイメージとして、アプリケーション開発チームが仕上げる という方法が考えられると思います。今回はインフラチームと一緒の検討を行ったわけではないので推測となりますが、以下理由により同じファイルを扱うのが良いと考えています。 別ファイルにすると取り込み漏れが発生しそう 例えば次期バージョンで必ず入れないといけない ミドルウェア のセキュリティアップデートがリリースされない、など FROM image:latest のように必ず最新版を参照するようにすれば必ず反映されるが…… リリース直前にベースイメージが変わってしまう可能性がある テストがやり直しになる ベースイメージはやはりバージョン指定で使いたい 同一ファイルの場合、更新制御にPull Requestなどの バージョン管理システム の機能が利用できる 別ファイルでも利用はできるが共通管理じゃないとやりにくそう まとめ コンテナはできるなら導入したほうがいい 7 。 ただし巨大プロジェクトを立ち上げて移行する必要はあまりなく、コンテナ化に向けて障害となる課題を一つずつ解消していく形で進めることができそうです。 そしてそれら一つ一つも 開発プロセス や設計の改善となっているため、無駄がないという印象を得られました。 直接的に売上が増えるわけでもないので「コンテナ化」だけでそんな予算取れないです。あと一度の改修は出来るだけ小さくしたいし、コンテナ化してCI回しやすくなれば改修の安全性も上がるので後から追加で直していくのが無難だと思っています。 ↩ もちろん段階的には本番環境でのコンテナ動作も目指したいとは思っていますが。風呂敷を広げすぎると計画が頓挫していつまで経っても改善出来なかったりするので、分割できる問題はできるだけ小さくしたいです。 ↩ 面倒というのはサボるとかそういう話ではなく、手間がかかって非効率という意味合いです。面倒な作業を面倒と認識することは改善する上で重要です。エンジニアの三大美徳である怠惰にも通ずる話ですね。 ↩ 「テンプレートとなる VM を構築する手順がないのはおかしいよね」ということで探していたら、テンプレートの解析が終わった頃に参考資料が見つかった。 ↩ むりやり ssh で実行することも可能だとは思いますが、今後の柔軟性を考えると一般的なRESTなどの TCP/IP 通信で連携できるようにしたほうが良いでしょう。 ↩ テストコードはなるべく静的なコードにすることが多いと思うので、意外とありがちなのではないかと思っています。 ↩ コンテナでの動作を前提としたツールが増えているため、今後の 開発プロセス を考えると必要になると思われる。 ↩
アバター
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ 今回の記事はOPcacheのpreloadが出来るようになろう! という内容です。 尚、OPcacheのpreloadの基本設定とかについては以下の記事を参考にしてください。 tech-blog.rakus.co.jp 今回は上記記事では話していなかったpreloadのよくありそうな失敗話になります。 PHPerKaigi2023では語れなかった部分だったので是非見てもらえればと思います。 そして preloadとrequire_onceとかとの関係性について理解を深め て、更なる改善につなげてもらえればと思います! では早速はじめていきましょう! 失敗談:preloadしても変化がない! 解決策 説明用のファイルはこちら 試してみよう! まとめ 失敗談:preloadしても変化がない! 前回も張った画像ですが、まずこの図ですね。 preloadの説明 この図の通りでいうなら、 「お?preload使えば動的にロードなくなるんだ!」 ってなるのですが、実際どうなるか? 手順は以下です。 preloadの設定解除して count(get_included_files()) を出力 apache を再起動 preloadの設定して count(get_included_files()) を出力 この数の差がpreloadで事前ロードが成功した数になります。 私のとある開発環境では300ファイルのinclude数が150ファイルぐらいまで減らす事ができました。 「そのpreload出来なかった150ファイルは一体なんぞや!」 これを今日は深掘っていきますよ! 解決策 説明用のファイルはこちら 今回のこの問題を説明する用に簡単な PHP ファイルを用意しました。皆さんも手元で試してもらうと良いかな! <?php # index.php require_once ( "autoload.php" ) ; print_r ( get_included_files ()) ; echo "loaded : index. \n " ; $ a = new A () ; $ a -> call () ; print_r ( get_included_files ()) ; <?php # autoload.php spl_autoload_register ( function ( $ className ) { echo "spl_autoload: $ className . \n " ; require_once ( $ className . ".php" ) ; } ) ; <?php # A.php class A extends AP { public static string $ BName = B :: name; public function call () { echo "call : " . __class__ . ", B#name : " . self ::$ BName . " \n " ; } } <?php # AP.php class AP { public function call () { echo "call : " . __class__ . " \n " ; } } <?php # B.php class B { public const name = "B#name" ; } <?php # preload.php $ root_path = dirname ( __FILE__ ) . "/" ; # 以下例えばのpreload 一旦コメントアウトしておきますね。 #opcache_compile_file($root_path."AP.php"); #opcache_compile_file($root_path."A.php"); #opcache_compile_file($root_path."B.php"); #opcache_compile_file($root_path."index.php"); #opcache_compile_file($root_path."autoload.php"); echo ( "Preloading Success \n " ) ; 試してみよう! まずはpreload. php を php .iniに設定しないでやるとどうなるでしょうか? $ php index.php Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. spl_autoload: A. spl_autoload: AP. spl_autoload: B. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php [ 2 ] = > /usr/ local /application/works/A.php [ 3 ] = > /usr/ local /application/works/AP.php [ 4 ] = > /usr/ local /application/works/B.php ) 実行時に index.php がロードされて、require_onceで autoload.php がロードされて、 各クラスが必要になったタイミングでspl_autoloadがされていってるんだな! っていうのがよくわかりますね。 では、 preload.php で A.php をpreloadするとどうなるでしょうか? $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. spl_autoload: A. spl_autoload: AP. spl_autoload: B. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php [ 2 ] = > /usr/ local /application/works/A.php [ 3 ] = > /usr/ local /application/works/AP.php [ 4 ] = > /usr/ local /application/works/B.php ) 変わってないじゃないか!!!!ログをみてみると... $ cat /var/ log /messages | tail -n1 Mar 28 20 : 19 : 18 auto7-dev045 php: PHP Warning: Can 't preload unlinked class A: Unknown parent AP in /usr/local/application/works/A.php on line 3 なるほど、 preloadというのは親クラスとかも一緒にpreloadしてあげないといけないんですね! よし、では AP.php もpreloadして再度トライ! $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. spl_autoload: A. spl_autoload: B. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php [ 2 ] = > /usr/ local /application/works/A.php [ 3 ] = > /usr/ local /application/works/B.php ) おーい!もっかいログを見てみましょう! $ cat /var/ log /messages | tail -n1 Mar 28 20 : 27 : 23 auto7-dev045 php: PHP Warning: Can 't preload class A with unresolved initializer for static property $BName in /usr/local/application/works/A.php on line 3 なになに、static propertyでつかってる$BNAMEがわかりませんと... static propertyとかで使われてる値も一緒にpreloadしないとダメ! って事ですね! よし、じゃ B.php もpreloadしましょう!再度トライだ! $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) 成功ですね!!!あとは autoload.php だけか!よし、これもpreloadしましょう! $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) loaded : index. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php [ 1 ] = > /usr/ local /application/works/ autoload .php ) なぜ!!ログを見てみましょう!ってログも何も出てないよ!なぜか?答えは preloadしていてもrequire_onceとかしちゃうと結局ロードされちゃいます!! ですので、index. php のrequire_onceを削ってみましょう! こんな感じです。 <?php # index.php print_r ( get_included_files ()) ; echo "loaded : index. \n " ; $ a = new A () ; $ a -> call () ; print_r ( get_included_files ()) ; 結果は・・・・? $ php index.php Preloading Success Array ( [ 0 ] = > /usr/ local /application/works/index.php ) loaded : index. call : A, B#name : B#name Array ( [ 0 ] = > /usr/ local /application/works/index.php ) 大成功!ついにincludesファイルが0個になりました!! まとめ はい、まとめです。preloadって事前ロードしてくれる機能ではあるのですが、内部で使われているクラスに漏れがあると、そのクラス自体のpreloadも出来ない! そして、require_onceなどしているものは結局preloadされてしまう!という事なんですね。 皆さんもこれを参考にしながらpreloadの設定頑張ってくださーい!!
アバター
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ なんと嬉しい事に PHPerKaigi 2023 での登壇が決まりました☆ この記事出る頃には登壇終わってるけど!(汗) 題材は「 パフォーマンスを改善せよ!大規模システム改修の仕事の進め方 」 https://fortee.jp/phperkaigi-2023/proposal/4a67cc68-83f0-492d-86ca-54304fc256c8 本セッションではパフォーマンス改善の具体的な手法まで深掘りせずに、広く浅く触れていこうかなと考えていますので、是非マネージャーなんかもご視聴頂ければと思います! という事で今回このブログでは逆に技術に深掘りした内容を話しちゃおうと思います! 今回は PHP のパフォーマンスチューニングの1つにあるOPcacheの中でもpreloadという機能に着目して説明していきたいと思います! 結論からいうと、 OPcacheのpreloadを使う事で php 処理が相当速くなる可能性があります!! 是非、皆さまの作成しているサイトでも採用を検討してくださいませっ! OPcacheとは? preloadとはどんな機能なのか? preloadの設定をやってみよう! preload.phpの中身 どのくらい動的にインクルードされているのか?見てみよう。 preloadの検証結果 さいごに 参考文献 OPcacheとは? 私たちが普段書いている PHP 言語が コンパイル されたコードをOPcode(オペコード)と呼ばれており、それをメ モリー 上に保持(キャッシュ)する事で、 PHP の処理を高速化しよう!という機能。 これまでもそういう取り組みはあったんですが、PHP5.5からはOPCacheが標準機能として採用され今に至っています。 preloadとはどんな機能なのか? preloadはOPcacheの更に一部の機能です。先ほどはOPcodeをメモリ上にキャッシュする所までやりましたが、 「じゃもぉこれ事前にloadしておいたらautoloadとか不要で更に早いんじゃないの?」という機能になります。 事前にload = pre-load うん、そのままですね! 絵で描くとこんな感じですね!(PHPerKaigi2023の登壇資料より抜粋) OPcacheの処理(PHPerKaigi2023の登壇資料から抜粋) preloadの設定をやってみよう! preloadが何か?がわかったので、具体的な設定の例を以下に記載しておきます。 [opcache] zend_extension=/path/to/opcache/opcache.so opcache. enable = 1 opcache.preload=/path/to/preload/preload.php opcache.preload_user=root opcache.jit_buffer_size = 256M opcache.memory_consumption= 256 opcache.interned_strings_buffer= 8 opcache.max_accelerated_files= 100000 opcache.revalidate_freq= 0 opcache.enable_cli= 1 opcache.validate_timestamps= 0 opcacheのオプションの細かい意味は 公式サイト にまとめられていますので、こちらを参考にしてください。 preload. php の中身 今回私の方では以下のようにしています。 <?php function _preload ( $ preload , string $ pattern = "/\.php$/" , array $ ignore = []) { if ( is_array ( $ preload )) { foreach ( $ preload as $ path ) { _preload ( $ path , $ pattern , $ ignore ) ; } } else if ( is_string ( $ preload )) { $ path = $ preload ; if ( ! in_array ( $ path , $ ignore )) { if ( is_dir ( $ path )) { if ( $ dh = opendir ( $ path )) { while (( $ file = readdir ( $ dh )) !== false ) { if ( $ file !== "." && $ file !== ".." ) { _preload ( $ path . "/" . $ file , $ pattern , $ ignore ) ; } } closedir ( $ dh ) ; } } else if ( is_file ( $ path ) && preg_match ( $ pattern , $ path )) { if ( ! opcache_compile_file ( $ path )) { trigger_error ( "Preloading Failed" , E_USER_ERROR ) ; } } } else { echo "IGNORE: $ path \n " ; } } } _preload ([ "/var/www/application" ] , "/\.php$/" , [ "/var/local/application/ignore/hoge.php]); _preloadメソッドは指定フォルダにある、指定拡張子のファイルを 再帰 的にpreload対象にするメソッドとして定義しています。 3つ目の引数でその中で除外したいファイルを指定できるようにしています。 正直ベストアンサーだとは言えませんが、とりあえず効果を見たいのであれば 普段 spl_autoload_register() を使って読み込んでいるファイルを網羅的に指定すればOKです! どのくらい動的にインクルードされているのか?見てみよう。 さて、設定が 終わった人 はきっと不安な事でしょう。 「動いてるけど、効果出てるの...?」 実際、1ページのロード時間でいうと体感できるかは難しいかと思います。 よって設定が有効が働いているかの確認は以下が宜しいかと思います。 preloadの設定解除して count(get_included_files()) を出力 apache を再起動 preloadの設定して count(get_included_files()) を出力 この数の差がpreloadで事前ロードが成功した数になります。 ※上記は動的にロードされたファイル数をカウントしています。なぜプレロード(事前ロード)されなかったのか?については長くなるので次のブログに書きますね! preloadの検証結果 試しに、私の担当する商材に対して50画面x4回=200回のアクセスを計測してみたところ、 約2.8%の改善効果がありました。 この数字はあくまで参考値だと思って下さい。というのは、今回早くなったのはファイルを事前ロードしておく事による効果です。 つまり、事前ロードにかかってる時間が占める割合が元々大きければ大きいほど改善するという事になります。 プロファイル結果(PHPerKaigi2023登壇資料抜粋) 上記は2023年3月24日のPHPerKaigi2023の登壇資料の一部です。私の担当する商材をプロファイリングした結果になるのですが、遅い原因がDBであればそこを改善しない事には改善しません。 ですが、実はこの登壇資料中では語っていませんが、上から2番目にあるSelf:5.50% となっている場所は spl_autoload_register() の処理にかかった時間の割合を指しています。 つまり この場合は最大5.5%の改善が見込める という事ですね! さいごに 皆さまも、是非ご担当されているサービスが遅い原因が何かを計測した後には、色んな改善方法があるんだなぁという一環でお試し頂ければと思います! 参考文献 Symfony DOCS - Performance Amazon Linux 2のPHP 7.4環境に、OPcacheを導入する OPcache のインストール手順 PHP7.4のpreloadいれたらLaravelは早くなるのだろうかと思って検証した @mpywさんの個人的なTweet ソースコードから理解するPreloadとJITの話 PHP: OPcache - Manual
アバター
こんにちは。 技術広報の syoneshin です。 明日、 PHPerKaigi2023 が開催されます。 今回当社はシルバースポンサーとして協賛させていただきました。 PHPerKaigi2023の概要は以下 開催:2023年3月23日(木)〜 3月25日(土) 場所: 練馬区 立区民・産業プラザ Coconeriホール および ニコニコ生放送 対象: PHP エンジニアおよびWeb技術のエンジニア 主催:PHPerKaigi 2023 実行委員会 参加申込: チケット購入サイト 公式サイト: PHPerKaigi2023 当社メンバー登壇情報 「楽楽販売」の開発に関わっているリードエンジニアの 前田 がレギュラー トーク で採択されました。 トーク タイトル「パフォーマンスを改善せよ!大規模システム改修の仕事の進め方」 登壇日時:3/24(金)16:25~ @Track A 詳細は以下 fortee.jp 他にも以下6名が3/24(金)、3/25(土)でLT登壇します。※以下は詳細情報 不幸を呼び寄せる命名の数々 ~君はそもそも何をされてる方なの?~ by 山村 光平 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp stdClassって一体何者なんだ?! by 寺西 帝乃 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp 特徴、魅力を知って、各PHPフレームワークを使いこなそう! by 浅野 仁志 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp 【実録】「PHP_CodeSniffer」で始める快適コードレビューライフ by 森下 繁喜 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp PHPマジックメソッドクイズ! by 加納悠史 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp フレームワークが存在しない時代からのレガシープロダクトを、Laravelに”載せる”実装戦略 by 廣部 知生 | トーク | PHPerKaigi 2023 #phperkaigi - fortee.jp 当日会場にお越しの方は、ぜひお気軽にお声がけいただけると嬉しいです! PHPer トーク ン 当社プロダクトの一部は PHP で開発しており、開発から得られた知見を PHP のコミュニティへ還元することで、技術発展に貢献したいと考えております! 毎月ブログを書いたり、Connpassで「PHPTechCafe」というイベントを主催したりしておりますので、ぜひ定期的にチェックいただけると嬉しいです! PHP 関連ブログ tech-blog.rakus.co.jp Connpassイベント rakus.connpass.com ここまでお読みいただきありがとうございます! PHPerKaigi2023のPHPer トーク ンは以下です #PHPTechCafe 皆さん、当日は盛上っていきましょう!!
アバター
弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2022年8月のイベントでは「 PHP8.2の新機能 」について語り合いました。 弊社のメンバーが事前にまとめてきたPHP8.2の新機能に関する情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 PHP8.2 新機能について Deprecate dynamic properties 「AllowDynamicProperties」アトリビュートによる救済措置 Readonly classes Constants in Traits 従来のトレイトの問題点 Deprecate ${} string interpolation ${}表記が非推奨になる理由 パターン1: 配列 パターン2: オブジェクトのプロパティ パターン3: メソッドコール Deprecate partially supported callable MySQLi Execute Query mysqli_execute_queryとは 使い方 Fetch properties of enums in count expressions Allow null and false as stand-alone types Add true type Random Extension 5.x Disjunctive Normal Form Types その他のRFC Remove support for libmysql from mysqli Make the iterator _*() family accept all iterables Redacting parameters in back traces Deprecate and remove utf8_decode() and utf8_encode() Locale-independent case conversion まとめ PHP8.2 新機能について PHP8.2の新機能は弊社のメンバーが事前にShowNoteにまとめています。 今回のイベントではこのノートに沿って新機能をみていきました。 hackmd.io Deprecate dynamic properties 未定義のプロパティに値を代入した時の動作が変更され、動的プロパティが実質使えなくなります。 PHP8.1まで: プロパティが生成される PHP8.2: Warningが発生するが、プロパティが生成される。 PHP9.0: 例外が発生する。 以下のコードを例では、Userというクラスで未定義のプロパティnane(nameの typo )に値を代入しようとしているため、PHP8.2ではWarningが発生するようになります。 <?php class User { public $ name ; } $ user = new User; // Assigns declared property User::$name. $ user -> name = "foo" ; // Oops, a typo: $ user -> nane = "foo" ; // PHP <= 8.1: Silently creates dynamic $user->nane property. // PHP 8.2: Raises deprecation warning, still creates dynamic property. // PHP 9.0: Throws Error exception. この新機能については「タイポなどによって、意図しないプロパティが生成されることを防げるため、良い変更点」と紹介されましたが、その一方で「影響の大きい変更であるため、バージョンアップの対応が辛そう」というバージョンアップの大変さを嘆く弊社エンジニアの意見もありました。 また、「trait経由だと生やせる」「一回serializeしてunserializeするときに生やせる」など抜け穴が見つかっていることも話題に上がっていました。 「AllowDynamicProperties」 アトリビュート による救済措置 昔のプロジェクトでは、未定義のプロパティを意図的に生やすということを行うこともあったようです。 このようなレガシープロジェクトへの救済措置として #[AllowDynamicProperties] という アトリビュート を付ければとりあえず例外を回避できます。 <?php #[AllowDynamicProperties] class User () {} $ user = new User () ; $ user -> foo = 'bar' ; 参加者からはこの救済措置について、以下の意見があがっていました。 PHP8.0の時は影響の大きい変更がありましたが、そんなに救済措置がなく「用意すべき」という声があがっていたので、こういう救済措置を入れるのは優しいなと思う。 PHP を長くご愛好している人ほど辛い思いをするっていうのは本意ではないと思うので、ちょっとは考えてもらわないと往年の PHP プロジェクトは辛いと思う。 結局これを色んなクラスにつけていく未来しか見えないので、中々の苦行が待っていそう Readonly classes PHP8.1では上書きできないプロパティであるReadonly propertyが追加されましたが、今回のPHP8.2ではReadonly classesが追加されます。 Read Propertyについては過去の PHP TechCafe「PHP8.1をもっと語り合う」でも触れられています。 ※参考 tech-blog.rakus.co.jp Readonly classesではクラスを readonly で宣言することによって、そのクラスのプロパティ全てがreadonly機能を持つことになります。 動的プロパティも当然できないため、先ほど話題に上がっていた #[AllowDynamicProperties] アトリビュート をつけてもエラーになります。 「Deprecate dynamic properties」についてはエンジニアの中で意見が割れていたようですが、この機能については反対意見があまりなかったようで、 イベント参加者からも「不変な値オブジェクトとして利用できるんじゃないかなと期待している。」といった前向きな意見がありました。 Constants in Traits PHP には、コードを再利用するための トレイト という仕組みがあります。 ※参考 PHP: トレイト - Manual PHP8.2ではこのトレイトに、定数を定義できるようになります。 従来のトレイトの問題点 これまではトレイトに定数を直接定義することができず、クラスかインターフェースに定義した定数を利用するしかありませんでした。 しかし、この2つの方法にはそれぞれ難点があります。 クラスに定数を定義する場合 トレイトを利用するための定数定義がトレイト自体によって提供されていない。 インターフェースに定数を定義する場合 インターフェース側で定義していることは実装上自然であるが、トレイトを利用するクラスがインターフェースを実装していないといけない。 以上の問題点もあったため、今回のようにトレイトで定数を定義する事ができるようになることで、モジュールとしてのトレイトの完全性が向上するということで提案されました。 参加者からも「本当に使いやすさだけが向上した素敵なアップデートだと思う」という喜びの声があがっていました。 Deprecate ${} string interpolation 文字列で変数展開する際の ${} 表記の挙動が非推奨になります。 <?php $ foo = "bar" ; $ bar = "foo" ; var_dump ( " $ foo " ) ; // OK var_dump ( " { $ foo } " ) ; // OK var_dump ( " ${ foo } " ) ; // 非推奨: 文字列での ${} の使用は非推奨 var_dump ( " ${$foo} " ) ; // 非推奨: 文字列での ${} (可変変数) の使用は非推奨 PHP のヒアドキュメントなどでも、変数の中身を出力する時に${}が使えましたが、PHP8.2からは非推奨になるようです。 ${} 表記が非推奨になる理由 ${} 表記が非推奨になる理由については、挙動がよく分からないというものがあります。 例えば、挙動が分かりにくくなるパターンとして以下のようなパターンが挙げられます。 パターン1: 配列 <?php $ foo = [ 'bar' => 'bar' ] ; var_dump ( " $ foo [ bar ] " ) ; var_dump ( " { $ foo [ 'bar' ]} " ) ; var_dump ( " ${ foo [ 'bar' ]} " ) ; // すべて "bar" が出力されるが最後の挙動も "bar" になることが理解しづらい // しかも、可変変数は利用できない こちらの例では、$fooという変数の中にbarという 連想配列 がある状態です。 var_dump("$foo[bar]"); や var_dump("{$foo[‘bar’]}"); というのは問題ないのですが、 var_dump("${foo[‘bar’]}"); とするとどういう挙動になるのか想像しにくくなります。 さらにこのパターンの場合、可変変数が利用できないという点でも問題になります。 パターン2: オブジェクトのプロパティ <?php $ foo = ( object )[ 'bar' => 'bar' ] ; var_dump ( " $ foo -> bar " ) ; var_dump ( " { $ foo -> bar } " ) ; // このパターンでは ${} パターンが利用できない オブジェクトのプロパティでは、そもそも${}の表記が使えないです。 こちらも${}の挙動の分かりにくさの一因になっています。 パターン3: メソッドコール <?php class Foo { public function bar () { return 'bar' ; } } $ foo = new Foo () ; var_dump ( " { $ foo -> bar() } " ) ; // メソッドコールで許容されているのは上記のみ メソッドコールでも {$foo} 表記しか使えません。 以上のように一番複雑なのは${}というパターンであり、これがあると分かりにくいということからPHP8.2からは非推奨、PHP9.0からはエラーになるようです。 こちらも影響の大きい仕様変更に思えますが、「検出が簡単」「それぞれのパターンの直し方が RFC で解説されているので、それを参考に書き直したら問題ない」という観点から「バージョンアップ作業はそこまで大変ではないのでは?」という結論に落ち着いていました。 ※参考: RFC Deprecate ${} string interpolation wiki.php.net Deprecate partially supported callable call_user_func($callable_func)では実行できるが、$callable_func()では実行できない以下のような構文が将来的に廃止されます。 <?php "self::method" "parent::method" "static::method" [ "self" , "method" ] [ "parent" , "method" ] [ "static" , "method" ] [ "Foo" , "Bar::method" ] [ new Foo, "Bar::method" ] この機能についてもPHP8.2で非推奨になり、PHP9.0で廃止されます。 PHP8.1でFirst-class callable syntaxという機能が追加され、今回の一部のcallableの廃止はそれに付随しているようです。 ※参考: RFC First-class callable syntax wiki.php.net こちらの機能の廃止については 「変な呼ばれ方や変な構文をなくしていきたいって狙いですね。」「さっきの ${} もそうですけど、統一されていないようなものはどんどん消していきましょうって事ですね。」と参加者の間で考察されていました。 MySQLi Execute Query MySQLiに新しい関数mysqli_execute_queryが追加されます。 mysqli_execute_queryとは 下記3つの関数をまとめた関数です。 mysqli_prepare() mysqli_execute() mysqli_stmt_get_result() 使い方 PHP8.1以前 <?php $ statement = $ db -> prepare ( 'SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)' ) ; $ statement -> execute ([ $ name , $ type1 , $ type2 ]) ; foreach ( $ statement -> get_result () as $ row ) { print_r ( $ row ) ; } PHP8.2以降 <?php foreach ( $ db -> execute_query ( 'SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)' , [ $ name , $ type1 , $ type2 ]) as $ row ) { print_r ( $ row ) ; } こちらの関数追加については投票結果が全会一致だったようです。 参加者からも以下のような賛成の声があがっていました。 確かにめんどくさいなと思っていたので、「うん、確かに」としか言いようがなかった。 Prepairとか使ってきれいに書かないと安全性が担保できないくらいなら使いやすくしようと、実に正しい解決法ですね。 Fetch properties of enums in count expressions PHP8.1で複数の定数をまとめる Enums型 が追加されました。 ※参考 PHP: 列挙型 / Enum - Manual しかし、PHP8.1の enum のcaseはオブジェクトである為、 連想配列 のキーに使うことができませんでした。 そこでPHP8.2では、 -> を使って定数式の中で enum のプロパティを取得できるようになりました。 <?php enum A : string { case B = 'B' ; // 現在コレはダメ const C = [ self :: B -> value => self :: B ] ; } enum E : string { case Foo = 'foo' ; } // 'Foo'が取れる const C = E :: Foo -> name ; ライブラリの作者さんが安全なコードを書くのにほしいという需要があったようですが、「いまいち使いどころが分からない」という意見もありました。 Allow null and false as stand-alone types 元々UNION型でのみ、利用可能だった False型 と Null型 が型宣言が許される位置全てにおいて利用可能になります。 関数がエラーであることを示すために、返り値としてfalseを返す場合があります。 これを表すためにFalse型というものが作られましたが、この型がどこでも使えるようになります。 この変更により「静的解析で便利になるのでは?」という期待の声があがっているようです。 Add true type 先ほど紹介したNull型とFalse型が入った後、じゃあ True型 もあっていいのでは? という流れからTrue型が追加されることになりました。 参加者からは「True型ってそんなに使うことあるんですか?」という声もあがりましたが、別の参加者から RFC に紹介されているサンプルコードをもとに ユースケース が説明されました。 True型のサンプルコード( PHP: rfc:true-type より抜粋) <?php class User { function isAdmin () : bool } class Admin extends User { function isAdmin () : true { return true ; } } UserクラスのisAdmin()の返り値はboolで型指定されていますが、Userクラスを継承したAdminクラスは必ずtrueを返す仕様になっています。 このような場合に返り値にTrue型を指定することで、静的解析でより有用なチェックができるようになるというケースがあるようです。 この説明を受けるとTrue型の有用性について疑問を抱いていた参加者も納得している様子でした。 Random Extension 5.x PHP で乱数を使用する関数の実装に問題があることが以前から指摘されていました。 具体的には メルセンヌ・ツイスター (疑似乱数ジェネレーター)の実装が壊れている メルセンヌ・ツイスター の状態は PHP のグローバル領域に暗黙的に格納される。そのため外部ライブラリで乱数が乱れたりといった問題が発生する。 shuffle(), str_shuffle(), array_rand(),random_int()といった組み込み関数では メルセンヌ・ツイスター が乱数ソースとして使用されるため、暗号的に安全な乱数が必要な場合は危険。 PHP での乱数の実装は、歴史的な理由から標準モジュール内に散らばっている。 などが挙げられます。 そのため、まざまなランダム化メソッドを提供する単一のクラス、 Random\Randomizer クラスが追加されます。 この RFC が出された理由は、 RFC を出された方のスライドを見ると全て分かると以下のスライドが紹介されていました。 speakerdeck.com こちらのスライドに目を通した参加者からは以下のような意見があがっていました。 外部の影響を受けて乱数の値が変わるのは、確かに乱数とは言えない。 そもそも乱数生成のPOSTが高かったみたいですね。なのできちんとライブラリを使って速度も早く安定したRandomを作りたいと。素晴らしい話だと思います。 Disjunctive Normal Form Types プロパティやパラメータに対して、論理式の形式で複雑な型指定をすることができるようになります。 RFC では A | (B&D) | (B&W) | null といった細かい例もあり、かなり細かい型指定ができるようになるようです。 ゲームのパラメータなどで、クラスに対して型を定義し、これとこれは受け取るけどこれは受け取らない、というように細かい制御を行うことがあるという事例が紹介されていましたが、 参加者からは「 PHP でそこまで型を使いこなすようなことをやるのか?」「何かに使えそうな気はするけどパッと例は出てこない」など疑問の声もあがっていました。 その他の RFC 上記で紹介した RFC 以外にも、「 PHP TechCafe」では以下の新機能や変更点について触れられていました。 Remove support for libmysql from mysqli MySQL のモジュールはmysqlndとlibmysqlの2種類が存在するため、このうちlibmysqlを削除するという内容です。 PHP5.4以降はmysqlndがデフォルトとなっており、libmysqlを選ぶ利点がほぼないことから全会一致でlibmysqlの削除が決まったようです。 Make the iterator _*() family accept all iterables foreachに渡せる反復可能なオブジェクトである iterator ですが、PHP8.2では iterator _to_array()と iterator _count()にiterable型の変数を渡せるようなります。 Redacting parameters in back traces スタックトレース に引数を出力しないようにする アトリビュート #[\SensitiveParameter] が追加されます。 参加者からは「DBの接続情報などが画面に表示されることによる情報流出を防ぐことができるのでは?」という期待の声があがっていました。 Deprecate and remove utf8_decode() and utf8_encode() utf8_decode()とutf8_encode()が削除されます。 この関数は、任意の文字列を UTF-8 に エンコード デコードできるかと思いきや、変換相手はLatin 1固定であり、誤解を招く関数名であることから削除されたようです。 Locale-independent case conversion ロケール の影響を受けていたstrtolowerなどの関数が、PHP8.2から ロケール の影響を受けないようになります。 そもそも従来のstrtolowerなどの関数が ロケール の影響を受けることに対して驚きの声があがっていました。 まとめ 今回はPHP8.2の新機能について、イベント参加者の生の声を交えてまとめてみましたがいかがでしたでしょうか? イベントでは追加される新機能の内容だけでなく、採用が決まるまでの裏話なども語られており、有意義なTech Cafeであったと思います。 PHP8.3についても導入される機能が続々と決まってきているので今後もどのような機能が PHP に追加されるのか注目していきたいです! 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
技術広報の syoneshin です。 いつも ラク スエンジニアブログをお読みいただき、ありがとうございます! 先日(2022/12/7)開催の ラク スMeetup。 今回は、 インフラ・CI/CD・自動化 をテーマに開催! 業務課題の カイゼン /効率化を目指すインフラエンジニアが登壇し取り組みをご紹介しました。 なお、本イベントは以下のような方にオススメとなっております。 ・ ラク スのインフラに関する取り組みについて知りたい方 ・ SaaS 企業のCI/CD, GitOps等の最新事例が気になる方 ・ レガシーシステム の改善事例を聞きたい方 ・自動化システムの継続運用にお悩みの方 ・システムを安全に運用するための施策を知りたい方 ・ ラク スのプロダクト、組織に興味がある方 ・ SaaS 開発に携わるエンジニアの話が聞いてみたい方 イベント内容の詳細は以下をご確認ください。 rakus.connpass.com 発表内容のご紹介 SRE課が開発中システムのCI/CDで取り組んでいるGitOpsの話 ラクスサービスを支えるAnsible活用のこれまでとこれから メール配信サービス「blastmail」のM&A後の軌跡 ~初めてのシステムに向き合う~ 終わりに 発表内容のご紹介 SRE課が開発中システムのCI/CDで取り組んでいるGitOpsの話 登壇:今本 光 [所属:SRE課] speakerdeck.com 発表内容 アプリ開発 において、面倒な作業はなるべく自動化して作業を楽にしたいと考える方も多いと思います。 今回は、新規システム構築というチャレンジしやすい環境の中で導入したGitOpsというCI/CDの手法についてご紹介。 GitOpsを導入すると、Git操作だけで自動デプロイが行われるので、デプロイの 工数 削減や安全性向上に繋がります。 主なテーマは以下になります。 CDツールArgoCDを使ってGitOpsに取り組んでいる話 GitOpsの実現方法 GitOpsによって得られるメリット ラク スサービスを支えるAnsible活用のこれまでとこれから 登壇:上畑 圭史 [所属:大阪インフラ開発課] speakerdeck.com 発表内容 2018年から開始したAnsibleによるサーバ構成管理。 導入から4年が経ち、それまでに整備したAnsibleを支える周辺環境の構成や取り組み内容をご紹介。 Ansible導入期 目的と背景 Ansible普及期の取り組み内容 今後の課題 まとめ メール配信サービス「blastmail」の M&A 後の軌跡 ~初めてのシステムに向き合う~ 登壇:柏木 達仁 [所属:東京インフラ開発2課/課長] speakerdeck.com 発表内容 M&A が流行っている昨今、 ラク スが M&A で取得したメール配信サービス「blastmail」について、 M&A 後にシステム側が何をしていったのかをトピックスでご紹介。 M&A に関わらず、「初めてのシステム」を担当する際の参考になればと思います。 M&A 直後 -システム移管~自社運用化- 改善その1 -監視の改善- 改善その2 -運用の自動化- 改善その3 -コスト削減- まとめ イベント当日はたくさんの方にご視聴、そしてコメントやご質問をいただきました。 お申し込み、ご参加いただいた皆さま本当にありがとうございました! 終わりに ラク スMeetupでは現場最前線のエンジニア/デザイナーから ラク スの SaaS 開発ならではの技術・運用ノウハウや、 新しい取り組みの成果や失敗談、プロダクト開発/運用で得た知見等の技術情報をお届けしております。 今後も定期的なイベントを計画しております、ぜひご参加ください。 最後までお読みいただきありがとうございました! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申し込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申し込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、tatsumiです。 今回は、前回の記事( シングルサインオン (SSO)の仕組みと認証方式)の最後にも書いた通り、 ラク スの各サービスでも使われている SAML 認証について解説したいと思います。 前回の記事をまだ見ていない方は、以下からご覧ください。 tech-blog.rakus.co.jp SAML認証とは? SAML認証における登場人物 ユーザー IdP(Identity Provider) SP(Service Provider) SAML認証のフロー SP起点(SP Initiated) IdP起点(IdP Initiated) 開発時の注意点 SAML認証の設定 SP側の設定 IdP側の設定 IdPユーザーとSPユーザーの紐づけ NameIDについて IdPユーザーとSPユーザーの紐づけ方法 まとめ SAML 認証とは? SAML (サムル)とは「Security Assertion Markup Language」の略称、インターネット ドメイン 間でユーザー認証を行うための XML をベースにした標準規格です。 異なる ドメイン 間で認証を行うことができるため、 クラウド サービスの シングルサインオン として利用されることが多いです。 実際に、 Google Workspaceや Microsoft 365、 Salesforce などの多くの クラウド サービスが SAML に対応しており、 ラク スのサービスでも SAML に対応しているサービスはいくつか存在します。 SAML 認証における登場人物 SAML 認証の解説に入る前に、 SAML 認証を構成する要素(登場人物)を紹介します。 SAML 認証では、以下の 三者 間で認証が行われます。 ユーザー 利用者を指します。 IdP(Identity Provider) ユーザーの認証情報を保存・管理するサービスを指します。 シングルサインオン を利用する場合、ユーザーはIdPへログイン認証を行う必要があります。 SP(Service Provider) ログイン先のサービスを指します。 具体的には、先ほど例に挙げた Google Workspace、 Microsoft 365、 Salesforce などの クラウド サービスや ラク スの各サービスがこれにあたります。 SPはIdPとユーザーの認証情報をやり取りし、認証処理を行います。 SAML 認証のフロー それでは次に、先ほど紹介した 三者 間でどのような流れで認証が行われるかを解説します。 SAML 認証ではSPを起点とする場合とIdPを起点とする場合で認証フローが異なるため、それぞれ解説します。 SP起点(SP Initiated) SP起点とは、最初にユーザーがSPにアクセスを行うパターンになります。 ユーザーがSPにアクセスすると、SPからIdPへリダイレクトが行われてIdP側で認証が行われます。 IdP側で未ログイン状態であった場合は、IdP側のログイン画面が表示されIdPへのログインを行います。 ログインが成功すると、今度はIdPからSPへリダイレクトが行われて、IdPから受け取った認証情報を元にSP側で認証を行い、認証に成功するとユーザーはSPへログイン成功となります。 上記内容を簡単に図にまとめてみると、こんな感じになります。 ユーザー目線では、 ①SPへアクセス ②IdPのログイン画面が表示される(IdP側で未ログイン状態であった場合のみ) ③IdPへログインすると、SPの画面が表示されてサービスを利用できる 上記のように、SPへのログイン画面は経由せずにサービスを利用することができます。 また、IdPへ既にログインした状態であった場合は、SPへアクセスするとIdPのログイン画面も経由せずにサービスを利用することが可能です。 IdP起点(IdP Initiated) IdP起点とは、最初にユーザーがIdPの画面から利用したいSPを選択して利用するパターンになります。 ユーザーがIdPへログインを行い、IdPのSP一覧画面から利用したいSPを選択すると、対象のSPに認証情報が送信されてログイン認証が行われます。 上記内容を簡単に図にまとめてみると、こんな感じになります。 開発時の注意点 SP起点とIdP起点では認証フローが少し異なるため、それぞれで開発が必要となります。 SAML 認証の設定 SAML 認証を利用する際は、SP側とIdP側のそれぞれでお互いの情報を登録しておく必要があるのですが、どのような情報を登録しておく必要があるかを紹介します。 SP側の設定 SPにIdPの情報を登録する際に必要な情報は以下になります。 ログインURL → SPからIdPへリダイレクトを行う際のリダイレクト先URL。 ログアウトURL → SPからログアウトした後のリダイレクト先URL。 主に、シングルログアウト時に利用されるURLで、シングルログアウトを提供していない場合は不要な項目になります。 エンティティID → IdPをグローバルに一意に認識するためのID。(IdPから取得) IdPの証明書 → IdPが認証応答の署名に用いる 秘密鍵 に対応する公開鍵。(IdPから取得) IdP側の設定 IdPにSPの情報を登録する際に必要な情報は以下になります。 Assertion Consumer Service URL → IdPからSPへリダイレクトを行う際のリダイレクト先URL。 エンティティID → SPをグローバルに一意に認識するためのID。(IdPから取得) SAML 認証に対応しているSPでは、上記情報を含んだ メタデータ をダウンロードできることが多いです。 ダウンロードした メタデータ をIdPへアップロードすることで、IdP側の設定が簡略化できます。 IdPユーザーとSPユーザーの紐づけ IdPとSPは別サービスで、それぞれでユーザー管理をしています。 IdPから送られてくる認証情報には、IdPで管理しているユーザーの情報が含まれているため、SP側ではそのユーザー情報がSPのどのユーザーを指すか、紐づけを行う必要が出てきます。 そこで登場するのが「NameID」です。 NameIDについて NameIDとは、IdPとSPで共有するユーザー識別子のことです。 基本的にはIdP側で設定されたNameIDを使って、SP側でSPユーザーとの紐づけキーとして利用します。 NameIDに何が設定されるかはIdPのサービス毎に異なり、IdPによってはNameIDに何を設定するかを変更することも可能です。 IdPユーザーとSPユーザーの紐づけ方法 紐づけ方法としては、 SAML 認証での初回ログイン時にIdPとSPの両方でログイン認証をさせる方法があります。(2回目以降はIdP側のログインのみで認証可能) 認証フローとしては、以下のようなイメージになります。 ただし、上記方法では初回ログイン時のみではありますが、ユーザーは2回ログインする必要があります。 NameIDはIdP側で設定されるものになり、NameIDに何が設定されるかをIdP側で確認することが可能です。 確認したIdPをSP側に設定することで、予めIdPユーザーとSPユーザーを紐づけることができます。 上記を行えば、初回ログインからIdPへのログインのみでSPへログインすることが可能になります。 まとめ 今回は、SSOの認証方式の一つである SAML 認証について解説しました。 SAML 認証は、今では ラク スのサービスをはじめ、様々な SaaS のサービスで実装されている認証方式になるので、仕組みについて理解しておいて損はないと思います!
アバター
特集:「Composer」を語り合う 弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2022年6月のイベントでは「 Composer 」について語り合いました。 弊社のメンバーが事前にまとめてきたComposerの基礎知識や使い方の情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 rakus.connpass.com 特集:「Composer」を語り合う Composer とは 概要 プロジェクトの依存関係管理ツール オートローディング 公式ページ 使い方 1. composer.json を作成 2. 依存関係のインストール composer.lock の Git 管理について Git 管理するとよいケース Git 管理しないほうがよいケース composer.lock のコンフリクト対策 Packegest 自作パッケージの登録方法 1. composer.json 作成 2. コマンド実行 3. GitHub 等に push 4. Packagist に登録 登録の手軽さと怖さ private 版 オートローディング オートローディングとは Composer を使ったオートローディング 使い方 クラス名とファイル名が必ずしも一致してない場合 関数をオートローディングしたい場合 Composer オートローディングのパフォーマンス Composer2.0.0 概要 新機能や変更点 Performance improvements Architectural changes and determinism Runtime features Error reporting improvements Partial updates with temporary constraints 編集後記 Composer とは 概要 プロジェクトの依存関係管理ツール Composer の主な機能は プロジェクトの依存関係管理機能 です。 プロジェクトで使用するパッケージの依存関係をまとめて管理することができます。 専用の設定ファイル ( composer.json ) に必要なパッケージを定義してコマンドを実行するだけで、 プロジェクト内の vendor ディレクト リにパッケージがインストールされます。 つまり設定ファイルさえあれば、プロジェクト新規参入者でもコマンド1つで簡単に環境構築を行うことが可能です。 オートローディング オートローディングとは読み込みたいファイルを include / requrire することなく、 自動でファイルを読み込んでくれる機能 です。 Composer では PSR-4 に基づいた 名前空間 を宣言します。 プロジェクト内の指定した ディレクト リ配下を 名前空間 として使用可能にします。 公式ページ Composer の公式ドキュメントやダウンロードは↓コチラです。 getcomposer.org 公式ページにアクセスすると指揮者のようなアイコンが表示されます。 Composer のアイコンはリロードするたびに色が変わります。 是非公式サイトにアクセスして試してみてください。 ※※※ 稀にあまり目に優しくないチェック模様になります ※※※ アクセスするたびに色が変わることに気づいていたが、 バージョンによるものだと思っている方もいらっしゃいました。 使い方 基本的な使い方は 公式ドキュメント に沿って、 monolog を例に説明されました。 1. composer. json を作成 Composer を導入したいプロジェクトに composer.json という名前のファイルを作成します。 require をキーとし、使用したいパッケージを vendor名/プロジェクト: バージョン の形式で指定します。 使用したいパッケージの vendor 名やプロジェクト、バージョンについては Packegest というサイトで確認することができます。 monolog を指定する場合 { " require ": { " monolog/monolog ": " 3.1.* " } } バージョンの指定には下記ルールが適用されます。 vX.Y.Z または X.Y.Z で指定する ワイルドカード が指定されている箇所は最新を取得するようにする 上記例では 3.1 系の最新が取得される 3.1.* は >=3.1, < 3.2 と同じ意味 詳細は コチラ 2. 依存関係のインストール 下記コマンドを実行することで composer.json に定義されたパッケージがインストールされます。 php composer.phar update このコマンドは大きく分けて2つの処理を行っています。 composer.lock の作成 / 更新 composer.json を基にインストールするパッケージのバージョンを確定させる 暗黙的に composer.phar install を実行 vendor ディレクト リを作成して必要なパッケージを取得 composer.lock の Git 管理について composer.lock は composer.phar update コマンドで自動生成されるファイルです。 自動生成されるファイルを Git 管理するか否かはよく議論になるとおもいます。 composer.lock を Git 管理するとよいケース/管理しないほうがよいケースについて、 それぞれ紹介されていました。 Git 管理するとよいケース composer.lock を Git 管理するとよいケースとして、 パッケージのバージョンを固定したい場合 が挙げられていました。 アプリケーション開発などでパッケージのバージョンを固定したい場面がよくあると思います。 composer.lock はバージョンが明記されており、どの環境にも同一バージョンのパッケージをインストールすることが可能なため、 composer.lock を Git 管理し、チーム内で共有することがおすすめされていました。 Git 管理しないほうがよいケース composer.lock の Git 管理をしないほうがよいケースとして、 composer.lock の内容が都度変更される場合 が挙げられていました。 PHP の複数バージョン (5.4~8等) に対応したパッケージの開発ではバージョンごとで composer.lock の内容が変わってしまいます。 そのため composer.lock はあえて .gitignore に入れ、都度 composer.phar update することがおすすめされていました。 composer.lock のコンフリクト対策 composer.lock を Git 管理するという話から、 複数の開発する場合、 composer.lock でコンフリクトが起きるケースがよくあります。 対応策などあったりしますか? という質問がありました。 この質問に対して参加者から様々な解決方法が寄せられました。 composer.json / composer.lock を弄るときはさっさとメインブランチに入れる、が解だと思います コンフリクトした人が master (main) から composer require / composer update しなおす ブランチごとに無関係のパッケージだったら composer.lock のコンフリクトだけなので気合で解消できる git rebase -i が可能なら、 git checkout HEAD composer.lock で戻してから composer require しなおします また、「コンフリクトに対しては密なコミュニケーションが重要」や「神速でマージすれば問題ないのでは」といった意見も挙がりました。 Packegest 前述の通り、 Packegest は Composer で使用したいパッケージの情報を確認するサイトです。 Packegest では登録されているパッケージを利用するだけでなく、 自身で作成したパッケージを Packagest に登録することも可能 です。 自作パッケージの登録方法 下記の4ステップで Packagest にパッケージを登録することができ、 非常に手軽だと参加者から驚きの声があがっていました。 1. composer. json 作成 Copmoser 利用時と同様に、まず composer.json を作成します。 作成後、下記例のようにパッケージの詳細を記載します。 monolog の composer. json { " name ": " monolog/monolog ", " type ": " library ", " description ": " Logging for PHP 8.0 ", " keywords ": [ " log "," logging " ] , " homepage ": " https://github.com/Seldaek/monolog ", " license ": " MIT ", " authors ": [ { " name ": " Jordi Boggiano ", " email ": " j.boggiano@seld.be ", " homepage ": " http://seld.be ", " role ": " Developer " } ] , " require ": { " php ": " >=8.0.0 " } , " autoload ": { " psr-0 ": { " Monolog ": " src " } } } Packagest の monolog ページと見比べて、どの設定がどの項目に反映されるかを確認してください。 2. コマンド実行 2つのコマンドを実行します。 1. composer.lock 更新 php composer.phar update 2. composer.json の書式チェック php composer.phar validate 3. GitHub 等に push いつも通り、 git push してください。 4. Packagist に登録 最後に Packagest にログイン後、 GitHub と紐づけます 登録の手軽さと怖さ GitHub に push して紐づけてしまえばすぐに登録されるという手軽さから、 誰の査読も受けていないパッケージを登録できてしまうのが怖いという意見がありました。 これをきっかけに 「Packagest はパッケージ名にユーザ名や組織名が付くため、 "だれが作ったかわからない" というリスクは少ない ですね」 「npm などでは短い名前の デファクト っぽいパッケージ、例えば 〇〇 parser を適当に選んで使いがちですもんね」 「確かに 〇〇 parser って簡潔な名前だとそれが王道なのかなって思っちゃいますもんね」 「 OSS 全体で言えることですが、より信頼できるパッケージを選びたいですね」 という話題で盛り上がりました。 private 版 Packagest の private 版 も紹介されました。 有料にはなりますが、 クラウド /自前のサーバどちらにもインストール可能です。 社内でオリジナルのパッケージを配布したいときに有効であるという意見がありました。 オートローディング 続いて Composer の主機能の1つであるオートローディングについて紹介されました。 オートローディングとは まずはオートローディングとは?という基本的な説明が行われました。 オートローディングとは何度も include / requrire しなくても自動的にファイルを読み込んでくれる機能です。 昔ながらの PHP プロジェクトでは include や requrire が ズラーッ と並ぶことが多いと思います。 シンプルなら問題ないですが、大半の場合はクラスごとに依存関係があるため読み込み順など管理が困難です。 <?php /** * 各ファイルから共通で読み込まれる */ namespace TechCafe; include_once __DIR__ . '/Hoge.php' ; include_once __DIR__ . '/Fuga.php' ; include_once __DIR__ . '/Piyo.php' ; include_once __DIR__ . '/Hogera.php' ; include_once __DIR__ . '/HogeHoge.php' ; include_once __DIR__ . '/Foo.php' ; include_once __DIR__ . '/Bar.php' ; include_once __DIR__ . '/Baz.php' ; include_once __DIR__ . '/Foobar.php' ; // etc...etc... そこで PHP には spl_autoload_register() というクラスローダを登録するメソッドが用意されています。 クラスローダとは引数に指定されたクラス名を読み込む機能を持った関数です。 spl_autoload_register() には独自実装したクラスローダの関数名または無名関数をセットできます。 <?php /** * 各ファイルから共通で読み込まれる */ namespace TechCafe; // 無名関数でクラスローダをセット spl_autoload_register ( function ( $ class ) { include 'classes/' . $ class . '.class.php' ; }) ; 難しく感じるかもしれませんが、 include / requrire の定義する代わりにを自動で読み込んでいるだけです。 Composer を使ったオートローディング Composer を使えば、自前でクラスローダを実装することなくオートローディングを利用できます。 使い方 composer.json に設定を追加し、コマンドを一度だけ実行します。 コマンド実行後は vendor/autoload.php を読み込むだけでオートローディングを利用できます。 composer. json { " autoload ": { " psr-4 ": { " TechCafe\\ ": " src/ " } } } コマンド実行 php ./composer.phar dump-autoload autoload. php 読み込み <?php /** * 各ファイルから共通で読み込まれる */ namespace TechCafe; require_once __DIR__ . '/../vendor/autoload.php' ; spl_autoload_register() の時のような独自実装もなく、 たったこれだけで include / requrire 地獄から解放されます。 クラス名とファイル名が必ずしも一致してない場合 上記設定でオートローディングを利用できますが、正常に読み込みが行われるのはクラス名とファイル名が一致している場合のみです。 既存プロジェクトでクラス名とファイル名が一致していない場合や、クラス名はキャメルケースだけどファイル名はスネークケースの場合等あると思います。 そんな場合は configmap を定義することで対応可能です。 例)src/tools/test_util. php を読み込みたい場合 <?php namespace TechCafe class TestUtil { public static function getTest () { return "てすとだよ" ; } } composer.json に classmap を設定し、コマンドを実行します。 composer. json { " autoload ": { " classmap ": [ " src/tools " ] , " psr-4 ": { " TechCafe\\ ": " src/ " } } } コマンド実行 php ./composer.phar dump-autoload classmap は有用ですが、デメリットもあります。 新しくクラスを追加した場合、psr4 で指定した場合はコマンドの再実行は不要ですが、 classmap で指定した場合は再度 dump-autoload を実行しなければクラスを読み込めません。 レガシープロジェクトなどでは難しいとは思いますが、 可能な限り、再実行の手間なく読み込むことができる psr-4 での指定が推奨されていました。 関数をオートローディングしたい場合 オートローディングに対応しているのは class や trait、interface のみです。 そのため global に定義された関数をオートローディングしたい場合は別途読み込む必要があります。 例) src/functions.php に定義された getHoge() を読み込みたい場合 <?php namespace TechCafe function getHoge () { return "Hoge!!" ; } composer.json に files を設定することで対応可能です。 { " autoload ": { " classmap ": [ " src/tools " ] , " files ": [ " src/functions.php " ] , " psr-4 ": { " TechCafe\\ ": " src/ " } } } Composer オートローディングのパフォーマンス 通常ファイルの読み込みを行う場合、コロン区切りで指定された ディレクト リ (include_path) を順に走査を行います。 そのため、指定された ディレクト リの分だけ走査が行われパフォーマンスが悪くなっていました。 Composer のオートローディングはファイルの走査処理が最適化されており、 通常の読み込み処理に比べパフォーマンスが良くなっています。 また、Composer のオートローディングは遅延ロードとなっているのもパフォーマンス上でメリットといえます。 ※遅延ロード:ファイルが必要になった時に読み込み処理を行うこと 例えば下記のような if 文があった場合、 $i が numeric なら HOGE が、そうでないなら FUGA が読み込まれます。 <?php if ( is_numeric ( $ i )) { HOGE :: getHoge () ; } else { FUGA :: getFuga () ; } 参加者からは「レガシープロジェクトでも設定次第で Composer のオートローディングの恩恵を受けられるので適用したい」といった声があがっていました。 Composer2.0.0 最後に Composer 8年ぶりのメジャーバージョンアップである Composer 2.0.0 についての紹介です。 概要 2020/10/24 リリース 2012年のリリース以来初のメジャーバージョンアップ 参考資料 Changelog アップグレードガイド Composer 2.0 is now available! 新機能や変更点 Performance improvements Composer2 の大きな変更点としてパフォーマンスの改善が紹介されていました。 速度とメモリ使用量の両方が大幅に改善され、Composer1 と比べ 50% 以上の改善 が見られたそうです。 この驚異的な改善に参加者から「これはすごい」「めちゃくちゃ速くなっている」という驚きの声があがっていました。 またパフォーマンス改善の要因として、パッケージ構造の変更による効率化が挙げられていました。 Architectural changes and determinism require / update 実行時の vendor ディレクト リ更新処理が中途半端に行われることが無くなりました。 Composer1 では更新中に LAN ケーブルが抜ける等でネットワークが切れた場合、 vendor ディレクト リが更新途中の状態になり復旧不可能になっていました。 Composer2 ではネットワークを跨いだ処理 (パッケージのダウンロード等) を全て終わらせてから更新を行うよう変更されたため、 更新中にネットワークが切れたとしても vendor ディレクト リが壊れることが無くなりました。 参加者からは「この機能は 必須 」や「更新するときの 安心感が段違い 」といった意見が寄せられていました。 Runtime features オートロード時にプラットフォームチェック機能が追加されました。 vendor/autoload.php が呼ばれた際、現在の PHP バージョンやエクステンションが対応したバージョンであるかをチェックし、一致していなければエラーにします。 Error reporting improvements 依存関係が解決できない場合に表示されるエラーの内容が改善されました。 Composer1 のエラー情報はごちゃごちゃしていたので、Composer2 で色がついたり見やすくなって嬉しいという意見がありました。 Partial updates with temporary constraints 特定のパッケージのバージョンを更新するためのコマンドが追加されました。 composer update vendor/package:1.0.* ↓では composer.json や cmposer.lock を更新したい場合は --with を付ける composer update --with vendor/package:1.0.* ちなみに Composer1 でも required パッケージ名 を実行することで特定のパッケージを更新することができると紹介されていました。 編集後記 以上、Composer の概要と Composer2.0.0 の変更点について取り上げました。 Composer はパッケージの依存関係管理だけでなくオートローディングまで備えているため、 Composer を利用しない開発はありえないなと感じました。 Composer は現在でも4~5ヵ月ごとにマイナーバージョンアップされており、 積極的に開発が行われています。 今後も Composer の新機能や改善などに着目していきたいと思います! 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。 connpass.com エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
弊社で毎月開催し、 PHP エンジニアの間でご好評をいただいている PHP TechCafe。 2022年11月のイベントでは「 PHP フレームワーク 」について語り合いました。 弊社メンバーがピックアップした PHP の代表的な フレームワーク 4種について、以下のShowNoteをベースに、参加者の皆様のご意見も伺いながら学んでいきました。今回はその内容についてレポートします。 rakus.connpass.com hackmd.io フレームワークとは 代表的なPHPフレームワーク Laravel Symfony CakePHP Slim 機能比較 ルーティング Laravel Symfony CakePHP Slim まとめ セッション管理 Laravel Symfony CakePHP Slim まとめ リクエスト管理 Laravel Symfony CakePHP Slim まとめ エラーハンドリング Laravel Symfony CakePHP Slim DBサポート Laravel Symfony CakePHP Slim 最後に フレームワーク とは まず初めに、 フレームワーク とは、 アプリケーション開発においてよく利用される機能をあらかじめ備えた枠組み のことです。 もう少し具体的に説明すると、以下の通りです。 Webアプリケーション フレームワーク 動的な ウェブサイト、Webアプリケーション、 Webサービス の開発をサポートするために設計されたアプリケーション フレームワーク Web開発で用いられる共通した作業に伴う労力を軽減する データベースへのアクセス テンプレートエンジン セッション管理 何故 フレームワーク が必要なのか? 開発速度向上 Webアプリケーション開発でよく利用する処理(セッション管理やDBアクセス、 Cookie など)が既に用意されているため、それらを再利用するだけで開発が進められる セキュリティ対応 脆弱性 が見つかった場合に修正版がリリースされる 開発ルールの順守 フレームワーク のルールに従って作成することが強いられる反面、開発チーム全体で共通のルールで開発できるため、ルールに逸脱するようなコードが生まれにくい ここでは、「セキュリティに関わる実装を自前で組むメリットは少ないため、実績のある フレームワーク を利用することが一番の正攻法ではないか」といった意見が挙がっていました。 代表的な PHP フレームワーク 次に、事前に弊社メンバーが抜粋した、代表的な PHP フレームワーク それぞれの設計思想について語り合いました。 (抽出対象や並び順に意図はございません) Laravel laravel.com プログレ ッシブ フレームワーク どのような規模や段階の Web アプリケーションにも対応できる フレームワーク の概念 依存性注入、 単体テスト 、キュー、リアルタイム イベント などのための堅牢なツールを提供 スケーラブルな フレームワーク システム規模や利用負荷などの増大に対応できる Laravel アプリケーションは、1 か月あたり数億のリク エス トを処理するように簡単にスケーリングされている コミュニティ フレームワーク コミュニティの活動が活発であり、意見交換も頻繁に行われている 参加者からは「確かにイー ジー なイメージがあり、何でもできて簡単。」といった意見や、「依存性注入や 単体テスト のために独自に拡充された機能が提供されており、色々出来て便利」といった意見が挙がりました。Laravleにはサー ビスコ ンテナと呼ばれる機能が備わっているため、依存性注入を簡単に行うことができたり、 ユニットテスト を考慮して構築されていることがその理由となります。 Symfony symfony.com 作成された コンポーネント を組み合わせて、 フルスタックフレームワーク を作成することもマイクロサービスを作成することも可能 開発者の目的に応じて規模を変えることができることが特徴 コンポーネント が標準化されており、アプリケーションが成熟しても使用したい コンポーネント を自由に導入することができる Java の Spring Framework や Ruby の Ruby on Rails の影響を受けている Symfony コンポーネント は Drupal , Prestashop , Laravel  で利用されている ここでは、 Symfony 自体がLaravelの中で使われているという点について活発にコメントが飛び交いました。 「 Symfony のリリースが遅れることによってLaravelにも影響が出たケースがあった」という声や、「Laravelのリリース頻度が Symfony のバージョンアップに依存するような形になったということを過去に記事で見た」という声です。 現時点の最新バージョンであるLaravel9についても、 Symfony の コンポーネント に依存していることが要因となりLTS(※LTSはLong Term Supportの略で長期サポートを意味)がなくなったりと、実際に様々な影響を及ぼしていることから、このような声が多く挙がっているようです。 CakePHP cakephp.org 「ケーキを焼くくらい簡単に開発できる」というコンセプトで設計されており、初心者向けであるといえる Ruby on Rails の概念の多くが取り入れられている 比較的小規模なWeb アプリ開発 向けである 「 MVC モデル」が採用されており、役割分担をさせて高速に開発を進められる ここでは、「 MVC に準拠しており、 命名 がすごく厳格だという印象がある」という意見が挙がりました。 Slim www.slimframework.com www.slimframework.com シンプルかつ強力な Web アプリケーションと API をすばやく作成するのに役立つ PHP マイクロ フレームワーク 必要なことだけを行う最小限のツール セットのみを提供 最小限の機能のみを持つため、ルールがシンプルで、開発の自由度が高く、学習コストが低い ここでは、マイクロ フレームワーク を謡っているという点について「テンプレートエンジンが標準で提供されておらず、細かな Bot 作成や小さな機能開発を行う際に利用しやすい」といった意見や、「自由に拡張できるという点がありがたい」といった意見が挙がりました。 機能比較 続いて、各 フレームワーク の基本的な利用方法に注目し、それぞれの特徴について比較しながらディスカッションを行いました。 ルーティング Laravel laravel.com ルートファイル デフォルトでは下記2つファイルにルーティングを定義する routes/web. php routes/ api . php 定義方法 UserControllerにindexメソッドを定義している場合、下記のように定義すると /user のパスに対して、UserControllerのindexメソッドが対応される <?php use App\Http\Controllers\UserController; Route :: get ( '/user' , [ UserController :: class , 'index' ]) ; 利用可能な ルーター メソッド <?php Route :: get ( $ uri , $ callback ) ; Route :: post ( $ uri , $ callback ) ; Route :: put ( $ uri , $ callback ) ; Route :: patch ( $ uri , $ callback ) ; Route :: delete ( $ uri , $ callback ) ; Route :: options ( $ uri , $ callback ) ; パラメータ <?php Route :: get ( '/posts/{post}/comments/{comment}' , [ CommentController :: class , 'show' ]) ; ここでは「 Route::get や Route::post のように、複数のHTTP動詞に対応したルートを登録できることが特徴的である」という意見が挙がりました。 Symfony routes.yaml に記載するパターン /lucky/number にアクセスすることで LuckyController の number メソッドにルーティングされる app_lucky_number : path : /lucky/number controller : App\Controller\LuckyController::number アノテーション または アトリビュート を利用するパターン(こちらが推奨) コントローラを以下の通り変更することで routes.yaml を作成しなくともルーティングが行われる <?php // src/Controller/LuckyController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class LuckyController { #[Route('/lucky/number')] public function number () : Response { $ number = random_int ( 0 , 100 ) ; return new Response ( '<html><body>Lucky number: ' .$ number . '</body></html>' ) ; } } ここでは「ルーティングの方法が複数ある」という点に注目が集まりました。 「デフォルトは routes.yaml だが推奨パターンは アトリビュート を使うパターンのようだ」といった声や、「サンプルコードのような アトリビュート を使うパターンが分かりやすい」といった声が挙がりました。 CakePHP book.cakephp.org routes.php に記載  例: / にアクセスすると ArticlesController の index() メソッドを実行する <?php use Cake\Routing\Router; // スコープ付きルートビルダーを使用。 Router :: scope ( '/' , function ( $ routes ) { $ routes -> connect ( '/' , [ 'controller' => 'Articles' , 'action' => 'index' ]) ; }) ; // static メソッドを使用。 Router :: connect ( '/' , [ 'controller' => 'Articles' , 'action' => 'index' ]) ;   /articles/15 にアクセスすると ArticlesController の view(15) メソッドを実行する <?php $ routes -> connect ( '/articles/:id' , [ 'controller' => 'Articles' , 'action' => 'view' ] ) -> setPatterns ([ 'id' => '\d+' ]) -> setPass ([ 'id' ]) ;  HTTPメソッドによって分けたいときは以下のような記述を行う <?php // GET リクエストへのみ応答するルートの作成 $ routes -> get ( '/cooks/:id' , [ 'controller' => 'Users' , 'action' => 'view' ] , 'users:view' ) ; // PUT リクエストへのみ応答するルートの作成 $ routes -> put ( '/cooks/:id' , [ 'controller' => 'Users' , 'action' => 'update' ] , 'users:update' ) ; ここでは「必ずしもルーティングは必要でなく、URLから勝手にクラスを推測してくれる」という点に注目が集まりました。 しかし、「知らないと迷いそうなので明記して欲しい」といった声もあり、やはり明示的に定義するのがベストだろうという考えに落ち着きました。 Slim www.slimframework.com get() / post() メソッドを使用 <?php $ app -> get ( '/books/{id}' , function ( $ request , $ response , array $ args ) { // Show book identified by $args['id'] }) ; <?php $ app -> post ( '/books' , function ( $ request , $ response , array $ args ) { // Create new book }) ; ここではルータメソッドが get() / post() であることから「Laravelと似ている」という意見が挙がりました。 まとめ ルーティングの利用方法に関して全体を見渡した後には、以下のような意見が挙がりました。 Symfony : アトリビュート によるルーティングが特徴的 CakePHP :ルーティングを記載せずとも フレームワーク で勝手に呼び出すクラスを推測してくれる点が特徴的 Laravel: Routes を見に行く必要があることが面倒である、 C# のルーティング定義に似ている 全般:どこで何をやっているのかが分かるルーティングの方法であることが望ましい composerのインストール/アップデートのように、1つの操作が複数の役割を持つような仕組みは分かりづらい セッション管理 続いて、セッション管理の方法について見ていきます。 Laravel laravel.com 設定ファイル config/session. php セッションの操作方法 グローバルセッションヘルパー と Requestインスタンス経由 の2つの方法がある     ・グローバルセッションヘルパー <?php $ value = session ( 'key' ) ;     ・Request インスタンス 経由 <?php public function show ( Request $ request , $ id ) { $ value = $ request -> session () -> get ( 'key' ) ; // } Symfony セッションの操作方法 RequestStack と RSessionInterface から取得する2つの方法がある RequestStack から取得するパターン セッションの設定は config/packages/framework.yaml に記載 HttpFoundation component を追加することで利用可能 その他の詳細 基本的な使い方 <?php use Symfony\Component\HttpFoundation\RequestStack; class SomeService { private $ requestStack ; public function __construct ( RequestStack $ requestStack ) { $ this -> requestStack = $ requestStack ; // Accessing the session in the constructor is *NOT* recommended, since // it might not be accessible yet or lead to unwanted side-effects // $this->session = $requestStack->getSession(); } public function someMethod () { $ session = $ this -> requestStack -> getSession () ; // stores an attribute in the session for later reuse $ session -> set ( 'attribute-name' , 'attribute-value' ) ; // gets an attribute by name $ foo = $ session -> get ( 'foo' ) ; // the second argument is the value returned when the attribute doesn't exist $ filters = $ session -> get ( 'filters' , []) ; // ... } } SessionInterface から取得するパターン SessionInterface でタイプヒントしてコントローラ引数に渡すだけ 基本的な使い方 <?php use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\SessionInterface; // ... public function index ( SessionInterface $ session ) : Response { // stores an attribute for reuse during a later user request $ session -> set ( 'foo' , 'bar' ) ; // gets the attribute set by another controller in another request $ foobar = $ session -> get ( 'foobar' ) ; // uses a default value if the attribute doesn't exist $ filters = $ session -> get ( 'filters' , []) ; // ... } CakePHP book.cakephp.org PHP のネイティブ session 拡張上に、ユーティリティ機能のスイートとラッパーを提供 設定  ・データベースセッション   ・セッションをデータベースに保持することを指定   ・セッション保持するためのカスタムモデルを定義することもできる(model => 'CustomSessions') <?php 'Session' => [ 'defaults' => 'database' , 'handler' => [ 'engine' => 'DatabaseSession' , 'model' => 'CustomSessions' ] ]   ・キャッシュセッション    ・CacheSession クラスをセッション保存先として 指定 <?php Configure :: write ( 'Session' , [ 'defaults' => 'cache' , 'handler' => [ 'config' => 'session' ] ]) ; セッションの利用 リク エス トオブジェクトを呼び出せる場所ならどこでも呼び出せる Controllers Views Helpers Cells Components <?php $ name = $ this -> getRequest () -> getSession () -> read ( 'User.name' ) ; // 複数回セッションにアクセスする場合、 // ローカル変数にしたくなるでしょう。 $ session = $ this -> getRequest () -> getSession () ; $ name = $ session -> read ( 'User.name' ) ; 利用するメソッド Session::read($key) Session::write($key) Session::check($key) Session::destroy() Slim なし まとめ セッションについては、それぞれの フレームワーク で利用方法が多岐に渡りました。 ここでは、 CakePHP について、「セッションの参照に Get ではなく Read になっているのは何故なのか?」といったコメントや、Slimにはそもそも実装されていない点について、「それがSlimの良いところであり、必要であればcomposerで導入すればよい」といったコメントが挙がっていました。 リク エス ト管理 Laravel laravel.com <?php $ name = $ request -> input ( 'name' ) ; $ name = $ request -> query ( 'name' ) ; Laravelでは Request クラスの インスタンス から取得します。 Symfony symfony.com <?php use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; public function index ( Request $ request ) : Response { $ request -> isXmlHttpRequest () ; // is it an Ajax request? $ request -> getPreferredLanguage ([ 'en' , 'fr' ]) ; // retrieves GET and POST variables respectively $ request -> query -> get ( 'page' ) ; $ request -> request -> get ( 'page' ) ; // retrieves SERVER variables $ request -> server -> get ( 'HTTP_HOST' ) ; // retrieves an instance of UploadedFile identified by foo $ request -> files -> get ( 'foo' ) ; // retrieves a COOKIE value $ request -> cookies -> get ( 'PHPSESSID' ) ; // retrieves an HTTP request header, with normalized, lowercase keys $ request -> headers -> get ( 'host' ) ; $ request -> headers -> get ( 'content-type' ) ; } Symfony もLaravelと同じ様に Request クラスの インスタンス から取得します。 CakePHP <?php $ controllerName = $ this -> request -> getParam ( 'controller' ) ; // URL は /posts/index?page=1&sort=title の場合に page を取得するとき $ page = $ this -> request -> getQuery ( 'page' ) ; // POSTデータにアクセスするとき $ title = $ this -> request -> getData ( 'MyModel.title' ) ; CakePHP もLaravel、 Symfony と似たイメージです。 Slim www.slimframework.com <?php use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Slim\Factory\AppFactory; require __DIR__ . '/../vendor/autoload.php' ; $ app = AppFactory :: create () ; $ app -> get ( '/hello' , function ( Request $ request , Response $ response ) { $ response -> getBody () -> write ( 'Hello World' ) ; return $ response ; }) ; $ app -> run () ; セッション管理は実装されていませんでしたが、リク エス ト管理はSlimにもちゃんと実装されています。 まとめ ここでは、「いずれもRequestクラスの インスタンス から取得するという点において、だいたいどれも似たような形である」といった意見や、「これが フレームワーク の大本みたいな感じがする」といった意見が挙がりました。 Webアプリケーションの基本となる部分であることからも、それぞれの フレームワーク に大きな差はないようでした。 エラーハンドリング Laravel readouble.com App\Exceptions\Handler クラスによって、アプリケーションが投げるすべての例外がログに記録され、ユーザーへレンダーされる エラーハンドリングのカスタマイズ Handler クラスは、カスタム例外レポートと レンダリング コールバックを登録できる register メソッドを持っている。 reportable メソッドで、例外をさまざまな方法で報告できる。(エラー監視ツールに登録するなど。デフォルトではログに記録される。) renderable メソッドで、特定の例外に対して、個別に レンダリング 方法を指定することができる。(デフォルトでは例外はHTTPレスポンスに変換される) HTTPエラーが返された場合、 resources/views/errors 下の HTTPステータスコード 名のbladeファイルが レンダリング される( 404.blade.php など) <?php use App\Exceptions\InvalidOrderException; /** * アプリケーションの例外処理コールバックを登録 * * @return void */ public function register () { $ this -> reportable ( function ( InvalidOrderException $ e ) { // 例外を報告 }) ; $ this -> renderable ( function ( InvalidOrderException $ e , $ request ) { return response () -> view ( 'errors.invalid-order' , [] , 500 ) ; }) ; } Laravelの場合、例外を投げた際にデフォルトのものを使うか別のカスタマイズされたものを使うか振り分けることができます。 例えば、404エラーの場合は 404.blade のように定義しておけば、自動で読み込んで画面に表示してくれます。 Symfony symfony.com github.com エラーが 404 Not Found エラーであろうと、コードで何らかの例外をスローすることによってトリガーされた致命的なエラーであろうと、すべてのエラーを例外として扱う。 組み込みの Twig エラーレンダラーを使用して、デフォルトのエラーテンプレートをオーバーライド可能。 composer require symfony/twig-pack これらのテンプレートをオーバーライドするには、標準の Symfony メソッドを使用し て、バンドル内にあるテンプレートをオーバーライドし、それらを templates/bundles/TwigBundle/Exception/ ディレクト リに配置する Copy templates/ └─ bundles/ └─ TwigBundle/ └─ Exception/ ├─ error404.html.twig ├─ error403.html.twig └─ error.html.twig # All other HTML errors (including 500) ここでは Twig に関して、デフォルトのエラーテンプレートをオーバーライドする際の利用方法が分かり易いといったコメントが挙がりました。 CakePHP book.cakephp.org <?php * ErrorController にてエラーページを描画 namespace App\Controller\Admin; use App\Controller\AppController; use Cake\Event\EventInterface; class ErrorController extends AppController { /** * Initialization hook method. * * @return void */ public function initialize () : void { $ this -> loadComponent ( 'RequestHandler' ) ; } /** * beforeRender callback. * * @param \Cake\Event\EventInterface $event Event. * @return void */ public function beforeRender ( EventInterface $ event ) { $ this -> viewBuilder () -> setTemplatePath ( 'Error' ) ; } } ここでは、「Cakeの場合はデフォルトのエラーテンプレートをオーバーライドするというより Controller をそれぞれ作るイメージだ」とのコメントがありました。 Slim www.slimframework.com slimが用意したエラー画面を出すかどうかを選択できたり、カスタムエラー画面を表示するなど柔軟な設定が可能 <?php use Slim\Factory\AppFactory; require __DIR__ . '/../vendor/autoload.php' ; $ app = AppFactory :: create () ; /** * The routing middleware should be added earlier than the ErrorMiddleware * Otherwise exceptions thrown from it will not be handled by the middleware */ $ app -> addRoutingMiddleware () ; /** * Add Error Middleware * * @param bool $displayErrorDetails -> Should be set to false in production * @param bool $logErrors -> Parameter is passed to the default ErrorHandler * @param bool $logErrorDetails -> Display error details in error log * @param LoggerInterface|null $logger -> Optional PSR-3 Logger * * Note: This middleware should be added last. It will not handle any exceptions/errors * for middleware added after it. */ $ errorMiddleware = $ app -> addErrorMiddleware ( true , true , true ) ; // ... $ app -> run () ; ここでは、Slimにはテンプレートエンジンがないことから、「用意したエラー画面を出すかカスタムエラーを出すか」であり、リク エス ト時に Add Error Middleware の設定を行うことでエラーハンドリングを有効/無効化が制御できるという説明がありました。 DBサポート Laravel readouble.com サポートされているDB MariaDB MySQL PostgreSQL SQLite SQL Server DBへのアクセス方法  ・DB ファサード $users = DB::select('select * from users where active = ?', [1]);  ・クエリビルダ $users = DB::table('users')->where('active', $isActive)->get();  ・Eloquent $users = User::where('active', $isActive)->get(); ここでは、「LaravelではEloquentを利用するの良いだろう」という意見が挙がりました。 「様々なブログでも紹介されている」とう点や、「 SQL を知っていると直感的で分かり易い」といったコメントが挙がっていました。 Symfony symfony.com Doctrine(ORM)を使用 composer require doctrine maker インストールが完了すると .env ファイルにデータベースへの接続設定に関する項目が書き足される DATABASE_URL の箇所を接続するデータベースに合わせて書き換える DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name php bin/console doctrine:database:create エンティティクラスを作成する symfony console make:entity hoge CakePHP SELECT文の実行 <?php use Cake\Datasource\ConnectionManager; $ connection = ConnectionManager :: get ( 'default' ) ; $ results = $ connection -> execute ( 'SELECT * FROM articles' ) -> fetchAll ( 'assoc' ) ; INSERT文の実行 <?php use Cake\Datasource\ConnectionManager; use DateTime; $ connection = ConnectionManager :: get ( 'default' ) ; $ connection -> insert ( 'articles' , [ 'title' => 'A New Article' , 'created' => new DateTime ( 'now' ) ] , UPDATE文の実行 <?php use Cake\Datasource\ConnectionManager; $ connection = ConnectionManager :: get ( 'default' ) ; $ connection -> update ( 'articles' , [ 'title' => 'New title' ] , [ 'id' => 10 ]) ; DELETE文の実行 <?php use Cake\Datasource\ConnectionManager; $ connection = ConnectionManager :: get ( 'default' ) ; $ connection -> delete ( 'articles' , [ 'id' => 10 ]) ; Slim なし 最後に 今回は PHP の主要な フレームワーク について、様々な観点から見比べてみましたがいかがでしたでしょうか? どの フレームワーク にも様々な特徴がありますが、チーム特性や作成するアプリケーションによっても採用する フレームワーク は変わってくると思います。 基本的な部分についてはどれも使い方は同じで、感覚的に使えるようになっているため、実際に触りながら色々と比べて見ると面白そうですね。 ShowNoteの方には「バリデーション」や「 マイグレーション 」についての比較も行っていますので是非ご覧になってください。 hackmd.io 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。皆さまのご参加をお待ちしております。 connpass.com エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、主催イベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
本記事ではJUnit5におけるパラメータ化テストの使いどころと実際の実装方法について紹介します。 使いどころ 実装方法 パラメータ化テストの宣言 @ParameterizedTest パラメータ指定 単一データの入力 @ValueSource 列挙型 @EnumSource 複数データの入力 @CsvSource まとめ 参考 使いどころ テストケースを作成する時は複数の振る舞いをテストすることがほとんどかと思います。 例えば、以下のように受け取った年齢の値から学年を返すメソッドがあるとします。 public String getGrade( int age) { if (age < 0 ) { return "存在しない年齢" ; } if (age <= 5 ) { return "園児" } else if (age <= 12 ) { return "小学生" } else if (age <= 15 ) { return "中学生" } else if (age <= 18 ) { return "高校生" } return "大人" } この場合テストしたい振る舞いは6ケースです。 存在しない年齢 園児 小学生 中学生 高校生 大人 境界値でテストをするならそれぞれ以下の値を入力値としてテストしたいです 存在しない年齢: -1 園児: 0, 5 小学生: 6, 12 中学生: 13, 15 高校生: 16, 18 大人: 19 このテストを素直に書くと以下のようになります。 public class 年齢から学年を判定する処理のテスト { @Test public void 年齢が 0 未満の場合_存在しない年齢とする() { assertEquals( "存在しない年齢" , getGrade(- 1 )); } @Test public void 年齢が 0 の場合_園児とする() { assertEquals( "園児" , getGrade( 0 )); } @Test public void 年齢が 5 の場合_園児とする() { assertEquals( "園児" , getGrade( 5 )); } @Test public void 年齢が 6 の場合_小学生とする() { assertEquals( "小学生" , getGrade( 6 )); } @Test public void 年齢が 12 の場合_小学生とする() { assertEquals( "小学生" , getGrade( 12 )); } @Test public void 年齢が 13 の場合_中学生とする() { assertEquals( "中学生" , getGrade( 13 )); } @Test public void 年齢が 15 の場合_中学生とする() { assertEquals( "中学生" , getGrade( 15 )); } @Test public void 年齢が 16 の場合_高校生とする() { assertEquals( "高校生" , getGrade( 16 )); } @Test public void 年齢が 18 の場合_高校生とする() { assertEquals( "高校生" , getGrade( 18 )); } @Test public void 年齢が 19 以上の場合_大人とする() { assertEquals( "大人" , getGrade( 19 )); } } このテストでも網羅性の観点で言えば問題は無さそうです。 ただ、入力値が異なるが期待値が同じといういわゆる 同値クラス のテストもメソッドが分割されていて冗長に感じます。 また、それらのメソッド名はシステムの振る舞いを適切に表せていません。例えば、上記のメソッドを見ただけでは年齢が7の時は何が返ってくるのが分からないので結局実装を見に行くことになります。 テストはシステムの仕様を表現するという大事な役割も担っていますが、上記の書き方だと仕様をメソッド名で表現しづらくなってしまいます。 こんな場面で使えるのがパラメータ化テストです。 同値クラス の箇所をパラメータ化テストに置き換えたのが以下になります。 public class 年齢から学年を判定する処理のテスト { @Test public void 年齢が 0 未満の場合_存在しない年齢とする() { assertEquals( "存在しない年齢" , getGrade(- 1 )); } @ParameterizedTest @ValueSource (ints = { 0 , 5 }) public void 年齢が 0 以上 5 以下の場合_園児とする( int age) { assertEquals( "園児" , getGrade(age)); } @ParameterizedTest @ValueSource (ints = { 6 , 12 }) public void 年齢が 6 以上 12 以下の場合_小学生とする( int age) { assertEquals( "小学生" , getGrade(age)); } @ParameterizedTest @ValueSource (ints = { 13 , 15 }) public void 年齢が 13 以上 15 以下の場合_中学生とする( int age) { assertEquals( "中学生" , getGrade(age)); } @ParameterizedTest @ValueSource (ints = { 16 , 18 }) public void 年齢が 16 以上 18 以下の場合_高校生とする( int age) { assertEquals( "高校生" , getGrade(age)); } @Test public void 年齢が 19 以上の場合_大人とする() { assertEquals( "大人" , getGrade( 19 )); } } 同値クラス がすっきりして見やすくなりました。 テストメソッド名も振る舞いを表現しやすくなりました。テストを見るだけで getGradeメソッド がどのような振る舞いをするかが分かるようになったかと思います。 パラメータ化テストが便利なことが分かりました。 しかし、使い方を誤ると逆にテストが分かりにくくなることもあります。 パラメータ化テストは複数データを一度にパラメータとして渡すこともできます。そうすると、上記のテストコードをもっと改良しようと全てのケースをパラメータに集約したくなってきます。 実際にやってみたのが以下です。 public class 年齢から学年を判定する処理のテスト { @ParameterizedTest @CsvSource ({ "-1, 存在しない年齢" , "0, 園児" "5, 園児" "6, 小学生" "12, 小学生" "13, 中学生" "15, 中学生" "16, 高校生" "18, 高校生" "19, 大人" }) public void 与えられた年齢から学年を判定する( int age, String grade) { assertEquals(grade, getGrade(age)); } } テストコードは短くなりました。しかし、何をテストしているのかよく分からなくなりました。 単純にパラメータが多すぎるのが一つの原因です。パラメータは値でしかないのでそれだけを書かれてもテストの意図は分かりません。 またテストメソッドが汎用的なメソッド名になっているのが分かるかと思います。色んな振る舞いを一度にテストしすぎて具体的な 命名 ができなくなってしまいました。 このようにパラメータ化テストはやりすぎるとテストコードを読みづらくし、本来のテストの目的である仕様の表現ができなくなってしまいます。 なので、ケースバイケースですがパラメータ化の際は振る舞いが同じで入力が異なる 同値クラス ごとに分割して行うのが良い粒度だと思います。パラメータ化をしすぎて本来の目的を忘れないように注意しましょう。 実装方法 ここまでも軽く触れましたが、パラメータ化テストの具体的な実装方法を紹介します。 色々と便利な アノテーション がありますが、本記事では実際の開発で特に使用するものに限定して紹介します。 パラメータ化テストの宣言 @ParameterizedTest このテストではパラメータ化テストをしますよ~という宣言を行う アノテーション です。 パラメータ化テストを行う対象のメソッドに必ず付与します パラメータ指定 単一データの入力 @ValueSource 基本型の アノテーション が行えます。以下の型が入力可能です。 short byte int long float double char java .lang.String java .lang.Class アノテーション の中にパラメータに渡す型と値を記述し、テストメソッドの引数で値を受け取って使用します。 値は記述した順番にテストメソッドの引数に渡されます。 コード例 @ParameterizedTest @ValueSource (ints = { 0 , 5 }) public void 年齢が 0 以上 5 以下の場合_園児とする( int age) { assertEquals( "園児" , getGrade(age)); } ここで、仮に age=0 のテストでこけた場合どうなるの?と思った方がいるかもしれません。 パラメータ化テストでは途中でテストがこけても全てのパラメータについて実行を行い、どのパラメータでこけたかが明確に分かります。 なので JUnit の アンチパターン である アサーション ルーレットが発生することもありません。 アサーション ルーレット: 一つのテストメソッドに複数のアサートを記述した場合、途中でテストがこけるとそれ以降のテストが実施されなくなるという アンチパターン 列挙型 @EnumSource Enum の値もパラメータ化できます。 例えば Grade という Enum があった場合、@EnumSourceの引数に Grade.class を指定し、パラメータに Grade クラスのオブジェクト名を記述します。 コード例 enum Grade { KINDERGARTEN( "園児" ), ELEMENTARY_SCHOOL_STUDENT( "小学生" ), JUNIOR_HIGH_SCHOOL_STUDENT( "中学生" ), SENIOR_HIGH_SCHOOL_STUDENT( "高校生" ), GROWN_UP( "大人" ); } int getFee(Grade grade) { if (grade == Grade.KINDERGARTEN || grade == Grade.ELEMENTARY_SCHOOL_STUDENT) { return 0 ; } return 1000 ; } @ParameterizedTest @EnumSource (value = Grade. class , names = { "KINDERGARTEN, ELEMENTARY_SCHOOL_STUDENT" }) public void 園児から小学生の場合_料金を無料とする(Grade grade) { assertEquals( 0 , getFee(grade)); } 複数データの入力 @CsvSource 前述したように使いどころには注意が必要ですが、 テスト対象のメソッドが複数の引数を必要とする場合や期待値が引数と連動するような場合に役立ちます。 コード例 @ParameterizedTest @CsvSource ({ "1901, 1, 1" , "2000, 12, 31" }) public void 1901 年から 2000 年の 100 年間は_20世紀とする( int year, int month, int day) { assertEquals( 20 , getCentury(year, month, day)); } 上記のコードを見るとstringで与えたパラメータがintに変換されています。 パラメータ化テストではこのような暗黙的な変換を行ってくれます。 暗黙的な変換の種類について まとめ JUnit のパラメータ化テストについて紹介しました。 使いどころさえ見極めればテストコードを書くのに非常に有効なテクニックになります。 ぜひ使いこなして、良い JUnit ライフを送りましょう! 参考 https://oohira.github.io/junit5-doc-jp/user-guide/#writing-tests-parameterized-tests エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは。新卒2年目のrksmskです。 今回は認証ライブラリを用いず、SolidStartでOAuth2.0認証クライアントを基本実装して クラウド ストレージサービスであるBoxを利用できるようになるまでをまとめた記事となります。 よろしくお願いします。 モチベーション 環境 準備 - SolidStart 準備 - Box 実装 API ページ ①&② アクセストークン発行用の承認トークンを取得するため、認証サイトにリダイレクトする サーバー側 クライアント側 ④&⑤ 承認コードを受け取り、受け取った承認コードを使用してアクセストークンを取得する サーバー側 ⑥ アクセストークンを使用して、認証ユーザーの情報取得を行い、アクセストークンと認証ユーザー情報をセッションに格納する サーバー側 クライアント側 +α Boxにアップロードしたファイルの情報をAPIから取得して、一覧表示する サーバー側 クライアント側 各画面一覧 ログイン画面 認証画面(外部サイト) ホーム画面 まとめ モチベーション 本記事は元々、SolidJSのメタ フレームワーク である SolidStart と、Next.js以外のウェブ フレームワーク でも扱えるように開発を進めており、NextAuth.jsから最近名前を変えた Auth.js を組み合わせて、 SolidStart + Auth.jsによるOAuth2.0認証付き クラウド ストレージ管理アプリ を作る構想でした。 ですが、 Google Cloud Platform は動作することが確認できたものの、 Dropbox や Box といったその他の クラウド ストレージサービスが一筋縄では動かなかったので(もしご存じの方がいらっしゃったら是非教えてください!)、「OAuth2.0の勉強も兼ねて自前実装してみよう」という運びとなりました。 環境 下記の環境を前提としています。 Node.js@18.13.0 pnpm@7.25.1 準備 - SolidStart まず、SolidStart用の ディレクト リを作成します。 pnpm solid create と入力すると、 CLI で簡単にテンプレート ディレクト リを作成することが出来ます。 $ pnpm create solid ../../.pnpm-store/v3/tmp/dlx-3444 | Progress: resolved 1, reused 0, downloaded 0, added 0 ...(略) ? Which template do you want to use? » - Use arrow-keys. Return to submit. > bare hackernews todomvc with-auth with-mdx with-prisma with-solid-styled with-tailwindcss with-vitest with-websocket √ Which template do you want to use? » bare ? Server Side Rendering? » (Y/n) √ Server Side Rendering? ... yes ? Use TypeScript? » (Y/n)Y √ Use TypeScript? ... yes found matching commit hash: 82901a8a21b24a90cbb740b304ba307d167e5d94 ...(略) ✔ Copied project files コマンドを入力してしばらくすると、テンプレート作成のために三つ質問が行われます。 一つ目の質問である Which template do you want to use?(訳:どのテンプレートを使用しますか?) では、最もシンプルなテンプレートである bare を選びます。 二つ目の質問である Server Side Rendering?(訳:SSRの機能を使用しますか?) では Y を入力します。 最後の質問である Use TypeScript?(訳:TypeScriptを使用しますか?) では、今回は Y を入力します。 処理が完了すると、テンプレート作成後に行うことがコンソール上に記載されるので、その通りに pnpm install と pnpm run dev --open を実行します。 すると、ひな形アプリが立ち上がります。簡単ですね。 アプリ画面 最後に、開発時とビルド時のポート番号を合わせておくと後の作業の都合がよいので、 vite.config.ts を下記のように編集しておきましょう。 vite.config.ts import solid from "solid-start/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [solid()], + server: { + port: 3000, + }, }); 以上でSolidStartの開発準備が整いました。 準備 - Box 続いて、BoxのOAuth2.0認証の準備を整えていきましょう。 Boxをご存知ない方に軽く説明すると、Boxとはファイル管理機能とセキュリティ機能に優れた クラウド ストレージサービスです。嬉しいことに、無料枠でも クレジットカード登録なしで 10GB分ファイルアップロードを行うことが出来ます。 BoxのPricing のページに飛び、今回は「Individual Free」の「 Sign Up」ボタンをクリックします。 そして、名前、メールアドレス、パスワードの欄を入力し、hCaptchaをクリックして「開始する」ボタンをクリックします。 すると、登録したメールアドレスに認証メールが送られてくるので、認証ボタンをクリックします。 これでアカウント登録は完了です。試しにログインしてみると、自身のマイページが閲覧できるようになっていることが確認できます。 ここからは、Box上でのOAuth2.0用のアプリ作成を行っていきます。 マイページ左下の「Dev Console」ボタンをクリックし、開発者ページを開きます。 「Create New App」ボタンをクリックし、Authentication Methodで「Auth2.0」を選択し、App Nameに適当な名前を入れ、「Create App」ボタンをクリックします。 これでOAuth2.0用のアプリケーションが用意できました。最後に各種設定を行います。 「Configuration」タブをクリックし、OAuth 2.0 Redirect URI を「 http://localhost:3000/api/auth/callback 」に変更し、Application Scopesの「Write all files and folders stored in Box」と「Manage users」にチェックを入れて保存します。 最後にClient IDとClient Secretを手元のどこかにメモしておきましょう(後で使います)。 以上でBox側の設定は完了です。 実装 OAuth2.0認証クライアントの実装に入る前に、ざっくりですがOAuth2.0の仕組みを記載します。 この図の①から⑥の手順に沿って実装していきます。 本格的な実装に入る前に、出来上がりの全体像を把握しておきましょう。最終的なsrc ディレクト リ下は下記のような構成になります。 それぞれのファイルの役割は下記となります。 API src/routes/ api /auth/callback/index.ts OAuth2.0認証での承認コード取得時のコールバック先の API 。アクセス トーク ンとユーザー情報の取得、セッションへの保存を行う。 src/routes/ api /auth/login/index.ts ログイン処理用の API 。OAuth2.0認証の承認先へのリダイレクトURLを返す。 src/routes/ api /auth/logout/index.ts ログアウト処理用の API 。セッションをクリアしてログイン画面にリダイレクトする。 src/routes/ api /file/index.ts ファイル一覧取得 API 。Boxからファイル一覧を取得し、その情報を返す。 src/routes/ api /user/me/index.ts ユーザー名取得 API 。セッション内に保管してあるユーザー名を返す。 src/routes/session.server.ts セッション管理用。 ページ src/routes/login/index. tsx ログインページ。 src/routes/index. tsx ホームページ。ログイン後、閲覧可能で、Boxにアップロードしているファイルの一覧を表示する。ログイン前に表示した場合、ログイン画面に遷移する。 それでは、実装に入っていきましょう。なお、今回はHTTP通信の記載の簡素化のため、 axios を用いています。Fetch API でも同様の実装が可能ですが、本記事を手を動かしながら試す場合には、事前に下記コマンドを実行してください。 pnpm install axios ①&② アクセス トーク ン発行用の承認 トーク ンを取得するため、認証サイトにリダイレクトする サーバー側 まず、サーバー側を実装します。 src/routes/api/auth/login/index.ts を下記の内容で作成します。 src/routes/api/auth/login/index.ts export async function GET () { // クエリパラメータに変換 const query = new URLSearchParams ( { client_id: import .meta.env.VITE_BOX_ID , client_secret: import .meta.env.VITE_BOX_SECRET , response_type: "code" , } ); // リダイレクト先をLocationに入れて返却する return new Response ( null , { status : 200 , headers: { Location: `https://account.box.com/api/oauth2/authorize? ${ query.toString() } ` , } , } ); } SolidStartでは src/routes/api 下にファイルを作成すると、ファイルパスがそのまま API のエンドポイントとなります。そのファイル内で大文字のGET/POST/PUT/PATCH/DELETEを関数名にした関数を作成すると、その関数がそのままそのエンドポイントでのHTTPメソッドとなります。 Box認証サイトのURLは https://account.box.com/api/oauth2/authorize にBox側の作業でメモしたClient IDとClient Secret、レスポンスタイプをクエリパラメータに付与したものなので、その情報をレスポンスのLocationヘッダーに付与して返却しています。 Client IDとClient Secretは公開してはいけない情報なので、.envファイルに記載します。その際に、型補完がきくようにvite-env.d.tsファイルへ記載するのと、誤ってGitにアップしてしまわないように 忘れずに .gitignoreに.envを追記しておきます。 .env VITE_BOX_ID=*** VITE_BOX_SECRET=*** vite-env.d.ts interface ImportMetaEnv { readonly VITE_BOX_ID: string ; readonly VITE_BOX_SECRET: string ; } interface ImportMeta { readonly env: ImportMetaEnv ; } .gitignore dist .solid .output .vercel .netlify + .env netlify # dependencies /node_modules # IDEs and editors /.idea .project .classpath *.launch .settings/ # Temp gitignore # System Files .DS_Store Thumbs.db クライアント側 続いて、クライアント側を実装します。 src/routes/login/index.tsx を下記の内容で作成します。 src/routes/login/index.tsx import { Title } from "solid-start" ; import axios from "axios" ; export default function Login () { return ( < main > < Title > Login < /Title > < h1 > Hello world ! < /h1 > < button onClick = { () => { axios. get( "http://localhost:3000/api/auth/login" ) .then (( res ) => { window .location.href = res.headers [ "location" ] || "/" ; } ); }} > login < /button > < /main > ); } SolidStartではサーバー側と同様に、 src/routes 下にファイルを作成すると、ファイルパスがそのままページのURLとなります。 内容はシンプルで、ログインボタンを押したら先ほど作成したサーバー側の /api/auth/login にGETリク エス トを行い、成功のレスポンスが帰ってきたらLocationヘッダーのURLに遷移するというものです。 前述したように、 /api/auth/login はBoxの認証サイトへのURLをLocationヘッダーに含めているので、これでログインボタン押下時に認証サイトにリダイレクトされるようになりました。 なお、現状だと動作確認しづらいので、 src/root.tsx にログインページへの遷移先を配置しておきましょう。 src/root.tsx // @refresh reload import { Suspense } from "solid-js"; import { A, Body, ErrorBoundary, FileRoutes, Head, Html, Meta, Routes, Scripts, Title, } from "solid-start"; import "./root.css"; export default function Root() { return ( <Html lang="en"> <Head> <Title>SolidStart - Bare</Title> <Meta charset="utf-8" /> <Meta name="viewport" content="width=device-width, initial-scale=1" /> </Head> <Body> <Suspense> <ErrorBoundary> <A href="/">Index</A> - <A href="/about">About</A> + <A href="/login">Login</A> <Routes> <FileRoutes /> </Routes> </ErrorBoundary> </Suspense> <Scripts /> </Body> </Html> ); } ④&⑤ 承認コードを受け取り、受け取った承認コードを使用してアクセス トーク ンを取得する サーバー側 続いて、認証サイトでの認証後の処理を作成していきます。 src/routes/api/auth/callback/index.ts ファイルを下記の内容で作成します。 src/routes/api/auth/callback/index.ts import { APIEvent } from "solid-start" ; import axios from "axios" ; export async function GET ( { request } : APIEvent ) { // クエリパラメータに含まれる承認コード取得用 const query = new URL ( request.url ) .searchParams ; const accessToken = await axios .post ( "https://api.box.com/oauth2/token" , { client_id: import .meta.env.VITE_BOX_ID , client_secret: import .meta.env.VITE_BOX_SECRET , code: query. get( "code" ), grant_type: "authorization_code" , } ) .then (( res ) => { return res.data.access_token ; } ); } BoxのOAuth2.0認証用 API は、エンドポイント https://api.box.com/oauth2/token にPOSTメソッドで下記の情報をbodyに含めてあげるとアクセス トーク ンを返してくれます(詳しくは Box APIリファレンス参照 )。 client_id…アプリで発行したClient ID client_secret…アプリで発行したClient Secret code…承認コード grant_type…認証のリク エス ト方式。アクセス トーク ン取得時は authorization_code を指定 承認コードについては、Boxの事前準備でRedirect URI を「 http://localhost:3000/api/auth/callback 」に変更しているため、認証後は /api/auth/callback の API がGETリク エス トで呼ばれ、そのクエリパラメータに承認コード(code)が含まれています。 なので、リク エス トURLに含まれるクエリパラメータから承認コードを取得し、その情報とClient ID、Client Secretをbodyに含めることでアクセス トーク ンを取得することが出来ます。これで、Box API を使用する準備が整いました。 ⑥ アクセス トーク ンを使用して、認証ユーザーの情報取得を行い、アクセス トーク ンと認証ユーザー情報をセッションに格納する サーバー側 アクセス トーク ンが取得できたので、そのままBox API から認証ユーザーの情報を取得してみましょう。 src/routes/api/auth/callback/index.ts ファイルを下記の内容に変更します。 src/routes/api/auth/callback/index.ts import { APIEvent } from "solid-start"; import axios from "axios"; export async function GET({ request }: APIEvent) { // クエリパラメータに含まれる承認コード取得用 const query = new URL(request.url).searchParams; const accessToken = await axios .post("https://api.box.com/oauth2/token", { client_id: import.meta.env.VITE_BOX_ID, client_secret: import.meta.env.VITE_BOX_SECRET, code: query.get("code"), grant_type: "authorization_code", }) .then((res) => { return res.data.access_token; }); + const user = await axios + .get("https://api.box.com/2.0/users/me", { + headers: { + authorization: `Bearer ${accessToken}`, + contentType: "application/json", + }, + }) + .then((res) => { + return res.data; + }); } リク エス トのAuthorizationヘッダーに Bearer ${アクセストークン} とすることで、そのアクセス トーク ンが有効であればBox API を利用することが出来ます。これで認証ユーザーの情報が取得できました。 以上のアクセス トーク ン、認証ユーザーの情報をアプリ内で認証中は使い回したので、これらをセッション内に格納します。 SolidStartのSessionsページ を参考に、 src/routes/session.server.ts を下記の内容で作成します。 src/routes/session.server.ts import { redirect } from "solid-start/server" ; import { createCookieSessionStorage } from "solid-start/session" ; const storage = createCookieSessionStorage ( { cookie: { name: "_session" , secure: process .env.NODE_ENV === "production" , sameSite: "lax" , path: "/" , maxAge: 60 * 60 * 24 , httpOnly: true , } , } ); export function getUserSession ( request: Request ) { return storage.getSession ( request.headers. get( "Cookie" )); } export async function logout ( request: Request ) { const session = await storage.getSession ( request.headers. get( "Cookie" )); return redirect ( "/login" , { headers: { "Set-Cookie" : await storage.destroySession ( session ), } , } ); } export async function createUserSession ( token: string , userName: string , redirectTo: string ) { const session = await storage.getSession (); session. set( "token" , token ); session. set( "userName" , encodeURIComponent ( userName )); const cookie = await storage.commitSession ( session ); return redirect ( redirectTo , { headers: { "Set-Cookie" : cookie , } , } ); } まず、 createCookieSessionStorage 関数を使用してセッションストレージを作成します。 次に、作成したセッションストレージにアクセス トーク ンとユーザー名を保存し、指定したリダイレクト先に遷移する createUserSession 関数を作成します。なお、ユーザー名の保存時には日本語でも保存可能なように URI エンコード をかけています。 最後に、セッション情報を破棄する logout 関数と、セッション情報を取得する getUserSession 関数を作成します。 これで、セッション周りの設定が完了しました。作成した createUserSession 関数を利用して、アクセス トーク ン、認証ユーザー取得時にそれらをセッションに格納します。 src/routes/api/auth/callback/index.ts import { APIEvent } from "solid-start"; import axios from "axios"; +import { createUserSession } from "~/routes/session.server"; export async function GET({ request }: APIEvent) { // クエリパラメータに含まれる承認コード取得用 const query = new URL(request.url).searchParams; const accessToken = await axios .post("https://api.box.com/oauth2/token", { client_id: import.meta.env.VITE_BOX_ID, client_secret: import.meta.env.VITE_BOX_SECRET, code: query.get("code"), grant_type: "authorization_code", }) .then((res) => { return res.data.access_token; }); const user = await axios .get("https://api.box.com/2.0/users/me", { headers: { authorization: `Bearer ${accessToken}`, contentType: "application/json", }, }) .then((res) => { return res.data; }); +return createUserSession(accessToken, user.name, "http://localhost:3000/"); } 以上でセッションへの格納処理が完成しました。これにより、ログイン後はリク エス トのクッキーに含まれるセッションIDからアクセス トーク ンとユーザー情報を取り出し、利用することが可能になります。セッションIDから認証ユーザーのユーザー名を返す API は下記のようになります。 src/routes/api/user/me/index.ts import { APIEvent , json } from "solid-start" ; import { getUserSession } from "~/routes/session.server" ; export async function GET ( { request } : APIEvent ) { const session = await getUserSession ( request ); let userName = await session. get( "userName" ); userName = userName ? decodeURIComponent ( userName ) : null ; return json ( { userName } ); } また、セッション情報を削除する API は下記のようになります。 src/routes/api/auth/logout/index.ts import { APIEvent } from "solid-start" ; import { logout } from "~/routes/session.server" ; export async function POST ( { request } : APIEvent ) { return await logout ( request ); } クライアント側 クライアント側では、ページ表示時に認証ユーザーのユーザー名を取得し、ユーザー名が取得できなければ(認証前)ログイン画面へ遷移、ユーザー名が取得できればユーザー名とログアウトボタンを表示する処理を作成してみましょう。 src/routes/index.tsx を下記の内容で作成します。 src/routes/index.tsx import { Title , useNavigate , useRouteData } from "solid-start" ; import axios from "axios" ; import { createServerData$ , redirect } from "solid-start/server" ; export function routeData () { return createServerData$ (async ( _ , { request } ) => { const user = ( await axios. get( "http://localhost:3000/api/user/me" , { headers: { Cookie: request.headers. get( "Cookie" ) } , } ) ) .data ; if ( ! user.userName ) throw redirect ( "/login" ); return { userName: user.userName } ; } ); } export default function Home () { const serverData = useRouteData <typeof routeData >(); const navigate = useNavigate (); return ( < main > < Title > Hello World < /Title > < h1 > Hello world ! < /h1 > < p > Hello , { serverData () ?.userName } ! < /p > < button onClick = { () => { axios.post ( "http://localhost:3000/api/auth/logout" ) . finally (() => { navigate ( "/login" ); } ); }} > logout < /button > < /main > ); } SolidStartでは レンダリング 前に情報を取得して コンポーネント にその情報を渡す場合は routeData 関数( コンポーネント へ情報を渡す)と useRouteData 関数(情報を受け取る)を用い、加えて routeData 関数の内部でサーバーサイドからデータを取得する場合には createServerData$ 関数を使用します( 参考 )。 今回は、 1. /api/user/me にGETリク エス トを投げてユーザー名を取得 1. 取得したユーザー名がnullの場合はログイン画面にリダイレクト の処理を routeData 関数で記載しています。そして、 useRouteData 関数で コンポーネント に情報を渡し、画面上に情報を表示しています。 ログアウトボタンでは、ボタン押下時に /api/auth/logout にPOSTリク エス トを投げることでセッション情報を削除し、処理完了後にログインページに遷移する処理を記述しています。 +α Boxにアップロードしたファイルの情報を API から取得して、一覧表示する サーバー側 Box API を使用できるようになったので、せっかくなのでファイル情報の一覧取得を行ってみましょう(ファイルはBox側で事前にアップロードしておいてください)。 src/routes/api/file/index.ts を下記の内容で作成します。 src/routes/api/file/index.ts import { APIEvent } from "solid-start" ; import { json , redirect } from "solid-start/server" ; import axios from "axios" ; import { getUserSession } from "~/routes/session.server" ; export async function GET ( { request } : APIEvent ) { const session = await getUserSession ( request ); const accessToken = session. get( "token" ); const items = await axios . get( "https://api.box.com/2.0/folders/0/items" , { headers: { authorization: `Bearer ${ accessToken } ` , contentType: "application/json" , } , } ) .then (( res ) => { return res.data ; } ) . catch (() => { throw redirect ( "/login" ); } ); return json ( { items: items.entries } ); } 行っていることは非常にシンプルで、セッションからアクセス トーク ンを取り出してAuthorizationヘッダーに付与し、 フォルダ内の項目のリストを取得するAPI からファイル一覧を取得した後、そのまま返却しています。 クライアント側 クライアント側では、ホームページにファイル一覧を取得し、それを表示する処理を記述します。 src/routes/index.tsx を下記の内容で作成します。 src/routes/index.tsx import { Title, useNavigate, useRouteData } from "solid-start"; import axios from "axios"; import { createServerData$, redirect } from "solid-start/server"; export function routeData() { return createServerData$(async (_, { request }) => { const user = ( await axios.get("http://localhost:3000/api/user/me", { headers: { Cookie: request.headers.get("Cookie") }, }) ).data; if (!user.userName) throw redirect("/login"); + const items = ( + await axios.get("http://localhost:3000/api/file", { + headers: { Cookie: request.headers.get("Cookie") }, + }) + ).data; - return { userName: user.userName }; + return { userName: user.userName, items: items.items }; }); } export default function Home() { const serverData = useRouteData<typeof routeData>(); const navigate = useNavigate(); return ( <main> <Title>Hello World</Title> <h1>Hello world!</h1> <p>Hello, {serverData()?.userName}!</p> + <ul> + {serverData()?.items.map((item: any) => ( + <li>{item.name}</li> + ))} + </ul> <button onClick={() => { axios.post("http://localhost:3000/api/auth/logout").finally(() => { navigate("/login"); }); }} > logout </button> </main> ); } こちらも実装としてはシンプルで、ホームページ内の routeData 関数内で、 /api/file にGETリク エス トを投げてファイル一覧情報を取得し、DOM内でmap関数を用いて一覧表示しています。 以上で実装は完了となります。 各画面一覧 それでは、最後にアプリケーションを立ち上げて各画面の確認を行いましょう。 pnpm run dev を実行してアプリケーションを立ち上げ、 http://localhost:3000 にアクセスします。 ログイン画面 ログイン画面 認証前はホーム画面にアクセスしてもリダイレクトされて、ログイン画面が表示されます。画面中央の「login」ボタンをクリックします。 認証画面(外部サイト) 認証画面 「login」ボタンクリック後、Boxの認証画面にリダイレクトされます。そこでログイン情報を入力して「Authorize」ボタンをクリックし、次のページで「Grant access to Box」ボタンをクリックして認証を完了します。 ホーム画面 ホーム画面 認証完了後は、ホーム画面に遷移し、Boxでのユーザー情報とBoxにアップロードしたファイル名の一覧が表示されます。 以上でOAuth2.0認証クライアントの導入は完了です。お疲れさまでした。 まとめ いかがだったでしょうか。私個人としては認証ライブラリを用いずに自前で実装したことで、OAuth2.0認証の理解を深めることが出来て良かったと思います。 読んでいただいている皆様にも同じように思っていただけたら光栄です。 今回はOAuth2.0認証クライアントの導入ということで、リフレッシュ トーク ンやエラーハンドリング周りの細かい実装、 API の型定義等は行いませんでしたが、もしライブラリを使わない実装を検討している方は是非そちらも実装してみてください。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは!2022年度新卒で楽楽精算開発課に配属されましたThinhと申します。 今回は 初めてJenkinsを使用する方に向けて、初期設定〜パイプラインの作成手順を紹介 させていただきます。 目次 目次 Jenkinsとは Jenkinsの初期設定 Javaのインストール Jenkinsのダウンロード Jenkinsの開始 初期設定 ジョブの作成 ジョブの構成 ジョブのビルド Jenkinsパイプラインを作成 Jenkinsパイプラインの作成手順 Jenkinsを他のツールと統合 Jenkinsを使用する際の注意点 参考情報 Jenkinsとは CIの中心となるのがJenkins Jenkinsは、ソフトウェア開発の様々なタスクを自動化するために広く使用されている オープンソース の自動化サーバです。ソフトウェアの構築、テスト、展開を自動化するためのプラットフォームを提供し、チームが 開発プロセス を合理化し、ソフトウェア配信を加速できるようにします。 Jenkinsは複数のプラットフォームと プログラミング言語 をサポートしているため、開発者にとって汎用性の高いツールとなっています。 Jenkinsはインストールが簡単で、数分でセットアップできます。インストールが完了すると、開発者はJenkinsでジョブを作成して、コードのビルド、テストの実行、本番環境へのコードのデプロイなどのタスクを自動化できます。 Jenkinsは プラグイン の使用もサポートしています。 プラグイン は追加機能を提供し、開発者が特定のニーズを満たすためにJenkinsの機能を拡張できるようにします。 Jenkins の重要な機能の 1 つはパイプライン機能です。これにより、開発者はコードのチェックアウトから展開まで、ソフトウェア 開発プロセス 全体を自動化できます。プロセスが明確に視覚的に表現されるため、ビルドと展開のステータスを簡単に追跡できます。 Jenkinsの初期設定 Java のインストール Jenkinsを実行するマシンに Java をインストール必要があります。 Java の最新バージョンは、 Javaの公式Webサイト からダウンロードできます。 Jenkinsのダウンロード Jenkinsは、Jenkinsの 公式Webサイト から、 スタンドアロン アプリケーションとして実行できるwarファイルの形式でダウンロードできます。 www.jenkins.io Jenkinsの開始 ターミナル又は コマンドプロンプト で次のコマンドを実行してJenkinsを開始します。 java -jar jenkins.war これにより、ポート8080でJenkinsサーバが起動します。 初期設定 Webブラウザ を開き、 http://localhost:8080 に移動します。指示に従って、管理者パスワードの設定や プラグイン のインストールなど、初期設定を完了します。 Jenkinsの初期画面 ジョブの作成 Jenkinsのセットアップが完了したら、ジョブを作成してタスクを自動化できます。Jenkinsのジョブは、Jenkins ダッシュ ボードから「新規ジョブ作成」リンクをクリックして作成できます。作成するジョブのタイプを選択し、ジョブの名前を指定します。 ジョブの構成 ソースコード リポジトリ 、ビルドトリガー、及びビルドステップを指定して、ジョブを構成します。通知の送信や別のジョブのトリガーなど、ビルド後のアクションを指定することもできます。 ジョブのビルド ジョブが構成されたら、Jenkins ダッシュ ボードの「今すぐビルド」ボタンをクリックして、ジョブをビルドできます。これによりビルドプロセスがトリガーされ、ジョブ構成で指定されたステップが実行されます。 ダッシュ ボードに実行したジョブが表示されています。 Jenkinsパイプラインを作成 Jenkinsパイプラインの作成手順 サーバにJenkinsをインストール、もしくは クラウド ベースのJenkins インスタンス を使用します。 Jenkins Web インターフェイス にログインし、「新規ジョブ作成」ページに移動します。 パイプラインに名前を付け、プロジェクトタイプとして「パイプライン」を選択し、「OK」をクリックします。 パイプラインセクションで「パイプライン スクリプト 」を選択し、テキストボックスにコードを入力するか、ソース管理 リポジトリ でJenkinsfileを指定します。 パイプライン スクリプト は、ビルド、テスト、デプロイなど、パイプラインのステージとステップを定義する必要があります。JenkinsfileRunnerやPipeline プラグイン などを使用して、パイプラインの機能を強化できます。 パイプラインを保存して実行し、結果を確認します。 プロジェクト構成ページの「パイプライン」セクションで、ビルドトリガー、電子メール通知、 環境変数 などの設定を構成することもできます。 Jenkinsfileにバージョン管理(Gitなど)を使用して、変更の追跡やほかのチームメンバーとのコラボレーションを容易にすることもオススメします。 Jenkinsを他のツールと統合 Jenkinsを様々なツールと統合して、その機能を強化し、ソフトウェア開発ワークフロー全体を改善することができます。Jenkinsと統合できる一般的なツールを紹介させていただきます。 変更の追跡とコードのコラボレーションのためのGit, SVN などの バージョン管理システム 。 Maven ,Gradleなどの、コード コンパイル 及びビルドツール。 テストケースを自動化するための Junit , Selenium などのテストツール。 静的コード分析とコード品質測定のための CheckStyle , SonarQubeなどのコード品質ツール。 チーム間のコミュニケーションとコラボレーションのためのSlack、 Microsoft TeamsなどのChatOpsツール。 これらのツールをJenkinsと統合するには、Jenkins プラグイン リポジトリ で利用可能な プラグイン を使用するか、カスタム スクリプト を記述します。Jenkins Web インターフェイス で統合設定を構成することもできます。 Jenkinsを使用する際の注意点 パフォーマンスを監視する Jenkins インスタンス のパフォーマンスを定期的に監視し、パフォーマンスを最適化するために、遅いビルドや高いリソース使用率などの ボトルネック を特定します。 クラウド リソースを使用する Jenkins インスタンス がオンプレミスで実行されている場合は、 クラウド リソースを使用して、必要に応じて インスタンス をスケールアップおよびスケールダウンすることを検討してください。 適切なハードウェアを使用する Jenkins マスターとエージェントに、ワークロードを処理するのに十分なリソース (CPU、メモリ、ストレージなど) があることを確認してください。 セキュリティ対策を実装する アクセス制御、暗号化、バックアップなどのセキュリティ対策を実装して、Jenkins インスタンス とデータを保護します。 パイプラインの最適化 並列ステージを使用してパイプラインを最適化し、複雑なタスクを小さなステップに分割してビルド時間を短縮します。 プラグイン を最新の状態に保つ Jenkins プラグイン を最新の状態に保ち、最新の機能とセキュリティ アップデートを確実に使用できるようにします。 参考情報 Jenkins公式サイト Jenkinsのダウンロード Jenkins Wiki 日本Jenkinsユーザ会 図書 www.amazon.co.jp Jenkinsの学習動画コース www.udemy.com エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、主催イベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、フロントエンドチームの亀ノ上です。 最近は画像生成AIやテキスト生成AIなど、AIによる自動生成に関する話題をよく目にします。特にここ最近は ChatGPT の勢いが凄まじく、毎日のようにニュースを見かけるような気もしています。 今回はそんな ChatGPT でも使用されている 言語モデル である GPT-3 を用いて、 Nuxt3 で簡単なテキスト生成アプリを作成します。 Nuxt3について Nuxt3とは Nuxt3の機能 Nuxt3インストール インデックスページの設定をする フロントエンド開発 バックエンド開発 OpenAIのAPI Keysを作成 フロントエンドとバックエンドを連携 おわりに 参考 Nuxt3について Nuxt3とは Nuxt3 とは、 Vue.js アプリケーションを構築するための フレームワーク です。特徴としては、高速な開発、シンプルな設計、高機能な機能などがあげられます。 Nuxt3の機能 今回 Nuxt3 を選択した理由として、 Nuxt3 には、サーバーサイド レンダリング や API Routes などの機能が備わっています。 API Routes は、アプリケーション内で API エンドポイントを定義し、簡単に呼び出すことができる機能です。この機能を使うことで、 API 開発とフロントエンド開発を効率的に行うことができます。 Nuxt3インストール それではまずは Nuxt3 をインストールしていきます。 公式 npx nuxi init gpt3-app 🎉 Another prime Nuxt project just made! Next steps: 📁 ` cd gpt3-app` 💿 Install dependencies with `npm install` or `yarn install` 🚀 Start development server with `npm run dev` or `yarn dev` インストールが終わったら、 ディレクト リを移動してパッケージをインストールします。 cd gpt3-app `npm install` or `yarn install` 今回は yarn install ですすめます。 ... success Saved lockfile. $ nuxt prepare Nuxi 3 . 1 . 2 ✔ Types generated in .nuxt ✨ Done in 113 .76s. パッケージのインストールが終わったら、 Nuxt を起動してみます。 yarn dev Nuxi 3 . 1 . 2 Nuxt 3 . 1 . 2 with Nitro 2 . 1 . 2 > Local: http://localhost:3000/ > Network: http://xxx.xx.xx.xxx:3000/ ℹ Vite client warmed up in 1388ms ✔ Nitro built in 421 ms http://localhost:3000/ にアクセスします。 Nuxt の画面が表示されていればOKです! インデックスページの設定をする 次にコードの方を修正していきます。 現在表示されている画面は app.vue の内容になります。 app.vue < template > < div > < NuxtWelcome /> </ div > </ template > まずは自身で作ったページを表示するために pages/index.vue を作成します。 pages/index.vue ファイルは、アプリケーションの / ルートに自動的に設定されます。 pages/index.vue < template > < h1 > Hello Nuxt3! </ h1 > </ template > 次に app.vue を変更します app.vue < template > < div > < NuxtPage /> </ div > </ template > <NuxtPage /> は pages/index.vue が存在する場合は、 pages/index.vue を自動的に レンダリング します。 それでは一度 yarn dev で画面を確認してみましょう。 問題なく表示されていますね。 フロントエンド開発 今回の GPT-3 アプリは、キーワードを入力して、テキストを生成するといったアプリにしたいと思いますので。キーワードの入力エリアと、ボタンを作成します。 pages/index.vue < script setup> const keyword = ref ( '' ) ; const handleClick = () => { // ここに生成する処理をかく } </ script > < template > < h1 > GPT-3 APP </ h1 > < div > < input type = "text" v-model= "keyword" > </ div > < div > < button type = "button" @click= "handleClick" > テキスト生成 </ button > </ div > </ template > Nuxt3 はもちろん Vue.js ベースの フレームワーク なので Vue3 の機能をフル活用できます。 v-bind や v-model といったディレクティブから ref() などの API も使用できます。ただ、 <script setup> で import { ref } from 'vue' しなくて良いのか気になった方がいるかもしれません。 Nuxt3 では自動 import の機能が搭載されています。この機能により、 API の import 文を記述することが不要になります。また、 API だけでなく、作成した コンポーネント なども自動で import してくれるので、これにより少ないコード量で Webアプリケーションを開発することができます。 バックエンド開発 次に、 server/api/generate.post.js を作成しますが、少しだけ API Routes について説明します。 冒頭でも少しお話しましたが、 Nuxt3 は API Routes をサポートしています。 API Routes は、サーバーサイドで動作する API エンドポイントを指し、外部のデータソースからデータを取得したり、データを更新することができます。 その API Routes を実装するための ディレクト リが server/api であり、この ディレクト リ内で API を簡単に実装することができます。 それではコードを書いていきます。 server/ api /generate.post.js export default defineEventHandler(async ( event ) => { const { prompt } = await readBody( event ); const payload = { model: "text-davinci-003" , prompt , temperature: 0.7, top_p: 1, frequency_penalty: 0, presence_penalty: 0, max_tokens: 300, n: 1, } ; const response = await fetch( "https://api.openai.com/v1/completions" , { headers: { "Content-Type" : "application/json" , Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? "" } ` , } , method: "POST" , body: JSON.stringify(payload), } ); const json = await response.json(); return json; } ) 処理は以下のような流れとなっています。 フロント側から渡されたリク エス トデータを取得 OpenAI の API に渡す payload を作成 fetch メソッドで OpenAI からレスポンスオブジェクトを取得 レスポンスオブジェクトから json データを取得しフロントに返す まず GPT-3 にテキスト生成してもらうキーワードをフロント側から受け取るのですが、リク エス トデータの body を readBody(event) で取得することができます。 その後フロント側から渡されたキーワードと定義した payload を fetch メソッドへ定義して応答を待ちます。 OpenAIの API Keysを作成 バックエンドの処理で、 fetch メソッドのヘッダーに API Key を設定していました。 OpenAI の API を使用するには API Key が必要なので作成していきます。 まずは 公式サイト へ行き右上の SIGN UP をクリックしてください。 ログインができたら右上のアイコンから View API Keys をクリックします 画面中央付近にある Create new secret key をクリックします 表示された API Key をコピーして下さい。右の緑のアイコンからもコピーできます。 コピーしたら .env を作成しコピーした API Key をはりつけます OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx フロントエンドとバックエンドを連携 それでは最後にフロントエンドとバックエンドを連携して確認してみましょう。 app.vue から作成した API を実行します。 app.vue const generateText = ref( '' ) const handleClick = async () => { const { data } = await useFetch( '/api/generate' , { method: 'POST' , } ) generateText.value = data.value.choices [ 0 ] .text } < template > < h1 > GPT-3 APP </ h1 > < div > < input type = "text" v-model= "keyword" > </ div > < div > < button type = "button" @click= "handleClick" > テキスト生成 </ button > </ div > < div > < h2 > 生成テキスト </ h2 > < div > {{ generateText }} </ div > </ div > </ template > 大まかな流れとしては以下の通りです。 template で表示するために、リアクティブな変数 generateText を宣言。 useFetch で API から生成テキストを取得。 generateText にセットし、画面に表示する。 useFetch は Nuxt3 が提供しているデータを取得するための関数です。もちろん自動 import なため、 import 文は不要です。今回は POST リク エス トのため method:'POST' を指定しています。 GET リク エス トの場合は第2引数を省略できます。 useFetch で返ってくるデータですが、 data の他には error, pending, execute, refresh が含まれます。今回は data のみを使用するので、分割代入で取得しています。また、 data は、リアクティブデータである Ref で返ってくるので、 data.value で取り出すことができます。 data.value には GPT-3 で生成されたデータが含まれており、 data.value.choices[0].text で取り出します。取り出したデータは generateText にセットします ただ、現在は入力したキーワードを渡せていません。 GPT-3 から期待したテキストが生成されるように、キーワードから文章を加工して、渡すようにします。 < script setup> const keyword = ref ( '' ) ; const generateText = ref ( '' ) const prompt = computed (() => ` 日本語で回答して下さい。 ${keyword.value} について最大150文字で説明してください。 ` ) const handleClick = async () => { const { data } = await useFetch ( '/api/generate' , { method: 'POST' , body: { prompt } } ) generateText.value = data.value.choices [ 0 ] .text } </ script > < template > < h1 > GPT-3 APP </ h1 > < div > < input type = "text" v-model= "keyword" > </ div > < div > < button type = "button" @click= "handleClick" > テキスト生成 </ button > </ div > < div > < h2 > 生成テキスト </ h2 > < div > {{ generateText }} </ div > </ div > </ template > それでは実行してみましょう(最大1分ほどかかる場合があります) キーワードを元にテキストの生成ができました! おわりに 今回触ってみて感じましたが、 GPT-3 アプリ作成は Nuxt3 の機能を理解するのにちょうどよい題材だと思いました。自動 Import についてや、特に API Routes について知識を深めることができていい経験になりました。 デザインを整えたり、生成履歴を残して ChatGPT 風にしたり、テキスト生成の待機中の状態を表示したり…など、まだまだできることが多いと思います。よかったら参考にいろいろ試してみて下さい。 参考 vercel.com エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、tatsumiです。 私は普段、チャットディーラーAIという製品の開発に携わっています。 www.chatdealer.jp 先日、チャットディーラーAIに シングルサインオン の機能を実装したのですが、機能開発を行うまでは シングルサインオン の仕組みについて何となくでしか理解していませんでした。 機能開発を行うにあたって、 シングルサインオン の仕組みを調べたので、当ブログでまとめたいと思います。 シングルサインオン(SSO)とは? シングルサインオン(SSO)の認証方式と仕組み 代行認証方式 リバースプロキシ方式 エージェント方式 フェデレーション方式 まとめ シングルサインオン (SSO)とは? シングルサインオン (SSO)とは、1つのIDとパスワードで複数のサービスにログインできる仕組みのことです。 通常であれば、サービスごとに個別にログインが必要で、IDやパスワードもそれぞれで設定・管理しなければならず、ユーザーにとって大きな負担となります。 シングルサインオン を利用すれば、このような負担が解消されます。 シングルサインオン (SSO)の認証方式と仕組み シングルサインオン を実現する方法として、5つの認証方式があります。 それぞれの仕組みと特徴を紹介します。 代行認証方式 代行認証方式は、ユーザーが利用する端末に導入したエージェントが、対象システムのログイン画面を監視して、ログイン画面が起動したらユーザーの代わりにID/パスワードを自動入力して認証する仕組みです。 リバースプロキシ方式 リバースプロキシ方式は、リバースプロキシと呼ばれる中継サーバを介して認証を行う方式です。 PCからリバースプロキシサーバに対してWeb認証を実施すると、リバースプロキシサーバから認証済みの Cookie が発行されます。 その後、ログインしたい各サービスにおいて、リバースプロキシサーバを経由し、対象のシステムにアクセスすることで シングルサインオン でのログインを行います。 エージェント方式 エージェント方式は、Webサーバや アプリケーションサーバ にエージェントを導入する方式です。 代行認証方式では、ユーザーが利用する端末にエージェントを導入しますが、エージェント方式の場合は対象のシステム側にエージェントを導入します。 初回ログイン時にSSOサーバから Cookie が発行されます。 2回目以降のログイン時や他システムへのログイン時には、システム側のエージェントがユーザーが保持している Cookie を利用してSSOサーバーにユーザー認証を行うことで シングルサインオン を実現します。 フェデレーション方式 フェデレーション方式は、対象サービス(SP)と認証を行うサービス(IdP)の間で認証情報をやり取りする方式です。 IdPにログインすれば、各サービスはIdPから認証情報を取得することで、ユーザーは各サービスをログインなしで利用することができます。 フェデレーション方式では、 SAML や OpenID Connectといった プロトコル がよく利用されます。 ラク スのサービスでは、楽楽精算、楽楽販売、メールディーラー、チャットディーラーAIといったサービスで シングルサインオン を利用することができますが、何れも SAML 認証によるフェデレーション方式で シングルサインオン を実現しています。 まとめ 今回の記事で紹介した通り、一口に シングルサインオン と言っても様々な方式が存在しています。 導入する際は、それぞれの方式のメリット/デメリットを把握した上でどの方式を採用するか判断した方がよいでしょう。 次回は、私が開発に携わっているチャットディーラーAIを含め、 ラク スのその他サービスで実装されている SAML 認証について詳しく解説したいと思います。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは! ラク ス1年目の koki _matsuraです。 今回は 掲示 板アプリ作成を通して、SvelteKitの基礎的な部分 をご紹介させていただきます。 目次は下記のようになっています。 はじめに Svelteとは SvelteKitとは 掲示板アプリ作成 アプリの概要 環境構築 SvelteKit データベース Prisma テーブル作成 ルーティング作成 新規登録画面 ログイン画面 スレッド投稿画面 スレッド一覧画面 スレッド詳細画面 ログアウト機能 エラー画面 終わりに はじめに Svelteとは WebアプリケーションやUIを構築するための JavaScript フレームワーク です。有名なものでは「React」や「Vue」が挙げられます。 Svelteにはこれらの フレームワーク と比べて下記のような特徴があります。 仮想DOMを用いない こちらが最も大きな特徴となります。 まず、仮想DOMとはその名前の通りプログラムの中で作られる仮のDOMのことを言います。従来の フレームワーク では変更点があれば毎回DOM全体を構築していましたが、仮想DOMは下図のように変更前と変更後の仮のDOMを比較し、差分を検出することで変更部分のみを構築でき、表示速度も速くなります。 しかし、Svelteでは仮想DOMを用いていません。従来のように変更のたびに全DOMを再構築しているわけでもありません。 代わりに コンパイル 時に特殊なことをします。 具体的にはビルド時にDOMの構造を解析し、変更の可能性がある部分をVanilla JSのコードとして落とし込む最適化を行います。そして、データに変更があれば該当のJSコードを動かし、実際のDOMを変更します。なので、仮想DOMを必要とせずに差分のみを変更することができ、高速な レンダリング を行えます。 また、JSファイルに全てのロジックが記述されているのでランタイムが不要となり、軽量です。 これらのことから分かる通り、Svelteはビルド時にJSコードを生成する「 コンパイラ 」です。 シンプルな構文で記述できる Svelteではボイラープレートを記述する必要がなかったり、変数や関数などのアプリケーションの状態を自動的に管理するため状態管理専用ライブラリが入りません。 よって、記述するコードがほかの フレームワーク より少なくて済みます。 また、テンプレートベースの シンタックス を使用してUIを記述するため、 JavaScript とHTMLの マークアップ を分離することができ、かなりコードが見やすくなります。 真にリアクティブである Svelteは、必要なデータを持つ変数を定義し、その変数が変更された時、自動的に画面に反映するといった仕組みを備えています。 このリアクティブな仕組みにより、変更を手動で書かなくて済み、結果的にコードを書く量が減り、開発効率が向上するといったメリットがあります。 SvelteKitとは Svelteを使ってWebアプリケーションを開発するための フレームワーク です。ReactにとってのNext、VueにとってのNuxtのようなものです。 開発する上で必要となる環境を自動でセットアップし、アプリケーション開発の作業を簡素化してくれます。具体的には ルーター や SSR の基本的なものからビルドの最適化、プリロード、柔軟な レンダリング の設定など高速なページロードをするための機能です。 多くのサポートがあるため、開発者はアプリケーションの開発に集中することができます。 掲示 板アプリ作成 ここからは実際にSvelteKitを使って、簡単な 掲示 板アプリを作成します。 ORMとして Prisma を使います。 アプリの概要 作成する 掲示 板アプリで開発する機能は次のようになっています。 ユーザー登録 ログイン・ログアウト スレッド投稿 コメント投稿 必要となるページは次のようになっています。 新規登録画面 ログイン画面 スレッド一覧画面 スレッド詳細画面 スレッド投稿画面 エラー画面 かなりシンプルな 掲示 板アプリです。 環境構築 SvelteKit 早速ですが、SvelteKitでプロジェクトを作成しましょう。ターミナルを開き、プロジェクトを作成する ディレクト リに移動し、下記のコマンドを打ちます。 プロジェクト名は「board-app」としましたが、お好きな名前で構いません。 npm create svelte@latest [プロジェクト名] 作成時にテンプレートや、JSかTSか、ESLintを使うか、Prettierを使うか、テストにPlaywright、Vitestを使うか聞かれます。今回の場合はテンプレートに「Skeleton project」を選択し、JSかTSかは「TypeScript」を選択します。その他は好きに選んでいただいて構いません。 プロジェクトが作成されれば、次やるべきことがターミナルに表示されます。 僕のターミナルには下記のように表示されました。 Next steps: 1: cd board-app 2: npm install (or pnpm install, etc) 3: git init && git add -A && git commit -m "Initial commit" (optional) 4: npm run dev -- --open プロジェクトに移動し、「npm install」をしましょう。Nextを使ってると、そのまま4に行けるのですがSvelteKitの場合は一旦、インストールを挟まないといけないようです。 「npm run dev -- --open」をコマンドで打つと、自動で開きます。下記のような初期画面が開いていれば成功です。 SvelteKitの環境構築は終わりです。 データベース 僕はデータベースにSupabaseを使います。特に理由はありません。なので、他の方法でデータベースを作成していただいても大丈夫です。 もし、Supabaseをこれから使いたいという方は僕が過去に書いた記事を参考にしていただければと思います。 この記事のSupabaseでプロジェクトを作成する部分までで大丈夫です。テーブルの定義は Prisma で行います。 ※ Supabaseのプロジェクト作成の際に設定するパスワードを忘れないようにしてください。 tech-blog.rakus.co.jp Prisma Prisma をインストールします。下記のコマンドをターミナルに入力します。 npm install prisma --save-dev npx prisma init コマンド入力後にプロジェクトに prisma ディレクト リが作成され、中には「schema. prisma 」が作成されていると思います。 providerは合ったものにしてください。僕はSupabaseを使用しているので postgresql となります。 urlは「.env」ファイルから「DATABASE_URL」という変数の値を持ってきています。 DATABASE_URLは未設定ですが、テーブル作成の際にSupabaseから取得してきたいと思います。 // schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } 一旦、必要なものは揃ったので環境構築は終わりです。 テーブル作成 Svelteを使ってガンガン開発していきたいところですが、まずはテーブルの作成をしていきます。先ほど自動で作成された「schema. prisma 」を開きます。 今回の 掲示 板アプリに必要なのは「User」「Post」「Comment」です。それぞれユーザのテーブル、投稿されたスレッドのテーブル、スレッドに対するコメントのテーブルになっています。 「schema. prisma 」に下記のようにテーブル定義します。 // 省略 model User { id String @id @default(uuid()) name String @unique password String authToken String @unique created_at DateTime @default(now()) Comment Comment[] Post Post[] } model Post { id Int @id @default(autoincrement()) userId String content String created_at DateTime @default(now()) Comment Comment[] user User @relation(fields: [userId], references: [id]) } model Comment { id Int @id @default(autoincrement()) userId String postId Int content String created_at DateTime @default(now()) post Post @relation(fields: [postId], references: [id]) user User @relation(fields: [userId], references: [id]) } テーブルが定義されたので、データベースに登録したいところですが、「DATABASE_URL」が未設定なのでその値を取得します。 Supabaseのプロジェクトのサイドバーから「Project Settings」-> 「Database」に移動します。下記のような画面が表示されていれば大丈夫です。 画面の下の方にデータベースの URI がありますのでコピーしましょう。 この URI の[YOUR-PASSWORD]と書かれている部分に自分が設定したパスワードを入力します。 そして、「.env」のDATABASE_URLにペーストします。 // .env DATABASE_URL= [コピーしたデータベースのURI] 準備が整ったので、ターミナルで下記のコマンドを打ちます npx prisma migrate dev --name init 成功すれば、 prisma ディレクト リ下にmigrationsフォルダが作成されます。また、Supabaseの方にもテーブルが作成されていることも確認できます。 データベースを操作する時には「PrismaClient」が必要となるので、 src 下に lib/prisma.ts を作成して、「 prisma .ts」に次のように記述ます。 // src/lib/prisma.ts import { PrismaClient } from "@prisma/client" ; export const db = new PrismaClient () これで、 Prisma によるデータベース操作の際はこのファイルをインポートすればすぐに行えます。 ルーティング作成 では、最初にアプリケーションのルーティングを作っていきます。 SvelteKitは src/routes がデフォルトのルートです。つまり、routes直下に配置されたページは「/」で表示されます。 src/routes/example の直下に配置されたページは「/example」で表示されます。 そして、ページを表すファイルは「+page.svelte」で定義できます。 実際に初期起動時に表示されたページは src/routes/+page.svelte の内容となっています。 すごくシンプルなので簡単にルーティングを作成できます。 改めてこの 掲示 板アプリで必要な画面は下記のようになっています。 新規登録画面 ログイン画面 スレッド一覧画面 スレッド詳細画面 スレッド投稿画面 特に複雑な構造になっていないので、下記のように src/routes 以下にフォルダを作成していきましょう。 一覧画面はデフォルトのルートに表示するため、フォルダは作成しなくて問題ありません。 詳細画面となる detail/[postId] の[postId]は動的なルーティングに対応します。 例えば、「/detail/1」や「detail/sample」などを入力しても detail/[postId] 直下にある「+page.svelte」が表示されます。 ルーティングは完成しました。 新規登録画面 最初に新規登録画面を作成いたします。 /register 下に「+page.svelte」を作り、下記のような登録フォームを作ります。 デザインは全く取り入れてないので少し見にくいかもです。 // register/+page.svelte < h1 > ユーザー登録 < /h1 > < form method = "post" action = "?/register" > < label > 名前: < input name = "name" type= "text" / > < /label > < label > パスワード: < input name = "password" type= "password" / > < /label > < button > 登録する < /button > < /form > 当然ながらこれではボタンを押してもリク エス ト先がないため400系のエラーしか返ってきません。 サーバサイドのロジックを提供するために必要なのは「+page.server.ts」です。同じ ディレクト リ内に作るだけで大丈夫です。 余談ですが、SvelteKitのページは「+page.ts」「+page.server.ts」によって支えられています。「+page.ts」はクライアントサイドで実行されます。クライアント側のロジックやユーザインタ ラク ションの処理に用いるのが推奨されます。「+page.server.ts」はサーバー側で実行されます。 SSR のロジックや、データベースからデータを取得する処理などを記述することが推奨されます。 「+page.server.ts」に次のように記述します。 // register/+page.server.ts import { fail , redirect } from "@sveltejs/kit" ; import type { Actions } from "./$types" ; import bcrypt from 'bcrypt' import { db } from "$lib/prisma" ; export const actions: Actions = { register : async ( { request } ) => { const data = await request.formData (); const name = data. get( "name" ); const password = data. get( "password" ); if (typeof name !== "string" || typeof password !== "string" || ! name || ! password ) { return fail ( 400 , { message: "名前・パスワードは必須です。" } ) } const user = await db.user.findUnique ( { where: { name } } ) if ( user ) { return fail ( 400 , { message: "既に存在するユーザーです。" } ) } await db.user.create ( { data: { name , password : await bcrypt.hash ( password , 10 ), authToken: crypto.randomUUID (), } , } ) throw redirect ( 303 , '/login' ) } } actionsには様々なサーバー側の処理を記述します。型はActionsです。Actionsのインポート先である ./$types はSvelteKitの コンポーネント や関数の型情報を提供しています。そして、Actionsはイベントや条件に対応して実行される関数です。 上記のコードでは「register」が定義されています。これは「+page.svelte」のformタグのactionの値に対応します。 引数にはrequestを入れていますが、他には cookie やsetHeaderなどサーバーサイドでの処理に必要な情報が提供されます。 具体的なコードの説明としては、下記のようになります。 1. request.FormData()でformで入力した名前とパスワードのデータを取得します。 2. data.get([inputのname値])で各々のデータを取得します。 3. 型のチェックと入力のチェックをします。もし、未入力の場合はfail関数を返します。第一引数に ステータスコード を第二引数に任意のデータをオブジェクトで入れます。 4. チェックが問題なければ、 Prisma を使って、既に存在する名前かデータベースに検索をかけます。存在しているならfail関数を返します。 5. 存在していないユーザーならCreateで名前とハッシュ化したパスワード、 トーク ンを登録します。 6. 全て完了すれば、redirect関数でログイン画面にリダイレクトさせます。 SvelteKitはデータベースへの接続を自動で切ってくれるのでdisconnectの処理を入れなくても問題ありません。 「+page.server.ts」での処理も書き終わりました。 登録ができて、ログイン画面のURLに飛ばされれば大丈夫です。僕は名前「user」パスワード「password」で登録すると、Supabaseの方にも反映されていました。 正常処理は実装できましたが、存在するユーザー名を入力した時に返されてくるfail関数の処理を実装できていません。 「+page.svelte」に下記のコードを追加するだけで簡単に登録バリデーションが実装できます。 // register/+page.svelte < script lang = "ts" > import type { ActionData } from "./$types" export let form: ActionData < /script > < h1 > ユーザー登録 < /h1 > { # if form?.message } < p class= "error" > { form?.message } < /p > { / if } /* HTML省略 */ < style > .error { color: red ; } < /style > actionはリク エス ト処理後、次の更新まで対応のページのformプロパティにデータを返します。つまり、fail関数の第二引数のデータはformに返されます。なので、form.messageでバリデーションメッセージを取り出せます。 実際に存在する名前を入力すると、次のように表示されると思われます。 これで新規登録画面が作成できました。 ログイン画面 ログイン画面を作ります。 まずは /login 下に「+page.svelte」を作成し、ログインフォームを作ります。 登録フォームとほとんど一緒です。今回は「+page.server.ts」からくるバリデーションメッセージを想定して、formを用意しておきます。 // login/+page.svelte < script lang = "ts" > import type { ActionData } from './$types' ; export let form: ActionData < /script > < h1 > ログイン画面 < /h1 > { # if form?.message } < p class= "error" > { form.message } < /p > { / if } < form method = "POST" action = "?/login" > < label > 名前: < input name = "name" type= "text" / > < /label > < label > パスワード: < input name = "password" type= "password" / > < /label > < button > ログイン < /button > < /form > < style > .error { color: red ; } < /style > このログイン機能ではセッションを使うので、「+page.server.ts」では、引数にrequestだけでなくcookiesを取り入れます。 // login/+page.server.ts import { db } from "$lib/prisma" ; import { type Actions , fail , redirect } from "@sveltejs/kit" ; import bcrypt from "bcrypt" ; export const actions: Actions = { login: async ( { request , cookies } ) => { const data = await request.formData (); const name = data. get( "name" ); const password = data. get( "password" ); if (typeof name !== "string" || typeof password !== "string" || ! name || ! password ) { return fail ( 400 , { message: "名前とパスワードを入力してください" } ) } const user = await db.user.findUnique ( { where: { name } } ) if ( ! user ) { return fail ( 400 , { message: "名前またはパスワードを間違えています" } ); } const correctPassword = await bcrypt.compare ( password , user.password ) if ( ! correctPassword ) { return fail ( 400 , { message: "名前またはパスワードを間違えています" } ) } const authenticatedUser = await db.user.update ( { where: { name } , data: { authToken: crypto.randomUUID () } } ) cookies. set( 'session' , authenticatedUser.authToken , { path: '/' , maxAge: 60 * 60 * 24 * 30 , } ) throw redirect ( 303 , '/' ) } } 前半部分は入力に対してのバリデーション、そして次に名前でUserテーブルに検索をかけ、存在すれば、パスワードを一致するか比較する。 パスワードも一致していれば、該当のユーザーのauthTokenを更新する。 そして、最後にcookiesにauthTokenをセットします。setの第一引数には cookie の名前、第二引数には値を、そして第三引数にはオプションをセットします。オプションにはいろいろありますが、今回はアプリ全体で使用したいのでpathを「/」にし、有効期限は1ヶ月にしました。 ログイン画面で名前に先ほど登録した名前、パスワードでログインしましょう。 成功すれば初期画面にリダイレクトされ、 cookie が登録されるはずです。 ログイン機能ができましたが、今のままではログインを通らずに一覧を見れてしまうため、 cookie の意味がありません。 なので、ログインと新規登録画面以外には cookie が登録されていない場合には遷移できないようにします。 「ログイン・新規登録画面以外に cookie 登録されていない場合」のような処理を書くのはあまりにも非効率です。一度に全ての画面に処理を反映したいですよね。 そのような時はルート ディレクト リ src/routes に「+layout.server.ts」を作成します。そして下記のようなコードを記述してください。 // +layout.server.ts import { redirect } from "@sveltejs/kit" ; import type { LayoutServerLoad } from "./$types" ; export const load: LayoutServerLoad = async ( { url , cookies } ) => { const session = await cookies. get( 'session' ) if( ! session && ( url.pathname !== '/login' && url.pathname !== '/register' )) { throw redirect ( 303 , '/login' ) } } 「+layout.server.ts」は作成された ディレクト リ以下の全てに処理を入れることができるファイルです。なので、遷移するたびに挟みたい処理などはlayoutファイルを使うことが推奨されます。 後にHeader作成時にも使いますが「+layout.svelte」を使うと同様にその ディレクト リ以下、全てにファイルの情報が共有されます。共通部分を作るときに推奨されます。 load関数はページが読み込まれる前に行う処理を記述できます。 cookiesにsessionが登録されていないときにログイン画面と登録画面以外に遷移すると強制的にログイン画面にリダイレクトされるようにしました。 これでログインせずに一覧へ遷移することはできなくなりました。 スレッド投稿画面 スレッドの投稿画面を実装します。 順序的には一覧画面から実装するべきかもしれませんが、一覧は投稿がなければ表示するものがないので、先に投稿できるようにします。 post/ 下に「+page.svelte」「+page.server.ts」を作成します。 「+page.svelte」は次のようなコードにします。コードは登録画面やログイン画面とあまり変わりません。 // post/+page.svelte < script lang = "ts" > import type { ActionData } from "./$types" export let form : ActionData < /script > < h1 > スレッド投稿 < /h1 > { # if form?.message } { form.message } { / if } < form method = "POST" action = "?/post" > < label > 内容: < input name = "content" type= "text" / > < /label > < button > 投稿する < /button > < /form > 「+page.server.ts」は少し考えなければなりません。 スレッドを投稿するためには、postテーブルにデータを渡すのですが、登録にはuserIdが必要となります。もちろん、userテーブルからauthTokenで検索をすれば解決する話です。しかし、コメント投稿の際にも同じようにuserIdが必要になります。このような部分は共 通化 しましょう。 src 下に「hooks.server.ts」を作成します。「hooks.server.ts」はサーバーがリク エス トを受けるたびに実行されます。このファイル内でユーザー情報を検索し、アプリケーション内で共有できるようにすれば、何度もuserを検索するコードを書く手間が省けます。 「hooks.server.ts」は次のように記述してください。 // src/hooks.server.ts import { db } from "$lib/prisma" ; import type { Handle } from "@sveltejs/kit" ; export const handle: Handle = async ( { event , resolve } ) => { const session = event.cookies. get( 'session' ); if ( ! session ) { return await resolve ( event ) } const user = await db.user.findUnique ( { where: { authToken: session } , select: { id: true , name: true } } ) if ( user ) { event.locals.user = { id: user.id , name: user.name } } return await resolve ( event ) } userテーブルから検索をするためにsessionの値が必要となるのでcookiesを取得します。また、検索をするときはユーザー情報を全て取り出すのではなく、IDと名前だけ取り出します。 そして、event.localsにuser情報を格納します。localsに格納した情報はアプリ内で使うことが可能になります。 TypeScriptで書いている方はuserを型定義しないといけません。「app.d.ts」を次のように書き換えてください。 // src/app.d.ts declare global { namespace App { // interface Error {} interface Locals { user: { id: string , name: string } } // interface PageData {} // interface Platform {} } } export {} ; これにより、「+page.server.ts」は次のように書くだけで投稿することができるようになります。 // post/+page.server.ts import { db } from "$lib/prisma" ; import { fail , redirect , type Actions } from "@sveltejs/kit" ; export const actions: Actions = { post : async ( { request , locals } ) => { const data = await request.formData () const content = data. get( "content" ); if (typeof content !== "string" || ! content ) { return fail ( 400 , { message: "タイトルと内容は必須入力です。" } ) } if ( ! locals.user ) return fail ( 400 , { message: "登録されていないユーザーです。" } ) await db.post.create ( { data: { userId: locals.user.id , content } } ) throw redirect ( 303 , '/' ) } } sessionを取得せずに、localsからuserIDを取り出して、postテーブルのuserIdに割り当てています。 投稿が成功すれば、一覧へリダイレクトします。 これで投稿画面が完成しました。 スレッド一覧画面 スレッドを一覧できる画面を実装します。 一覧では、それぞれのスレッドを コンポーネント を使って表示してみたいと思います。 src/lib に「Thread.svelte」を作成してください。コードは次のようにしてください。 // lib/Thread.svelte < script lang = "ts" > export let id : number ; export let content: string ; < /script > < div class= "thread" > < div class= "thread-content" > < h2 > { content } < /h2 > < a href = "detail/{id}" > スレッドの詳細を見る < /a > < /div > < /div > < style > .thread { display: flex ; flex-direction: row ; width: 100 % ; margin: 10px auto ; box-shadow: 0 10px 20px rgba ( 0 , 0 , 0 , 0.19 ), 0 6px 6px rgba ( 0 , 0 , 0 , 0.23 ); } .thread-content { width: 100 % ; margin-left: 10px ; } .thread-content a { display: inline-block ; font-size: 14px ; color: blue ; text-decoration: none ; } < /style > 少しだけデザインをしてみました。目的は コンポーネント を作ることなので正直デザインはどちらでも大丈夫です。 大事なポイントはIDとContentを変数で宣言し、それをHTML内で使用していることです。ReactのPropsと同じです。 では一覧を作成していきます。 src/routes 下にデフォルトで作成されていた「+page.svelte」のコードを下記のように書き換えてください。 // +page.svelte < script lang = "ts" > import { goto } from "$app/navigation" ; import type { PageData } from "./$types" ; import Thread from "$lib/Thread.svelte" ; export let data: PageData < /script > < main > < h1 > スレッド一覧 < /h1 > < button type= "button" on:click = { () => goto( "/post" ) } > 投稿する < /button > { #each data.threads as thread } < Thread id = { thread.id } content = { thread.content } / > { /each } < /main > Thread コンポーネント は他と同じようにインポートするだけで使えるようになります。 PageData型はサーバーサイドで生成されたデータを、クライアント側で使用するためのものです。なので、dataにはサーバー側からスレッド一覧のデータが送られてくることを想定しています。 HTMLには「投稿する」ボタンとeach文でスレッド コンポーネント を並べていきます。Propsと同じでidにthread.idを、contentにthread.contentを渡しています。 「投稿する」ボタンをクリック時には、goto関数で投稿画面へナビゲーションをしています。 次に、「+page.server.ts」を作成して次のようなコードにしましょう。 // +page.server.ts import { db } from "$lib/prisma" import type { PageServerLoad } from "./$types" ; export const load: PageServerLoad = async () => { const threads = await db.post.findMany ( { orderBy: { id: 'desc' } , } ); return { threads } } データを取得したいタイミングは一覧表示前なので、PageServerLoad型を使います。 そして、postテーブルからID降順で全件取得するだけです。 うまくいけば、次のように投稿した内容がThread コンポーネント を介して表示されるはずです。 また、「スレッドの詳細を見る」を押すと、「/detail/[id]」に飛ぶことも確認できます。もちろん、詳細画面は未作成なので404エラーが表示されます。 スレッド詳細画面 詳細画面では、コメントを投稿することができるようにします。と言っても、特に新しいことをするわけではありません。 いつも通り、「+page.svelte」「+page.server.ts」を作成します。 これからやるべき事は load関数で詳細情報(タイトル・作成者・作成日時)とコメント(内容・作成者・作成日時)の取得 詳細画面にスレッドの情報と投稿されたコメントを表示 コメントフォームの作成 コメントを投稿するアクションの作成 今までやってきたことの振り返りみたいな感じですね。 「load関数で詳細情報とコメントの取得」からやっていきます。 postテーブルから詳細情報を、commentテーブルからpostIdに該当するコメントを取得してくるのがシンプルでいいかもしれませんが、postテーブルが定義されている「schema. prisma 」を見返してみましょう。 model Post { id Int @id @default(autoincrement()) userId String content String created_at DateTime @default(now()) Comment Comment[] user User @relation(fields: [userId], references: [id]) } PostテーブルはCommentの情報を持っています。また、Userの情報も取り出せそうです。 なので、次のように書くことで複雑ではありますが一回の操作で欲しい情報を取得することが可能です。 // detail/[postId]/+page.server.ts import { db } from "$lib/prisma" import { fail } from "@sveltejs/kit" ; import type { PageServerLoad } from "./$types" export const load: PageServerLoad = async ( { params } ) => { const threadDetail = await db.post.findUnique ( { where: { id : Number ( params.postId ) } , include: { Comment : { orderBy: { id : 'desc' } , select: { content: true , created_at: true , user: { select: { name: true } } } } , user: { select: { name: true } } } , } ); if( ! threadDetail ) throw error ( 404 , { message: "存在しないスレッドです。" } ) return { threadDetail } } paramsには { postId : {id} } が入っているため、params.postIdで必要なIDを取得できます。 次にincludeを使って、Commentテーブルからはコメントと作成日時、作成者を取り出していますUserテーブルからはこのスレッドの作成者を取り出しています。 includeとは、関連するデータを一緒に取得するために用いられます。これによりクエリ数が減少し、パフォーマンスが上がるというメリットがあります。 スレッドの詳細情報とそれに付随するコメントをまとめたthreadDetailをクライアント側に返します。もし、存在しないスレッドの場合はエラーを投げます。エラーページについては後で書きます。 「 詳細画面にスレッドの情報と投稿されたコメントを表示」 をします。「+page.svelte」に次のコードを書いてください。 // detail/[postId]/+page.svelte < script lang = "ts" > import type { ActionData , PageData } from "./$types" ; type comment = { user: { name: string } content: string created_at: Date } export let data: PageData ; const threadAuthor : string = data.user.name const threadContent : string = data.threadDetail.content const threadCreatedAt: Date = data.threadDetail.created_at const comments : comment [] = data.threadDetail. Comment < /script > < div > < a href = "/" > < 一覧に戻る < /a > < /div > < h1 > スレッド詳細 < /h1 > < h2 > { threadContent } < /h2 > < p > 作成者: { threadAuthor } 作成日時: { threadCreatedAt } < /p > < h2 > コメント < /h2 > { # if comments.length } { #each comments as comment } < p > 名前: { comment.user.name } < /p > < p > 日時: { comment.created_at } < /p > < p > コメント: { comment.content } < /p > < br / > { /each } { : else } < p > コメントはありません < /p > { / if } サーバーから取得してきたデータは少々複雑になっているので、スレッド作成者、内容、作成日時、コメントの4つに分けます。 また、コメントの中にも名前、作成日時、内容があるのでcomment型を定義して、commentsに格納します。今回の場合、カスタムな型定義がこれだけなので同じファイルに書きましたが、複数ある場合は src/lib 下などに型定義用のファイルなどを作るようにしましょう。 スレッドの情報が取得できていれば、下記のように画面に表示されると思います。 続いて、「コメントフォームの作成 」をします。全然難しくありません。「+page.svelte」に次のコードを加えるだけです。 // detail/[postId]/+page.svelte < script lang = "ts" > // 省略 /* 追加 => */   export let form: ActionData ; < /script > /* HTML省略*/ < div > { # if form?.message } < p class= "error" > { form.message } < /p > { / if } < /div > < form method = "POST" action = "?/comment" > < input name = "comment" type= "text" / > < button > コメントする < /button > < /form > < style > .error { color: red ; } < /style > "コメントがありません"の下にコメント用のフォームが作成されたはずです。 あとは「コメントを投稿するアクションの作成」をすれば終わりです。「+page.server.ts」のload関数の下に次のコードを加えてください。 // detail/[postId]/+page.server.ts export const actions: Actions = { comment : async ( { request , locals , params } ) => { const data = await request.formData (); const comment = data. get( "comment" ); if (typeof comment != "string" || ! comment ) { return fail ( 400 , { message: "コメントは必須入力です。" } ) } await db.comment.create ( { data: { userId: locals.user.id , postId: Number ( params.postId ), content: comment } } ) } } 詳細画面で適当なコメントをしてみましょう。 ID降順なので、最新のものが上に来るようになっていれば問題ありません。 また、他のスレッド詳細を見てもコメントがないことを確認しておきましょう。 詳細画面も完成しました。 残すはログアウト機能とエラーページ作成です。しかし、やる事はそれほど難しくありません。おまけみたいな感じで聞いてもらえるとありがたいです。 ログアウト機能 ログアウトするというのは cookie 情報を消すということです。それだけです。 src/routes 下にlogoutフォルダとlogoutフォルダ下に「+page.server.ts」を作成してください。 「+page.server.ts」内で行うことは cookie を消して、ログイン画面にリダイレクトさせるだけです。念の為にもし、「/logout」に遷移した場合でもload関数で一覧へ戻るようにしておきます。 // logout/+page.server.ts import { redirect } from "@sveltejs/kit" import type { Actions } from "./$types" export const load: PageServerLoad = async () => { throw redirect ( 303 , '/' ); } export const actions: Actions = { logout: async ( { cookies } ) => { cookies. delete( 'session' ); throw redirect ( 303 , '/login' ) } } ログアウトの処理はできたのでログアウトボタンをヘッダーに配置したいと思います。ヘッダーは全ての画面で共有したいので、 src/routes 下に「+layout.svelte」を作成します。 「+layout.svelte」は次のようなコードにします。 // +layout.svelte < script lang = "ts" > import { page } from '$app/stores' < /script > { # if ! $page.data.user } < a href = "/login" > ログイン < /a > < a href = "/register" > 新規登録 < /a > { : else } < form action = "/logout?/logout" method = "POST" > < button > ログアウト < /button > < /form > { / if } < slot / > $app/storesは「+layout.server.ts」から返されるlocals.userの情報を含んでおり、ログインしていれば「$page.data.user」には値が入っているのでログアウトを表示し、値がない時には新規登録とログインへのリンクを表示します。 ログアウトボタンを押せば、 logout/+page.server.ts のlogoutが発火します。 これでログアウト機能ができました。 エラー画面 最後にエラー画面を作っていきます。SvelteKitでは簡単にエラー時に特定のエラー画面に飛ばすことができます。 先ほど、詳細画面を作成しているとき、存在しないIDを取得しようとすると「+page.server.ts」ではerrorを投げていました。 // detail/[postId]/+page.server.ts if( ! threadDetail ) throw error ( 404 , { message: "存在しないスレッドです。" } ) SvelteKitでは、loadの中でerrorが発生した場合、最も近くにある「+error.svelte」が呼び出されます。なので、 detail/[postId] に「+error.svelte」を作成することでカスタムなエラー画面を表示させることができます。 // detail/[postId]/+error.svelte < script lang = "ts" > import { page } from '$app/stores' ; < /script > < h1 > エラーページ < /h1 > < h3 > { $page.error?.message } < /h3 > pageからerrorに格納したmessageを取り出すことができます。とても簡単に実装できました。 実際に「detail/[存在しない投稿のID]」に訪れると、エラーページが表示されました。 ちなみに、エラーが発生した場所から最も近い「+error.svelte」が呼び出されるという特徴から、 src/routes/ 下に「+error.svelte」を作るとログイン画面や登録画面でerrorが投げられても src/routes/+error.svelte が表示されるようになります。 404エラー画面が完成しました。 しかし、500エラーには対応できていません。例えば、「/detail/sample」に訪れてください。postIdは数値を想定しているので文字列を入れられると500エラーが発生します。 「+error.svelte」が表示されると思います。 特に問題はないのですがデフォルトの文字列が表示されています。この文字列を変えたい場合には「hooks.server.ts」に次のコードを加えるだけです。 // src/hooks.server.ts export const handle: Handle = async ( { event , resolve } ) => { // 省略 } export const handleError: HandleServerError = ( { event } ) => { return { message: event.url.pathname + "で500エラーが発生!" , } ; } HandleServerErrorは予期せぬエラー発生時に実行されます。もう一度、「/detail/sample」に訪れると表示が変わると思います。 今回はパスを表示していますが、他にも色々な情報を表示できるのでお好きなように設定してください。 500エラーにも対応したエラー画面が完成しました。 これにて、シンプルすぎる 掲示 板アプリが完成しました!お疲れ様でした!! 本来であれば、自身の投稿やコメントを編集・削除できるべきなのですが、記事がさらに長くなってしまうことと、投稿を実装できていれば編集・削除はそれほど難しくないため省かせていただきました。ぜひ、独力で実装してみてください! 終わりに 今回は 掲示 板アプリ作成を通してSvelteKitの基礎をご紹介させていただきました。 と言っても、まだまだSvelte・SvelteKitには様々な機能がありますので、これを機にもっと勉強していただけると嬉しいです。 僕自身も最近になって勉強し始めたばかりなので、もっとSvelte・SvelteKitの知識を深めていくつもりです。 本当に長い記事になってしまいましたが、ここまで読んでいただいた方々、少しでも覗いていただけた方々、ありがとうございました! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに こんにちは、 ラク スフロントエンド開発課の斉藤です。 記事タイトルはReact開発者なら知る人ぞ知る りあクト! TypeScriptで始めるつらくないReact開発 のパロディです。とてもわかりやすい入門書なのでReact初学者の方には学びの第一歩として自信を持ってオススメできます! さて今回は、モダンなフロントエンド技術を採用したうえで、極力シンプルで開発体験を損なわないような ディレクト リ構成を考えてみたので共有したく記事にしました。現在実際に運用しているのですが、今のところ大きな問題も無くチームからの不満も上がっていません。しかし、個人的に微妙な部分もあるのでそちらの紹介も行いたいと思います。 今回、構成を考えるにあたって重視したポイントは以下の3点です。 新しく参入するメンバーでもすぐに理解できるシンプルな構成にしたい テストやリファクタしやすい構成にしたい できればルールが厳しすぎず、快適な開発がしたい ラク スフロントエンドチームは現在急拡大中で、新卒や中途から新しく開発に参加してくれるメンバーも多くなってきました。したがって 新しく参入するメンバーでもすぐに理解できるシンプルな構成 にすることは MUST です。また、新規に開発する製品は長く開発が続いていく (はず) と見込まれるためプロジェクトの規模が大きくなっても、カオスになりにくくテストしやすい構成を目指します。 現在実際に運用しているのですが、今のところチームからの不満も上がっておらず、問題なく快適に開発できています。しかし個人的に微妙な部分もあるのでその辺りの紹介もしていければと思います。 前提として採用している主なライブラリは以下になります。 React v18 MaterialUI ReactQuery Recoil ReactHookForm zod 結論 src/ ├─ common/ ├─ components/ │ ├─ features/ │ │ ├─ generic/ │ │ ├─ todo/ │ │ │ ├─ container/ │ │ │ ├─ presenter/ │ │ │ ├─ hooks/ │ │ │ ├─ actions.ts │ │ │ ├─ todo.constant.ts │ │ │ ├─ todo.type.ts │ │ │ └─ todo.validation.ts │ ├─ ui/ │ └─ pages/ ├─ lib/ フロントエンドの ディレクト リ構成としてまず思いつくのがアトミックデザインですが、最近は bulletproof-react という リポジトリ の アーキテクチャ を参考にした 機能ごとに ディレクト リを分けるパターン が採用されつつあるように思います。例えば @sakito さんが以下の記事で機能ごとに ディレクト リを分ける構成を紹介しています。 zenn.dev アトミックデザインは明確に コンポーネント の責務を分けることができ、再利用性も高いのですが、メンバー間で コンポーネント の切り出し方に差異が生まれやすくカオスになりがちです。一方機能ごとに ディレクト リを分けるパターンは、どのように コンポーネント を切り出せばよいかがイメージしやすく、メンバー間で コンポーネント の分け方にブレが生まれにくいというメリットを感じました。したがって シンプルさ という観点で軍配が上がり、今回重視するポイントにもマッチしていたので機能ごとに分けるパターンをベースとすることにしました。 依存関係は以下のようになっています。 コンポーネント の依存関係 図の上側にある コンポーネント は下の コンポーネント importでき、UIは他のUIを、Containerは他のContainerをimportすることができます。このような依存関係の構成は以下の @yoshiko さんの記事を参考にさせていただきました。 zenn.dev ui ui ディレクト リの中身は以下のようになっています。 ui/ ├─ button/ │ ├─ Button.tsx │ └─ IconButton.tsx ├─ textField/ │ ├─ TextField.tsx │ └─ RhfTextField.tsx ├─ index.ts 基本的にはMUIの コンポーネント をラップし、独自にスタイルを当てた コンポーネント 群になり、 ドメイン を持たせないようにします。 コンポーネント が ドメイン を持つか、持たないかは、その コンポーネント をそのまま別のプロジェクトで使い回すことができるか、否かで判断します。またReactHookFormを使っている場合は、同じui ディレクト リ内にReactHookFormのロジックを付加した コンポーネント を配置します。 RhfTextField.tsx import { FieldValues , useController , UseControllerProps , } from 'react-hook-form' ; import { TextField , TextFieldProps } from './TextField' ; export type RhfTextFieldProps < T extends FieldValues > = TextFieldProps & UseControllerProps < T >; export const RhfTextField = < T extends FieldValues >( props: RhfTextFieldProps < T > ) => { const { name , control } = props ; const { field: { ref , ...rest } , fieldState: { error } , } = useController < T >( { name , control } ); return ( < TextField inputRef = { ref } { ...rest } { ...props } errorMessage = { error && error.message } / > ); } ; またui配下の コンポーネント はindex.tsで一気にexportしてしまいます。 components/ui/index.ts // button export * from "./button/Button" ; export * from "./button/IconButton" ; // textField export * from "./textField/TextFiled" ; export * from "./textField/RhfTextField" ; このようにすることで コンポーネント 使用側でuiパーツを一度にimportすることができます。 import { Button , RhfTextField } from "src/components/ui features features ディレクト リは以下のような構成になっています。 features/ ├─ generic/ ├─ todo/ │ ├─ container/ │ ├─ presenter/ │ ├─ hooks/ │ ├─ (actions.ts) │ ├─ (todo.constant.ts) │ ├─ (todo.type.ts) │ └─ (todo.validation.ts) features内の ディレクト リは画面仕様書単位で分けます。 この運用方法のメリットは誰が開発しても同じ切り分け方になるということです。これにより、実装に入る前にメンバー間で「どのように ディレクト リを分けるか」という議論をしなくてもよくなります。また機能の開発担当も画面仕様書単位で振り分けることで、features ディレクト リを横断して開発を行うことが少なくなりコンフリクトを防ぐことができます。 画面仕様書単位で分けると1つの機能が大きくなりすぎてしまう、という場合は画面仕様書の切り分けを先に検討します。そもそも画面仕様書の記述量が膨大だと読むコストが高く、仕様の把握漏れが頻発してしまいます。これは健全な状態ではないので先に画面仕様書の改善から行っていき、機能を小さく分けた上で開発を行います。 またContainer/Presenterパターンを採用するため、各feature ディレクト リの中にcontainer ディレクト リとpresetner ディレクト リを配置します。ロジックをcontainerやhooksに逃がすことでpresenterを純粋に保ち、テストしやすい構成にします。hooksには機能を実現するために必要な API を叩くカスタムフックや、複雑なロジックをまとめるカスタムフックを記述します。 hooks/ ├─ useFetchTodo.ts ├─ useUpdateTodo.ts ├─ useDeleteTodo.ts ├─ useToggleTodoStatus.ts hooksがあればcontainerは不要かと思われがちですが、ロジックを全てカスタムフックに書くと容易にfatなhooksになってしまうためcontainerがあるとコードも見通しやすくなり便利です。 actions.tsにはその機能で使うRecoilのロジックを記述します。また[feature].constant.tsにはその機能で使う定数、[feature].type.tsには型、[feature].validation.tsにはzodで定義する scheme を記述します。基本的にはこれらのファイルは無くてもよく、コードの見通しが悪くなってきたら作るというユルい運用をしています。 またfeaturesにはgenericというプロジェクト内共通の機能を担う ディレクト リも作成します。こちらもtodo ディレクト リと同じようにcontainer、presenter、actions.tsなどを持ちます。こちらの ディレクト リ内には ドメイン を持ち(=他のプロジェクトには流用できない)、アプリケーション内の様々な場所で汎用的に用いられる機能のロジックを記述します。例えばヘッダに表示するユーザープロフィール コンポーネント などはこちらに記述します。 pages pagesはurl単位で分けfeatures内のcontainerを呼び出すだけです。 import { TodoContainer } from 'src/components/features/todo/container/TodoContainer' ; export const Todo = () => { return < TodoContainer / > } ; 依存関係としてはrouter→pages→containerのようになっています。routerから直接containerを呼び出しても良いのですが、将来的にSuspenseやErrorBoudanryを導入したり、特定のページだけサイドバーを表示したいといった需要に対応しやすいようにこのような構成としました。 common commonにはプロジェクト内の様々な場所で呼び出される変数や型などを定義します。 common/ ├── constants.ts ├── cssVariables │ ├── color.ts │ ├── variables.ts │ └── zIndex.ts ├── messages.ts ├── reactQueryKeys.ts ├── recoilKeys.ts ├── types.ts └── utils ├── sum.ts ├── index.ts constants.ts constantsの記述例 export const FONT_SIZE_MAP = { XS: "10px" , S: "12px" , M: "14px" , L: "16px" , XL: "18px" } as const export const FRUIT = { apple: "りんご" , banana: "バナナ" , orange: "みかん" , } as const types.ts typesの記述例 export type Fruit = typeof FRUIT [ keyof typeof FRUIT ] ; export type ErrorResponse = /// cssVariables カラーコードやz-index、ヘッダの高さなどの css にかかわる変数を管理する ディレクト リです。styled-componentを使っているのでtsファイルで管理します。 messages.ts エラーメッセージやバリデーションメッセージを管理するファイルです。 reactQueryKeys.ts/recoilKeys.ts ReactQueryとrecoilは使用する際に一意なキーを設定しなければならないため1つのファイル内で一元管理し、重複しないようにします。 utils 汎用的に使える便利関数を管理する ディレクト リです。 lib ライブラリの初期設定などはこちらの ディレクト リに記述します。 lib/ ├── reactQueryClient.ts ├── zod.ts 改善したいポイント features配下の ディレクト リを画面仕様書単位で分けている点 画面仕様書単位で ディレクト リを分けるとチーム感での ディレクト リ構成の認識齟齬が起こりづらくなるというメリットがある一方、長くプロジェクトを続けていった場合に破綻してしまうのでは無いかという不安があります。プロジェクトの初期段階では機能の数も少ないため、画面仕様書の記述量も少なくすみますが、フェーズ2、3と時が経つごとに機能が追加され、画面仕様書も更新されていきます。画面仕様書が肥大化してきてファイルを分けようとなったとき、 ディレクト リも同時に分けなければルールの一貫性が損なわれてしまいます。しかし、実際に ディレクト リを分割するようリファクタする 工数 をそのときに用意できるかわからないため、有耶無耶になってしまいルールが破綻してしまう懸念があります。 src/common配下のconstantsファイルやrecoilKeysファイルの肥大化 定数やキーは開発が進むに連れて多く書かれるためすぐに肥大化してしまいます。特にrecoilキーなどは一元管理しないとキーの値がかぶりやすくなり、安易にファイルを分け辛く悩ましいです。 constantsファイルとtypesファイルが分かれていてめんどくさい 定数から型を生成することはよくありがちなので、そのままconstantsファイルに書いてしまいたくなることがよくあります。しかしファイル名がconstantsなので型が記述されていると違和感です。constantとtypeの両方を記述するファイルにしてもいいのですが、良いファイル名が思いつきません。また、同じファイルに書いてしまうとコードが容易に肥大化してしまうので面倒ですが分けています。 まとめ 総括としてモダンフロントエンドの つらくないけどちょっとつらい ディレクト リ構成 に落ち着いたと感じています。完璧とは言えませんが比較的開発体験がよく、シンプルな構成にすることはできたのではないかと思います。これから運用を続けていくことで知見を貯めていき、さらなる改善を重ねていきたいです。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに はじめまして。 ラク スの某インフラ担当者です。 今回は個人的に今後キーになりそうなネットワークの高速化技術について触れたいと思います。 具体的な実装方法やコマンドの話ではありませんので、「へぇ」という感じで読んでいただければ幸いです。 はじめに ネットワーク高速化の必要性 RDMA(Remote Direct Memory Access) RDMA概要 実際に検証してみた DPUについて というわけで ネットワーク高速化の必要性 現代のインフラ界隈は仮想化技術を使うことが当たり前になってきています。 仮想化といえば一昔前は大型のホストサーバを用意してESXiなどを入れた単純なハイパーバイザー型の仮想化が主流でしたが、 近年はより複雑な仮想化/集約が行われるようになりました。 HCI(Hyper Converged Infrastructure)による複数のコンピュートリソース/ストレージの集約だったり SDN(Software Defined Network)によるネットワーク仮想化だったり、 所謂 プライベートクラウド を構築するような事例も世の中にはたくさん増えてきています。 さらにはコンテナ、 kubernetes といったものも注目を集め始めており(ぱっと見はシンプルそうに見えますが)非常に高度な構造の仮想化が主流になろうとしています。 そんなこんなで、集約をしてサーバリソースを有効活用しよう!なんてキラキラした話が空中を飛び交っているわけですが、 ここで無視することが出来ないのがネットワークです。 VM が集約されれば トラフィック は当然集中しますし、HCIで複数ノードを集約した場合にはたくさんの通信がノード間を行き来します。 ネットワークもその集約化に追随できなくてはなりません。 また、AIやら ディープラーニング やらで GPU を使って高速に並列処理を回して処理するようなシステムも 近年話題になる事が多いですが、大掛かりで大規模な処理が増えれば トラフィック への影響も膨大なものとなるでしょう。 GPU で高速処理してるのにネットワークが遅く、結局 GPU が遊んでいるなんてあっては元も子もないので、 今後のIT業界においてはネットワークの高速化、広帯域化は必須と言えると思います。 そこで今回は、このネットワーク周りの処理の高速化について調べてみようと思います。 RDMA(Remote Direct Memory Access ) ネットワークの高速化技術の一つにRDMA(Remote Direct Memory Access )というものがあります。 RDMA概要 RDMAは元々はInfinibandの世界で実装されている技術になります。 Infinibandって何?と思った方もいらっしゃるかと思いますが、Infinibandというのはみんなが慣れ親しんでいるであろう イーサネット とはまた別の 世界線 の話で、 スーパーコンピュータの世界で使われているような超高速ネットワークになります。 RDMAの仕組みは簡単に言うと、データ転送の一連の処理は通常、アプリ⇒メモリ⇒OSにコピー⇒ネットワークカードといったようなコピー処理が必要になってくるが、 そのコピーが遅いしCPUも使うし大変だから、各サーバーのメモリ間をダイレクトに接続して転送しちゃえば良いんじゃ?といった塩梅。 間のメモリからOSに落としていく処理を回避するためCPUリソースもほとんど使わないし処理も高速というハナシ。ゼロ・コピーなどと呼ばれることもあるようです。 そして、この技術をInfinibandだけじゃなく、広く使われている イーサネット の世界にもどうにか持ち込めないか?というニーズも世の中に当然出てくるわけで、 そこで生み出されたのがRoCE(RDMA over Converged Ethernet )で、世に出てきたのは2010年頃のようです。(読み方は「ロッキー」と読むのが主流のようです) どうやってそんなことやるの?と思われるでしょうが、これを実現するにはまずRDMAに対応する専用のネットワークカードが必要になってきます。 この対応ネットワークカードで業界をけん引する有名な製品にMellanox制のConnectXシリーズがあります。 正確に言うと、Mellanoxは2020年に NVIDIA に買収されていますので、現在は NVIDIA の製品ブランドとなります。 あれ、聞いたことあるぞ?というゲーマーさんもいらっしゃると思いますが、はい、あのグラボで有名な NVIDIA さんです。 このRDMAに対応したネットワークカードを使い、かつ、kernelもそれに対応している必要があります。 近年の新しいkernelであればおおよそ対応している(?)と思われますので、ご興味があれば今使用されているOSが対応しているのか確認いただければと思いますが、 さらに加えて、 ロスレス なネットワークを前提として QoS などを設定、高速化NWで効率よく転送するためにMTUを変更する、 帯域がネックにならないように可能ならば25Gbpsや40Gbpsといった広帯域を確保したい、 経路のスイッチもRDMA対応している必要などもあり、実は導入難易度は結構高いです。 また、RoCEと通常の イーサネット ネットワークを混在させた場合の互換性もありません。 そのため、既存のシステムを変更していくというよりは、 RDMAに対応したシステム全体を別で構築したのちに移行するなどの対応が必要となってくるでしょう。 そこのハードルの高さこそありますが、新規システムを1から構築したい場合などはぜひ検討してみていただきたいものになります。 実際に検証してみた ということで、実は弊社ではRDMAを使った高速ネットワークを実際に導入/検証してみました。 正確には イーサネット での導入になりますのでRDMAというかRoCEになり、さらにそのバージョン2にあたるRoCEv2が実装 プロトコル になります。 HCIのシステムおよび VMware 製品を使用して高速なvSAN(※)のシステムを組んで VMware 社様と一緒に共同で ベンチマーク テストなどを実施しました。 ※vSAN 7.0 Update 2からRoCEv2に対応しています。また、vSANで組む場合はキャッシュディスクはNVMeにする必要があります。 正確な ベンチマーク 結果などは現時点での公表が難しいのですが、従来の TCP/IP の イーサネット よりも10~20%はIOPS(※)が上がることが分かりました。 (※read、writeの比率を変えつつ限界まで負荷をかけレイテンシが上がってきた際のIOPS。) ただ、ちょっとややこしいのですが、vSANの通信となると例えばストレージポリシーに従って二重書き込みや三重書き込みをするケース、 あとはキャッシュディスクからキャパシティディスクへデータを落とす処理なども存在するため単純にRoCEv2単体の性能を見る検証という話ではありません。 この件についての詳細はここでは長くなるため割愛しますが、 あくまでvSANなどネットワーク通信がたくさん発生するような場所でRDMAは有効に働く可能性があり、 ぜひ今後導入する場合は検討しておきたい事項の一つになる、という塩梅でとらえていただければ、、と思います。 恐らく、、より細かい数値は別の機会に皆様にお伝えできるはず、、(現在情報を色々まとめています。) なので少々お待ちください! DPUについて 似たような話でインフラ界隈で最近高速化の技術としてDPU(データ プロセッシング ユニット)というものが話題に上がるようになりました。 DPUはCPU、 GPU 、に並んで第3の刺客といった具合の存在で、簡単に言うと NIC にプロセッサを載せてネットワーク処理やストレージ管理、セキュリティ処理などをオフロードさせる仕組みのことを言います。 簡単に図示すると以下のような形です。 全部CPUがやってたやつを・・ こうやって分散すれば良くね? より一層CPUが本来やりたかったアプリケーションの処理に専念することができます。 DPUで有名なのは、ここにも NVIDIA さんの名がありますが、CPU界隈の猛者の Intel や AMD も当然ながら覇権を争っています。 CPUとDPUで親和性があった方が良いのかまだ未検証なので詳細は分かりませんが、ARMベースなのか x86 ベースなのか サーバのCPU アーキテクチャ に何を採用しているのか?というのがDPUの選択にも関わってくるかもしれません。 ちなみに、 VMware でもvSphere 8.0からDPUが対応されるようになり、RDMAと併用することでより一層の高速化を実現することが可能になってるようです。 (まあ併用というかDPU搭載のsmartNICは標準機能として今後RDMAも普通に対応していくんだと思います) VMware の詳しい記事はこちら docs.vmware.com 今後もし仮想基盤の新規導入を検討する場合は、ぜひ検討に入れたい内容になります。 というわけで 本当に雑多で申し訳ないのですが、ネットワーク高速化で今後キーになりそうな技術の簡単なご紹介をしました。 DPUについてもどうにか機会を見つけて検証してみたいと思っております。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに こんにちは!hy094です。 今回はZero-Runtime CSS -in-JSである「macaron」(macaron- css )の使い方を調べてみたので、 それをまとめたいと思います。 ※本記事は大部分が公式の GitHub と 公式ドキュメント の和訳で構成されています。 ※英語がとても苦手なので翻訳アプリを駆使して書いています。誤りがあったらこっそり教えていただけると嬉しいです。 はじめに macaron(macaron-css)って結局どうなの? macaron(macaron-css)って何? どんな特徴があるの? どうやってインストールするの? バンドラの設定は? どうやって使うの? 1. styled componentを作成する 2. スタイルを追加する 3. variantsを追加する 4. デフォルトのvariantsを設定する 5. コンポーネントをレンダリングする 6. サンプルコードを見てみる おわりに macaron(macaron- css )って結局どうなの? Vanilla-Extract の辛い部分である 動的なスタイルがやや当てにくい 同じファイル内でスタイルと コンポーネント の両方を記載できない といった問題が解決されていて、とてもよかったです。 (1点目は私の問題、2点目は好みの問題かもしれませんが・・・) 感覚的には styled-components と Stitches と Vanilla-Extract の いいとこ取り をしたライブラリといった印象ですね。 macaron(macaron- css )って何? 一言で言うと CSS -in-JS with zero runtime, type safety and colocation github.com 和訳すると「ゼロランタイム・型安全・コロケーションを実現する CSS -in-JS」です。 アイコンから察するに、おそらくお菓子のマ カロン で合っていると思います。 また、1.0.0のリリースが2022/12/06とかなり新しいライブラリとなっています。 正式名称が macaron-css なのか macaron なのか迷いどころですが、 本記事では公式サイトの macaron.js.org を信じて以降は macaron と呼称します。 公式サイトは以下を参照してください。 macaron.js.org どんな特徴があるの? macaronの特徴として、以下のようなものがあります。 ビルド時にスタイルを抽出する(ゼロランタイムである) スタイルと コンポーネント のコロケーション(繋がりが強くなる) TypeScriptのサポート styled-componentsとvanillaの両方の API をサポート Stitches のようなvariants API が利用できる 追加の設定など不要でReact,Solidで使用可能 esbuild,viteのサポート どうやってインストールするの? npm/yarnからインストールできます。 Reactの場合は以下のコマンドを実行します。 # npm npm install @macaron-css/core @macaron-css/react # yarn yarn add @macaron-css/core @macaron-css/react Solidの場合は以下のコマンドを実行します。 # npm npm install @macaron-css/core @macaron-css/solid # yarn yarn add @macaron-css/core @macaron-css/solid この後は yarn x React x Vite での環境を想定して記載します。 そのほかの書き方などは 公式ドキュメント )を参照してください。 バンドラの設定は? viteの プラグイン をインストールします。 yarn add @macaron-css/vite vite.config.js に以下のように記載します。 import { macaronVitePlugin } from '@macaron-css/vite' ; import { defineConfig } from 'vite' ; export default defineConfig ( { plugins: [ macaronVitePlugin (), // other plugins ] , } ); macaronVitePlugin() は他の プラグイン の設定よりも 前に 記載する必要があるとのことです。 どうやって使うの? 1. styled componentを作成する @macaron-css/react から styled をインポートし、styled componentを作成します。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , {} ); 2. スタイルを追加する コンポーネント に適用される基本スタイル(base styles)を追記します。 hoverやメディアクエリ、ネストされた セレクタ などを含めることが可能です。 macaronの全てのstyling API は入力としてstyle objectを受け取り、型安全です。 その恩恵としてオートコンプリートもされます。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { // --- ↓add↓ --- base: { backgroundColor: 'gainsboro' , borderRadius: '9999px' , fontSize: '13px' , padding: '10px 15px' , ':hover' : { backgroundColor: 'lightgray' , } , } , // --- ↑add↑ --- } ); 3. variantsを追加する variantsとは、可変のスタイルを実装しやすくするための機能です。 例えば以下のようなものです。 // Buttonコンポーネントでvariantsを設定 const Button = styled ( "button" , { variants: { color: { violet: { color: "blueviolet" } , gray: { color: "gainsboro" } , } , } , } ); // 利用側でこう使える () => < Button color = { "violet" } > hello ! < /Button > macaronでもvariantsキーを利用し、variantsを追加できます。 また、追加できる数に制限ありません。 そしてもちろん、基本スタイルと同様にstyle objectを受け取ります。 これを活用することで、動的なスタイル変更が容易になります。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { base: { backgroundColor: 'gainsboro' , borderRadius: '9999px' , fontSize: '13px' , padding: '10px 15px' , ':hover' : { backgroundColor: 'lightgray' , } , } , // --- ↓add↓ --- variants: { color: { violet: { backgroundColor: 'blueviolet' , color: 'white' , ':hover' : { backgroundColor: 'darkviolet' , } , } , gray: { backgroundColor: 'gainsboro' , ':hover' : { backgroundColor: 'lightgray' , } , } , } , } , // --- ↑add↑ --- } ); 4. デフォルトのvariantsを設定する defaultVariants を利用して、デフォルトのvariantsを設定できます。 この例の場合、何も指定しなければ color='violet' となります。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { base: { backgroundColor: 'gainsboro' , borderRadius: '9999px' , fontSize: '13px' , padding: '10px 15px' , ':hover' : { backgroundColor: 'lightgray' , } , } , variants: { color: { violet: { backgroundColor: 'blueviolet' , color: 'white' , ':hover' : { backgroundColor: 'darkviolet' , } , } , gray: { backgroundColor: 'gainsboro' , ':hover' : { backgroundColor: 'lightgray' , } , } , } , } , // --- ↓add↓ --- defaultVariants: { color: 'violet' , } , // --- ↑add↑ --- } ); 5. コンポーネント を レンダリング する 通常のReact コンポーネント と同じように使用できます。 macaronでは styled-components のように コンポーネント と同じファイル内でスタイルを宣言できるため、より繋がりが強くなります。 これをmacaronは 真の コロケーション( true colocation)と表現しています。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { base: { backgroundColor: 'gainsboro' , borderRadius: '9999px' , fontSize: '13px' , padding: '10px 15px' , ':hover' : { backgroundColor: 'lightgray' , } , } , variants: { color: { violet: { backgroundColor: 'blueviolet' , color: 'white' , ':hover' : { backgroundColor: 'darkviolet' , } , } , gray: { backgroundColor: 'gainsboro' , ':hover' : { backgroundColor: 'lightgray' , } , } , } , } , defaultVariants: { color: 'violet' , } , } ); // --- ↓add↓ --- () => < Button color = "gray" > Click me ! < /Button >; // --- ↑add↑ --- 6. サンプルコードを見てみる 最後に、公式の github に記載されているサンプルコードです。 今までの1~5に記載した内容で概ね理解できるかと思います。 import { styled } from '@macaron-css/react' ; const Button = styled ( 'button' , { base: { borderRadius: 6 , } , variants: { color: { neutral: { background: 'whitesmoke' } , brand: { background: 'blueviolet' } , accent: { background: 'slateblue' } , } , size: { small: { padding: 12 } , medium: { padding: 16 } , large: { padding: 24 } , } , rounded: { true : { borderRadius: 999 } , } , } , compoundVariants: [ { variants: { color: 'neutral' , size: 'large' , } , style: { background: 'ghostwhite' , } , } , ] , defaultVariants: { color: 'accent' , size: 'medium' , } , } ); // Use it like a regular solidjs/react component function App () { return ( < Button color = "accent" size = "small" rounded > Click me ! < /Button > ); } compoundVariants は組み合わせのvariantsです。 このサンプルコードでは、 color={'neutral'} かつ size={'large'} の場合に、 background: 'ghostwhite' が反映されます。 おわりに 新しく出てきたばかりで これから伸びていく感 をひしひしと感じるmacaron、いかがでしたでしょうか? 新しすぎることや日本語のドキュメント・記事が全くないこともあり業務で使うにはまだ早いと思いますが、 個人開発ではどんどん使っていきたいポテンシャルを感じました。 macaronは 次に来る CSS -in-JS かもしれません。興味が湧いた方はぜひ使ってみてください。 そして日本語の記事を書いてくれると僕が喜びます笑 それでは、ご覧いただきありがとうございました! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
こんにちは、tatsumiです。 システム開発 を行っていると、非同期処理を実装する場面は少なくないと思います。 ということで、今回は私自身の備忘録も兼ねて、Laravelでの非同期処理についてまとめてみました。 Laravelで非同期処理を行うには? Laravelでキューを使ってみよう キュー用のテーブルを作成する ジョブクラスを作成する ジョブクラスのタイムアウト時間/試行回数を設定する キューにジョブを投入する キューを非同期実行に変更する おまけ ShouldBeUniqueインターフェイスを実装する uniqueIdを定義する さいごに Laravelで非同期処理を行うには? Laravelで非同期処理を行う場合、キューを利用します。 まず、キューを利用する上での登場人物3名を紹介します。 キュー キューはある決まった処理を非同期で実行するための仕組みのことです。 キューにジョブを登録し、登録されたジョブから実行されていきます。 ジョブ ジョブは実行する処理自体のことを指します。 ワーカー ワーカーはジョブを処理するプロセスのことを指します。 キューについて理解していない方は、スーパーのレジを想像してみてください。 レジで順番待ちをしている人の列がキューになります。 また、一人一人の会計処理がジョブを指し、レジ打ちをしている店員がワーカーとなります。 思い返せば、飲食店の行列や役所や病院での待ちもキューで、日常生活の中にも多く存在しています。 キューとキューを利用する上での登場人物について理解したところで、早速実際にLaravelでキューを使ってみましょう。 Laravelでキューを使ってみよう キュー用のテーブルを作成する まずはLaravelでキューを利用する際に必要となるテーブルを作成しましょう。 テーブルの作成は下記コマンドで行うことができます。 $ php artisan queue:table $ php artisan migrate コマンドを実行すると、jobsテーブルとfailed_jobsテーブルが作成されます。 それぞれのテーブルの役割は以下の通りです。 jobs キューに登録されているジョブを管理します。 ジョブが登録されるとjobsテーブルに登録され、テーブルに登録されているジョブのうち、古いものから順に処理されます。 failed_jobs ジョブ実行の失敗を記録します。 ジョブの処理が何等かの理由で失敗した際に、failed_jobsテーブルに登録されます。 合わせてexceptinカラムにジョブ実行時に発生したexceptionと スタックトレース が出力されます。 ジョブクラスを作成する テーブルの用意ができたら、次にジョブクラスを作成しましょう。 ジョブクラスの作成は下記コマンドで行うことができます。 $ php artisan make:job SampleJob 実行すると、 app/Jobs フォルダの中に SampleJob.php が作成されます。 作成されたクラスには、 __construct() メソッドと handle() メソッドがあり、それぞれの役割は以下の通りです。 __construct() ジョブをキューに投入する際にジョブに対して引数を渡すことができます。 (キューへジョブを投入する方法は後述します) その引数を当メソッドで受け取ります。 handle() ジョブ上で実行したい処理を記述します。 上記の他に failed() メソッドでジョブ処理失敗時の処理を定義することができます。 ジョブ処理内で何等かのエラーが発生した際に、 failed() メソッドが呼び出され、引数としてジョブの失敗の原因となったThrowable インスタンス が渡されます。 ジョブクラスの タイムアウト 時間/試行回数を設定する ジョブクラスでは、ジョブの タイムアウト 時間や試行回数を指定することもできます。 ・ タイムアウト 時間  ジョブ1回あたりの タイムアウト 時間を指定できます。  指定する場合は、クラス変数として $timeout を定義します。  (指定しない場合は、デフォルト値の60秒となります。) public $timeout= タイムアウト時間(秒数指定);  ※指定した秒数でジョブ処理が終了しなかった場合は、失敗扱いとなります。 ・試行回数  ジョブが失敗した際に、何回まで再試行するかを指定できます。  指定する場合は、クラス変数として $tries を定義します。  (指定しない場合は、1度失敗すると再試行は行われずに即失敗扱いとなります。) public $tries = 試行回数;  ※試行回数上限に達した場合は、 failed() メソッドが呼び出され、ジョブ失敗扱いとなります。   (failed_jobsテーブルに登録される) タイムアウト 時間/試行回数はコマンドでも指定可能ですが、クラス変数の値が優先されるため注意! キューにジョブを投入する ジョブの実装が完了したら、いよいよ作成したジョブをキューに投入してみましょう。 キューにジョブを投入するには、 dispath() メソッドを使います。 SampleJob::dispatch($Jobへの引数) これで作成したジョブがキューに投入され、ワーカーによってジョブに実装した処理が実行されるようになります。 キューを非同期実行に変更する デフォルトでは、キューは同期実行されるようになっているため、非同期実行とするために .env の設定を変更します。 QUEUE_CONNECTION=database QUEUE_DIRVER=database これで基本的な準備は完了です! あとは実際に動かして、動作を確認してみましょう。 おまけ ここからはおまけです! dispath() メソッドでキューにジョブを投入すると、投入した分だけキューにジョブが溜まっていきます。 しかし、キューの中に同じジョブは常に一つだけにしたい!というケースもあると思います。 そんなときは、一意なジョブを作成することで解決できるので、おまけでは一意なジョブの作成方法について紹介します。 ShouldBeUnique インターフェイス を実装する まず一つ目の方法は、ジョブクラスに ShouldBeUnique インターフェイス を実装する方法です。 class SampleJob implements ShouldQueue, ShouldBeUnique { ・・・ } 上記のように、ジョブクラスに対して インターフェイス を実装するだけです。 この インターフェイス では、ジョブクラスへメソッドを追加する必要はありません。 この インターフェイス を実装すると、対象のジョブがキューに存在している、または処理中のタイミングで dispath() メソッドを実行しても、キューに投入されません。 ジョブの処理が完了した後、または再試行にすべて失敗した後、一意のジョブがロック解除され、キューに対象のジョブを投入できる状態となります。 もし、ジョブが実行される直前にロックを解除したい場合は、 ShouldBeUnique ではなく ShouldBeUniqueUntilProcessing インターフェイス を実装しましょう。 uniqueIdを定義する 二つ目の方法は、ジョブクラス内で特定の「キー」を定義する方法です。 public function uniqueId() { return 任意のID; } 上記のように、ジョブクラスに uniqueId() メソッドを実装します。 このメソッドを実装すると、先ほどの ShouldBeUnique インターフェイス を実装したときと同じように、同じIDを持つジョブは既存のジョブの処理が完了するまで`dispath()``メソッドを実行しても、キューに投入されません。 また、時間経過でジョブの一意ロックを解放することもできます。 その場合は、ジョブクラスに uniqueFor プロパティを定義します。 既存のジョブがuniqueForで指定した時間内に処理されない場合、一意ロックが解放され、同じキーを持つ別のジョブをキューに登録できるようになります。 public $uniqueFor = ジョブの一意ロックが解放されるまでの秒数; さいごに 本記事では、Laravelでのキューを利用した非同期処理についてまとめてみました。 ここで紹介した内容は、Laravelの公式ドキュメントにも載っているので、是非そちらも参照してみてください! readouble.com それでは良き PHP ライフを! エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター
はじめに おはようございますこんにちはこんばんは。 筆者は PHP の経験がまだ2年に満たない程度なのですが、 PHP の比較は何かとクセがあるなぁと思いながらコーディングする日々です。宇宙船 演算子 や エルビス 演算子 など筆者もまだあまり使いこなせていない面白い 演算子 もありますので、学習も含めて改めて皆さんと一緒に比較 演算子 を見ていこうというのが本記事の趣旨となっております。 はじめに 大前提 ==や!=を書くのは控えましょう 型を揃えて比較しましょう 基本の比較演算子 曖昧な比較と厳密な比較 宇宙船演算子 三項演算子 エルビス演算子 null合体演算子 おわりに 参考文献 大前提 ==や!=を書くのは控えましょう 既存からあるものならともかく新規で生み出すのは止めましょう。思いも寄らない比較結果になる可能性があります。新規で書く場合は === と !== を使いましょう。逆に既存からあるものを === に置き換えるのは注意しましょう。一見大丈夫そうに見えても思わぬ値が入ってきて既存と判定が変わってしまう場合があります。 型を揃えて比較しましょう 1つ目の大前提で等しい・等しくないについては問題無くなりましたが、 > と < についてはまだ問題が残ります。例えばですが、 2 > "1" は想定通り true になります。実際 PHP はこの場合stringの "1" を数字としてみなし、比較を行います。基本的に数字型文字列を使っている場合は問題ないのですが、そもそも別の型同士を比較すること自体あまりしっくりこないですし、今後言語仕様に変更がある恐れも大いにあるのであくまで同じ型同士で比較することを心がけましょう。 基本の比較 演算子 演算子 説明 == 等しければtrue 曖昧な比較 === 等しければtrue 厳密な比較 != 等しくなければtrue 曖昧な比較 <> !=と同じ 曖昧な比較 !== 等しくなければtrue 厳密な比較 < 左辺が右辺より小さければtrue <= 左辺が右辺より小さいか等しければtrue > 左辺が右辺より大きければtrue >= 左辺が右辺より大きいか等しければtrue 曖昧な比較と厳密な比較 簡単にいうと曖昧な比較は == で厳密な比較は === です。 JavaScript と同じ感覚ですね。曖昧な比較は、 PHP が型の相互変換を行なってくれて比較結果を返してくれます。逆に厳密な比較は型についてもチェックを行います。型が違えばその時点で別物と見なすのです。当然と言えば当然な気もしてきますが、以下のような結果の差が出てきます。 <?php 1 == "1" // true 1 === "1" // false これはまだ簡単な方で null や 0 が絡み出すと一気に複雑になります。他言語を同時に書いている人はそのままの流れで == を使わないように気をつけましょう。 Java を使っている人にはstringの比較を == で行わずに equals() で行うのと同じような感覚で厳密な比較を使っていただけるとわかりやすいかなと思います。また、曖昧な比較で使用される型の相互変換にはルールが存在するので過去コードを読み解くときには 公式ドキュメント とにらめっこしましょう。 宇宙船 演算子 <=> これが宇宙船 演算子 です。全ての プログラミング言語 にあるわけではないので初めて聞いた方もいらっしゃるのではないでしょうか。そういう私も PHP を初めてから知りました。この 演算子 は以下のように判定されます。 パターン 結果 左辺が右辺より小さい -1 左辺が右辺より大きい 0 左辺と右辺が等しい 1 一応この 演算子 は文字列や配列に対しても使用できるのですがあまり直感的ではないですし、数値に対して使いましょう。便利そうに見えていつ使うんだろう?というような 演算子 ですね。実際、通常のコードに出てくることはあまりないと思います。なぜならこの比較結果によって3分岐するようなコードは設計から見直したほうが良い可能性が高いからです。sortを書く場合はとてもシンプルに書けるようになるのでまたその時に思い出してみてください。sortはひたすら大きい小さい等しいの比較を連続して行うので便利です。 ちなみになぜ宇宙船 演算子 かというと <=> の形が スターウォーズ に出てくる宇宙船に形が似ているなど諸説あるそうです。(タイ・ファイターで検索してみてください) 三項演算子 みんな大好き? 三項演算子 です。以下のように書きます。 (1) ? (2) : (3) 判定は以下の通りです。 パターン 結果 (1)がtrue (2) (1)がfalse (3) 少しわかりづらいのでコードっぽく書いてみましょう。 <?php $ おやつ = 洋菓子は好きですか () ? ケーキ : お饅頭 正確にはコードではないのですがこうするとわかりやすいです。「洋菓子は好きですか」関数がtrueならケーキがおやつ変数に入りますし、falseならお饅頭がおやつ変数に入ります。 三項演算子 を使わずに書くと以下の通りです。 <?php if ( 洋菓子は好きですか ()) { $ おやつ = ケーキ } else { $ おやつ = お饅頭 } こんなにも行数も書く量も変わってきます。もちろん最初のうちは 三項演算子 は読みづらいのですが慣れてくるととてもシンプルに見えてきます。変数代入だけでなくもちろん関数実行もできます。 <?php $ averageScore > 80 ? お小遣いプレゼント () : 参考書プレゼント () ここで注意点ですが、何でもかんでも 三項演算子 を使えばいいというわけではありません。あくまで見やすくするため、すなわち可読性を上げるために使用してください。慣れないうちは一旦普通にif-else文で書いてから 三項演算子 に変換してみてください。そこで悩んでしまったり、出来た式を見た時に理解に時間がかかりそうなコードになってしまった時は 三項演算子 を使う場面では無いということです。例えば以下のような時は基本的に 三項演算子 を無理に使用する必要はありません。 if文がネストしている else ifが入ってきて3分岐以上になる if文の中の行数が2行以上ある 特に PHP は他の言語と違い 三項演算子 を左から右に読んでいくので、1行に複数の 三項演算子 が積み重なっている場合は他言語使用者からすると全く違う結果になってしまいます。PHP8.0以上では()を使用して1つの 三項演算子 の範囲を明確にしてあげないとエラーになります。それでも読みにくいことは確かなので極力控えましょう。 エルビス 演算子 ?: PHP の公式ドキュメントには エルビス 演算子 というワードは出てこないのですが、いわゆる エルビス 演算子 というやつです。こちらは 三項演算子 の短縮記法の1つです。見ての通り 三項演算子 の条件式がtrueだった時の部分( 三項演算子 の紹介項目でいう(2)の部分)がギュッと圧縮された形になります。書き方は以下のような形。 <?php $ 貰えるお小遣い = $ テストの点数 ?: 1 微妙にわかりにくくなってしまったような気がしますが、テストの点数が1点以上(true)ならそのぶんのお小遣いをあげ、テストの点数が0(false)なら0は可哀想なので1だけお小遣いをあげます。要するにtrueだった場合は条件式の値がそのまま値になります。falseだった場合は 三項演算子 の場合と同じで : の右側が値として入ります。これを エルビス 演算子 を使わずに 三項演算子 で書くと以下のようになります。 <?php $ 貰えるお小遣い = $ テストの点数 ? $ テストの点数 : 1 $テストの点数 を2回書くことになってしまいました。これは確かに短縮したくなりますね。これも最初のうちはステップを踏むと書きやすいです。 if-else文で書く 簡単に 三項演算子 で書けそう! → 三項演算子 で書く 三項演算子 の条件式とtrueの時に同じこと書いてる! → エルビス 演算子 で書く 以上のステップがスムーズに書けた時は完璧です。気持ちいいですね〜。慣れてくると一発で 三項演算子 で書けたり、 エルビス 演算子 で書けたり、はたまたこれは普通にif-elseで書いた方が良いという判断が付くようになるのでif文を書くときは頭にこのステップを入れておきましょう。あくまで条件式の部分に入る値がfalsy(falseと同等の値)の時に右辺が値となることがポイントです。 PHP の場合はこの後出てくるnull合体 演算子 と少し紛らわしいので注意してください。 ちなみに エルビス 演算子 の由来は エルビス プレスリー の顔文字から来ているらしいです。( Wikipedia ) null合体 演算子 こちらも エルビス 演算子 とは別の 三項演算子 の短縮記法です。書き方は以下のような形。 <?php $ 野菜 = $ 冷蔵庫 [ '野菜室' ] ?? 八百屋さんに買い物 () 感覚としては エルビス 演算子 と同じです。ただ、 演算子 の名前にもあるように ?? の左辺がnullだったときに右辺が値となります。null合体 演算子 を使わずに 三項演算子 で書くと以下の通り。 <?php $ 野菜 = isset ( $ 冷蔵庫 [ '野菜室' ]) ? $ 冷蔵庫 [ '野菜室' ] : 八百屋さんに買い物 () エルビス 演算子 の条件式部分に自動で isset() を付けてくれているような感じです。比較 演算子 なのにtrue,falseの判定じゃなくなったと思うかもしれませんが、実際には見えない isset() の返り値であるtrue,falseで判定しているのです。これだけ聞くと PHP ではnullはfalsyなので エルビス 演算子 だけでいいと思われるかもしれませんが、これまた便利な場面があるのです。それは配列のキー操作をする場合です。 PHP のコードでやたらめったら isset() を使用してから配列操作を行なっているのを目にしたことはないでしょうか。一回 エルビス 演算子 を使用して以下のコードを実行してみましょう。 <?php $ refrigerator = [] ; $ vegetable = $ refrigerator [ 'vegetableRoom' ] ?: 'tomato' ; 実行結果は以下の通りです。 Warning: Undefined array key "vegetableRoom" in /tmp/preview on line 3 野菜室というキーは冷蔵庫配列に存在しないよ と怒られてしまうわけですね。   では、null合体 演算子 を使用して同じコードを実行してみましょう。 <?php $ refrigerator = [] ; $ vegetable = $ refrigerator [ 'vegetableRoom' ] ?? 'tomato' ; 結果はエラーが出ないはずです。このまま $vegetable を出力すると中身は'tomato'です。ということでnullチェックをするときはnull合体 演算子 を使用した方が安全にチェックできるというわけです。使い道としてWebアプリケーションを想定するとして、フロント側からパラメータが配列で渡ってくることが多いと思います。その時、値を取り出して処理を行うわけです。ただ、全ての値がきちんと送られてくるとは限りません。空欄可のアンケートフォームもあるでしょう。そんな時、何も値がない場合は内部的にはデフォルト値として一定の値を使おうという場面が出てくるわけです。こういった場面ではnull合体 演算子 はとても短く処理を書く事ができます。例のコードだと、野菜室キーは野菜を入れるときに初めてキーを作成する可能性もあるわけで、それを全て考慮してコードを書くのは面倒くさいですよね。 PHP ではピタッと使いどきがハマる場面が結構あると思いますので存在を頭の中に入れておきましょう! おわりに 本記事では PHP の比較 演算子 を改めて見ていきました。言語の公式ドキュメントを読むのって楽しいですよね。本来調べたかったことと別の発見があったりしてついつい読んでしまいます。 PHP にこんな機能があったんだとかこんな仕様なのかよ!と突っ込みたくなってしまったりと改めて基本の機能や仕様を見返してみると案外ベテランの方でも面白い発見があるかもしれません。 PHP は何かとクセのある言語ですがそれも含めて愛おしい言語だと思いますので今後もPHPerライフ楽しんでいきましょう。 参考文献 PHP: 比較演算子 - Manual エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com
アバター