TECH PLAY

株式会社ラクス

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

935

こんにちは 配配メール開発課 Jazumaです。 業務やプライベートでChatGPT等のAIを使いながらもより良い回答を得るためのプロンプトの作り方が分からないという方は多いのではないでしょうか。かく言う私もその一人です。 そこで今回は Best practices for prompt engineering with OpenAI API を見てプロンプトエンジニアリングのベストプ ラク ティスを整理します。 より良い出力を得るためのプロンプト 最新のモデルを使う プロンプトの冒頭で指示を出す・区切り文字で指示と文脈を明確に区切る 具体的に指示を出す・説明する・詳細を記載する 例を提示して出力のフォーマットを明確に示す zero-shotから始め、few-shot, fine-tuneの順に試す あいまいな指示や不正確な指示を減らす 「してほしくないこと」ではなく「してほしいこと」を指示する コードを生成させる際は特定のパターンに誘導するための文言を含める 終わりに より良い出力を得るためのプロンプト 最新のモデルを使う より良い回答を得るために最新のモデルを使うことが推奨されています。2023年4月時点においてはテキスト生成:GPT-4, 画像生成: DALL・E, 音声変換:Whisper が有力な選択肢になると思われます。 1 プロンプトの冒頭で指示を出す・区切り文字で指示と文脈を明確に区切る # 悪い例 Summarize the text below as a bullet point list of the most important points. {text input here} # 良い例 Summarize the text below as a bullet point list of the most important points. Text: """ {text input here} """ OpenAIのドキュメントでは上記のように文脈部分を区切り文字で囲う例が紹介されていましたが、別の資料では指示の部分を「###」で囲っていました。 どちらがより良いプロンプトかは未確認です。 具体的に指示を出す・説明する・詳細を記載する コンテキスト・出力結果・出力の文字数・出力形式等はなるべく具体的に詳しく指定します。 # 悪い例 経費精算システムを紹介するメールを作成してください。 # 良い例 経費精算システムを宣伝するためのメールの本文を作成してください。 詳細:""" 形式:HTML 対象:企業の経理担当者 容量:20KB程度 内容:https://www.rakurakuseisan.jp/ を本文に含める """ 例を提示して出力のフォーマットを明確に示す 出力のフォーマットを具体的に示してあげるとモデルからより良い回答が得られます。 また、出力形式が定まることにより、出力結果をプログラムで解析しやすくなります。 # 悪い例 以下のテキストで言及されているエンティティを抽出します。次の 4 つのエンティティ タイプを抽出します: 会社名、人名、特定のトピックとテーマ。 テキスト: {text} # 良い例 以下のテキストで言及されている重要なエンティティを抽出します。最初にすべての会社名を抽出し、次にすべての人名を抽出し、次にコンテンツに適合する特定のトピックを抽出し、最後に一般的な包括的なテーマを抽出します 望ましい形式: 会社名: <comma_separated_list_of_company_names> 人名: -||- 特定のトピック: -||- テーマ: -||- テキスト: {text} zero-shotから始め、few-shot, fine-tuneの順に試す 前提事項 zero-shot: プロンプトに例を示さずに指示だけを出すこと few-shot: プロンプトに何件か例を提示して指示を出すこと fine-tune: 特定のタスクをより高精度に実施するために、既存のモデルにデー タセット を与えて再学習させること まずは簡単なzero-shotプロンプトで指示を出し、上手くいかない場合はより高度なプロンプトを使います。 あいまいな指示や不正確な指示を減らす 指示を出す際は「なるべく」「少なく」等の形容詞ではなく、具体的な数値を使うことが望ましいとされます。 # 悪い例 楽楽精算についてなるべく短く、いくつかの文で一文が長くなりすぎないように説明してください。 # 良い例 楽楽精算について3 ~ 5分程度で説明してください。1文あたりの文字数は40字程度です。 「してほしくないこと」ではなく「してほしいこと」を指示する # 悪い例 以下は、エージェントと顧客の間の会話です。ユーザー名やパスワードを聞かないでください。繰り返さないでください。 顧客: アカウントにログインできません。 エージェント: # 良い例 以下は、エージェントと顧客の間の会話です。エージェントは問題の調査と解決策の提案を試みますが、個人情報に関する質問は控えます。ユーザー名やパスワードなどの情報を尋ねる代わりに、ユーザーにヘルプ記事 www.samplewebsite.com/help/faq を案内します。 お客様: アカウントにログインできません。 エージェント: ※ 良い例でも「控えます」のようにしてほしくないことを指示しているように見えますが... 否定文ではなく肯定文で指示をすることが重要なのでしょうか。 この点についてはOpenAIのドキュメントには記載されていませんでした。 コードを生成させる際は特定のパターンに誘導するための文言を含める 例えば SQL を生成したい時は「SELECT」、 Python や JavaScript のコードを生成したい時は「import」をプロンプトの末尾に記載すると 期待するコードをモデルが生成しやすくなると言われています。 # 悪い例 # Write a simple python function that # 1. Ask me for a number in mile # 2. It converts miles to kilometers # 良い例 # Write a simple python function that # 1. Ask me for a number in mile # 2. It converts miles to kilometers import 終わりに OpenAIのドキュメントでは「なんとなくこうすると上手くいく」という漠然とした経験則が 言語化 されていました。 業務でAIを使う場合、やはりコードを生成するのが主な用途になると思います。 今後コード生成に特化したプロンプトのベストプ ラク ティスも 調べてみたいと思います。 Models - OpenAI API ↩
アバター
2回目の投稿です。rks_mnkiです。 インフラエンジニア的なことをやっています。 さて今回は、「ブラウザ環境上の操作のみでシステムログを取得する環境構築」について、実際の設定内容などを踏まえながらご紹介したいと思います。 目次: 1.導入を進める背景、課題 2.システム要件 3.利用ツールの選定 4.システム全体の構成イメージ 5.各種コンポーネントの説明 Jenkinsジョブ Promtail Loki Grafana Nginx 6.Docker Composeによる実装 7.操作の流れをご紹介 8.まとめ 1.導入を進める背景、課題 弊社では、日々のシステム運用業務の中でインフラ以外の開発やカスタマーサポート部門でも、エラー調査や顧客からの問い合わせ対応などでログを必要とすることがあります。 その際、従来はシステムが稼働する本番環境上にある専用サーバに対して特殊な経路でリモートログインし、以下のような流れでログを取得していました。 取得したい対象サーバ名を記載したリストを作成 定期的に「なんらかの処理」が走って、対象サーバから決められたログが取得されている その中身を確認 ここで「なんらかの処理」と表現したのは、このログを取得する仕組みが大昔に作成されたものであり、正直あまり詳しく理解できていない為です。 また、 Linux 環境上での操作となるために、あまり慣れてない部門のメンバーでは対応しづらく、そのほかの問題もあって利用頻度は低い状態でした。 結局は、インフラ部門に対してログの提供依頼があり、その都度インフラにてログを取得するという対応が発生しています。 課題をまとめると、このようになります。 用意されているログ取得の仕組みが古すぎてメンテナンスされていない 現行の仕組みでは取得対象サーバを登録してから最長5分の待ち時間が発生 Linux 環境( コマンドライン 操作)に慣れていないメンバーには使いづらい ログの容量が大きい環境では処理が タイムアウト になることも  ↓↓ 用意された仕組みは使われず、インフラへのログ提供依頼ケースが多くなる そのため、ログ取得という「生産性の高くない」業務稼働がインフラ部門で一定の割合を占めてました。 あわせて、昔の仕組みを理解して改修するのは嫌だったので、それならイチから新たにログを取得できる仕組みを構築しようと考えました。 2.システム要件 構築するにあたり、まずは以下のようなシステム要件を検討しました。 基本的にブラウザ環境での操作で完結させる 取得したい対象期間を指定可能 利用ユーザが本番環境へのログインは不要 その他、以下の前提条件を定義。 対象サーバは1台のみ指定 各部署単位で必要なログの種類を選定し、それ以外は選択できないようにする これ以外のケースでログが必要な場合は、これまで通り別途インフラへの対応を依頼頂く運用を想定しています。 3.利用ツールの選定 既にインフラでの業務で利用しているJenkinsでのジョブ実行をベースに検討。 Jenkins+各種 スクリプト promtail+loki+Grafana Nginx 今回は、とりあえず取得したログの中身をそのまま閲覧できればいいので、比較的シンプルに導入できそうなツールを検討しました。 4.システム全体の構成イメージ 以下のような構成を検討し、構築を進めました。 システム構成イメージ ログデータを取得するための ダウンロードサイト と、ログ閲覧用サイトは別々で提供するようにしました。 なお、このイメージ図では掲載できていないが、JenkinsジョブのURLなども含めてアクセス先URLリンクをまとめたサイトを1つ構築して、Nginxにて提供しています。 5.各種 コンポーネント の説明 今回、長期間のログに関する傾向分析ではなく、必要な期間に限定したログの内容を参照する目的にフォーカスして構成しています。 Jenkinsジョブ 対象ホストやログ、期間を指定できる ユーザインタフェース を提供 サービス環境内の管理サーバに設置された スクリプト を実行して、必要なログを取得 取得したログを「ログサーバ」に格納 【補足】 指定されたパラメータは、 スクリプト に引き渡されて実行されます。 その中で、以下のコマンドにて「対象期間」に該当するログファイルを抽出しています。 find {対象ログのフルパス*} -newermt '{開始日付} 0:0:0' -and ! -newermt '{終了日付} 23:59:59' この時、ログファイルの「タイムスタンプ(最終更新日時)」をもとに検索をかけるので、ログの中身で記録されている日時ではないことに注意です。 例)1週間単位でログローテーションされているケースが多い -rw------- 1 root root 346K 3月 12 04:02 messages.5.gz -rw------- 1 root root 2.1M 3月 19 04:02 messages.4.gz -rw------- 1 root root 1.9M 3月 26 04:02 messages.3.gz -rw------- 1 root root 2.0M 4月 2 04:02 messages.2.gz -rw------- 1 root root 2.2M 4月 9 04:02 messages.1.gz -rw------- 1 root root 33M 4月 15 18:03 messages 上記における問題あるケースとして、対象期間を【4/4~4/6】で指定してしまうと、該当するタイムスタンプのログファイルが存在しないので、何もアクションされない事になります。 当然、ログの中身としては該当期間のものが出力されていますが、ファイル単位での検索処理となっているため、このような動きとなってしまいます。 そのため、ジョブ実行時にログが存在しないといったエラーになると、以下のようにメッセージを返すようにしています。 #### WARNING #### No data. Please check HostName or change the specified period and try again. Promtail 所定の位置に格納されたログを収集し、Lokiに転送 各ログの種別に応じてタグ付け Timestampの設定 サンプルコード server: http_listen_port: 9080 grpc_listen_port: 0 positions: filename: /tmp/positions.yaml clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: - job_name: messages static_configs: - targets: - localhost labels: job: messages __path__: /mnt/data/collect_logs/*/*/messages* デフォルト設定では、ログを収集した時刻(promtailが読み込んだ時刻)をタイムスタンプとして扱う事になります。これでは、ログが出力されている日時で検索したいケースに対応することができません。 そのため、ログに出力された日時をタイムスタンプとして扱えるように設定します。 具体的には、 timestamp に関する設定によりデータを解析し、Lokiにて保存されたログエントリのタイムスタンプを上書きします。 例)/var/log/messagesの場合:このログに出力されている日時をタイムスタンプとして認識させる。 Apr 9 05:54:04 {ホスト名} systemd[1]: session-51560.scope: Succeeded. pipeline_stages を以下のように設定。 pipeline_stages: - match: selector: '{job="messages"}' stages: - regex: expression: "^(?s)(?P<time>.+? \\d\\d:\\d\\d:\\d\\d):? (?P<content>.*)$" - timestamp: source: time format: 'Jan _2 15:04:05' location: Asia/Tokyo regex にて日時のタイムスタンプを 正規表現 で指定し、 time 変数に格納。 その下部にある timestamp の source で先ほど設定した time 変数を指定し、 format でタイムスタンプ形式を指定。 この辺りは、公式サイトなどを参考にしながらトランアンドエラーにて設定した為、あまり詳しく理解できていませんので、適切な設定方法があればご指摘ください。。 grafana.com Loki ログの集約と検索が行える Grafanaと連携 ※基本的には、configはデフォルトのまま運用。 Grafana 可視化プラットフォーム Lokiから連携された時系列データを処理する Nginx Webによるログの ダウンロードサイト を提供 URLリンクの管理サイトを提供 デフォルトのconfigを利用し、必要に応じてHTMLファイルを作成。 6.Docker Composeによる実装 今回、上記の各種 コンポーネント に関する複数のコンテナを、Docker Composeにより一括で管理しています。 サンプル)docker-compose.yml version: '3' services: nginx: image: nginx container_name: nginx environment: TZ: Asia/Tokyo ports: - "80:80" volumes: - /data/sample-log_collect/dev:/usr/share/nginx/html/download/sample-develop - /data/sample-log_collect/cs:/usr/share/nginx/html/download/sample-support - /log_aggregation/nginx/config/default.conf:/etc/nginx/conf.d/default.conf - /log_aggregation/nginx/config/index.html:/usr/share/nginx/html/index.html - /log_aggregation/nginx/config/main.css:/usr/share/nginx/html/main.css - /log_aggregation/nginx/config/manual.html:/usr/share/nginx/html/manual.html networks: - sample-log_aggr promtail: image: grafana/promtail:2.3.0 container_name: promtail environment: TZ: Asia/Tokyo command: -config.file=/mnt/config/promtail-config.yaml volumes: - type: bind source: /data/sample-log_collect target: /mnt/data/collect_logs - type: bind source: /log_aggregation/promtail/config/promtail-config.yaml target: /mnt/config/promtail-config.yaml networks: - sample-log_aggr loki: image: grafana/loki:2.3.0 container_name: loki environment: TZ: Asia/Tokyo ports: - 3100:3100 command: -config.file=/mnt/config/loki-config.yaml volumes: - /log_aggregation/loki/config/loki-config.yaml:/mnt/config/loki-config.yaml networks: - sample-log_aggr grafana: image: grafana/grafana:latest container_name: grafana environment: TZ: Asia/Tokyo hostname: grafana volumes: - ./data/grafana:/var/lib/grafana networks: - sample-log_aggr ports: - 8080:3000 user: "0:" networks: sample-log_aggr: name: sample-log_aggr ipam: driver: default config: - subnet: 10.***.***.0/24 なお、デフォルトだとdockerのログは docker-compose logs コマンドで確認が必要ですが、正直使いづらく過去ログも保管しておきたいので、syslog経由でログを出力するように設定しています。 以下の設定を各コンテナごとに追記。 logging: driver: syslog options: syslog-facility: daemon tag: log-aggregation/{{.Name}}/{{.ID}} すると、このようなログが生成されます。 ファイル名:/var/log/docker/log-aggregation.log ログの中身: Apr 15 17:46:07 HOST log-aggregation/loki/197fdf4139ac[207994]: level=info ts=2023-04-15T08:46:07.056567716Z caller=table_manager.go:476 msg="creating table" table=index_19073 準備が整い docker compose up -d コマンドにて起動すると、以下のようなプロセスが生成されます。 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ab60c21fffff grafana/grafana:latest "/run.sh" 34 hours ago Up 34 hours 0.0.0.0:8080->3000/tcp grafana ce89712d582a nginx "/docker-entrypoint.…" 34 hours ago Up 34 hours 0.0.0.0:80->80/tcp nginx 197fdf4139ac grafana/loki:2.3.0 "/usr/bin/loki -conf…" 34 hours ago Up 34 hours 0.0.0.0:3100->3100/tcp loki 691b10c43729 grafana/promtail:2.3.0 "/usr/bin/promtail -…" 34 hours ago Up 34 hours promtail 7.操作の流れをご紹介 まず、対象サーバや期間を、取得したいログの種別を選択してJenkinsジョブを実行します。 Jenkinsジョブの入力画面 ジョブが正常実行されたら、ブラウザ上より欲しいログファイルをダウンロードできるようになります。 対象ログファイルをクリックすると、ダウンロードされます。この場合、利用ユーザのローカル環境にログをダウンロードし、 テキストエディタ 等でログを確認するケースを想定しています。 ダウンロードサイト または、Grafanaによるブラウザ上でのログの閲覧も可能です。 ダッシュ ボード画面も用意していますが、左メニューから Explore を選択すると、取り込んだログファイルを指定して、その内容を閲覧することができます。 Grafana_explore さらに、対象期間の絞り込みや、キーワードによる対象ログの抽出も可能ですので、特定ログを調査したい場合に活用できます。 以下の例では、 systemd のワードで抽出しています。 キーワード検索 8.まとめ 使い勝手の良さを意識して、 ブラウザ上での操作で完結する本番システムログの取得環境 の構築に取り組み、なんとか思い描いた形に仕上げることができました。 これにより、 従来の仕組みと比べてお手軽に使えるようになり、今まで一定の頻度で発生していた 「インフラ部門へのログ提供依頼」 の件数が少しでも減少することを期待 しています。 そのためにも、少しでも見やすいマニュアルを作成して、他部門へ展開しています。 ただし、 対象サーバが複数であったり、対象期間が1ヵ月を超えるようなケースにおいては、本システムを使っての取得はNG としているので、まだ一定数以上の依頼は発生する想定です。 また、リリースしたばかりのシステムであるため、今後利用されていく中で様々な不具合や改修ポイントが見つかると考えており、より良い形になるよう今後もメンテナンスしていきたいと思います。 (実際、「あれ…」ってなる箇所を逐次発見しては改修している状況です。) 以上です。本記事が少しでもお役に立てれば嬉しいです。
アバター
はじめに こんにちは、imamoto です。 今年も プロ野球 が開幕し、すっかり春だなぁと感じる今日この頃です。 さて今回は、Go言語でのWeb アプリ開発 をした際のチーム内のノウハウを、 GitHub 上で公開してみた話を書いていきたいと思います。 目次 はじめに 目次 公開したGitHubリポジトリの紹介 資料の構成について モジュラーモノリスで実装した背景 ノウハウ公開に至った経緯 おわりに 公開した GitHub リポジトリ の紹介 私はSRE課に所属しているのですが、SRE課ではGo言語でWebアプリを開発することが多いです。 独立した機能を持った複数のGo Moduleを、1つの モノリス なアプリケーションとしてデプロイする、 いわゆる モジュラー モノリス という設計での アプリ開発 にもチャレンジしているのですが、 その開発中にチームとして蓄積した実装ルールやノウハウのうち、汎用化できそうな部分をまとめて、 以下の GitHub リポジトリ 上に公開したので紹介させてください。 github.com この記事では、以下の3点について説明していきたいと思います。 資料の構成について モジュラー モノリス で実装した背景 ノウハウ公開に至った経緯 資料の構成について まとめた内容は、以下の GitHub 上のREADMEファイルを出発点にして学習できるようになっています。 github.com 前提として、Webフレームワークは gin-gonic/gin 、ORMは GORM 、DI ツールは google/wire を使用しています。 また、 3.推奨する学習順序 の項にも記載してありますが、資料は以下の9つの領域で構成されています。 (2023年4月時点での内容であり、予告なく構成を変更する可能性があります) Basics : DDD の基本説明、 ディレクト リ構成等、当資料を参照するにあたって認識いただきたい前提知識を記載しています。 Multi Module Project : モジュラー モノリス 構成のアプリに対して、Go の Workspace 機能を用いて名前解決する方法を記載しています。<3. Layered Architecture : 単一の Go Module はレイヤード アーキテクチャ の構成を取っています。各レイヤーの説明と基本的な実装方法を記載しています。 Dependency Injection : 各レイヤーをまたいだオブジェクトのファクトリ関数を google/wire で生成する手法を記載しています。 Transaction : DB トランザクション の制御の実装方法を記載しています。 Routing And Middleware : Web フレームワーク Gin を用いたルーティングの実装方法を記載しています。 Row Level Security : PostgreSQL の Row Level Security を、ORM の Gorm を用いて実装する方法を記載しています。 Goroutine Job : Goroutine を使った バッチ処理 の実装方法を記載しています。 Unit Test : 単体テスト の基本的な記述方法を記載しています。 DDD 、 レイヤード アーキテクチャ 、 モジュラー モノリス 等の全体的な設計に関わるトピックだけでなく、 DB トランザクション 、 RLS 、 DI 等の具体的な実装に関わるトピックも含まれています。 多くの方に少しでもお役に立てていただくことができますと幸いです。 モジュラー モノリス で実装した背景 今回まとめたノウハウはモジュラー モノリス のアプリケーションであることが前提となっていますが、 設計としてモジュラー モノリス を選択した背景は以下の3点に集約されます。 業務ロジックの複雑度 単一チームでの開発のためMSA化の必要性が低い Go Moduleを使えばシンプルにモジュラー モノリス 構成を実現可能 実現したい業務ロジックがかなり複雑なアプリを開発する必要があったので、 単純な モノリス のアプリケーションにすると複雑度が高くなりすぎるというリスクが開発当初からありました。 そのため、コンテキストマップの設計をすることで適切なモジュール分割を実施し、 実際のアプリケーションの構成と一致させることを目指しました。 ただ、単一チームでの開発で複数のマイクロサービスに分けて開発するとメンテ 工数 だけ増加しメリットがあまり無い点、 Go言語のGo Moduleを連携させればコンテキストマップの構成とアプリの構成を一致させることができる点を踏まえて、 モジュラー モノリス という形を取ることにしました。 ノウハウ公開に至った経緯 私が所属するSRE課は、私を含めメンバー全員がほぼGo言語の開発経験がないところからのスタートだったので、 有識者 がみんなを引っ張っていくスタイルを取ることができませんでした。 そのため、学習・試行錯誤・軌道修正をを繰り返しながらより良い実装の仕方を固めていくという形で開発を進めました。 一方、設計や実装の背景と、それらに付随する用語を理解する必要がある領域が多い構成になっているにも関わらず、 それらの ドキュメンテーション が追い付いていなかったため、 途中から参画してくるメンバーに対するオンボーディングにおいては口頭で説明せざるを得ない状況になっており、 既存メンバーと新規メンバーの間で、重要な概念に対する理解度に開きが生まれてしまうリスクが生まれてしまっていました。 そのリスクを解消するべく、必要なノウハウを汎用的にまとめてチームで共有することにしました。 また、自分たちの試行錯誤の結果を一般に公開することで、他のエンジニアの助けになれば良いなという思いと同時に、 より良い実装方針のフィードバックも外部からいただけるかもしれないという思いもあり、 今回は GitHub 上で一般公開するという結論に至りました。 おわりに 今回は、SRE課におけるGo言語のWeb アプリ開発 のノウハウを公開したお話について書かせていただきました。 もしご興味があれば、公開させていただいた資料をご一読いただき、感想をいただけると嬉しいです。 それでは、また!
アバター
楽楽精算開発部の id:smdr9p です。主に Java を使ったサーバーサイドを担当しています。 前置き GoF の デザインパターン はご存知でしょうか。 ご存知の方も多いかと思いますが簡単に説明すると、 GoF の デザインパターン とは Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides の4人、通称 Gang of Four 、略称 GoF によって書かれた書籍、Design Patterns: Elements of Reusable Object-Oriented Software(邦題: オブジェクト指向 における再利用のための デザインパターン )に掲載されている23の デザインパターン のことです。 GoF パターンや単に GoF と呼ばれることもあります。(この記事では以降は GoF パターンと呼びます。) これらは本のタイトルにも示されているとおり オブジェクト指向 プログラミングの領域における デザインパターン をまとめたものですが、この本、そしてこれに掲載された デザインパターン があまりにも有名であるため、単に「 デザインパターン 」と言った場合にこの GoF パターンを思い浮かべる人も数多くいるほどです。 GoF パターンのうち、特に有用性の高いものは言語仕様自体に取り込まれたり、メジャーな ミドルウェア でサポートされたりしているものもあります。 このように GoF パターンは多くの人々によって長年活用され続けてきているものの、 デザインパターン はこれだけ知っていれば十分、というわけにはいきません。なにしろ GoF パターンは書籍が発行されたのが 1994 年、 Java も PHP もリリースされる前であり、それから オブジェクト指向 プログラミング界隈にも様々な知見が蓄積され、新たな常識やベストプ ラク ティスが作られてきました。 デザインパターン についても新たなパターンの発見や既存のパターンの再評価、ブラッシュアップがなされています。 そこで、それらの中から個人的によく見る、または使いやすいパターンを何回かにわけていくつか紹介したいと思います。 前置き Null Objectパターン パターン適用前 Null Objectパターンを適用してみる まとめ 関連するデザインパターン Strategyパターン Stateパターン Compositeパターン 参考文献 Null Objectパターン 今回は Null Objectパターンです。 このパターンを語弊を恐れずに簡単に言うと 「 null をオブジェクトとして扱う 」 です。 これにより null を ポリモーフィズム に組み込むことができ、null に関するさまざまな面倒や障害を避けることができます。 このパターンでは、実際のオブジェクトと同じインターフェースを持つ null オブジェクトを作成します。このオブジェクトは null 値の代わりに使用され、null 値のときに期待される振る舞いをします。これにより、今まで null チェックを行って null の場合はスキップ、null でない場合はオブジェクトのメソッドを実行、のような処理をしていた場面で、null チェックなしにオブジェクトのメソッドを実行することができるようになります。 パターン適用前 以下の ありがちな コードを Null Object パターンを使って改善してみます。 /** インターフェース */ public interface Shape { void draw(); int sumOfInteriorAngles(); } /** 実装クラス1 */ public class Triangle implements Shape { public void draw() { System.out.println( "△" ); } public int sumOfInteriorAngles() { return 180 ; } } /** 実装クラス2 */ public class Square implements Shape { public void draw() { System.out.println( "□" ); } public int sumOfInteriorAngles() { return 360 ; } } /** Factoryクラス */ public class ShapeFactory { public static Shape createPolygon( int numOfCorners) { return switch (numOfCorners) { case 3 -> new Triangle(); case 4 -> new Square(); default -> null; } } } 図形を表す Shape インターフェースの実装として、 Circle クラスと Square クラスがあります。 図形を描画する draw() メソッドに加え、図形の内角の和を取得する sumOfInteriorAngles() メソッドも持っています。 またそれらの実装クラスを生成する ShapeFactory クラスもあります。角の数を指定するとそれに応じた図形クラスの インスタンス を返してくれますが、対応する実装がない場合は null を返します。 そして以下がそれらのクラスを利用する利用側のクラスです。 /** 利用側クラス */ public class Main { public void drawAll(List<Shape> shapes) { for (Shape shape : shapes) { if (shape == null ) { // nullチェック continue ; } shape.draw(); } } public int totalInteriorAngles(List<Shape> shapes) { return shapes.stream() .mapToInt(Shape::sumOfInteriorAngles) .sum(); } } 図形リストの shapes を渡してリストの図形を描画したり、リスト全体の内角の総和を取得できたりします。 ここで渡されるリストの生成方法については記載していませんが、リストの要素はすべて ShapeFactory クラスによって生成されたものが入っていると考えてください。 上記のとおり ShapeFactory クラスは null を返すことがあるのでリストには null が含まれている可能性があります。リストから取り出した Shape の実装クラスのオブジェクトはしっかり null チェック して、「 null の場合は何もしない 」ようにしています。 …のつもりだったのですが、実は一箇所 null チェックを忘れている箇所がありました。 public int totalInteriorAngles(List<Shape> shapes) { return shapes.stream() .mapToInt(Shape::sumOfInteriorAngles) .sum(); } このメソッドに渡されるリストにも null が含まれる可能性がありますので、ここでも null チェックが必要でした。 public int totalInteriorAngles(List<Shape> shapes) { return shapes.stream() .filter(Objects::nonNull) // ここも要nullチェックでした .mapToInt(Shape::sumOfInteriorAngles) .sum(); } このケースではここだけの修正で済みましたが、他の Shape を参照している箇所で同様のチェック漏れがないか確認する必要がありそうです。また、 Shape に新たなふるまいが追加された場合にも継続して注意していく必要がありますし、 Shape 以外のクラスでも類似の例がないか確認する必要もあるかもしれません。 このように、null を null のまま使い続ける限り、常に null を意識してコードを書いていく必要があります。 しかも null チェックを忘れていたとしても コンパイラ はエラーを吐いてはくれず、実行時に条件が合った場合に初めて NullPointerException が発生するのも頭の痛いところです。テストコードでチェックするとしてもテストケースから抜けていたら同じことです。 Null Objectパターンを適用してみる このような場合に Null Objectパターン が役立ちます。このパターンを適用することで、利用側で null チェックが不要になり、コードがシンプルで読みやすくなるだけでなく、今後の機能追加や変更に対しても安全性が向上します。 それでは、上記の実装に Null Objectパターンを適用してみましょう。 Null Objectパターンでは、null の場合に適用されるクラスを作成し、いままで各利用箇所に書かれていた「 null の場合のふるまい 」をあらかじめ持たせておきます。 Shape であれば draw() のときは文字どおり「何もしない」ふるまいをします。 sumOfInterirorAngles() であれば「何もない」数値である0を返すふるまいをします。 /** Null Object実装クラス */ public class NullShape implements Shape { public void draw() { // 何もしない } public int sumOfInteriorAngles() { return 0 ; // 何もない } } そして、 Factory クラスは適切な Shape の実装クラスの インスタンス を生成できない場合、Null Object である NullShape の インスタンス を返すようにします。 /** Null Object対応 Factoryクラス */ public class ShapeFactory { public static Shape createPolygon( int numOfCorners) { return switch (numOfCorners) { case 3 -> new Triangle(); case 4 -> new Square(); default -> new NullShape(); // ここが変わった } } } ※ ここでは簡易的に都度 new していますが、可能であれば Singletonパターンで NullShape の インスタンス は1つのみ生成されるようにしておくのが望ましいです。 これによりリストには null の代わりにこの NullShape オブジェクトが入るようになりますので、リストから取り出した Shape の null チェックは不要になります。 /** Null Objectの恩恵を受けた利用側クラス */ public class Main { public void drawAll(List<Shape> shapes) { for (Shape shape : shapes) { // if (shape == null) { // nullチェック不要! // continue; // } shape.draw(); } } public int countCorners(List<Shape> shapes) { return shapes.stream() // .filter(Objects::nonNull) // ここもnullチェック不要! .mapToInt(Shape::numOfCorners) .sum(); } } もちろんこの変更によってエラーは出ませんし、出力結果も変わりません。 いままでは利用側のコードで「オブジェクトが null であれば何もしないようにする、null でなければオブジェクトに命令する」判断が必要でした。 しかし Null Objectパターンを適用したコードでは、 利用側は「オブジェクトに命令する」のみ でOKです。オブジェクトが Null Object でなければ今までと同じふるまいをしてくれますし、Null Object であれば 「何もしない」をしてくれます 。 このように null を NullShape として Shape の派生とすることで、null を特別扱いすることなく他のクラスと同等の扱いができるようになります。将来的に Shape を利用するコードが追加されたとしても null に関する問題が発生する可能性を軽減できるでしょう。 まとめ このパターンは、null チェックが頻繁に行われるプログラムや、null が意図しないエラーやバグの原因となりやすい場面で特に有用です。 Null Objectパターンは null すらオブジェクトにすることによって ポリモーフィズム の活用範囲を広げる、非常に オブジェクト指向 らしいパターンだと思います。 あなたの既存のコードで Null Objectパターンが活用できる場所がないか、ぜひ探してみてください。 関連する デザインパターン Null Objectパターンに関連、あるいは活用できる他の デザインパターン をいくつか紹介します。 Strategyパターン Strategyパターンは アルゴリズム や実装を実行時に柔軟に切り替えることができるようにするパターンです。Strategy として何もしたくないケースがある場合に Null Object で Strategy を実装する事が可能です。開発やテストのときにログが出力されないようにするために NullLogger のようなクラス使ったことがあるかもしれませんが、それがまさに Null Object です。 Stateパターン Stateパターンはオブジェクトの内部状態にもとづいてオブジェクトのふるまいを変更するパターンです。State が何もない状態のときのふるまいを Null Object に実装することが可能です。初期化前などの状態に null を使用しているのであればそれを Null Object にしてみるといいかもしれません。 Compositeパターン Compositeパターンは階層構造を表現するパターンで、容器と中身を統一的に扱えるようにします。中身が何もないケースを Null Object で実装することで、容器の中身が何もない場合も変わらず統一的な操作を行うことが可能になります。 参考文献 リファクタリング -プログラムの体質改善テクニック Martin Fowler:著 児玉 公信 / 平澤 章 / 友野 晶夫 / 梅沢 真史:翻訳 ピアソン・エデュケーション オブジェクト指向 における再利用のための デザインパターン (改訂版) Erich Gamma / Richard Helm / Ralph Johnson / John Vlissides:著 吉田 和樹 / 本位田 真一:監修 SBクリエイティブ 増補改訂版 Java 言語で学ぶ デザインパターン 入門 結城 浩:著 SBクリエイティブ
アバター
配配メール開発課moryosukeです。 2023/03/23(木) ~ 03/25(土)の3日間に渡ってPHPerKaigi2023が開催されました。 今回も前回に引き続きハイブリッド開催となり、現地・配信ともに大盛況でした。 このイベントは 日本PHPユーザ会 主催のイベントで、 ラク スはスポンサーとして協賛させていただいています。 https://phperkaigi.jp/2023/ ラク スからは7人が登壇した他、多くのメンバーが参加しました。 そこで今回は参加者によるレポート、そして ラク スからの登壇者本人によるレポートを紹介させていただきます。 3/23(木)前夜祭 名著「パーフェクトPHP」のPart3に出てきたフレームワークを令和5年に書き直したらどんな感じですかね? レポート ある日オレオレフレームワークを作りたくなったぞ!! レポート 名付けできない画面を作ってはならない - 名前を付けるとは何か レポート 3/24(金) 1日目 作って理解するバックドア レポート 時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる レポート 防衛的 PHP: 多様性を生き抜くための PHP 入門 レポート 詳説「参照」:PHP 処理系の実装から参照を理解する レポート Composerを「なんとなく使う」から「理解して使う」になる レポート PHP Parserで学ぶPHP レポート 安全にプロセスを停止するためにシグナル制御を学ぼう! レポート パフォーマンスを改善せよ!大規模システム改修の仕事の進め方 発表者本人によるレポート 不幸を呼び寄せる命名の数々  ~君はそもそも何をされてる方なの?~ 発表者本人によるレポート stdClassって一体何者なんだ?! 発表者本人によるレポート 特徴、魅力を知って、各PHPフレームワークを使いこなそう! レポート 3/25(土) 2日目 実例から学ぶ変化に強いテーブル設計 - 責務の分解とRDBMSの上手い使い方 レポート いろいろなフレームワークの仕組みを index.php から読み解こう レポート 約10年もののPHPアプリケーションとの付き合い方と、今後10年改善し続けるための取り組みについて レポート 計測できるレガシーさを捉え、コード改善に対処する レポート 【実録】「PHP_CodeSniffer」で始める快適コードレビューライフ 発表者本人によるレポート PHPマジックメソッドクイズ! 発表者本人によるレポート フレームワークが存在しない時代からのレガシープロダクトを、Laravelに”載せる”実装戦略 レポート まとめ PHPerのためのコミュニティ PHPTechCafe 3/23(木)前夜祭 名著「パーフェクト PHP 」のPart3に出てきた フレームワーク を令和5年に書き直したらどんな感じですかね? report by id:rakuinoue きんじょうひできさん (@o0h_) による発表です speakerdeck.com レポート 2010年頃、同様に MVC フレームワーク を作成し社内に広めていた身からすると、非常に刺さる内容でした。 当時と比べると、 Composerの登場・PSR-xxなどの デファクト 化が進む PHP のバージョンアップに伴う型に関するサポート・静的解析の普及 フルスタ ックより、様々なライブラリを組み合わせていく これに伴う抽象化やDIの概念の必要性 等が変わってきた内容として挙げられていました。 改めて設計技法等含めて、おさらいできる良い内容だったと思います。 ある日オレオレ フレームワーク を作りたくなったぞ!! report by id:akikuchi_rks 果物リンさん (@FruitRiin) による発表です レポート 独自で作成した フレームワーク 、いわゆるオレオレ フレームワーク を作成した過程をお話しいただきました。 composer init で フレームワーク の雛形を作成するところからPakegistに公開するところまでプロジェクト作成の全体の流れを知ることができ、とても勉強になりました。 また、発表の冒頭ではオレオレ フレームワーク の歴史的背景が語られていたのですが、2000年代初頭の今普及しているような フレームワーク が登場していない頃には 当たり前のようにオレオレ フレームワーク が作られていたというお話には衝撃を受けました。 登壇者もお話ししていたようにオレオレ フレームワーク はセキュリティの問題や引継ぎが大変という理由から現代のプロダクションに活用するのは難しいと思いますが、 今回のように フレームワーク を1から作ってみることは普段意識していない フレームワーク の内部的な仕組みを知ることができ、かなり役立つのではないかと感じました。 名付けできない画面を作ってはならない - 名前を付けるとは何か report by id:mrstsgk_rks ちゃちいさん (@chatii) による発表です speakerdeck.com レポート トーク 名は「名付けできない画面を作ってはならない」ですが、画面についての話ではなく 命名 についての話になります。 名前をつける大切さが伝わる発表でした。特に名前を付けて認識を共有するについて、 ユビキタス 言語の例は納得できました。 また、技術的な発表で心理学の概念(愛着理論)が出てきたのは面白かったです。 最後に、リーダブルコードはやっぱり良書でした。 3/24(金) 1日目 作って理解する バックドア report by id:hirobex Rokuさん (@ad5jp) による発表です speakerdeck.com レポート バックドア を設置されるとどうなるのか なぜ バックドア が設置されてしまうのか バックドア の攻撃手法 バックドア を設置されないようにするには といった、 バックドア に関する知識が詰まった トーク でした 中でも、実際に採集された バックドア のサンプルは一見の価値ありだと思います! 時間を気にせず普通に カンニング もしつつ ISUCON12 本選問題を PHP でやってみる report by id:hirobex sji さん (@sji_ch) による発表です speakerdeck.com レポート ISUCON12優勝チームのレポジトリを参考にしつつ、 PHP でハイスコアを目指すという内容でした データベースの ボトルネック 解消後、徐々にGoと引き離されて行く中で、 いかに PHP としてアプリケーション側のスコアを上げていくのかという挑戦が非常に面白かったです Webアプリケーションパフォーマンスに興味のある人は、ぜひ見るべき トーク だと思いました! 防衛的 PHP : 多様性を生き抜くための PHP 入門 report by id:hirobex しけちあさん (@s6n_jp) による発表です レポート 多様性を生き抜くためにどのような防衛手法が取れるのか、という話から始まり PSR-12などの規約や、様々な静的解析ツールの使い方を紹介していただきました コード品質を担保するために、静的解析を使ってみたい人におすすめの トーク です! 個人的に、この トーク で紹介されていた「Rector」というツールが気になりました✍ 詳説「参照」: PHP 処理系の実装から参照を理解する report by id:mrstsgk_rks nsfisisさん (@nsfisis) による発表です github.com レポート PHP における参照の不思議クイズから始まり、 PHP の内部でどのような処理が行われているかの解説を挟んで クイズでの参照の挙動を説明していただき、 PHP における参照を少し理解できました。 特に、 PHP 処理系は C言語 で書かれていますが、 C言語 の知識はあまり必要ではなかったので C言語 を触ったことのない私からすれば、発表が聞きやすかったです。 Composerを「なんとなく使う」から「理解して使う」になる report by id:hiro_ji あすみさん (@asumikam) による発表です speakerdeck.com レポート Composerではどのように依存関係を管理しているかや、 DockerにおけるComposerの取り扱いについてお話しいただきました。 Composerを理解して使うためにはまずcomposer. json /composer.lockがいつ更新されるかを理解する 本番環境に余計なものを入れないためには Copmoser install に[--no-dev]オプションをつける マルチステージビルドでDockerイメージを作成 タイトルにもあるようにComposerを「なんとなく」使っていた身としては必見の内容でした! PHP Parserで学ぶ PHP report by id:takaram inouehiさんによる発表です speakerdeck.com レポート PHP コードを 構文解析 して抽象 構文木 (AST) に変換できるライブラリ PHP -Parser ( nikic/php-parser ) を通して、 PHP 自体について学ぼうという発表です。 PHP -Parserは PHP 本体のコントリビューターでもある Nikita Popov さんが開発しており、PHPStanやPsalmといった広く使われている静的解析ツールも PHP -Parserの上に成り立っています。 普段何気なく書いたり読んだりしている PHP ですが、文と式の違い、関数と言語構造の違い、 スカラー 値の種類など、把握している方はどれくらいいるでしょうか? 私はそれなりに知っているつもりでいましたが、「そんな書き方できたんだ!」ということもあり 目から鱗 でした。 PHP 自体は C言語 で実装されていて、なかなか中身を読むのはハードルが高いですが、 PHP で実装された PHP -Parserは、 PHP を詳しく学びたい人にはちょうどいいと感じました! 安全にプロセスを停止するためにシグナル制御を学ぼう! report by id:hirobex やまもとひろやさん (@HiroyaYamamoto1) による発表です speakerdeck.com レポート プロセスとシグナルの関係を、なんとなくで理解している人におすすめの トーク です! 私自身、とりあえず Ctrl+C でプロセスが止まるんでしょ?といった程度の理解だったのですが この トーク を聞いてプロセスの止め方に適したシグナルがあることや、プロセスの止まり方によって PHP プログラムが制御できることを知りました! パフォーマンスを改善せよ!大規模システム改修の仕事の進め方 report by id:taclose 弊社 前田啓佑 (@taclose) による発表です speakerdeck.com 発表者本人によるレポート パフォーマンス改善の失敗談と成功談を話した内容です。 改めて「計測するのが大事なんだな」と理解してもらえたかと思います。 スライドのほとんどは非エンジニアでも読めるような構成をとっており、若手やマネージャーの方でも読める構成になっています。 今一度パフォーマンス改善のやり方をチーム内で話し合って、共通認識を持ってもらえればと思います。 不幸を呼び寄せる 命名 の数々  ~君はそもそも何をされてる方なの?~ report by id:yamamuuu 弊社 山村光平 (@yamamu096454848) による発表です speakerdeck.com 発表者本人によるレポート 開発をしていると「この変数は何を表しているのだろう?」とか、「この関数は何をしているのだろう?」とか、一目見ただけでは何を伝えたいのかが分からない 命名 を目にすることがあります。 意図が伝わらないだけであれば調べれば済むかも知れませんが、時には 命名 が誤解を招き大問題に発展することもあります。 そんな不幸を呼び寄せる 命名 をあなたも知らず知らずのうちにしているかも知れません。 スライドでは「君はそもそも何をされてる方なの?」と言いたくなる 命名 の数々を紹介し、改めて 命名 の重要性を感じていただきます。 stdClassって一体何者なんだ?! report by id:daina_rks 弊社 寺西帝乃 (@dainabook) による発表です speakerdeck.com 発表者本人によるレポート stdClassを使う場面はよくあるかと思いますが、「そもそもこのクラスは一体何者なのか」と質問されると答えに困るPHPerは多くいるのではないでしょうか。 この発表ではstdClassの生成方法 ⇒ stdClassにできること・できないこと ⇒ stdClassに関する議論の順番で、「stdClassとは一体何者なのか」の答えに近づく内容です。 PHP8.2で導入される「動的プロパティの廃止」にも関連する内容となっております。 stdClassがよくわからない初心者PHPerや、今までなんとなく使ってきた中堅PHPer、stdClassのあり方について考えるベテランPHPer達の理解の手助けになると思います。 特徴、魅力を知って、各 PHP フレームワーク を使いこなそう! report by id:radiocat 弊社 浅野 仁志 (@hitoshi_a0) による発表です speakerdeck.com レポート フレームワーク 選定は「冒険を共にするパートナー選びのようなもの」とのことで冒険のパートナーになぞらえて PHP フレームワーク が紹介されました。 Laravel:多くの人に頼られる人気者、能力も高く王道の主人公 CakePHP :真面目、曲がったことが大嫌い、正義感が強い委員長 Yii:玄人好みの影の立役者、 切れ者 軍師 Slim:最低限の荷物だけで旅をする自由人 永遠の 宗教戦争 的テーマなので「あの フレームワーク は?」という意見も多々あり、反響のあまり発表者が「 Symfony はすみません」とコメントするなど、会場一体の議論を巻き起こすキラーLTとなっていました。また、会場だけでなくオンラインからもコメントが多く寄せられて、会場で流されていたニコ生のコメントともコラボした盛り上がりを見せていました。 SNSでの盛り上がり も合わせてお楽しみ頂ければと思います。 3/25(土) 2日目 実例から学ぶ変化に強いテーブル設計 - 責務の分解と RDBMS の上手い使い方 report by id:hirobex 曽根 壮大さん (@soudai1025) による発表です レポート 実際にやりがちな設計はこうだけど、こう変更が加わったときに辛いよね、だからこう設計すると楽だよね といった、実例から正しいテーブル設計を紹介する流れが非常に勉強になりました 今楽だからとりあえず既存テーブルにカラム追加しちゃう といったことは、私自身やりがちなので気をつけようと思いました いろいろな フレームワーク の仕組みを index. php から読み解こう report by id:hirobex おかしょい/岡田 正平さん (@okashoi) による発表です speakerdeck.com レポート 「 フレームワーク がなぜWebページを表示できるのか」という話を PHP でWebページを表示する方法から始まり、 CakePHP 、Laravel、Slim、 Symfony と様々な フレームワーク のindex. php を読み解いてWebページの表示の仕組みを解説する トーク です 意外と、 フレームワーク しか触ったことのない人におすすめの トーク だと思いました! 約10年ものの PHP アプリケーションとの付き合い方と、今後10年改善し続けるための取り組みについて report by id:Jazuma たけてぃさん (@takeokunn) による発表です レポート 物流システムという商材の特性上、システム障害を起こしてはいけないという要件が存在します。 その制約をクリアするためにSREチーム主導で以下のような取り組みが行われました。 Datadogによるシステムモニタリング。 アラートが発生した場合、Slackに通知する仕組みも構築 JobサーバをDockerizeしてECS化。 これによりMWバージョンアップ等のメンテナンスコストが低下した gitと AWS の権限管理 PHPUnit の高速化 <感想> メトリックスの監視やMWバージョンアップ等システム運用に必須の作業を効率化するための 施策に先手を打って取り組まれているという印象を受けました。 計測できるレガシーさを捉え、コード改善に対処する report by id:Jazuma 大橋 佑太さん (@blue_goheimochi) による発表です レポート コードを「計測」することで効果的な リファクタリング ができるという視点で コードのレガシーさを測定する方法が紹介されました。 重複コードはどれくらいあるか phpcpdというツールにより計測可能。重複コード = 悪というわけではないため検知されたコードの目視確認が必要 循環的複雑度はどのくらいか terryyin/ lizard により計測。(if/else, for, switch等の数を計測する。 これらはコードがどれだけ複雑化の指標となる) 未使用コードの検出 ツールは様々なものがあるため複数のツールを試して比較してみると良い ファイルの変更回数 Gitコマンドでファイルの変更回数を測定可能。変更回数が多い = 責務過多の可能性がある、あるいは価値検証の場合がある Gitコマンドでファイルの変更回数を測定するのは特別なツールを導入することなく実現できるので測定の第一歩として適切だと思いました。 【実録】「 PHP _CodeSniffer」で始める快適コードレビューライフ report by id:mrstsgk_rks 弊社 森下 繁喜による発表です speakerdeck.com 発表者本人によるレポート 私の所属する配配メール開発課の事例をもとに PHP _CodeSnifferを導入したときの恩恵と思わぬ落とし穴を説明しました。 オンライン登壇で ニコニコ生放送 と発表時の音声を片耳ずつ聞いていたので、 音声のラグでスタートに戸惑いましたので、次はオフライン登壇でリベンジしようと思います。 次回「 PHP _CodeSnifferをPhpStormに設定したら実装 工数 が減少できた件」でお会いしましょう!!! PHP マジックメソッドクイズ! report by id:Y-Kanoh 弊社 加納悠史 (@YKanoh65) による発表です speakerdeck.com 発表者本人によるレポート 「このメソッド、定義されていないはずだけど... あ、マジックメソッドを呼び出しているのか」Laravel のコードを読んでいるときに、一瞬迷ったのがこの発表の元でした。 知っていれば「ああ、こういうことね」と脳内で変換できますが、知らないと関係性がわからなくなってしまうのがマジックメソッドだと思います。特に一般的な開発時には自分で実装することが一部を除き多くはないため、まずは知ることが重要です。 このクイズで興味を持った方は、ぜひ一度調べてみてください。 フレームワーク が存在しない時代からのレガシープロダクトを、Laravelに”載せる”実装戦略 report by id:radiocat 弊社 廣部 知生 (@tomoki2135) による発表です speakerdeck.com レポート 20年モノのレガシープロダクトの新UI導入をきっかけにLaravelを導入した事例の紹介です。 Laravelに”載せる”実装戦略として、 アーキテクチャ に ADR パターンを採用して既存コードをできる限りそのまま維持してLaravel上で動作させる戦略が紹介されました。 この戦略によって、載せ替える前の構造を維持したままスピーディーな移植を実現できたようです。また、移植によってデータが返り値となる仕組みになったことでテストが書きやすくなりました。あくまで移植しただけなのでまだまだ課題はあるようですが、レガシープロダクトにモダンな フレームワーク を導入する参考事例のひとつとして大きな反響があり、LTではなくレギュラー枠でじっくり聞きたいという意見もあがっていました。 まとめ 初心者向けの内容からマニアックな内容まで幅広い人が聴いていて楽しいイベントとなっていました。LTの時間は現地ではペンライト、配信では色付きのコメントが流れるなど大盛りあがりでした。 PHPerのためのコミュニティ PHPTechCafe ラク スでは PHP に特化したイベントを毎月開催しております。その名も「PHPTechCafe」!! 次回は4/25(火)に『「PhpStormを語る」』 をテーマに開催します! まだまだ参加者を募集していますので、ぜひお気軽にご参加ください。 👉 PHPerのための「PhpStormを語る」 PHP TechCafe 参加申込は以下フォームよりお願いします! rakus.connpass.com 最後までお読みいただきありがとうございました!
アバター
1. はじめに 2. Google Homeのしくみ Home Graph について 3. Local Home SDK とは Local Home SDK を使って、やろうと思ったこと 4. Local Home SDKの導入手順 ①あなたが始める前に ②入門 ③スターター アプリを実行する ④クラウド フルフィルメントの更新 ⑤ローカル フルフィルメントを構成する ⑥ローカル フルフィルメントの実装 ⑦スマートウォッシャーを開始する ⑧TypeScript アプリをデバッグする ➈おめでとう 5. 実装手順 任意のタイプの家電を登録する デバイスに備わってる機能を変更する SYNC:追加された機能の詳細 Query:現在の家電の状態を報告する EXECUTE:追加される「EXECUTE」リクエスト に対応する 6. さいごに 1. はじめに 家電にはリモコンが欠かせません。 テレビ、エアコン、照明など、私たちの生活に密接に関わっている家電製品は、その操作のために専用のリモコンが必要になります。 私の場合、テレビにブルーレイレコーダーとスピーカーを繋いでいたため、リビングの机の上に3つのリモコンを常設していたのですが、リモコンを置く場所をとったり、リモコンを見失ったり と、解決したい問題が発生していました。 そこで、SwitchBotのリモートリモコンを使い、リモコンをアプリ管理に変更することにしてみました。 SwitchBotのリモートリモコンとは、家電製品を スマートホーム に対応させるための便利なデ バイス です。 このリモコンは Wi-Fi に接続してネットにつながることができ、事前にリモコンの赤外線を学習させることで、対応する スマートフォン のアプリを介して家電を操作できるようになります。 ただ、SwitchBotを使っていても、アプリをタップしてからリモコン操作が可能になるまでに時間がかかり、不便を感じていました。 そこで、 Google Home を使って 声で命令を出せるようにしたのですが、それでも、下記のような不満を抱えてしまいました。 (微妙な言葉の揺れを吸収してくれたりしたので、機能性に関しては満足しました。) 私が使用している照明は、リモコンの「電源」ボタンを押下すると、ON→常夜灯→OFF→ONのサイクルで状態が変わります。 Google Home は、ONとOFFのステータスしか管理しないため、電気を消したいと思ったとき、 ・「OK、 Google 、照明を消して」-> 照明が常夜灯になり、 Google Home 上のステータスは「OFF」になる ・もう一度「OK、 Google 、照明を消して」 -> 照明がOFFになるが、 Google Home 上のステータスは「ON」になる といったことが起きました。 照明を消すために、「OK、 Google 、照明を消して」を2回いう必要があり、その上、ステータスもずれてしまうのです。 ほかにも、スピーカーの音量を調節できない、扇風機の風量を変更できない等、細かい部分で不満点が残りました。 調べていると、 Google Assistantのプロジェクトを自作することで、家電を操作することが可能であることを知りました。 自分で家電の操作のロジックを作ることができるのであれば、抱えている不満点を解消できるのではと思い、実践してみました。 本記事では、Local Home SDK を用いて、実装を行ったので、その手順を紹介しようかと思います。 詳細は後述しますが、上記を利用すると 家電を操作するために作成するコード、アプリを外部に公開する必要がなくなり、私にとって大きなメリットに感じたため採用しました。 もし悪用されて、家の家電が見知らぬ人によって勝手に動き出すことを考えると、公開するべきじゃないと思いました、、 また、サンプルのGit プロジェクトがあるため、迷う時間を短縮できるとも思いました。 該当のサンプルは、Local Home SDK を使わないパターンのコードも含まれているため、「コードを外部に公開することに躊躇いがない」場合も、そちらを流用できます。 2. Google Home のしくみ まず、 Google Home がスマート家電を操作するときの仕組みを理解しておく必要があるので、簡単に説明します。 ①例えば、 Google Home に「OK、 Google 、照明を消して」と命令したとします。 ② Google Home は受け取った音声を、 クラウド 上の Google Assistant に投げます。 ③ Google Assistantが家電操作の命令と判断した場合、受けとったワードに関連するスマート家電情報をHome Graphから取得します。 Home Graph について 急に現れたワード、"Home Graph" についての説明を簡単に挟みます。 Home Graphとは、端的に言うと Google Home が操作可能な家電を管理するデータベースのようなものになります。 Google Home と対応したプロジェクトの連携を行ったとき、そのプロジェクトが管理している スマートデバイス リストを取得し、保持します。 調べたところ、「ライト」「照明」 などといった言葉の揺れを吸収しているのが、Home Graphのようです。 複数のライトが登録されている時、「リビング」の Google Home から「ライトをつけて」とだけ命令しても、暗黙的に「リビングのライト」を選択してくれます。 Home Graph の詳細 developers.google.com ④Home Graph から受け取った情報と紐づくプロジェクトに対して、実際に家電を動かすように「EXECUTE」リク エス トを送ります。  例えば、受け取った家電情報が SwitchBot で登録した照明だった場合、紐づいているプロジェクトの設定に従って、SwitchBot に向かってリク エス トが飛びます。 ⑤「EXECUTE」リク エス トを受け取ったSwitchBot サービスがからリモートリモコンに対して、「照明の電源ボタンを1回押す」といった命令を渡し、 ⑥それをリモコンが実行することで、照明が消灯(私の場合は常夜灯)します。 もし、ライトがスマート家電だった場合は、リモートリモコンを介さずに照明に対して直接「照明を消す」命令が飛ぶということになります。 以上のような原理で、 Google Home を使って家電を音声操作することができます。 3. Local Home SDK とは Local Home SDK とは Google が提供する技術で、こちらを使うことで Google Home がローカルのネットワークを使って家電に対して、 操作コマンドを直接送信することができるようになります。 developers.home.google.com 図を使って、簡単に説明します。 まず前提として、プロジェクトはあらかじめ「Local Home SDK 」を用いて実装した Google Home 上で動くプログラムファイル(言語はTypeScript系)を用意しておく必要があります。 Google Home は、プロジェクト連携時にそのプログラムファイルを取得します。 ※プロジェクトを紐づけるか、端末を再起動することで自動的に取得する模様 命令を受け取り、Home Graph から家電情報を受け取るところまでは同じですが、ここから先のフェーズが異なります。 ④ Google Assistant は、受取った情報とともに「EXECUTE」リク エス トを" Google Home "に返します。 ⑤「EXECUTE」リク エス トを受け取った Google Home は、あらかじめ取得していたプログラムファイルに従って、 操作コマンドをローカルネットワーク経由で家電に対して直接実行します。 ⑥リモコンが家電を操作する仕組みは同じです。 Local Home SDK を使って、やろうと思ったこと Google Home は定期的にローカルネットワークに対して、操作可能なデ バイス がないか、スキャンして確認しています。 上記に対して、HomeGraph に登録されている家電情報に紐付くIDを返すデ バイス を発見すると、 Google Home はそのデ バイス を該当のスマート家電と認識します。 つまり、同じローカルネットワーク内にあるPCなどで Google Home のスキャンにして、該当するIDを返す仕組みを実装すると、 以降、 Google Home はそのPCをスマート家電と認識するので、「EXECUTE」リク エス トを受け取れるようにすることができるようになります。 ※ラズパイ等でも代用は可能ですが、知識がない(そもそも持っていない)ので、PCを採用しています。 「EXECUTE」リク エス トを任意のデ バイス で受け取ってしまえば、そこから先の処理は自作することができます。 図の通り、SwitchBot API を叩いてスマートリモコンを経由で家電の操作も可能になるということです。 上記のような仕組みを利用することで、先述した通り 家電を操作するアプリを外部に公開する必要がなくなります。 4. Local Home SDK の導入手順 最初に話した通り、Local Home SDK のサンプルコードを使って実装の準備を進めます。 Google 公式の CodeLabs もあるので、その手順に従って、準備します。 developers.home.google.com 本記事では、上記に書いてある実装・操作手順的な内容はばっさりそぎ落とし、簡単な補足情報をまとめようかと思います。 ①あなたが始める前に 本記事の「2. Google Home のしくみ」「3.Local Home SDK とは」にあたる内容が記載されています。 ②入門 準備として Action on Google と Firebase の初期化を行います。 Firebase を使って、「 Google Assistant のプロジェクト」のアプリ部分を作成します。 Action on Google の設定を完了することで、上記で作成したアプリを「 Google Assistant のプロジェクト」として登録するイメージです。 ③スターター アプリを実行する ここから用意されているサンプルコードを用いて、実際に実装していくフェーズです。 github.com コードの更新がしばらく行われておらず、node のバージョンが古くなっていました。 私の場合、node のバージョンが環境と合わず、バージョンの更新とそれに伴う依存関係の修正を行いました。 ここまでの準備が完了すると、Firebase Functions にデプロイしたプログラムに対して、 Google Assistant から「EXECUTE」リク エス トが送信されるようになります。 既存では、パラメータに応じて Firebase Realtime Database のステータスを変更する 処理が走るのみですが、コードを変更すれば実装可能です。 Local Home SDK の仕組みを使用していませんが、家電を操作する実装を行うことができるようになります。 Local Home SDK の機能を使わなくてもいいのであれば、ここまで準備すれば Google Home を使って家電操作できます。 ・functions > index.js を実装することで、家電操作が可能になります 具体的な実装方法の詳細は、後述します。 Local Home SDK を使った仕組みを目指すのであれば、次へ進みます ④ クラウド フルフィルメントの更新 Local Home SDK を使用するための準備フェーズになります。 Google Home からのスキャンに対して、otherDeviceIds に指定したIDを返すことで、 Google Home は該当のデ バイス だと認識します。 ⑤ローカル フルフィルメントを構成する ここで設定した内容に従って、 Google Home がローカルネットワークのスキャンを行います。 ⑥ローカル フルフィルメントの実装 ここで、Local Home SDK を使った実装になります。 identifyHandler Google Home がローカルネットワークをスキャンし、その結果を処理する箇所になります。 対応しているデ バイス は、otherDeviceIds に対応した ID を返してくるので、それをそのまま返すようにしています。 executeHandler Home Graph からきた「EXECUTE」リク エス トを処理する部分です。 基本的に、 Google Assistant から受け取ったコマンドをそのまま仮装デ バイス に渡すだけで良いと思うので、 ここのコードは公式の内容から特に変更はしなくて良いかと思います。 ここでSwitchBot API を叩くという荒技もできそうですが、やめておきました。 ⑦スマートウォッシャーを開始する node 上で動く 仮想のスマート家電を起動します。 Local Home SDK を使った音声での命令が成功すると、ログにそのことが出力されます。 また、併せて Firebase Realtime Database のステータスも変更します。 ⑧TypeScript アプリを デバッグ する 記載の通り Google Home 上で動くコードは、同じネットワークにつないでいるPCの Chrome ブラウザ > inspect でデバックすることが可能です。 また、 スクリプト を更新した場合、 Google Home を再起動しないとその変更は反映されません。 ➈おめでとう ここまで完了すると、 Google Home と仮想スマート家電との疎通が完了します。 サンプルコードはログを出力するだけですが、コードを変更すれば家電操作ロジックを実装できます。 実装方法の詳細は、次の章で解説します。 5. 実装手順 既存のサンプルコードの状態で扱うことができるスマート家電は「洗濯機」で、その機能も限られています。 もちろん、上記は変更可能です。 ここではその方法と、実装手順を紹介いたします。 任意のタイプの家電を登録する まずは、任意の種類を 家電を登録する方法です。 Google Home からくる、「Sync」リク エス ト に応答することで、任意のスマート家電を登録することが可能です。 サンプルコードでいうと、functions > index.js の onSync で返すオブジェクトが、これにあたります。 家電の種類は、payload.devices.type の値で報告できます。 公式のドキュメントに報告できる種別がすべて乗っています。 developers.home.google.com 上記から一つだけ選択して、報告します。 例えば、照明に変更したい場合は、"action.devices.types.LIGHT"を指定します。 payload: { agentUserId: USER_ID, devices: [{ id: 'washer' , type: 'action.devices.types.LIGHT' , // ここを変更する .... これでアプリ上のロゴもライトに変わり、Home Graph で解釈する言葉の揺れも、照明用に代わります。 言葉の揺れ に関して、payload.devices.name.nicknames を変更することで、任意のワードを登録することができるようになります。 .... name: { defaultNames: [ 'My Washer' ] , name: 'Washer' , nicknames: [ '間接照明' , 'テレビ台の上のライト' , ] , // ここを変更する } , .... 上記のように設定すると、「OK、 Google . 間接照明をつけて」や「OK、 Google . テレビ台の上のライトを消して」と話しかけた場合も、登録した機器が選択されるようになります。 「Sync」リク エス ト で報告する内容の詳細は下記を参考にしてみてください。 developers.home.google.com デ バイス に備わってる機能を変更する こちらは、payload.devices.traits の値を変更することで解決することができます。 デ バイス タイプと同様に、公式対応表を参照します。 developers.home.google.com こちらは、複数指定することができます。 例えば、"action.devices.traits.Brightless"を指定することで 、明るさを調整するための命令ができるようになり、対応する「EXECUTE」リク エス トが送られるようになります。 余談ですが、、 私は、"action.devices.traits.Modes" を乱用しています。 任意の「モード」を追加することができ、様々なことに汎用することができます。 「テレビ」に対して、「 NETFLIX モード」「 YouTube モード」「 Amazon Prime モード」を設定すれば、「OK、 Google . テレビをAmazo Primeモードにして」の一言で、 テレビで Amazon Prime を再生させることも可能になります。 SYNC:追加された機能の詳細 実は、traits で任意の機能を報告するだけでは、機能を追加することができない場合があります。 Volume (音量調整) がわかりやすいので、例としてみてみたいと思います。 developers.home.google.com 「デ バイス の属性」の章を確認すると、「Sync」リク エス ト で次の情報を報告する必要があることがわかります。 volumeMaxLevel:ボリュームの最大値 volumeCanMuteAndUnmute:ミュート機能に対応しているかどうか volumeDefaultPercentage:デフォルトのボリューム値 levelStepSize:ボリュームを相対的に調整するときの変化量 commandOnlyVolume:現在のステータスを報告できるかどうか traits = action.devices.traits.Volume と併せて、上記を報告することで初めて、ボリューム調整機能を追加することができます。 Query:現在の家電の状態を報告する 現在の状態やステータスを、HomeGraph に報告することで Google Home アプリに現在のスマート家電の状態を表示することができます。 これがなくても動かすことは可能ですが、実装することをお勧めします。 サンプルコードでは、functions > index.js の onQuery に実装されています。 return { on: data.on, isPaused: data.isPaused, isRunning: data.isRunning, currentRunCycle: [{ currentCycle: 'rinse' , nextCycle: 'spin' , lang: 'en' , }] , currentTotalRemainingTime: 1212, currentCycleRemainingTime: 301, } ; 報告が必要な key は、報告している traits によって決まります。 traits の詳細のページで、確認することができます。 'action.devices.traits.OnOff'(電源のオンオフ切り替え)と'action.devices.traits.Brightless'(明るさ調整)機能を報告した照明を登録した場合を考えてみます。 action.devices.traits.OnOff https://developers.home.google.com/cloud-to-cloud/traits/onoff?hl=ja#device-states action.devices.traits.Brightless https://developers.home.google.com/cloud-to-cloud/traits/brightness?hl=ja それぞれを確認すると、 on と、 brightness を返す必要があることがわかります。 return { on: true , brightness: 50 } ; ※電源がついており、その明るさが 50% であることを報告している EXECUTE:追加される「EXECUTE」リク エス ト に対応する payload.devices.traits を追加して機能を増やすと、その機能に対応した「EXECUTE」リク エス トを処理する必要が出てきます。 サンプルコードでいうと、app-start > functions > index.js の onExecute、もしくは、virtual-device > washer.js の state(既存の関数名だと微妙ですが)で「EXECUTE」リク エス トを処理しています。 公式のドキュメントを確認することで、どのような「EXECUTE」リク エス トが来るかを確認することができるので、 それを参考に、実装を行います。 "action.devices.traits.OnOff"を例に、私の実装を例として簡単に紹介します。 developers.home.google.com 「ライトをオンにして」と Google Home に話しかけたら、下記のような「EXECUTE」リク エス トを受け取ることがわかります。 { "command" : "action.devices.commands.OnOff" , "params" : { "on" : true } } params > on の値を参照し、下記のように実装すれば、家電の電源を切り替えることができます。 // req = 受け取った「EXECUTE」リクエスト const data = req.body; // コマンド種別を取得する const command = data.command switch (command) { case 'action.devices.commands.OnOff' // 切り替えたいステータスを取得 const isOn = data.params.on; if (isOn) { // 電源を入れるケース switchBotApi.on(); } else { // 電源を切るケース // 電源ボタンを2回押下する switchBotApi.off(); } case 'action.devices.commands.BrightnessAbsolute' // 省略 } オフの時だけ電源ボタンを2回押下するように実装できるので、初めに挙げていた照明のステータスがずれる問題を解決することができました。 ※SwitchBot API の叩き方の詳細は、下記をご覧ください。 github.com 6. さいごに 「OK、 Google テレビをつけて」の指示で、テレビとスピーカーの電源を同時に入れるようにしたり、 「OK、 Google 広告をオフにして」の指示で、 YouTube の広告をスキップさせたりと、好きなように家電をコン トロール することができるようになります。 単に Google Home と、SwitchBot サービスを連携させるだけでは上記は実現できませんし、最初に上げたような不具合が発生したりします。 そこを自分で解決することができたので、試して良かったと感じています。 また、コードを動かすトリガーを声にすることができるので、SwitchBot API に限らず、様々なこと(家電操作以外も)ができそうです。 他になにかできないか模索し、より便利な スマートハウス の構築を目指していこうかと思います。
アバター
技術広報の syoneshin です。 いつも ラク スエンジニアブログをお読みいただき、ありがとうございます! ! 今回は2023/2/8に開催した 「RAKUS Tech Conference 2023」の内容を当日発表資料 と共にご紹介します! ◆ 関連記事 昨年度(2022年)の開催レポートは以下をご覧ください tech-blog.rakus.co.jp 【目次】 イベント概要 発表の紹介 オープニングセッション 短納期でも進化をあきらめなかった新規プロダクト開発 フロントエンド横断組織のチームトポロジー ベテラン社員が抜けても若手が成長できるエンジニア組織づくり デザイン組織が社内下請けから脱却するためにやったこと ゼロから始めるクラウドネイティブ 「開発優先」の中で取り組む組織的な新技術への挑戦 クロージング 終わりに イベント概要 日時 :2022/2/8(火) 14:00-18:00 会場 :オンライン 参加費:無料 主催 : 株式会社ラクス開発本部 Twitter ハッシュタグ : #RAKUSTechCon techcon.rakus.co.jp 【イベントメッセージ】 株式会社 ラク スは「ITサービスで企業の成長を継続的に支援します!」をミッションに掲げ、 メール共有・管理システムの「メールディーラー」や、経費精算システムの「楽楽精算」など、 延べ80,000社を超えるお客様に自社開発したサービスを提供してきました。 『RAKUS Tech Conference2023』はこうした今までの取り組みや知見を紹介する、 ラク ス開発本部主催の技術カンファレンスです! 当社ではローンチ20年を超えるプロダクトもあり、決して目新しい技術ばかりを扱っているわけではありませんが、 一人ひとりが地道な" カイゼン "のための努力、そして"挑戦"を続けています。 「日本を代表する」組織を目指し、失敗を恐れずに成長を続けるエンジニア/デザイナーの生の声をお届けします。 発表の紹介 それではここから各発表内容と資料を共有させていただきます! オープニングセッション 登壇:公手 真之 [ 執行役員 開発本部長] オープニングセッションでは、 ラク ス開発本部長の公手より、 以下内容を紹介させていただきました! ラク スについて エンジニア組織について CTOが取り組んできたこと ラク スのエンジニア組織のこれから 開発組織の成長過程で遭遇したさまざまな課題と取組み事例、また進行形の課題と取組み、今後のチャレンジをご紹介しました。 speakerdeck.com 短納期でも進化をあきらめなかった新規プロダクト開発 登壇:松浦 孝治 [楽楽明細開発2課 課長]     川上 正博 [楽楽明細開発2課] 2022年12月現在、約25,000社の方に申込頂いている『楽楽電子保存』サービスですが、初期開発に与えられた猶予は弊社のプロダクト開発史の中で最短となる「半年」という、非常にタイトなスケジュールの中で完遂させる必要がありました。 短期間でサービスインまで導いたマネジメント 中長期的なサービス運用を見据えたゼロベースでの アーキテクチャ ー選定 を中心に、初期開発で特に力を入れた取り組みと今後の展望をご紹介しました。 speakerdeck.com フロントエンド横断組織のチーム トポロジー 登壇:國枝 洋志 [フロントエンド開発課 課長] 設立20年目にして作られたフロントエンド開発組織は発足から2年経ちました。 発足時2名しかいなかったフロントエンドエンジニアは18名に増えましたが、まだまだ人手不足。課題も山積みです。 ベスト・オブ・ブリード戦略で成長してきた様々なサービスに対して、各開発課と共にサービス開発を行うストリームアラインドチームになるのか、フロントエンドの スペシャ リストとして技術支援を行うイネイブリングチームになるのか、横断組織ならではの悩みにどのように取り組んで行くのか、これからの組織設計についてお話ししました。 speakerdeck.com ベテラン社員が抜けても若手が成長できるエンジニア組織づくり 登壇:大塚 正道 [配配メール開発課 課長]     荒巻 拓哉 [配配メール開発課]     久山 勝生 [配配メール開発課] ラク スの開発組織には毎年多数の新卒社員が入社しており、私たちのチームにもたくさんの若手社員が所属しています。 一方で スペシャ リスト職などのベテラン社員は不足気味です。 重要度の高いプロジェクトに専念するためチームから抜けていくこともあります。 しかし、私たちは逆にこれを若手社員の成長の機会と捉えて積極的にチャレンジできる環境をつくり、若手が活躍し成長できるチームを目指して取り組んできました。 実際に取り組みを行ったメンバーからその事例をご紹介するとともに、マネジメントサイドからそのための組織の向き合い方についてお話ししました。 speakerdeck.com デザイン組織が社内下請けから脱却するためにやったこと 登壇:小林 肇 [プロダクトデザイン課 課長] デザイナーが少なく、 開発プロセス の中でデザイナーの役割の定義が曖昧で、デザイナーって何をしてくれる人なのか、プロダクト開発においてデザイナーがいるとどんなメリットがあるのかがちゃんと認識されていませんでした。キレイなUIを作ってくれるということは認知されていても、そのためにはどのようにデザイナーがプロダクト開発に関わるべきか不明瞭なために、下請け的な仕事になっていました。 課題解決をミッションとするデザイナーが、プロダクト開発チームの一員になるために、どのようにして役割を認識してもらい、役割を広げていっているのかをご紹介しました。 speakerdeck.com ゼロから始める クラウド ネイティブ 登壇:見形 親久 [SRE課 課長]     松本 隆二 [東京インフラ開発1課] クラウド ネイティブな開発をやってはみたいが、機能開発が忙しくなかなかそのチャンスが生まれない、そんな時に新しいサービスを立ち上げるチャンスが舞い込んできました。 これ幸いと、イメージしていた構成を構築しようとしてみましたが、あれやこれやとハマりポイントや考慮できてないモノがあり思っていたほど簡単なものではありませんでした。 Kubernetes など クラウド ネイティブ技術が一般的になっては来ていますが、実際に導入するにはエコシステムの構築など、周辺のシステムも考慮する必要があり、どこから手を付けるべきか悩んでいる方もいらっしゃるのではないでしょうか。 システム構成の紹介/その背景 環境構築時のハマりポイント/考慮点 今後やっていきたいこと を中心に、ゼロからどの様に環境を構築していったのかと今後の展望をご紹介させていただきました。 speakerdeck.com 「開発優先」の中で取り組む組織的な新技術への挑戦 登壇:堀内 泰秀 [技術推進課 課長]     鈴木 勇 [技術推進課] 「機能開発を優先しているため リファクタリング ができない」この状態に陥っている方、多いのではないでしょうか。 サービス開発では立ち上げから売れ始めるまでの間、プロダクトマーケットフィットを目指して最短距離を走ることを求められます。 その中で行われる意思決定では必ずしもすべてが最適解を得られるわけではありません。また時を経るにつれて最適解が最適解でなくなることもあるでしょう。 「サービスがある程度軌道に乗り、開発体制も整ってきたら リファクタリング やリアーキテクトしたい」――しかしベテランエンジニアの方はそんなタイミングが訪れないと知っているはずです。 そんな厳しい現実を直視して、 ラク スではどのような対応を取ってきたのかをご紹介しようと思います。 本発表では以下のような内容をお話しします。 ラク スにおける取り組みの概要 取り組みの立ち上がりから継続していくための仕組みづくり 取り組み実施のサイクル 自社で検討することのメリット speakerdeck.com クロージング 登壇:公手 真之 [ 執行役員 開発本部長]     大塚 正道 [配配メール開発課 課長]     小林 肇 [プロダクトデザイン課 課長]     見形 親久 [SRE課 課長]     堀内 泰秀 [技術推進課 課長] クロージングセッションでは、 ラク ス開発本部長の公手と 当日登壇者複数名で 本編では伝えれなかったこと 自身のチームで今後やろうとしていること テーマにパネルディスカッションを実施しました。 イベント当日はたくさんの方にご視聴、そしてコメントやご質問をいただきました。 お申し込み、ご参加いただいた皆さま本当にありがとうございました! 終わりに ラク ス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
アバター
こんにちは、技術推進課の uemura_rks です。 私が所属する技術推進課では「各商材の開発チームから課題を ヒアリ ングし、その解決に向けた調査を行う」という取り組みをしています。 取り組みの一環で Java のパフォーマンス分析、診断ツールである JDK Flight Recorder(JFR) について、その機能と導入することによるアプリへの影響を調査・検証しました。 「小さいオーバーヘッドで使えるらしいけど本当にそうなのか?」 を確認してみたので、その結果を報告します。 JFR を使えばトラブルの 原因調査に役立つ情報 を 手軽に収集 できます。 Java を扱っている方であればどなたにとっても有益なツールとなりますので是非最後までお付き合いください。 目次 目次 JDK Flight Recorder(JFR)の特徴 オーバーヘッドが小さい 手軽に使い始められる JVM やアプリ動作に関する情報を取得できる トラブルの再発を待たなくていい 取得情報はカスタマイズ可能 イベントログのデータフロー オーバーヘッドによる影響度検証 検証結果 CPU Javaヒープ使用量 プロセスが消費する最大メモリ量 ディスク吐き出し時のアプリ動作への影響 ダンプサイズ まとめ 参考文献 JDK Flight Recorder(JFR)の特徴 検証の前にまずはJFR がどんなツールなのか、その特徴を紹介します。 オーバーヘッドが小さい アプリケーションを分析、診断するためには多くの情報を計測する必要があります。 JFR は JVM に組み込まれている ため、その計測のオーバーヘッドは小さく、1,2%程度だといわれています。 商材に導入したらアプリ全体のパフォーマンスが悪化した、なんてことが起きにくいです。 (と、言いつつこの部分を後半で検証します) 手軽に使い始められる JFR は各ベンダーの JDK に実装されているので、現在使っている JDK はそのまま、 機能を有効化するだけ で利用可能です。(有効化には JVM の起動オプションや jcmd を使います) アプリサーバに何かしらの ミドルウェア をインストールしたり、 ソースコード に手を加えたりすることなく使い始められます。 JVM やアプリ動作に関する情報を取得できる JFR のログからは JVM のリソース状況 GC の情報 スレッドダンプ コンパイル やコード・キャッシュの情報 ログを自動分析したレポート など様々な情報を得ることができます。 JVM の動作に関するような低レベル情報を集めてくれるので、トラブル調査に役立つと思います。 いくつか例を載せておきます。 < JVM のリソース状況> CPU 使用率やヒープ使用量、オブジェクトのメモリへの割当てなどを確認できます <自動分析のレポート> ログの包括的な分析レポートです。 潜在的 な問題をスコア化して警告してくれます。 <スレッドダンプ> 1分間隔でスレッドダンプを記録してくれています。 ◆ログの可視化 JFR のログ自体はバイナリになっています。 JDK Mission Control(JMC) というビューアーを使うと上記キャプチャのように可視化できます。 その他、JFR で得られる情報やその見方についてはこちらの チュートリアル が分かりやすいです。 トラブルの再発を待たなくていい JFR は基本的に 常に有効化しておく ツールです。 これは「オーバーヘッドが小さいこと」と後ほど紹介する「イベントログのデータフロー」から成っている強みになりますが、それ故に JFR は 「何かトラブルが起きてデータが欲しくなったから計測開始」 ではなく、 「何かトラブルが起きたときに備えてずっと計測しておく」 という使い方ができます。 航空機のフライトレコーダーから名前を取っているようなのですが、まさしくその Java 版といったところです。 (もう少し身近なところで、 ドライブレコーダー の Java 版 と言った方が馴染みがあるかもしれません。) 取得情報はカスタマイズ可能 JFR は事前に定義された イベント という単位で情報を集めます。 *1 数あるイベントのうち、どれを取得するかは設定ファイルでカスタマイズすることができます。 独自の設定を作成することもできますし、テンプレートとして2種類の設定ファイルが JDK に同梱されていますので、それをそのまま使うこともできます。 default.jfc 本番環境での継続的な記録に向いたテンプレート設定 オーバーヘッドは1%ほど profile.jfc 検証向けのテンプレート設定 オーバーヘッドは2%ほど <設定ファイルの一部> <event name="jdk.ThreadAllocationStatistics"> <setting name="enabled">true</setting> <setting name="period">everyChunk</setting> </event> <event name="jdk.ClassLoadingStatistics"> <setting name="enabled">true</setting> <setting name="period">1000 ms</setting> </event> 設定ファイルは xml 形式になっています。 enabled 属性の true / false を切り替えることで取得イベントの選択ができます。 イベントログのデータフロー JFR が取得するイベントがログファイルとして出力されるまでの流れを紹介します。 登場人物の多くは JFR のオプションでサイズなどの調整が可能です。 *2 流れを把握し、環境に合わせて適切なオプションを指定することで、導入によるサーバリソースへの負荷を抑えることができます。 ◆ アーキテクチャ JFR ではイベントを「スレッドローカルバッファ」「グローバルバッファ」の2つのメモリ領域と「 リポジトリ 」と呼ばれるディスク領域で保管しています。 ◆データフロー ①拾ったイベントを「スレッドローカルバッファ」に格納  スレッドローカルバッファはスレッド単位の専用バッファのため、他のスレッドと競合することがありません。  初動でこのバッファを使うことで高速なイベント収集を実現しているそうです。 ②「スレッドローカルバッファ」から「グローバルバッファ」にコピー  スレッドローカルバッファは小さいためすぐに一杯になります。溢れた情報はグローバルバッファに移ります。  グローバルバッファはスレッド間で共有するプロセスのメモリで、循環バッファの形になっています。 ③「グローバルバッファ」から「 リポジトリ 」にファイル出力  グローバルバッファからも溢れた情報は リポジトリ というディスク領域に .part ファイルとして  出力されます。 ④part ファイルからチャンクダンプに変換  グローバルバッファから溢れた情報はどんどん .part ファイルに溜まっていきます。  こちらも一定のサイズたまると、 .jfr ファイルに変換されます。  (これをこの記事では チャンクダンプ と呼びます)  チャンクダンプは JFR のログファイルとして完全なバイナリになっており、JMC などのビューアーを使って  可視化することができます。  また、 リポジトリ 領域はログの 最長保管期間 や 最大保管サイズ をオプションで指定することができます。  ストレージを圧迫しすぎないような設定をおすすめします。 ⑤jcmd やプロセスの停止などによって JFR ファイルを出力  ③④で出力されるチャンクダンプはチャンクサイズごとに細切れになったファイルです。  jcmd によるダンプ出力の指示やプロセスの停止をトリガーとして、すべての記録が一つにまとまった  JFR ファイルを生成することができます。 *3  (これをこの記事では フルダンプ と呼ぶことにします)  フルダンプと一つひとつのチャンクダンプの中身は基本的に一緒ですが、フルダンプにはまだチャンクダンプに  吐き出されていなかったメモリ領域のイベントログも出力されます。 オーバーヘッドによる影響度検証 オーバーヘッドが小さく、故に常に有効化しておくことのできる JFR ですが、 そのオーバーヘッドが実際にアプリにどれぐらいの影響を与えるかを CPU・Javaヒープ使用量・プロセスが消費する最大メモリ量・ディスク吐き出し時のアプリ動作への影響・ダンプサイズ(ストレージへの影響) の5つの観点で検証してみました。 検証結果 少し長くなるので結果だけ先に述べておきます。 5つの観点ですが... 「 Java ヒープ使用量」への影響のみ要注意 その他は無視できるか、オプションで調整可能な範囲の影響 だと言える結果になりました。詳細をこれから記していきます。 CPU CPUへの影響は次の観点とメトリクスで見ていきます リクエスト処理中 の影響 → CPUが高稼働になっていた時間 を計測 待機中(リクエストを処理していない時) の影響 → CPU使用率 を計測 ◆計測方法 リク エス ト処理中にひたすらCPU負荷をかけるサンプルを用意してリク エス トを繰り返し送りました。 次の3パターンで試行し、2つのメトリクスを計測結果から抽出します。 JFR 無効 JFR 有効 / 本番向け設定ファイル(default.jfc)を使用 JFR 有効 / 検証向け設定ファイル(profile.jfc)を使用 ◆計測結果 各パターンの計測値を比較した結果です。 <リク エス ト処理中の影響(CPUが高稼働になっていた時間)> CPU( JVM ) 高稼働時間 CPU(マシン) 高稼働時間 ※平均 JFR 無効との差 [増加率] ※平均 JFR 無効との差 [増加率] JFR 無効 56.88 秒 - 56.90 秒 - JFR 有効(default.jfc) 57.15 秒 + 0.27 秒 [+ 0.47 %] 57.35 秒 + 0.47 秒 [+ 0.79 %] JFR 有効(profile.jfc) 57.21 秒 + 0.33 秒 [+ 0.58 %] 57.35 秒 + 0.47 秒 [+ 0.79 %] ※平均:20リク エス トの平均 ⇒ 増加率1%未満 の範囲でわずかに増加 という結果になりました。 <待機中の影響(CPU使用率)> CPU( JVM ) 使用率 CPU(マシン) 使用率 ※平均 JFR 無効との差 ※平均 JFR 無効との差 JFR 無効 0.986 % - 1.207 % - JFR 有効(default.jfc) 1.640 % + 0.654 % 1.738 % + 0.531 % JFR 有効(profile.jfc) 1.830 % + 0.844 % 1.869 % + 0.662 % ※平均:19期間の平均 ⇒ 増加量1%未満 の範囲でわずかに増加 という結果になりました。 Java ヒープ使用量 Java ヒープへの影響を見ていきます。 こちらも リクエスト処理中 と 待機中 の2つの観点で使用量の変化を確認しました。 ◆計測方法 リク エス ト処理中にメモリ負荷をかけ続けるサンプル と シンプルなDBアクセスのみをするサンプル の2種類のサンプルを用意して、それぞれでヒープの変化を計測しました。 また、こちらは正確な関係がわかっていないのですが、JFR を有効化していると初回のリク エス ト終了後にフル GC が走るという動きが見られました。 ( Java ヒープの New 領域や Old 領域に余白があってもフル GC が走っていました) そのため、 リクエスト処理中 待機中 それぞれで フルGC前 フルGC後 の2地点の計測を行いました。 こちらもCPUと同じく JFR の有無で結果を比較します。 ◆計測結果 こちらはいくつか条件を組み合わせて計測したのですが、その条件の違いや同一条件内でも試行ごとで計測値のブレが大きい結果となりました。 結果の一例を載せますが、こちらの結果では 一部(平均値で)20%近い増加率 も記録されています。 サンプルアプリの範囲での結果ですので実商材でも同じ結果になるとは言い切れませんが、 導入を検討する際の要注意項目となりました。 <リク エス ト処理中の影響( Java ヒープ使用量)> Java ヒープ使用量(フル GC 前) Java ヒープ使用量(フル GC 後) ※平均 JFR 無効との差 [増加率] ※平均 JFR 無効との差 [増加率] JFR 無効 247.8 MB - 32.9 MB - JFR 有効(default.jfc) 270.6 MB + 22.8 MB [+ 9.2 %] 36.8 MB + 3.9 MB [+ 11.9 %] JFR 有効(profile.jfc) 279.5 MB + 31.7 MB [+ 12.8 %] 34.5 MB + 1.6 MB [+4.9 %] ※同一条件で3回計測した平均 <待機中の影響( Java ヒープ使用量)> Java ヒープ使用量(フル GC 前) Java ヒープ使用量(フル GC 後) ※平均 JFR 無効との差 [増加率] ※平均 JFR 無効との差 [増加率] JFR 無効 205.2 MB - 30.6 MB - JFR 有効(default.jfc) 244.1 MB + 38.9 MB [+ 19.0 %] 34.7 MB + 4.1 MB [+ 13.4 %] JFR 有効(profile.jfc) 238.4 MB + 33.2 MB [+ 16.2 %] 32.5 MB + 1.8 MB [+5.9 %] ※同一条件で3回計測した平均 プロセスが消費する最大メモリ量 JFR がイベントログを溜める上で、プロセスとしてどこまでメモリを使用するか確認しました。 おそらくグローバルバッファのサイズに比例するだろうと仮定しつつも、際限なく食い潰される危険がないかを検証します。 計測メトリクスとしては「3日間アプリを稼働させた中で記録した RSS の最大値」を見ていきます。 検証でいろいろ試していく中で、 JFR のイベントログはCヒープ領域に保持されていそう だと理解したため、 RSS を取っています。( JVM に組み込まれているという特徴も相まってそう判断しています) ◆計測方法 サンプルアプリを立ち上げ、3日間ひたすらリク エス トを送り続けました。 ◆計測結果 ⇒ 想定通り、 グローバルバッファのサイズに準じて増加する という結果が得られました。 グローバルバッファのサイズは JVM 起動時のオプションなどで指定できます。 サイズの大きさとディスク吐出しの頻度は トレードオフ ですが、 環境に合わせてサイズを指定してあげれば影響は小さくできると言えます。 ディスク吐き出し時のアプリ動作への影響 JFRはそのデータフローの中で3種類のディスク吐出しを行います .part ファイル出力 チャンクダンプ出力 フルダンプ出力 それぞれのファイル出力時において、アプリに遅延が発生しないかを確認します。 リク エス トの処理時間をメトリクスとし、ファイル出力の有無による時間の差を見ます。 ◆計測方法 サンプルアプリに対して定期的にリク エス トを送ります。 .part ファイルとチャンクダンプは一定リク エス トを送れば自動で出力されますので、そのときのリク エス ト処理時間を計測します。 フルダンプは jcmd で出力指示を出して、そのときのリク エス ト処理時間を計測します。 ◆計測結果 ディスク吐出し時の処理時間と、平均処理時間を比較した結果です。 < .part ファイル出力時> 吐出しサイズ※ リク エス ト処理時間 平均 ファイル出力時 増加量 [増加率] 39.98 MB 8.809 秒 8.728 秒 - 0.081 秒 [- 0.92 %] 79.96 MB 9.090 秒 + 0.281 秒 [+ 3.19 %] 119.69 MB 8.917 秒 + 0.108 秒 [+ 1.23 %] ※partファイルの増加量 <チャンクダンプ出力時> 吐出しサイズ※ リク エス ト処理時間 平均 ファイル出力時 増加量 [増加率] 40.4 MB 8.883 秒 8.927 秒 + 0.044 秒 [+ 0.50 %] 80.7 MB 8.708 秒 - 0.175 秒 [- 1.97 %] 119.0 MB 8.917 秒 + 0.034 秒 [+ 0.38 %] ※(出力されたチャンクダンプのサイズ) - (直前のpartファイルのサイズ) <フルダンプ出力時> フルダンプサイズ リク エス ト処理時間 平均 ファイル出力時 増加量 [増加率] 111.7 MB 8.564 秒 9.645 秒 + 1.081 秒 [+ 12.6 %] 226 .3 MB 8.582 秒 9.912 秒 + 1.33 秒 [+ 15.5 %] 456.0 MB 8.677 秒 10.505 秒 + 1.828 秒 [+ 21.1 %] ⇒ 気にするべきは「フルダンプ出力時」になります。 フルダンプ出力時は、指示を出したタイミングで別プロセスが大量に立ち上がり、 アプリ( JVM )の使えるCPUが30%ほど減っていました。 結果、増加率 21.1 % という大きな数値につながっています。 とはいえ、フルダンプ出力は普段プロセスが動いているときに勝手に実行される機能でもありません。 フルダンプ出力の命令タイミングだけ気を付けることで回避可能 な影響ですのでクリティカルなものではないと考えています。 また、 .part ファイルやチャンクダンプ出力時の増加率は1桁%となっており、その影響は十分小さいと言えます。 ダンプサイズ JFR のダンプサイズがどれぐらいになるか計測します。 自社の商材ではログはアプリサーバからログサーバに転送しているのですが、 そのログサーバを圧迫するようなことがないかを確認します。 ◆計測方法 15時間アプリを稼働させ、次の観点の組み合わせで15時間後のダンプサイズを計測します。 設定ファイル default.jfc / profile.jfc リク エス ト回数 15時間で... 0リク エス ト / 約26,500 リク エス ト / 約53,000 リク エス ト / 約106,000 リク エス ト ◆計測結果 ⇒ ダンプサイズは リク エス ト回数に応じて微増 しました。 同じリク エス トを投げていたため同じイベントを大量に収集したことになりますが、同一イベントであれば JFR がいい感じに圧縮してくれていそうです。 計測結果から1日当たりのダンプサイズを試算してみると、 数万~数十万リク エス ト/日 規模のシステムであれば 、本番運用向けの default.jfc 設定でざっくり 300MB/日 程のサイズになると見込めます。 また、ダンプファイルは gzip や bzip2 で 圧縮をかければ3割のサイズにまで小さく できました。 実質 90MB/日 程です。 このサイズをどう捉えるかは各々の環境次第ではありますが、商材の持つ リク エス ト数 や アプリサーバの台数 なども加味して、場合によってはログローテートを見直すなどの対応が必要になりそうです。 まとめ JDK Flight Recorder の特徴と、オーバヘッドによるアプリへの影響を検証した結果を紹介しました。 アプリへの影響としては、 Java ヒープへの影響のみ要注意 だが、 その他は無視できるか、オプションで調整可能な範囲 だと言える結果になりました。 また、 機能を有効化するだけで使い始められる手軽さ と 収集できる情報の豊富さ は大きな魅力だと思っています。 小さくはじめられて大きなリターンを期待できます ので、この記事を読んでくださった方も是非 JFR を使ってみてください。 参考文献 OpenJDK での JDK Flight Recorder の使用 OpenJDK 11 | Red Hat Customer Portal フライト・レコーダを使用したパフォーマンス問題のトラブルシューティング JDK Flight Recorder入門 - Speaker Deck 目次 · 入門: JDK Flight Recoder 使ってみよう!JDK Flight Recorder OpenJDK 8uでJDK Flight Recorderの入門 - 赤帽エンジニアブログ GitHub - thegreystone/jmc-tutorial: A hands-on-lab/tutorial for learning JDK Mission Control 7+. *1 : 独自イベントを実装することもできます 参考: カスタムイベント API の定義と使用 *2 : オプションの参考: コマンドラインを使用した JDK Flight Recorder の設定 *3 : リポジトリ 領域の最長保管期間・最大保管サイズを越えて削除されたログは出力されません
アバター
こんにちは。 株式会社 ラク スで先行技術検証をしたり、ビジネス部門向けに技術情報を提供する取り組みを行っている「技術推進課」という部署に所属している鈴木( @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
アバター