TECH PLAY

株式会社ラクス

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

935

ユーザーによるカスタマイズ 型と実体(クラスとインスタンス) Type Object パターン 例 申請書クラス 既定の申請書のクラス カスタム申請書のクラスの検討 Type Object パターンを適用 まとめ 関連するデザインパターン Flyweight パターン Interpreter パターン、Command パターン 参考文献 ユーザーによるカスタマイズ 楽楽精算開発部の id:smdr3s です。主に Java を使ったサーバーサイドを担当しています。 弊社のサービスである 楽楽精算 は、その名のとおり経費精算のサービスです。 交通費や出張費、交際費といったさまざまな経費を申請でき、上司や 経理 担当部署による承認を経て、 経理 処理の完了までをサポートしています。 基本的には経費精算に関わる処理が主な機能ですが、上記中の申請~承認の処理、いわゆる承認フローを経費精算以外の業務に使用することもできます。この機能は 汎用ワークフロー と呼ばれ、お客様(管理者ユーザー)が自由に申請書の種別を作成、中身をカスタマイズし、承認フローを関連づけておくことができます。 例えば稟議書、押印申請、重要資産アクセス申請など、さまざまな種別の申請書を作成できます。 そして(一般ユーザーが)それぞれの種別の申請書を使用して申請を行い、設定された承認フローに沿って承認、決裁を行うことができます。 このように、ビジネスアプリケーションではユーザーが自由にカスタマイズした雛形を作成し、その雛形から多数の書類を作成する、といった要求があることがあります。 特に B2B の SaaS プロダクトではユーザー企業ごとに規程が異なるものの、個別に実装を行うことはないため、ユーザー自身がカスタマイズを行える機能の必要性は高まります。 型と実体(クラスと インスタンス ) さて、多くのアプリケーションでは、決められた型に沿った実体を生成し、その実体を処理しています。 クラスベースの オブジェクト指向言語 で書かれたプログラム上では、型がクラス、実体が インスタンス になります。 経費精算アプリケーションで言えば「交通費精算申請書」という「型」を表すクラスが実装されており、個々の申請ごとにこのクラスの実体である インスタンス を作成( new )して処理します。 一方、先ほどの汎用ワークフロー機能で作成する申請書の種別のような、ユーザーが自由に作成する「型」は、そのままクラスとして実装することはできません。 クラスは実装時に作成する必要がありますが、その仕様は実行時にユーザーが指定するまでわからないためです。 Type Object パターン このような場合に役立つのが Type Object パターン です。 Type Object パターンでは、実行時に作成された「型」の情報を「オブジェクト」に入れて保持し、そのオブジェクトを他のオブジェクト( インスタンス )から参照させることで「型」としての情報を与えます。型のメタ情報をオブジェクトにしているとも言えます。 例 経費精算アプリケーションを例に、Type Object パターンを使用してみます。 アプリケーションの要件は以下のとおりです。 既定で交通費精算と経費精算の申請書を作成できる。 交通費精算の申請書には「行先、 交通機関 、金額」の入力項目がある。 経費精算の申請書には「品名、単価、数量、金額」の入力項目がある。 自由に申請書の種別を追加できる。(カスタム申請書) カスタム申請書には、申請書名を設定できる。 カスタム申請書には、任意の数の入力項目を追加でき、それぞれに項目名を設定する。 申請書クラス まずは各申請書の共通の構成を抜き出し、それをもとに申請書の親クラスを作成します。 申請書名(申請書の種別ごとに決まっている) 複数の入力項目 項目名(申請書の種別ごとに決まっている) 項目値(個々の申請書ごとに入力する) public abstract class Application { private Map<String, Object> fields = new HashMap<>(); // 入力項目 public abstract String getApplicationName(); // 申請書名 public abstract String[] getFieldNames(); // 項目名リスト // 項目値設定 public void setField(String name, Object value) { if (Arrays.asList(getFieldNames()).contains(name)) { fields.put(name, value); } } // 項目値取得 public Object getFieldValue(String name) { return fields.get(name); } } 既定の申請書のクラス 申請書の種別=型、と考えると、既定の種別の申請書であれば型の仕様はすでに確定しているため、クラスを実装することが可能です。 交通費精算クラスや経費精算クラスは以下のように実装できます。 // 交通費精算クラス public class TransportApplication extends Application { @Override public String getApplicationName() { return "交通費精算" ; } @Override public String[] getFieldNames() { return new String[] { "行先" , "交通機関" , "金額" }; } } // 経費精算クラス public class ExpenseApplication extends Application { @Override public String getApplicationName() { return "経費精算" ; } @Override public String[] getFieldNames() { return new String[] { "品名" , "単価" , "数量" , "金額" }; } } もちろん、これらのクラスは new で インスタンス を作成することができ、その インスタンス は当然そのクラスの情報を持っています。 public class Main { public static void main(String[] args) { // 交通費精算の申請書インスタンスを作成 Application app1 = new TransportApplication(); // 交通費精算の申請書インスタンスは交通費精算の型の情報を持つ System.out.println(app1.getApplicationName()); // "交通費精算" System.out.println(app1.getFieldNames()[ 0 ]); // "行先" // 経費精算の申請書インスタンスを作成 Application app2 = new ExpenseApplication(); // 経費精算の申請書インスタンスは経費精算の型の情報を持つ System.out.println(app2.getApplicationName()); // "経費精算" } } カスタム申請書のクラスの検討 次にカスタム申請書の実装を行います。 当然、カスタム申請書は実装時には申請書の型の仕様が決まっていないため、種別ごとの値をハードコードすることはできません。 // カスタム申請クラス(実装不可) public class CustomApplication extends Application { @Override public String getApplicationName() { return ????; // 実装時に申請書名は不明 } @Override public String[] getFieldNames() { return ????; // 実装時にどんな項目があるか不明 } } 申請書の実体生成時にカスタム申請書の型の情報を渡して直接 インスタンス を生成するのはどうでしょうか。 // カスタム申請クラス(直接生成) public class CustomApplication extends Application { private final String applicationName; // カスタム申請書の申請書名 private final String[] fieldNames; // カスタム申請書の項目名リスト // 生成時に型の情報を渡す public CustomApplication(String applicationName, String[] fieldNames) { this .applicationName = applicationName; this .fieldNames = filedNames; } @Override public String getApplicationName() { return applicationName; } @Override public String[] getFieldNames() { return fieldNames; } } これでも インスタンス にカスタム申請書の型の情報を持たせることはできていますが、これは単に「カスタム申請クラスの インスタンス 」を作成しているだけになっています。その インスタンス がどのカスタム申請書の種別か、すなわち「型」自体の情報がなくなってしまっています。 (この例では applicationName でカスタム申請書の種別を判別できる可能性がありますが、それはたまたまそうなっているだけで、型の識別子としては不十分です。) Type Object パターンを適用 そこで、Type Object パターンを適用します。 まず、「型」を表すクラスを作成します。ここではカスタム申請書の種別ごとの「型」ですので、その設定を入れられるクラスにします。 // カスタム申請書の型クラス public class CustomType { private final String applicationName; // カスタム申請書の申請書名 private final String[] fieldNames; // カスタム申請書の項目名リスト // 生成時に型としての情報を渡し、保持する public CustomType(String applicationName, String[] fieldNames) { this .applicationName = applicationName; this .fieldNames = fieldNames; } public String getApplicationName() { return applicationName; } public String[] getFieldNames() { return fieldNames; } } そして、この型クラスのオブジェクトへの参照を持つカスタム申請書クラスを作成します。 // カスタム申請書クラス public class CustomApplication extends Application { private final CustomType customType; // 型オブジェクトへの参照を持つ // 生成時に型オブジェクトを渡し、型を持たせる public CustomApplication(CustomType customType) { this .customType = customType; } @Override public String getApplicationName() { return customType.getApplicationName(); } @Override public String[] getFieldNames() { return customType.getFieldNames(); } } 基本的なクラスの準備は以上です。実際にカスタム申請書の種別を作成し、その申請書の インスタンス を作成してみます。 public class Main { public static void main(String[] args) { // カスタム申請書の種別「稟議書」を作成 CustomType proposal = new CustomType( "稟議書" , new String[] { "件名" , "内容" }); // 稟議書の申請書インスタンスを作成 CustomApplication app1 = new CustomApplication(proposal); // 稟議書の申請書インスタンスは稟議書の型の情報を持つ System.out.println(app1.getApplicationName()); // "稟議書" System.out.println(app1.getFieldNames()[ 0 ]); // "件名" // 同じ種別の複数のインスタンスを作成可能 CustomApplication app2 = new CustomApplication(proposal); // 別の種別「押印申請」を作成可能 CustomType stamp = new CustomType( "押印申請" , new String[] { "書類種別" , "相手方社名" }); // 押印申請の申請書インスタンスを作成 CustomApplication app3 = new CustomApplication(stamp); // 押印申請の申請書インスタンスは押印申請の型の情報を持つ System.out.println(app3.getApplicationName()); // "押印申請" } } CustomType の インスタンス に注目してください。 これが、型を表すオブジェクト Type Object として CustomApplication の インスタンス にカスタム申請書の種別という「型」を与えています。 これにより CustomApplication の インスタンス は与えられた型の情報を持つようになっています。 型を表すオブジェクトである CustomType の インスタンス は使い回しが可能です。 (むしろ同じ型であれば CustomType の インスタンス は同一であることが望ましいです。システム全体でユニークとなるよう管理が必要です。) このコードでは説明のため CustomApplication の インスタンス を CustomApplication として扱っていますが、もちろん親クラスである Application として扱うこともできます。そうすればカスタム申請書の各種別も、既定の申請書も同一のインターフェースで扱うことができ、 ポリモーフィズム が捗ります。 まとめ Type Object パターン を使用すると、実行時に動的に型を作成し、その型のオブエクトを作成することができます。 作成した型は一般的なオブジェクトですので容易に管理、再利用が可能です。 アプリケーションに動的にひな形を作成するカスタマイズ性が求められたときには、Object Typeパターンで型管理を導入できないか、ぜひ検討してみてください。 関連する デザインパターン Flyweight パターン 共通の インスタンス を他のクラスの複数の インスタンス から参照するという点は Flyweight パターンと共通です。 また、型オブジェクトを型ごとにシングルトンにする際に Flyweight パターンの実装が参考になるかもしれません。 Interpreter パターン、Command パターン 型ごとにクラスを実装する際には実装クラスごとにメソッドに自由にロジックを記述することができますが、Type Object パターンでは個別にメソッドを記述することはできないため、型オブジェクトごとにロジックをカスタマイズすることはできません。 型オブジェクトを作成する際に設定できる情報はコンスト ラク タメソッドに渡せるような第一級オブジェクトに限られますので、型ごとにロジックを変えたい場合は Interpreter パターンや Command パターンなどと組み合わせるのが有効です。 参考文献 Nystorm, Robert. Game Programming Patterns. Genever Benning, 2014, 354p. https://gameprogrammingpatterns.com/ Johnson, Ralph; Woolf, Bobby. "The Type Object Pattern". 1996. http://www.cs.ox.ac.uk/people/jeremy.gibbons/dpa/typeobject.pdf Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John. オブジェクト指向 における再利用のための デザインパターン (改訂版). 吉田和樹, 本位田真一監修. SBクリエイティブ , 1999, 418p.
アバター
こんにちは、neige_ gnome です。 プライベートでは2児の母で、子どもに自宅の壁をボッコボコにされています。 会社では開発管理課という部署で、PSIRT(※)のようなことをやってます。 開発管理課は、開発部の中の1部署で、「エンジニアが働きやすい環境を提供し、成果の最大化に貢献する」をMissionに、当社の提供するサービス開発におかるセキュリティ・品質面のサポートを行っている部署です。 今回は、 ラク スにおけるプロダクトセキュリティ対策について、私たちの活動内容 をチョコっとだけご紹介します。 ※PSIRTとは:製品(Product)レベルのセキュリティ担当者のことを意味します。  会社レベルのセキュリティ担当であるCSIRTよりもスコープが開発寄りな分、  開発に特化したセキュリティ対策を行う人とイメージいただければと思います。 はじめに 前提説明 ・ラクスの組織体制ご紹介 ・OSSとは 本題 ①ライセンスチェックのながれ: ②脆弱性チェックのながれ: ③EOLチェックのながれ: 今後の課題 課題その1.人力対応が多い 課題その2.ステークホルダが可変式 課題その3.ウォッチすべき技術領域が広がりすぎ さいごに はじめに 昨今、残念なことに マルウェア やフィッシング被害のインシデントニュースを聞かない日はありません。 自社サイトが マルウェア の発信源にされていた、という話も聞きます。 そんな状況の中で、私たちは攻撃者たちからどのように自社を守れば良いのでしょうか。 今回は我々(以下、PSIRT担当と記載)のコミットメントが大きい「 OSS の 脆弱性 、EOL、ライセンスのチェック」に絞って ラク スの事例をご紹介します。 前提説明 本題の前に2点、 ラク スの組織体制と、 OSS について説明を入れさせて頂きます。 何やってるかだけ知りたい人は飛ばして『本題』を見てくださいネ! ・ ラク スの組織体制ご紹介 当社は企業の成長を支援するITサービスを提供する会社として、バックオフィス系の SaaS サービスを展開しています。 さて問題です。当社が公開している SaaS サービスの数をご存じでしょうか。 楽楽精算、楽楽明細あたりはTV CMを御覧になった方も居ると思いますが、他にも沢山あるんです。 また、公開しているサービスだけではなく、より良いサービスを世に出すために日々研究や開発を重ねています。 詳しくはコチラ→ クラウド事業 | 株式会社ラクス そしてそれらの SaaS サービスを開発するには、プログラム言語をはじめとした開発知識だけではなく 関連法や、対象製品に対する深い理解が必要になります。 そのため、 ラク スではサービスごとに専門の開発部隊が存在します。 しかも!ベスト・オブ・ブリードで開発を進める方針の都合上、 各サービスに最適な技術を取り入れているため、サービス間でシステム構成が異なります。 私たちPSIRT担当はそういった、人もシステム構成も異なる全サービス開発担当者に向けて、 横断的にセキュリティやライセンスの方針を作ったり、方針適用のサポートをしています。 体制図 ・ OSS とは OSS は一般公開されているプログラムのことを言います。 サービス開発を家の建築に例えるなら、 OSS は洗面台パーツやトイレパーツのようなものです。 パーツを持ってきて組み込むことで、トイレを自分で1から作るよりも、遥かに効率的に家を作ることが出来ますよね。 プログラムも同じです。 しかも有償の洗面台やトイレとは違い、 OSS は特定の条件の基であれば無償で利用できるケースがあることが特徴です。凄いですよね。 そんな素敵な OSS ですが、善意で公開されている性質上、開発の進みが遅かったり、 EOL(=開発がストップ) する可能性もあります。 酷いケースだと、明らかに大きな 脆弱性 (=セキュリティ的な不具合) があっても、放置され続ける場合もあります。 更に、毎日新しいパターンの 脆弱性 が発見されているため、いま 脆弱性 の無い OSS でも明日にはどうなるか分かりません。 そういった新しい 脆弱性 を持つ OSS や、EOLした OSS はクラッカーたちの格好の攻撃ターゲットになります。 つまり、 OSS を安心して使い続けるには、 OSS がEOLしたり 脆弱性 が発生していないかを、コンスタントにチェックして対応する力が必要になるのです。 勿論、 ラク スでも数百にも上る OSS を利用していますので、継続的に監視・対応しています。 なお、 OSS は"特定の条件の基であれば"無償で利用できると書きましたが、 その"特定の条件"というのが、"ライセンスと利用条件"です。 特に ライセンス は、よくあるライセンスだけでも600以上の種類があり、それぞれでどんな利用方法がOK/NGなのかが異なります。 独自のライセンスを持つ OSS も多いため、使いたい OSS が本当に使えるのかを確実にチェックすることが重要です。 ラク スでは、NGな利用方法をしないよう、PSIRT担当がチェックしています。 本題 さてここでは、 ラク スにおける OSS のライセンス、 脆弱性 、EOLのチェックのながれをご紹介します。 主な登場人物は、PSIRT担当とサービス開発担当の2者です。 ①ライセンスチェックのながれ: ライセンスチェックフロー サービス開発担当が利用 OSS 情報をPSIRT担当に共有 → PSIRT担当がライセンスチェック → サービス開発担当へフィードバック となっています。 新出パターンのライセンスが出てきた場合は、このフローに法務担当部署が加わり、PSIRT担当と認識をすりあわせます。 ② 脆弱性 チェックのながれ: 脆弱性 チェックフロー (①でPSIRT担当が利用 OSS を把握) → PSIRT担当が情報収集・サービス開発担当へ通知 → サービス開発担当にて影響確認・対応・情報共有 となっています。 サービス開発担当は、対応方針を決めたら開発・PSIRT担当全体に共有しますが、 その際 『対応方針を決めるのは通知から最遅でもX時間以内、 クリティカルな 脆弱性 があった場合に OSS のアップデートを適用した製品をリリースするのはX日以内』 という ラク ス独自のルールに従います。細かい数字はセキュリティに影響するため言えませんが、 Xに入る数字は相当短い、とだけ書かせて頂きます。(サービス開発担当者の技術力が伺えますね。) なおPSIRT担当の情報収集源は多岐にわたります。 商用ツールだけでなく、 脆弱性 の一覧サイト(NVD, JVN )、 Apache などベンダーのサイト、 RSSフィード から直接拾うこともあります。 ツールも利用していますが、機械チェックでは誤検知が多いため、人の目である程度フィルタリングした後にサービス開発担当へ通知するようにしています。 (実はこの部分に、PSIRT担当・サービス開発担当の双方が少し前まで多大な 工数 をかけていました。 それを先日自動化し、年間数百時間の 工数 削減に成功しました★そのお話はまた後日。) ③EOLチェックのながれ: 脆弱性 チェックと同じですので、図は割愛させていただきます。 PSIRT担当がEOLの情報収集をする上でトリッキーなポイントがあります。 実はEOL予定というものは、 OSS の公開ページに直接記載されているものから一切書かれていないものまで、様々です。 そこで、 ラク スでは独自に『X年以上開発がストップしている OSS はEOLと見做す』という基準を設けて、 EOL予定が近い OSS を定期的にリストアップし開発へ通知しています。 サービス開発担当は、EOLが来る時期を見据えて別の OSS に乗り換えたり、小さい機能であれば自社開発するなどして備えます。 以上が、 ラク スにおける OSS のライセンス、 脆弱性 、EOLのチェック体制です。 ちなみに上記3つの体制において、当社では情報共有に当社製品『楽楽販売』を利用しています。 楽楽販売は API 対応、ノーコードで色々な処理が自動化できて、承認プロセスもケアできる便利なツールです。 ご利用・ご検討頂いてるお客様へ CRM 以外にもご活用頂けますよ、ということでご紹介させていただきました。 今後の課題 ここまで、当社のライセンス・ 脆弱性 ・EOL管理についてご紹介してきましたが、 我々もPSIRT担当として他社の例に漏れず、課題を抱えています。 個人的に大きいと思っている課題を3点を挙げてみました。 課題その1.人力対応が多い 文中にも若干記載した通り、自動化は適宜進めていますが まだまだ手動の箇所が多く、PSIRT担当として生産的な活動に廻せるリソースが少ない状況です。 そのため、品質は担保したまま、ツールなどを活用しライセンスチェック~ 脆弱性 検知をトータルで効率化できないか検討中です。 課題その2.ステークホルダが可変式 セキュリティルールの策定・変更に対して、10以上の関連組織(総勢300名~)に100%コンセンサスを得るのは無理です。 かといって運用との乖離は出したくないので、ある程度はネゴりたいところです。 しかしながら、要件次第でどの組織の誰に訊けば情報があるのか/誰に承認貰えば良いかが変わるため、 いろいろな現場で相異なるご意見をいただいた場合は、方針を取り纏めるのに苦労することもあります。 (もういっそ開き直って、分かる範囲で調整した上でサッサとルールを全体周知してしまい、 指摘を受ければ即是正する方が当社の変化のスピード感に合っているかも・・と思っています。) 課題その3.ウォッチすべき技術領域が広がりすぎ その時々で理想とされるシステム構成や世間のニーズに追随するため、 ラク スのサービスで扱う技術領域は日々広がっています。 古き良きオンプレの ファイルシステム 、 クラウド 、モバイルアプリ、Dockerと、セキュリティ面でカバーすべき範囲もその都度広がってきました。 各技術に共通するセキュリティルールは多いものの、新技術独自のルールも整備していく必要があります。 ただ、新しくルールを作るとしてそれをチェックできる体制を敷かねばなりません。 その一方で、新技術の確立で陳腐化してしまったルールがあれば、早急に改善する必要があります。 こういった技術トレンドの変化に全てオンタイムでルールを整備するのは厳しいものがあります。 特に、多数のサービス開発者にコンセンサスを得る場合は余計に時間がかかります。 優先度を決めて順次対応していますが、それまでは個別に問合せや連絡対応をする必要がありサービス開発担当の方に申し訳なく思っています。 生成系AIと少しのコーディングで、そういったルールのアップデートや ネゴシエーション を 大幅に自動化できるのではないかと妄想はしていますが、今はまだ、手を出せていません。 さいごに セキュリティ対策は、サービス開発効率との トレードオフ の側面があります。 やり過ぎた結果、開発効率やサービスレベルを下げてしまうのは可能な限り避けるべきです。 とはいえ、 セキュリティインシデント のニュースは増える一方ですので、MAXに警戒する必要があります。 それを踏まえて、私たち開発管理課は、サービス開発担当との情報共有を密に行い、時流と事業規模に合った標準化/方針策定をすることで、自社に最適なセキュリティ強化を図っていきたいと思っています。 と、真面目なことをつらつらと書きましたが、現場では毎日、和気あいあいと改善に向けて取り組んでいます。 特に、失敗があっても許容してくれる社風のお陰で気楽にトライ&エラーが出来て助かっています。 チャレンジとお喋りと勉強が好きな方、いつでもお待ちしています。 ★開発管理課では一緒に働くメンバーを募集してます!!★ ↓ ↓ ↓ プロダクトセキュリティエンジニア の項をご参照願います。 募集職種 | 株式会社ラクス キャリア採用
アバター
はじめに PHP_CodeSnifferとは 運用フロー 問題点 PHP_CodeSnifferの指摘をまとめて受け取る PHP_CodeSnifferの指摘を確認するために、都度CIを実行しなければならない 改善 PhpStormにPHP_CodeSnifferを設定する 導入 PHP_CodeSnifferをインストールする PhpStorm の設定画面を開き、PHP_CodeSniffer の実行設定を行う インタプリタの設定を追加 その後 実装中にPHP_CodeSnifferの確認ができるようになった CIの自動チェック完了までにタイムラグがあり、待ちの時間ができる問題について まとめ はじめに こんにちは。配配メール開発チームに所属しているmrstsgkです。 2023年3月23日から3月25日に開催されたPHPerKaigiに参加しました! (LT枠で登壇しました!とても楽しかったです!) speakerdeck.com 発表後、 PHP _CodeSnifferについて進展があったので、このブログで紹介いたします。 PHP _CodeSnifferとは コーディング規約の違反を検出するライブラリです。 PSRや PEAR などの様々なコーディング規約を指定して検査を実施しています。 PSR とは: PHP Standards Recommendationsの略で、 PHP -FIG(The PHP Framework Interop Group)が策定している PHP の規約です。 PEAR とは: PHP Extension and Application Repositoryの略で、 PHP で利用する事ができるライブラリ(パッケージ)を提供しているサービスのことです。 似た名前で PECL がありますが、こちらは C言語 で書かれた拡張ライブラリ (extension) を提供するサービスのことです。 独自の規約を追加することも可能です。 私たちのチームでは、独自のルールセットを採用しています。 原則 PSR12 に準拠するルールセットになっていますが、 else if の代わりに elseif を使用するなど既存コードの影響で大量に指摘が出そうなルールは除外しています。 運用フロー CIでの自動チェック。 親ブランチと比較して増加したエラーのみが出力されます。 \ 問題点 現状の運用フローでの問題点 PHP _CodeSnifferの指摘をまとめて受け取る チームの開発の流れとして、実装完了時に今までの commit をまとめてGitLabに push することが多くありました。 なので、 PHP _CodeSnifferの指摘をまとめて受けるため、対応で 工数 が増加する問題が発生しました。 PHP _CodeSnifferの指摘を確認するために、都度CIを実行しなければならない 「 PHP _CodeSnifferの指摘をまとめて受け取る」という問題を解決するために、こまめにGitLabに push するようになりました。 しかしCIでの自動チェックが完了するまでにタイムラグがあり、待ちの時間ができてしまいました。 CI実行中にほかの作業に着手するのでスイッチングコストの増加が見られ、実装に集中しにくい環境になっていました。 改善 PhpStormに PHP _CodeSnifferを設定する 現状の運用フローだと、 PHP _CodeSnifferを確認できるのがGitLabに push することだけだったので、 確認できる段階をもっと早くするためにPhpStormに PHP _CodeSnifferを導入することになりました。 また、PHPerKaigi後に寄せられたフィードバックを確認していると、以下のフィードバックをいただきました。 (フィードバックありがとうございます!!) IDE に組み込めばCIにあげる前に手元でチェックできるので、フィードバックサイクルを早められてよいと思いました。 PhpStormに PHP _CodeSnifferを設定する際は、公式のドキュメントを参考にすれば導入しやすいと思います。 pleiades.io 導入 PHP _CodeSnifferをインストールする 公式のドキュメントによると PHP _CodeSnifferをComposerと共にインストールすると、PhpStormは必要な スクリプト を自動的にダウンロードし、 IDE に登録し、オプションで対応するコードインスペクションを有効にして構成します。 とのことなので、composerをインストールします。 php composer.phar install PhpStorm の設定画面を開き、 PHP _CodeSniffer の実行設定を行う Settingsを開きます。 PHP > Quality Tools内に PHP _CodeSnifferがあるので、そこから設定することができます。 インタプリタ の設定を追加 ※設定例 PHP _CodeSniffer path: /usr/local/phpcs/vendor/bin/phpcs Path to phpcbf: /usr/local/phpcs/vendor/bin/phpcbf その後 実装中に PHP _CodeSnifferの確認ができるようになった PhpStormに PHP _CodeSnifferを導入することで、問題点であった「 PHP _CodeSnifferを確認できるのがGitLabに push することだけ」を解決することができました。 PHP _CodeSnifferをCIに組み込んだときに出た良い副産物として、 開発中に細かいCommit粒度でGitLabへpushするようになり、「開発→CIでコードを確認→修正」のサイクルで実装に集中しやすい状態になりましたが、 PhpStormに PHP _CodeSnifferを導入することで、「開発→静的解析→修正」のサイクルを回すことができるようになり、より実装に集中しやすい環境になりました!! CIの自動チェック完了までにタイムラグがあり、待ちの時間ができる問題について こちらの問題は、PhpStormに PHP _CodeSnifferを導入しても解決することはできません。 根本的な原因として、CIのジョブがいつ完了しているかわからないので定期的にGitLabを確認しなければなりませんでした。 なので、CIのジョブが完了したら連絡が来る仕組みを作って対応しています。 まとめ 静的解析ツールはやはり最高でした!! コーディング時の早い段階でエラーを検出できるため、バグ修正コストを抑制できることや、共通のコーディングスタイルを推進できるため、コードの可読性や保守性が向上するなどの多くの利点があります。 皆さんも是非、静的解析ツールを利用して快適な開発ライフをお過ごしください。 ただプロジェクトにもよりますが、 PHP _CodeSnifferは「 IDE への導入」「CIでの自動チェック」「CIのジョブ連絡」この3つが揃うことで真の快適さが得られると感じました。
アバター
はじめに はじめまして、 ラク スフロントエンド開発課の斉藤です。 普段フロントエンド開発課では、一部のプロダクトにおいて新しく開発した機能を実装した画面や、パフォーマンスの劣化が懸念される画面に対して、性能計測を行っています。今回はフロントエンド開発課が どのようなWebパフォーマンス指標を計測しているのか 、 なぜその指標を採用したのか 、 どのように計測しているのか を紹介したいと思います。また、パフォーマンスを計測している中で 不便に思っていること や 改善していきたいこと についてもご紹介します。 注意点として今回紹介する指標や計測ツールは、あくまでも ラク スのプロダクトに合わせて選定したものであるため、他社様、あるいは個人にとって最適なものと言えるわけではありません。参考程度にお読み頂ければ幸いです。 はじめに 対象読者 この記事で扱わないこと 結論 計測する指標と目標スコアの一例 計測ツール 選定理由 各指標の概要と選定理由 FCP (First Contentful Paint) 概要 選定理由 参考スコア LCP (Largest Contentful Paint) 概要 選定理由 参考スコア TTI(Time to Interactive) 概要 選定理由 参考スコア TBT(Total Blocking Time) 選定理由 参考スコア CLS(Cumulative Layout Shift) 選定理由 参考スコア Response 概要 選定理由 目標 Animation 概要 選定理由 目標 計測手順 webパフォーマンス計測環境の準備 計測対象画面 devtoolsの設定 view portの設定 スロットルの設定 各webパフォーマンス指標の測定 LCP, TTI, LCP, CLSの測定 TBTの測定 Responseの測定 Animation パフォーマンス測定シート 現状問題に感じていること パフォーマンス計測を始めてよかったこと おわりに 参考文献 対象読者 フロントエンドのパフォーマンスを計測したいが、どの指標を計測すればよいかわからない方 フロントエンドのパフォーマンスを計測したいが、どのツールを使って計測すればよいかわからない方 フロントエンドのパフォーマンス測定について興味がある方 この記事で扱わないこと Webパフォーマンスの改善方法 結論 計測する指標と目標スコアの一例 WANT MUST FCP <= 1.8s <= 3.0s LCP <= 2.5s <= 4.0s TTI <= 3.8s <= 7.3s TBT <= 200ms <= 600ms CLS <= 0.1 <= 0.25 Response <= 100ms <= 1000ms Animation <= 16.7ms(60FPS) <= 33.3ms(30FPS) ※目標スコアはあくまでも一例になります。実際にはプロダクトごとに異なる値となっています。 計測ツール パフォーマンス指標計測には以下ツールを用いる Chrome devtools: Performance Insight 測定対象:FCP, LCP, TTI, CLS Chrome devtools: Performance 測定対象:TBT, Response, Animation 選定理由 devtoolsに内包された機能のため実施コストが低い ネットワーク・CPUのスロットルが行えるため実ユーザー環境に近い計測が行える web.dev で紹介されている指標に加え、 RAILモデル の測定も行えるため 各指標の概要と選定理由 ユーザーがパフォーマンスをどのように認識しているかに関する指標として、以下の項目が 挙げられています。 使用可能性 :ユーザーがページを操作できているか。ビ ジー 状態になっていないか。 快適さ :遅延やエラーがなく、スムーズで自然な操作ができているか。 知覚される読み込み速度:  ページがすべての視覚要素を読み込み、画面に レンダリング する速度。 読み込みの応答性:   コンポーネント がユーザーの操作に対してすばやく応答するために、ページが必要な JavaScript コードを読み込んで実行する速度。 実行時の応答性:  ページの読み込みが完了した後、ページがユーザーの操作にどの程度すばやく応答できるかを示す指標。 視覚的な安定性:  ユーザーが予期しないような手法でページ上の要素が移動し、ユーザーの操作に支障をきたす可能性があるかどうかを示す指標。 滑らかさ:   トランジション やアニメーションが一定のフレーム レートで レンダリング され、ある状態から別の状態へと流れるように移動しているかどうかを示す指標。 フロントエンド開発課では上記を網羅的に測定することを目指し各指標を選定しています。 FCP (First Contentful Paint) 概要 FCPは、ユーザーがアクセスしたウェブサイトで最初に表示されるコンテンツがどれくらい早く描画されるかを示す指標です。この指標における "コンテンツ" は、テキスト、画像 (背景画像を含む)、 <svg> 要素、白以外の <canvas> 要素のことを指しています。 引用元: https://web.dev/fcp/ 上記のタイムラインでは2フレーム目にコンテンツが初めて表示されています。このようにFCPはwebサイトを読み込んでから一部のコンテンツが表示されるまでの時間を計測します。全てのコンテンツが レンダリング される時間では無いことに注意してください。 選定理由 読み込みの応答性を測定するため 参考スコア MUST:3秒以下 WANT:1.8秒以下 参考: https://web.dev/fcp/#fcp-における良いスコアとは? LCP (Largest Contentful Paint) 概要 LCPは、ビューポート内で最も大きなコンテンツがどれだけ早く描画されるかを示す指標です。この指標における "コンテンツ" は、以下の要素を指します。 <img> 要素 <svg> 要素内の <image> 要素 <video> 要素 (ポスター画像が使用されます) テキスト ノードやその他のインラインレベルのテキスト要素の子要素を含むブロックレベル要素 h1, h2, h3 p articleなど LCPとしてレポートされる要素のサイズは、通常ビューポート内でユーザーに対して表示されるサイズとなります。要素がビューポートからはみ出していたり、要素の一部が切り取られていたり、画面に表示されないオーバーフローが発生したりしているような場合、そういった部分は要素のサイズには含まれません。あらゆる要素において、 CSS を介して適用されているマージン、パディング、ボーダーはすべて考慮されません。 引用元: https://web.dev/lcp/ 上記の例ではLCPは5フレーム目で発生しています。最後の大きな画像が表示されるまではテキスト要素がLCPのターゲットとなっていましたが、ページの読み込みが進み、最終的なLCPのターゲットが変化した例です。 遅れて読み込まれたコンテンツの方がすでにページ上に表示されているコンテンツよりもサイズが大きいといったケースはよく見られますが、必ずしもそうなるわけではありません。次の 2 つの例では、ページが完全に読み込まれる前に LCPが発生しています。 引用元: https://web.dev/lcp/ 選定理由 知覚される読み込み速度を測定するため 参考スコア MUST:4秒以下 WANT:2.5秒以下 参考: https://web.dev/lcp/#lcp-における良いスコアとは? TTI(Time to Interactive) 概要 TTI は、ページの読み込みが開始されてから主なサブリソースの読み込みが完了するまでの指標です。改善することでページがユーザーの入力に対してすばやく確実に応答できるようになります。 TTIは以下の手順に沿って計測されます。 FCPから時間の計測を開始します。 少なくとも 5 秒間の「落ち着いている期間(以下quiet window)」を時間の経過順に探していきます。quiet windowは以下のように定義されています。 LongTask(50msを超えて実行されるタスク)がない 実行中のネットワーク GET リク エス トが 2 件以下 quiet windowより前の期間内で、一番最後に現れる長く時間がかかっているタスクを見つけ出します。長く時間がかかっているタスクが見つからない場合には、FCP まで遡ります。 quiet windowより前の期間内で一番最後に現れる長く時間がかかっているタスクの終了時間が、TTI となります。(長く時間がかかっているタスクが見つからない場合には、FCP と同じ値になります)。 以下の図は、上記の手順を視覚化したものです。 引用元: https://web.dev/tti/ 選定理由 実行時の応答性を測定するため 参考スコア MUST:7.3秒以下 WANT:3.8秒以下 参考: How Lighthouse determines your TTI score TBT(Total Blocking Time) TBTは、読み込みの応答性を測定するために重要となる指標です。ページが確実に操作可能になるまでの間の操作不可能性の重大さの数値化に役立ち、TBT が低ければ低いほどページが確実に使用可能となることを示しています。 メインスレッド上にLongTask(50msを超えて実行されるタスク)が存在する場合、そのメインスレッドは"ブロックされた" とみなされます。"ブロックされた" と表現されるのは、 ブラウザー が進行中のタスクを中断することができないからです。したがって、Long Task実行中にユーザーがページを操作した場合、 ブラウザー は応答する前にタスクの終了を待たなければなりません。これによりユーザーから見るとページが遅い、または質が低いと感じてしまう可能性があります。 TBTは、FCPとTTIの間で発生する各長いタスクの ブロック時間の合計により計算されます。 たとえば、ページを読み込んでいる最中の ブラウザー のメイン スレッドの図は、以下のようになります。 引用元: https://web.dev/tbt/ 上記のタイムライン上には 5 つのタスクがあり、そのうちの 3 つは継続時間が 50 ms を超えているため、長く時間がかかっているタスクとなります。以下の図は、長く時間がかかっているタスクそれぞれのブロック時間を示しています。 引用元: https://web.dev/tbt/ このため、メイン スレッドでのタスク実行の総時間は 560 ミリ秒ですが、そのうちの 345 ミリ秒のみがブロック時間としてみなされます。 タスクの継続時間 タスクのブロック時間 タスク 1 250 ミリ秒 タスク 2 90 ミリ秒 タスク 3 35 ミリ秒 タスク 4 30 ミリ秒 タスク 5 155 ミリ秒 合計ブロック時間 345 ミリ秒 選定理由 使用可能性、読み込みの応答性を測定するため 参考スコア MUST:600ミリ秒以下 WANT:200ミリ秒以下 参考: how-lighthouse-determines-your-tbt-score CLS(Cumulative Layout Shift) CLSは、視覚的な安定性を測定するための指標です。画面に表示されたコンテンツのユーザーの予期しない突然の移動やズレをレイアウトシフトと呼びます。このレイアウトシフトに遭遇する頻度の数値化に役立つ指標がCLSであり、CLS が低ければ低いほど、そのページが快適であることが保証されます。 CLSスコアは以下の計算式で算出されます。 レイアウトシフトの影響を受けた面積 × 実際にずれて動いた距離 以下のようにテキストが表示されたあとに「Click Me」ボタンが表示され、要素のズレが起こった場合を考えます 引用元: https://web.dev/cls/ レイアウトシフトの影響を受けた面積(赤点線部に囲まれた分)はビューポート全体の50%を占めるため0.5となります。また実際にずれて動いた距離(紫色の矢印の長さ)はビューポート全体の15%を占めます。したがってCLSスコアは次のように計算できます。 0.5(レイアウトシフトの影響を受けた面積)× 0.15(実際にずれて動いた距離)=0.075 レイアウトシフトが複数箇所で発生する場合は上記計算式の合計値がCLSスコアとなります。 参考: https://gmotech.jp/semlabo/seo/blog/cwv_cls/ 選定理由 視覚的な安定性を測定するため 参考スコア MUST:0.25以下 WANT:0.1以下 参考: https://web.dev/cls/#cls-における良いスコアとは? Response 概要 Responseは、ユーザーがアクションを起こしたときWebサイトが反応するまでを測定する指標です。100ms以内に完了することで、ユーザーはやりとりが瞬時に行われていると感じます。例えば以下のアクション時間を測定します。 チェックボックス にチェックを入れてからチェック状態が反映されるまで リンクをクリックしてから画面が遷移するまで ボタンをクリックしてからダイアログが表示されるまで API を叩いてから何らかの通知(スナックバーやトースト等)が表示されるまで 選定理由 快適さ、実行時の応答性を測定するため 目標 MUST:1000ms以下 WANT:100ms以下 参考: https://web.dev/i18n/ja/rail/#ユーザーに焦点を合わせる Animation 概要 Animationは、アニメーション動作の各フレームの動作時間を測定する指標です。この指標はアニメーションの視覚的な滑らかさを数値化したものです。 選定理由 滑らかさを測定するため 目標 MUST:16.7ms以下 WANT:33.3ms以下 ※ それぞれ30FPS/60FPSを満たす目標 参考: https://web.dev/i18n/ja/rail/#アニメーション:フレームを10ミリ秒で生成する 上記以外にも重要な指標としてFIDが挙げられます。FIDは、ユーザーが最初にサイトを操作したとき から、その操作に実際に応答するまでの時間を測定する指標です。この指標は 実際のユーザー環境で測定する指標である ため測定難易度が高くなります。したがって、FIDの代わりにTBTを測定することで対処する事とします。 TBTはFIDと相関性が高く、かつ検証環境での測定が可能 なことが理由です。 計測手順 webパフォーマンス計測環境の準備 webパフォーマンスを計測するために以下の条件を満たす環境を用意します 検証環境 本番ビルドかつBEと疎通が行える環境を用意します ただしフロントエンドのパフォーマンス測定のみに焦点を当てる場合、バックエンドはモック環境でもOKとします。その場合でもフロントエンドは本番ビルドを行った環境で測定してください。 理由:ユーザーが使用する実際の環境に近い条件で測定するため 理論上表示できる最大数のデータを用意する 例えば1つのページで最大500件の検索結果を表示できるような仕様の場合、ダミーデータを500件以上用意しかつ、その全てを表示した上で計測を行います 最大表示件数が決まっていないページに関しては、ビューポート全体に表示できる数のダミーデータを用意すればOKとします 理由:予測される最大の レンダリング コストがかかる環境のパフォーマンス改善を行うことができれば、その画面全体のwebパフォーマンス向上が期待できるため 計測対象画面 新しく機能を追加した画面において、パフォーマンスの低下が懸念される画面 新しく作成した画面 類似した画面を複数実装する場合いずれか1つでOKとします devtoolsの設定 webパフォーマンスの計測を行う前にdevtoolsの設定を行います。 view portの設定 view portによりwebパフォーマンスの測定結果は変化するためview portを固定します。 devtoolsを開き「Toggle device toolbar」を押下します。 画面サイズを任意の値に設定します。基本的には提供するアプリケーションの推奨する画面サイズに設定します。 スロットルの設定 商材を利用するユーザーが用いるPCはエンジニアの開発用PCと比較して性能が低いことが懸念されます。したがってユーザーの実環境に近い条件でwebパフォーマンスの測定を行うため、devtoolsでスロットルの設定を行います。 devtoolsの「Performance Insights」パネルを開きます。 セレクトボックスを押下しNetwork、CPUを想定するユーザーの環境に合わせて設定します。ユーザーは基本的にキャッシュされた状態のページを参照するので、「Disable cache」のチェックは外します。 同様にdevtoolsの「Performance」パネルを開きます。 設定ボタンを押下します。 CPU、Networkを設定します。 各webパフォーマンス指標の測定 LCP, TTI, LCP, CLSの測定 LCP, TTI, LCP, CLSは Chrome DevToolsの「Performance Insights」パネルで測定することができます。 Chrome DevToolsを開き、「Performance Insights」パネルを押下します。 「Measure page load」を押下しパフォーマンス測定を行います。 devtoolsに表示されるFCP,LCP,TTIにマウスホバーすると各値が表示されます。 CLSは画面にレイアウトシフトが発生している場合のみ自動で計測されます。「Insights」タブから値を確認できます。 TBTの測定 TBTは Chrome DevToolsの「Performance」パネルで測定します。 Chrome DevToolsを開き、「Performance」パネルを押下します。 画面左上の「Start profiling and reload page」を押下します。 devtools下部に表示される「Total blocking time」の値がTBTです。 Responseの測定 Responseは Chrome DevToolsの「Performance」パネルで測定します。 今回はボタンを押してからダイアログが開くまでの 応答時間 の測定を例にとります。 Chrome DevToolsを開き、「Performance」パネルを押下します。 「Screenshots」にチェックを入れます。 devtools左上の「record」を押下します。 アプリケーション上でボタンを押下し、ダイアログを表示させます。 devtools左上の「stop」を押下します。 スクリーンショット を確認しながらボタンを押してからダイアログが表示されるまでの範囲をドラッグして絞り込みます。 TotalがResponseの測定値になります。 Animation Animationは Chrome DevToolsの「Performance」パネルで測定します。 今回はダイアログが開く際のアニメーションを測定します。 1~5まではResponseの測定と同様 スクリーンショット を確認しながらアニメーション動作中の各フレームの動作時間を記録する 2で記録した各フレームの動作時間の平均値がAnimationの測定値になります。(1フレームずつ確認するのは非効率なためぱっと見でもOK) パフォーマンス測定シート フロントエンド開発課では各webパフォーマンス指標の測定結果を以下のような スプレッドシート にまとめています。 全測定値のうち75パーセンタイル以下の値が合格基準を満たしていれば、その指標は合格となります。例えば測定回数が全部で4回の場合、そのうちの3回が合格基準を満たしていればOKということになります( 参考 )。合格基準に満たない場合でも「訳あり合格」として合格とすることもできます。現実的には全ページ、全機能で合格基準を達成することは難しいです。達成が難しく改善できない合理的な理由を説明できるのであれば「訳あり合格」とすることができるようにしています。 現状問題に感じていること ここまで読んでいただいている場合すでにお察しかもしれませんが、各パフォーマンスを手動で計測しているため 実施コストが高い です。少しでも測定コストを減らすため、Animationの指標は測定してなかったり( SaaS プロダクトにおいて重要度の高くない指標のため)、BEを MSW でモックした環境で測定を行うこともあります。信頼性の高いデータを測定することとパフォーマンスを測定するコストは トレードオフ の関係にあるので、 工数 と相談しながら計測しているのが現実です。今後の展望として Lighthouse CI や Sentry 等を用いて自動的にパフォーマンス測定&レポート作成ができないか検証を進めていきたいです。 パフォーマンス計測を始めてよかったこと 各Webパフォーマンスの指標に基づいて具体的な数値を計測するため、パフォーマンス劣化の ボトルネック がどこにあるかを特定しやすくなったので良かったです。「なんとなくページが遅い気がする」といった感覚値から「LCPの値が基準値を◯%下回っているためパフォーマンスが悪い」のように実測値で判断できるようになりました。これにより、このページのパフォーマンスを改善するためにはチャンク分割が効果的なのではないか、と具体的な根拠を持って仮説を立てることができるようになり、効率的に改善に取り組めています。またパフォーマンス改善を行なった後もう一度計測を行うことで、具体的にどの項目がどれだけ改善されたのかがわかるようになったため、パフォーマンス改善のために行なったことが本当に効果があったのか検証するのにも役立っています。 おわりに ラク スのフロントエンド開発課が測定するWebパフォーマンス指標と測定方法を紹介しました。計測はまだ始めたばかりで非効率な部分も感じています。少しずつ改善しながら実施コストを下げつつ、定期的にパフォーマンス測定を行うことで、より品質の高い製品をリリースしていきたいです。 参考文献 ユーザーを中心としたパフォーマンス指標 RAILモデルでパフォーマンスを評価する Core Web Vitals を測定するためのツール Lighthouseの計測結果を見ていく コアウェブバイタルのCLSとは? The Science Behind Web Vitals How Percentile Approximation Works Web Vitals Webフロントエンド ハイパフォーマンス チューニング
アバター
PHP で「文字列に特定のキーワードが含まれているか」や「文字列中に特定の文字列を含むか」を確認したい場合、どのようなコードを書くだろうか? もし、あなたが strpos() や strstr() を使う方法を思いついたのなら、これだけは覚えて帰ってほしい。 文字列検索には str_contains() を使え 。 結論:文字列検索には str_contains() を使う サンプル 昔はstrpos()やstrstr()などを使っていた strpos() や strstr() ではダメなのか? strpos() を使うべきでない理由 strstr() を使うべきでない理由 preg_match()を使うべきでない理由 「いや、キーワードが先頭にあるかを知りたいんだ」という人は 「日本語でも使えるの?」との疑問について もっと詳しく? これでわかっただろう 結論:文字列検索には str_contains() を使う str_contains() は、「指定した部分文字列が、文字列中に含まれるかを調べる」ための関数である。この関数はPHP8.0 で追加された。 php .net: str_contains str_contains(string $haystack, string $needle): bool needle が haystack に含まれるかを調べます。 大文字小文字は区別されます。 〜中略〜 haystack に needle が含まれていた場合 true そうでない場合、false を返します。 サンプル <?php if ( str_contains ( "なんだお前たち!" , "なんだ" ) ) { echo "なんだかんだと聞かれたら、" ; // 表示される!! echo "答えてあげるが世の情け" ; } if ( str_contains ( "誰だお前たち!!" , "なんだ" ) ) { echo "なんだかんだと聞かれたら、" ; // 表示されない echo "答えてあげるが世の情け" ; } <?php $ result = str_contains ( "なんだお前たち!" , "なんだ" ) ; var_dump ( $ result ) ; // bool(true) $ result = str_contains ( "誰だお前たち!" , "なんだ" ) ; var_dump ( $ result ) ; // bool(false) 上記の通り、検索結果が bool で返ってくる。とても明確な仕様だ。 名前もすばらしい。 contains (含む、包含する)とあるので、読めば何をする関数かわかりやすい。 少なくとも strpos() 、 strstr() よりは直感的だ。 よって、文字列検索には str_contains() を使え。 昔は strpos() や strstr() などを使っていた しかし、 Google 検索で以下のような検索を行うと strpos() や strstr() (もしくは mb_strpos や mb_strstr )を提案するサイトが出てくる。 PHP で文字列を検索する PHP で文字列を含むか検索する PHP で文字列の有無を確認する なぜか? それは str_contains() が 2020/11/26 にリリースされた PHP8.0 で導入された関数だから だ。 *1 *2 それ以前の PHP には str_contains() が存在せず、 strpos() や strstr() を代替品として使う手法が一般的だったのだ。 しかし、2023年7月現在サポートされている PHP8.0、8.1、8.2 には str_contains() が存在する。 つまり、 str_contains() が使えないバージョンの PHP はもうEOLを迎えているのだ。 *3 そのため文字列検索には str_contains() を使え。 strpos() や strstr() ではダメなのか? 今まで使っていた strpos() や strstr() がなくなるわけではないから、今まで通り使っても問題ないでしょ?? ひょっとしたら、あなたはそう思うかもしれない。確かにもっともだ。 しかし、よくよく考えて欲しい。もともと strpos() は「文字列内の部分文字列が最初に現れる位置を見つける」ための関数だ。 *4 また、 strstr() も「文字列が最初に現れる位置を見つけ、そこから文字列の終わりまでを返す関数」である。 *5 つまり、 これらの関数は、文字列が含まれているかを確認する関数ではない 。本来の目的が純粋な検索ではないのだ。 紙を切るために包丁を使っている人を見たら「いやハサミを使いなよ!」と言いたくなるだろう?それと似た感覚だ。 違和感が伝わったのであれば、文字列検索には str_contains() を使え。 strpos() を使うべきでない理由 もっと現実的な理由がある。 それは、 strpos() などを文字列検索に使うと、バグを生み出す可能性が高くなってしまうことだ。 strpos() などの戻り値は、FALSE だけでなく 0 を返すこともあるのがその原因だ。 まず、 strpos() の定義を確認しよう。 php .net: strpos strpos(string $haystack, string $needle, int $offset = 0): int|false 文字列 haystack の中で、 needle が最初に現れる位置を探します。 〜中略〜 needle が見つかった位置を、 haystack 文字列の先頭 (offset の値とは無関係) からの相対位置で返します。 文字列の開始位置は 0 であり、1 ではないことに注意 しましょう。 needle が見つからない場合は false を返します。 ドキュメントには上記の説明に合わせて、以下のように警告も記載されている。 つまり、 strpos() 関数は、 「文字列の先頭にキーワードが存在する場合は 0」を返し、「文字列にキーワードが存在しない場合は FALSE」を返す のだ。 これがどれだけ危険なことか、あなたが PHPer であればわかるだろう。 ❌ strpos() が意図しない動きをするコード <?php if ( strpos ( "お前たちは誰だ!" , "なんだ" ) ) { echo "なんだかんだと聞かれたらああああ、" ; // 表示されない echo "答えてあげるが世の情け" ; // (意図通り) } if ( strpos ( "なんだお前たち!" , "なんだ" ) ) { echo "なんだかんだと聞かれたら \n " ; // 表示されない!! echo "答えてあげるが世の情け" ; // ("なんだ"を含むのに...) } <?php $ result = strpos ( "お前たちは誰だ!" , "なんだ" ) ; var_dump ( $ result ) ; // bool(false) ← "なんだ"がないので FALSE が返る $ result = strpos ( "なんだお前たち!" , "なんだ" ) ; var_dump ( $ result ) ; // int(0) ← "なんだ"が文字列の先頭に存在するので 0 が返る! 上記の通り、文字列の先頭にキーワードが存在する場合は 0 が返ってくるので、 そのまま条件式として利用すると、"暗黙の型変換"が行われ意図していない比較結果になってしまう。 この問題はさまざまな記事で取り上げられているように、 !== による「厳密な比較」を行うことで回避は可能だ。 これにより、厳密に FALSE が返ってきた時のみ検知することができる。 ✅ strpos() を意図通りに動かすコード <?php if ( strpos ( "なんだお前たち!" , "なんだ" ) !== FALSE ) { echo "なんだかんだと聞かれたら \n " ; // 表示される echo "答えてあげるが世の情け" ; // (意図通りの挙動) } もちろん、このとき != は使ってはいけない。0 が返ってきたときに true と判定してしまうからだ。 いかがだろうか? strpos() を文字列検索目的に利用する場合、これだけのことを理解した上で利用する必要がある。 また、これらの考慮をうっかり忘れると、「キーワードが文字列の先頭にあるときだけ意図しない挙動をするバグ」を生み出すことになるだろう。 余計なことを考えながら実装するぐらいだったら、文字列検索には str_contains() を使え。 strstr() を使うべきでない理由 strstr() も同じだ。比較結果が意図しない値になる可能性がある。 strstr() の定義は以下のとおり。 php .net: strstr strstr(string $haystack, string $needle, bool $before_needle = false): string|false haystack の中で needle が最初に現れる場所を含めてそこから文字列の終わりまでを返します。 〜中略〜 部分文字列を返します。 needle が見つからない場合は false を返します。 strpos() の場合は以下のようなコードを書くことで意図しない動きをしてしまう。以下の例では、 検索対象である "0" が文字列中に存在するのに関わらず、文字列が表示されてしまう。 ❌ strstr() が意図しない動きをするコード <?php if ( strstr ( "今すぐ買うべき技術書トップ10" , "0" ) ) { echo "文字列中に 0 があるよ!" ; // 意図せず表示されない!! } <?php $ returnVal = strstr ( "今すぐ買うべき技術書トップ10" , "0" ) ; var_dump ( $ returnVal ) ; // string(1) "0" 何より strstr という名前が検索っぽくない。それより文字列検索には str_contains() を使おう。 preg_match() を使うべきでない理由 単純な文字列検索であれば、 preg_match() も使うべきでない。 php .net: strpos preg_match( string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0 ): int|false pattern で指定した 正規表現 により subject を検索します。 〜中略〜 preg_match() は、pattern が指定した subject にマッチした場合に 1 を返します。 マッチしなかった場合は 0 を返します。 失敗した場合に false を返します ここでの「失敗した場合」とは、指定した 正規表現 のパターンが不正な場合も含まれる。 つまり、 preg_match() では 指定した 正規表現 が間違っていた場合は FALSE を返し、文字列にマッチしなかった場合は 0 を返す のだ。 よって前述の関数と同じように結果の比較で誤った実装を行なってしまう可能性が高くなってしまう。黙って文字列検索には str_contains() を使うべきだ。 「いや、キーワードが先頭にあるかを知りたいんだ」という人は 少なくとも strpos() は危ない。文字列の先頭にキーワードがあるときは 0 、キーワードが存在しない場合は FALSE を返すため比較時にバグが発生しやすくなる。 この場合は、同じく PHP8.0 で追加された str_starts_with () を使おう。戻り値が bool のみであり安全になるだろう。 *6 「日本語でも使えるの?」との疑問について もちろん使える。 str_contains() の RFC にマルチバイト文字列についての記載がある。 *7 安心して文字列検索には str_contains() を使え。 もっと詳しく? この関数が PHP8.0 で実装されることになった詳しい経緯は、当時の RFC や マージリク エス トを確認するといいだろう。 スモールスタートにするため大文字小文字を区別する仕様のみ実装したことや、マルチバイト文字への対応についての意見などが確認できる。 PHP RFC: str_contains Internals Mailing Lists 提案と議論 投票開始 投票完了 マージリクエスト しっかりと議論が行われたことを確認したら、文字列検索には str_contains() を使え。 これでわかっただろう 文字列検索には str_contains() を使うべき理由がわかっただろう。 str_contains() の方が他の関数を使うより直感的であり、なにより無用な心配をしなくて済む。 なぜ今までこの関数がなかったのか不思議だが、実装されたからには積極的に str_contains() を使え。 ん?なんだって? str_contains() が undefined function でエラーになる?? ... PHP のバージョンが 7.4? なるほど。それじゃあ、しかたがない。諦めは心の養生というものだ。 www.php.net Written by: Y-Kanoh *1 : https://www.php.net/archive/2020.php#2020-11-26-3 *2 : https://www.php.net/manual/ja/function.str-contains.php *3 : https://www.php.net/supported-versions.php *4 : https://www.php.net/manual/ja/function.strpos.php *5 : https://www.php.net/manual/ja/function.strstr.php *6 : https://www.php.net/manual/ja/function.str-starts-with.php *7 : https://wiki.php.net/rfc/str_contains
アバター
はじめに みなさんこんにちはインフラエンジニアのa_renrenです。 日々、サーバを運用される方であれば、サーバの不具合や高負荷などで引き起こされるアラートの対応しているかと思います。 その対応手順は手順書としてまとまっていることが少なく、今までの経験則に基づいて対応を行ったり、ベテランメンバーに頼ったりしているところも少なくないかと思います。 自分のチームも各々で手順やノウハウをまとめたりしていますが、共通の手順がほぼない状況でした。そのような現状を打開するため、アラートの対応手順を作成したので、今回はその時の振り返りをしようかと思います。 文字ばかりになってしまい、読みにくいかと思いますがご容赦ください。 ※ 障害が発生した際は別途対応フローや手順がありますので、対象外としています ※ 一部、実際の運用方法と変えて記載したりしていますので、ご了承ください はじめに アラートの対応方法 なぜ取り組んだのか 作成したもの アラート対応手順を作成してみて 今後について アラートの対応方法 まずは、自分のチームでどういう感じでアラートを対応しているか軽くご紹介します。 自分が所属するチームでは2つの商材を担当しており、その2つの商材のアラートの対応をおこなっています。 各サービスごとサーバ監視システム(Zabbix)を使用してサーバの運用管理を行っており、そのシステムからのアラートをトリガーに対応を開始しています。 もし、アラート対応方法がわからない、判断できない場合は、チーム内で確認し、詳しいメンバーに判断仰ぎながら対応を行っています。 なぜ取り組んだのか この取り組みをする前は以下のような課題がありました。 新しいメンバーがアラートの対応を行いにくい 人によってアラートの対応方法に差がある アラート対応のノウハウがあまり共有できていない 一部のアラートの対応が属人化している チーム外の人がアラートの対応を行いにくい 経験が浅いとすぐに対応が必要なものとそうでないものの判断が難しい これらの課題を克服するために、アラートの対応手順を今回作成しました。 目的をまとめると以下のような感じです。 アラートの対応方法を統一化し、対応する人によるばらつきをなくす 各メンバーが持っているアラートの対応手順を共有できる場をつくる 新しいメンバーやチーム外のメンバーでもアラートの1次対応がスムーズに行えるようにする ターゲットを新しいメンバーやチーム外などのあまり商材の知識がない人にし、なるべく判断に迷わない具体的な手順を意識して作成を行いました。 また、対応手順と言っても時と場合によって対応方法が変わるため、最低でもこの手順を見れば1次対応が行えるようにしました。 作成はある程度、自分の経験などをもとにたたき台をつくり、情報が足りなければ詳しい人に確認しつつ作成を行いました。 作成したすべてのアラートの対応手順をチーム内でレビュー会を開いて意見を募り、最終的な形にしていきました。 作成したもの 今回、アラート対応手順は、 Markdown 形式で作成し、それを社内のGitLabにあげて管理するようにしました。 Markdown にしたのは、シンプルに作成でき、ある程度雛形を作成しておけば、今後他のメンバーが作成する際も あまり負担がないかと思ったからです。 GitLabで管理したのは、変更の差分が管理しやすく、 Markdown 形式で表示されて見やすいからです。 今回作成したフォーマットは以下のような感じです。 実際のアラート対応手順はお見せできないので、フォーマットとサンプル情報でお許しください。。。 ()内のは説明で実際には記載していません。 アラートの大まかな分類以外は目次にならないように Markdown 上で別途設定しています。 ## ディスク関連 (アラートの大まかな分類 ディスク関連のアラートはこの分類の中にまとめる) ### ・Free disk space is less than 10% on volume /test (通知されるアラートのタイトル) #### 【対象サーバ】 Webサーバ #### 【アラート内容】 /testのディスク容量が10%以下の状態 (アラートの発報条件やアラートが発報されることでどういう状態なのかを記載) #### 【アラート発生原因】 - 無駄なファイルが配置され続けている - Bの処理で不具合が発生している (考えられる原因を記載) (調査の際にここに記載した考えられる原因から調査が行いやすいようにする) #### 【アラート対応手順】 (アラートの対応手順を記載) 1. ディスク使用量を確認する 監視システムで直近の/testのディスクの使用量がいつからどのくらい増えているのか確認する 1. 削除してもよい不要なファイルを確認する 不要なファイルの判断ができない場合はチーム内に確認する ●削除してもよいファイル ・ファイル名にtestが含まれているもの ・最終更新日が1年前のもの ```bash /testの現状の空き容量を確認 $ df -h /test /test配下にあるファイルを確認し、不要なファイルをリストアップする $ ls -la /test ``` 1. 不要なファイルを削除する ```bash 上記で確認した不要なファイルを削除する $ sudo rm /test/不要なファイル ``` 1. Bの処理が正常に動いているログから確認する エラーが出ているようであれば、チーム内に共有し、原因の調査をおこなう ```bash Bの処理のログを確認し、エラーが出力されてないか確認する $ grep 'error' /var/log/B.log ``` GitLab上では、以下のように表示されます。 GitLabでの表示内容 アラート対応手順を作成してみて 今回アラートの対応手順を作成してから、まだ月日は経っていないため、あまり活躍はできてなさそうですが、 以前よりアラート対応方法が統一になったり、ノウハウの共有が行いやすくなったり、 新メンバーへのレクチャーも行いやすくなったかと思います。 上記以外にも、アラートの対応手順を作成していく過程でメリットがありました。 一つ目は、不要なアラートを洗い出し、削除することが出来たことです。 アラートの対応手順を作成していく中で、いろいろなアラートが見つかりました。。。 何のためのアラートなのかわからないアラート 今はもう不要になったが、削除されず残り続けているアラート 同じようなことを監視しているアラートなど 上記のように必要なアラート以外に様々なアラートが見つかりました。その見直しをこの機会に行えたことで、不要なアラートを削減することができ、不必要な対応する負担が減りました。また、より良い監視方法が見つかったりと、あまり注目されてこなかった箇所にメスを入れることができました。                                                                                                                                                                                                                  二つ目は、アラートがならないよう仕組みづくりの見直しをおこうことができたことです。 アラートの予測される発生原因を探っていく中で現状の設定値がベストでないことに気づき、設定値の改善をおこなったり、 ある条件で失敗する可能性がある スクリプト の処理を失敗しないように スクリプト の実装方法を変えるなどの見直しを行うことができました。 また、作成していくなかで対応しきれなかった課題もありました。 似たようなアラートの対応手順を一つにまとめきれず、アラートごとに一部似たような対応手順をそれぞれに記載したため、メンテナンス性が悪い状態になりました。 似たような手順を外出ししようかとも思ったのですが、対応するメンバーがいろんなファイルを行ったり来たりすると対応しにくいかと思い、あえて外出しはしませんでした。 今後、対応できるサーバを増えるにつれ、アラートの種類も増え、よりメンテナンス性が悪くなりそうなので、早めに作成者と対応者のどちらの負担も軽減できるように構成を変えていく必要がありそうです。 今後について 今回は、一部のサーバのアラート対応手順しか作成できなかったため、今後も上記の課題を解決しつつ、引き続き残りのサーバのアラート対応手順を作成をチームで進めていこうかと思います。 また、並行してアラートの削減やアラート対応の自動化などの根本的な対応も進めていければと思っています。 皆さんもこの機会にぜひアラートの見直しや対応手順の作成に取り組んでみてはいかがでしょうか。 最後までお読みいただきありがとうございました。
アバター
はじめまして。配配メール開発課所属Jazumaです。 本稿では昨年2022年に当社プロダクト配配メールにおいてCI/CDパイプラインを整備した過程やその結果についてご紹介します。 CIツールの使い方やCI/CDとは何かといった内容は取り扱いません。あらかじめご了承ください。 プロダクトについて 2022年開始時点の状況 施策1: 運用ルールの整備とCIツールの移行 施策1の結果 施策2: サブシステム構築に伴うユニットテスト・静的解析の整備 施策2の結果 施策3: サブシステムの検証環境への自動デプロイ 施策3の結果 施策4: メインシステムの検証環境への自動デプロイ 施策4の結果 施策5: アーキテクチャテストの導入・静的解析の拡充 施策5の結果 現時点の課題 1. 昔からあるコードにはテストを追加できていない 2. 自動デプロイのスコープがアプリケーションのみに留まっている 最後に プロダクトについて 弊社プロダクト配配メールは2007年サービス開始の長寿サービスです。 www.hai2mail.jp 長らくお客様にご愛顧いただいていましたが、CI環境が未整備・ ユニットテスト が少ない・静的解析がなく目視でコードレビューする等開発環境・体制に課題がありました。 そこで、開発体制を改善すべく2022年度にCI環境を拡充しました。 2022年開始時点の状況 ユニットテスト はちょっとある E2Eテストは整備されている 静的解析は存在しない Jenkinsでpushの度にテストを実行しているが、あまり運用されていない 施策1: 運用ルールの整備とCIツールの移行 まずは4つ目の問題を解消するためにCIの運用ルールを整備しました。 ルールと言っても複雑なものではなく、「テストが失敗したらメンバーのメールアドレスに通知が飛ぶ。通知を受けたら原因を調査する」というシンプルなものでした。 とはいえルールが整備されたことでテストが失敗した時の対応漏れが減りました。 また、合わせてCIツールをJenkinsからGitLabCI/CDに移行しました。 理由は以下の通りです。 Jenkinsfileよりもgitlab-ci.ymlの方が読み書きしやすいという意見があり、それに対する反論が特になかった JenkinsとGitLabを連携する手間が減る GitLabCI/CDの方がUIが洗練されており使いやすい 施策1の結果 テストが拡充したわけではないため、劇的な効果はありませんでした。 とはいえこの施策によってチームのCI基盤が整ったため、後の改善の土台となる重要な改善だったと言えます。 施策2: サブシステム構築に伴う ユニットテスト ・静的解析の整備 2022年8月に「業種業態・配信目的別スコアの確認」という機能をリリースすることになりました。この機能では【全サーバ全アカウントのスコア(メールの 開封 率や挿入されたURLのクリック率)を集計する】という要件が求められました。 www.hai2mail.jp この要件を満たすために、データ集計用の新規サブシステムを構築することになりました。 新規サブシステムは既存システムのコードや アーキテクチャ の影響を受けないということで、「静的解析と ユニットテスト をしっかりと作り込もう」という方針で開発を進めました。 新規サブシステムはメインシステムと同様に PHP で開発するため ユニットテスト には PHPUnit ・静的解析にはPHPStanを採用しました。 この辺りは極めてオーソドックスな技術選定だったのではないかと思います。 CIの実行ルールも特に変わったところはなく、pushやマージの度にパイプラインを実行するというものです。 施策2の結果 この施策は結果としては成功だったと言えます。具体的には2つの成果につながりました。 1つは「業種業態・配信目的別スコアの確認」機能を計画通りリリースできた上、この機能に関して2023年6月現在不具合が発生していないことです。 この機能は難易度が高く計画の遅れなどが懸念されていましたが、上記の通り大きな問題なくリリースすることができました。その要因の一つとして静的解析と ユニットテスト で ソースコード の品質を作り込むことができたこともあったのではないかと思います。 もう1つは「 ユニットテスト を書く」という文化がチーム内に広がったことです。「業種業態・配信目的別スコアの確認」機能の次のバージョンにて「フォローメール」という大規模な新機能がリリースされました。この機能の開発時にも可能な限り ユニットテスト が作成されました。 www.hai2mail.jp 施策3: サブシステムの検証環境への自動デプロイ パイプラインにサブシステムを検証環境に自動でデプロイする処理を追加しました。 この施策には明確な目的があったわけではなく「せっかくの機会だし自動化できる所はしておこう」くらいの意識で進みました。 仕組みとしてはごくシンプルでタグを作成する ⇒ アプリケーションに必要なファイル一式を含んだtar.gzファイルを作成 ⇒ 検証環境にデプロイする というものでした。 施策3の結果 この施策はあまり効果がありませんでした。サブシステム自体の変更頻度が低くデプロイが実行される機会がほとんどなかったことに加え、仕組みが完成した時期が遅く、活用できる場面が無かったことが原因です。 しかし、今までCI/CDパイプラインの実装経験が無かったメンバーが担当したことでチーム内に知見が広まった他、逆説的に以下のような教訓が得られました。 自動デプロイは変更頻度が高いシステムから優先的に実装すべきである 機能が完成してから自動デプロイの実装に着手しても遅い。機能開発と並行して進める必要がある 施策4: メインシステムの検証環境への自動デプロイ 2022年の秋に システムテスト を改善する取り組みを行いました。 ここでは「テストの品質を上げる( = バグを検知できるようにする)」「テストを効率化する( = 工数 を削減する)」という2つの目的の元改善作業を行いました。 自動デプロイは2つ目の目的を達成するために実施しました。 試算では システムテスト 中のデプロイおよびデプロイ作業漏れに起因する手戻りが年間10時間程度かかっていたため、自動化の効果が大きいと判断されました。 今回は施策3の教訓を踏まえて「変更頻度の大きいシステムを対象にする」「 システムテスト までに完了させる」という方針で進めました。 具体的には対象をメインシステムに絞り、デプロイ処理を追加しました。 今回は全自動化したいということで、タグの作成ではなく「リリース用のブランチへのマージ」をトリガーとしてデプロイ スクリプト を起動する実装としました。 施策4の結果 この施策は成功しました。 「定期的にリリースブランチへのマージを確認して検証環境へのデプロイを実行する」という雑務が無くなったことで稼働に空きができた他、割り込み作業がなくなりました。 また、検証環境が常に最新の状態に保たれていることが担保されるようになったため 不具合が見つかった場合の原因調査がスムーズになりました。 施策5: アーキテクチャ テストの導入・静的解析の拡充 2023年5月に添付ファイルを直接メールに添付せずに送信する機能をリリースしました。 www.hai2mail.jp この機能においてもサブシステムを新しく構築しました。 今回は施策2のような ユニットテスト ・静的解析に加えて新しく2つのことを試みました。 1つは アーキテクチャ テストです。 deptrac というツールを用いてクラス間の依存関係を検査するようにしました。 これにより、関数やクラス単位の品質だけではなく「クラス同士の依存関係が適切に設定されているか」という点まで担保できるようになりました。 もう1つは静的解析の拡充です。施策2で導入したphpStanでは主に型定義や未定義変数の検出等、一般的な観点を検査しました。 今回はコード 規約違反 の検出など、よりチームの実情に即した観点を検査することになりました。 静的解析ツールとしては PHP_CodeSniffer を採用しました。 (静的解析はこまめに実行できる方が手戻りのコストが小さく済むので、 PHP _CodeSnifferはCIに加えて IDE 上でも実行できるようにしました。本稿の趣旨からは逸れますので詳しくは取り上げません。) 施策5の結果 この施策も施策2と同様に成功しました。特に PHP _Codesniffer導入の恩恵が大きかったです。 今までは目視で確認していた項目を 機械的 に検出できるようになったため、コードレビューの負担が大きく減りました。 PHP _Codesniffer導入についてはこちらの記事でも触れていますのでぜひご一読ください。 tech-blog.rakus.co.jp 現時点の課題 2022年には配配メール開発チームにおいてCI/CDパイプラインが大きく拡充されました。 しかし、課題も多く残っています。 1. 昔からあるコードにはテストを追加できていない テストコードは主として新規に追加されるコードを対象に実装されました。 しかし、配配メールのコア機能を支え続けている古いコードにはほとんどテストコードを追加することができていません。 昔からあるコードは複雑度が高かったり密結合だったりしており、テストコードを書くことができない状態です。 テストを書くために リファクタリング しようとしても影響範囲が大きくなかなか手が出せないという(よくある) ジレンマに直面し続けています。 2. 自動デプロイのスコープがアプリケーションのみに留まっている 現状では自動デプロイの対象はアプリケーションコードのみです。環境設定やデータベース・ ミドルウェア の変更は手動で行う必要があります。 本来であれば環境・データベース・ ミドルウェア 含めて常に正しい状態を再現できるようになっているべきですが、稼働に余裕がない・ノウハウが足りないため手が回っていません。 最後に ここまでお読みいただきありがとうございました。 今後も配配メール開発チームではCI/CDパイプラインを活用して より高品質なプロダクトを開発できるように努めていきます。
アバター
弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2022年10月のイベントでは「 PHP のリーダブルなコード」について語り合いました。 弊社のメンバーが事前にまとめてきたコードの書き方の事例にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 rakus.connpass.com 特集:PHPのリーダブルなコード 初級 Sample 1 BADコード 良くない理由 解消例 Sample 2 BADコード 良くない理由 解消例 パターン1:早期リターンを活用 パターン2:条件を関数に閉じ込める Sample 3 BADコード 良くない理由 解消例 中級 Sample 4 BADコード 良くない理由 解消例 Sample 5 BADコード 修正前 修正後 良くない理由 解消例 パターン1:関数ごとに処理を分割 パターン2:デリミタ(区切り文字)を引数にする Sample 6 BADコード 良くない理由 解消例 上級 Sample 7 BADコード 良くない理由 解消例 Sample 8 BADコード 良くない理由 解消例 編集後記 特集: PHP のリーダブルなコード この回では弊社が用意したBADコードをトピックに、「どこが悪いのか」・「どうすれば良くなるか」を議論しました。 BADコードは難易度別に初級・中級・上級に分かれており、全部で8問ございます。 元ネタ: GitHub - piotrplenik/clean-code-php: Clean Code concepts adapted for PHP 初級 Sample 1 BADコード <?php if ( $ foo === $ bar ) { return true ; } else { return false ; } 良くない理由 bool 値を返したいときにif文を書くのは冗長 解消例 結果をbool値で返したい場合、明示的に if-else 文を書かなくとも、return 文に条件式を書くことで比較結果を戻り値にできます。 <?php return $ foo === $ bar ; 参加者からは次のようなご意見を頂きました。 あまり馬鹿にできなくて、何回か現場でも実際に見たことがあります 初学者は「"比較" = "IF文"」と錯覚しがちですね Sample 2 BADコード <?php if ( $ isOK && ! hasPermission && $ hoge !== “sample” ) //何らかの処理 } 良くない理由 if文の条件が複雑になると、単純に読みにくくなるだけでなく、不具合も起こりやすくなる isOK という変数名が微妙 解消例 パターン1:早期リターンを活用 <?php if ( !$ isOK ){ return "NG" ; } if ( hasPermission ){ return "OK" ; } if ( $ hoge !== "sample" ){ //なんらかの処理 } パターン2:条件を関数に閉じ込める <?php if ( 条件がわかりやすく言語化された関数名 ()){ // なんらかの処理 } Sample 3 BADコード <?php if ( $ input == 0 ) { echo ( ‘ 0 です!’ ) ; } 良くない理由 == の場合、判定が曖昧なのでバグのもとになりやすい PHP8.0以前の場合、文字列と数値の比較をする際に文字列が数値にキャストされるので危険 PHP の比較 演算子 については弊社のブログでも深掘りしておりますので、ご興味のある方はぜひ御覧ください tech-blog.rakus.co.jp 解消例 === に比較 演算子 を変更します。 これにより、左辺・右辺の値が等しく、かつデータ型も一致する場合のみ true を返すため、データ型の不一致による予期しない結果を避けることができます。 <?php if ( $ input === 0 ) {   echo ( '0です!' ) ; } 中級 Sample 4 BADコード <?php /** * お店が営業日かをチェックする * * @param $day 曜日の文字列 * @return bool */ function isShopOpen ( $ day ) : bool { if ( $ day ) { if ( is_string ( $ day )) { $ day = strtolower ( $ day ) ; if ( $ day === 'friday' ) { return true ; } elseif ( $ day === 'saturday' ) { return true ; } elseif ( $ day === 'sunday' ) { return true ; } return false ; } return false ; } return false ; } 良くない理由 このコードについて良くない理由として、以下の2点が考えられます。 どういうチェックが必要なのかがわかりにくい 分岐をすべて読まないとチェックしたい内容がわからない $day が以下の場合 true friday saturday sunday 分岐が進むにつれて、該当処理が実行される条件の数が多くなる これまでの分岐条件を記憶していかないといけない 引数に型指定されていない メソッド内で型チェックが必要 ロジックを読むまで$day が String 型であることがわからない 引数名からDateTimeクラスの インスタンス とも捉えられる だが実際はString型で受け取ることを前提とした処理になっている 参加者からは次のようなコメントが寄せられました。 「if-elseで条件分岐を作る際は、単 純化 できないか考えるチャンス」 その他、参加者からは「ネストがV字に広がっている様子がまるで 波動拳 のようだ」というコメント から有名な ミーム 画像が紹介され、「本格的な 波動拳 ですね」、「今度コードレビューで使いたい!」などコメントが寄せられ、かなり盛り上がりを見せていました。 解消例 解消例は以下になります。 <?php /** * お店が営業日かをチェックする * * @param string $day 曜日の文字列 * @return bool */ function isShopOpen ( string $ day ) : bool { // 値がセットされているか(これがガード節) if ( empty ( $ day )) { return false ; } $ openingDays = [ 'friday' , 'saturday' , 'sunday' ] ; return in_array ( strtolower ( $ day ) , $ openingDays , true ) ; } 主な修正点は以下のとおりです。 $day の引数型をString型で宣言することで、データ型のミ スリード をなくす 前提部分のチェックをガード節で対応することで、余計なネストを生まないようにする 営業日を$openingDays に入れることでいつが営業日かわかりやすくなり、変数名が説明変数を担っている こちらに関して、参加者からは次のようなコメントを頂きました。 match文 でもかけそう empty() はないほうが良い ※empty() の場合、変数が存在し、かつ値が空でない場合のみ false が返されます。ここで実施したいのは空文字チェックですが、empty() では仕様上、空文字でも true が返却されるため、空文字チェックとしてempty()を使用するのは適切ではありません。 PHP Sandbox - Execute PHP code online through your browser 列挙型(enum) も活用できそう Sample 5 BADコード このケースは少し特殊で、 修正前のコード を間違えて 修正後のコード に直してしまった、というケースを想定して作られています。 修正前 <?php public function getRecipeListString () : string { $ recipeList = getRecipeList () ; // なにかレシピのリストを配列で取得するもの return implode ( "," , recipeList ) ; } 修正後 <?php public function getRecipeListString ( bool $ isSpace ) : string { $ recipeList = getRecipeList () ; //何かレシピのリストを配列で取得するもの if ( $ isSpace ) { return implode ( “ ”, $ recipeList ) ; } else { return implode ( “,”, $ recipeList ) ; } } 良くない理由 違う要件が来たときにまたif文が増える 仮に「レシピのリストを ハイフン区切り で表示する」といった仕様になった場合、新たに分岐を追加する必要がある 解消例 解消例として以下の2パターンが挙がりました。 パターン1:関数ごとに処理を分割 「カンマ区切り」・「スペース区切り」と、要件ごとに同じ処理を関数に区切ったパターンです。 <?php public function getRecipeListStringWithComma () : string { $ recipeList = getRecipeList () ; // なにかレシピのリストを配列で取得するもの return implode ( "," , $ recipeList ) ; } public function getRecipeListStringWithSpace () : string { $ recipeList = getRecipeList () ; // なにかレシピのリストを配列で取得するもの return implode ( " " , $ recipeList ) ; } パターン2:デリミタ(区切り文字)を引数にする <?php public function getRecipeListString ( string $ delimiter ) : string { $ recipeList = getRecipeList () ; // なにかレシピのリストを配列で取得するもの return implode ( $ delimiter , $ recipeList ) ; } またパターン2について、参加者からは 「$delimiter に初期値を入れてはどうか?」 という意見がありましたが、こちらについて以下のような反応がありました。 確実に必要ではない引数にはじめからデフォルト値をセットしないほうが良いと思う 既存のものを移行するなどのケースであれば、オプショナルにするのもあり データが収束しているように見えるからメソッドが気になる Sample 6 BADコード <?php //メルマガ購買顧客リストまたはYoutubeチャンネル登録会員リストを更新する function updateMailMagazineListOrYoutubeList ( $ accountId , $ MailMagazine , $ Youtube , $ isMailMagazine , $ isYoutube ) { if ( $ isMailMagazine ) { # code… } elseif ( $ isYoutube ) { #code… } if ( $ isMailMagazine ) { $ sql = “update mailMagazineList set …”; } elseif ( $ isYoutube ) { $ sql = “update youtubeList set…”; } } 良くない理由 別のビジネス概念を無理やり1つの関数の処理にまとめている フラグを引数で渡しているのでビジネス概念が増えるほど引数が増える 今後さらにビジネス概念が増えた場合、より分岐が複雑化する 実装者は共 通化 したいという意図を持っていたと思われるが、結局共 通化 できていない 参加者の方々からも次のようなコメントが寄せられました。 割りとよく見る 既存のものに焼き増しで追加した結果こうなってしまった? なかなかリアルなケース 等、現場でも見覚えのある方が多くいらっしゃったようです。 解消例 異なるビジネス概念を扱うなら、関数・クラスは分けるべき <?php function updateMailMagazineList ( $ accountId , $ Mail Magazine ) { #code… $ sql = “update mailMagazineList set,,,” #code… } function updateYoutubeList ( $ accountId , $ Youtube ) { #code… $ sql = “update mailMagazineList set…” #code… } 上級 Sample 7 BADコード <?php /** * PHPによる形態素解析処理 * * * @param string $code 文章 */ function parseBetterPHPAlternative ( string $ code ) : void { $ regexes = [ //… ] ; $ statements = explode ( ‘ ‘ , $ code ) ; $ token = [ ] ; foreach ( $ regexes as $ regex ) { foreach ( $ statements as $ statement ) { //… } } $ ast = [ ] ; foreach ( $ tokens as $ token ) { //lex… } foreach ( $ ast as $ node ) { //parse… } } 良くない理由 1つの関数で複数の処理を行っている 修正時の影響が大きくなる 修正箇所の後続処理への影響を考えて修正しなければならない 不具合が発生した際の問題箇所の特定が困難 複数の処理が組み込まれていると処理の前後関係を理解する必要がある 処理が分割していれば問題箇所を特定しやすくなる 内部処理の再利用ができない 処理の中に組み込まれてしまうと特定処理だけを他の処理でも利用したくても利用できない テストが書きにくい 処理が分割されていれば細かな条件のテストコードが書ける 参加者からは次のようなコメントを頂きました。 一度に一つ以上のことをやらないでほしい... サブルーチンに切り出してほしい 等の意見が挙がっており、やはり1つの関数で複数の処理を実行することに否定的な意見が多く寄せられました。 解消例 parseBetterPHPAlternative を3つのクラスに分割することで、可読性を高めつつ、各処理が依存していない状態に修正しました。これならば ユニットテスト も書けそうですね。 <?php /** * トークナイザ */ class Tokenizer { /** * 文章を単語に分解する * * @param string $code 文章 * @return array $tokens 単語のリスト */ public function tokenize ( string $ code ) : array { $ regexes = [ // ... ] ; $ statements = explode ( ' ' , $ code ) ; $ tokens = [] ; foreach ( $ regexes as $ regex ) { foreach ( $ statements as $ statement ) { $ tokens [] = /* ... */ ; } } return $ tokens ; } } /** * 字句解析器 */ class Lexer { /** * 単語の解析処理 * * @param string $tokens 単語のリスト * @return array $ast 解析結果のリスト */ public function lexify ( array $ tokens ) : array { $ ast = [] ; foreach ( $ tokens as $ token ) { $ ast [] = /* ... */ ; } return $ ast ; } } /** * PHPによる形態素解析処理 * * @param string $code 文章 */ class BetterPHPAlternative { /** @var Tokenizer */ private $ tokenizer ; /** @var Lexer */ private $ lexer ; public function __construct ( Tokenizer $ tokenizer , Lexer $ lexer ) { $ this -> tokenizer = $ tokenizer ; $ this -> lexer = $ lexer ; } /** * 形態素解析 * * @param string $code 文章 */ public function parse ( string $ code ) : void { $ tokens = $ this -> tokenizer -> tokenize ( $ code ) ; $ ast = $ this -> lexer -> lexify ( $ tokens ) ; foreach ( $ ast as $ node ) { // parse... } } } Sample 8 BADコード FavoriteRecipe クラス <?php class FavoriteRecipe { public function getRecipe ( string $ name ) : void { $ limit = 10 ; $ recipeRepository = new RecipeRepository () ; $ recipes = $ recipeRepository . findByName ( $ name , $ limit ) ; foreach ( $ recipes as $ recipe ) { var_export ( $ recipe ) ; } } } RecipeRepository クラス <?php class RecipeRepository { // Cookpadからレシピを取得する function findByName ( string $ name , int $ limit ) : Recipe { $ cookPad = new CookPad ( new \PHPHtmlParser\Dom ) ; $ result = $ cookpad -> search ( $ name , 1 , $ limit , false ) ; return $ result ; } } 良くない理由 FavoriteRecipeクラスがRecipeRepositoryクラスの実装に依存している RecipeRepository::findByName に何かしら影響が発生した場合、FavoriteRecipeクラスにも影響が及ぶ 例. findByNameメソッドの引数を増やした場合 <?php class RecipeRepository { // 引数を修正 - function findByName(string $name, int $limit):Recipe { + function findByName(string $name,int $pageNum, int $limit, bool $isRandom):Recipe[] { // ... } } class FavoriteRecipe { public function getRecipe(string $name):void { // ... // reposiotoryの修正を受けて引数を修正 - $recipes = $recipeRepository.findByName($name, $limit); + $recipes = $recipeRepository.findByName($name, $pageNum, $limit, $isRandom); } } 解消例 SOLID の原則の D:依存性逆転の原則(DIP) を活用して、 上位モジュールのFavoriteRecipeクラス が 下位モジュールのRecipeRepositoryクラス に依存しなくなるようにクラス構成を修正します。 RecipeRepositoryインターフェースを作成 <?php interface RecipeRepository { public function findByName ( string $ name , $ pageNum = 10 , $ limit = 10 , $ isRandom = false ) : Recipe [] ; } RecipeRepositoryクラスはRecipeRepositoryインターフェースを実装する <?php class RecipeRepositoryImpl implements RecipeRepository { public function findByName ( string $ name , $ pageNum = 10 , $ limit = 10 , $ isRandom = false ) : Recipe [] { $ cookpad = new Cookpad ( new \PHPHtmlParser\Dom ) ; $ result = $ cookpad -> search ( $ name , $ pageNum , $ limit , $ isRandom ) ; return $ result ; } } FavoriteRecipeクラスはRecipeRepositoryインターフェースを参照する <?php class FavoriteRecipe { private RecipeRepository $ recipeRepository ; public function __construct ( RecipeRepository $ recipeRepository ) { $ this -> recipeRepository = $ recipeRepository ; [ f : id : d - t - kong : 20230530144526j : plain ][ f : id : d - t - kong : 20230530144526j : plain ] } public function getRecipe ( string $ name ) : void { $ pageNum = 10 ; $ limit = 10 ; $ isRandom = false ; $ recipes = $ this -> recipeRepository . findByName ( $ name , $ pageNum , $ limit , $ isRandom ) ; foreach ( $ recipes as $ recipe ) { var_export ( $ recipe ) ; } } } こうすることで、FavoriteRecipeクラスがFavoriteRecipeクラスに依存しないクラス構成になりました。 こちらの解消例について、参加者の方から次のようなご指摘をいただきました。 RecipeRepositoryImpl::findByNameの ユニットテスト を実行するたびに Cookpad にHTTPリク エス トが飛ぶことになる 複数人で ユニットテスト を同時に実行したら、 Dos攻撃 になるのでは!? ユニットテスト 時には、外部 API へのHTTPリク エス トを避ける必要があります。参加者の間では以下の改善案が提案されました。 テストモックを準備する Cookpad の インスタンス 生成部分をもう一段階DI Cookpad が PSR-18 に準拠している場合、HTTP通信部分のみを外部から切り出す 編集後記 以上、 PHP のリーダブルなコードについてまとめました。 今回提示した アンチパターン を業務でやってしまった!という方も中にはいらっしゃったのではないでしょうか? 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております!
アバター
弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2023年2月のイベントでは「 Laravel10の新機能 」について語り合いました。 弊社のメンバーが事前にまとめてきたLaravel10の新機能の情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 rakus.connpass.com 特集:Laravel10の新機能 Laravelのリリースサイクルについて PHP8.0系の非対応 Laravel Pennant フィーチャーフラグとは?? 導入手順 機能利用のON/OFFの定義 サービスプロバイダで定義する方法 クラスでの定義 機能フラグの利用方法 クラスに定義している場合 Blade内での利用方法 Native type declarations in Laravel 10 skeleton Invokable Validation rules are the default Processes 関連機能 プロセスの実行方法 便利なメソッド群 並行プロセスの管理 テストのプロファイル オプション Pest Scaffolding パスワード生成ヘルパー Str::password() 設定ファイルパスのカスタマイズ doctrine/dbal is not needed anymore to modify columns in migrations Laravel 10 requires at least Composer 2.2 非推奨となる変更点 参考資料 まとめ 特集:Laravel10の新機能 PHP TechCafeでは過去に何度かLaravelを取り上げています。 下記が過去のLaravel回のまとめになります。資料には過去のバージョンのものも記載していますので、参考にしていただければと思います。 PHPerによるPHPerのための「Laravel8を中心に語り合う」TechCafe PHPerのための「Laravel/PHP8/Dockerで開発環境作りを語り合う」TechCafe PHPerのための「2020年のPHP/Laravel振り返り+2021年」を語るTechCafe PHPerのための「Laravel 入門を語り合う」PHP TechCafe PHPerのための「2021年のPHP/Laravel振り返り+2022年」を語るTechCafe PHPerのための「Laravel 9 について語る」PHPTechCafe Laravelのリリースサイクルについて LaravelはLaravel8以降1年に1回のメジャーバージョンアップになっています。 PHP8.0系の非対応 Laravel10はPHP8.1以上が必要です。 PHP8.0系は2023年の11月末くらいでセキュリティサポートが切れるので妥当かなという意見がありました。 Laravel Pennant 下記のような特徴を持つLaravel Pennantというパッケージが追加されました。 新しいファーストパーティパッケージ フィーチャーフラグを追加できる Composer 経由でインストールが可能 フィーチャーフラグとは?? フィーチャーフラグとはコードを書き換えることなく、システムの振る舞いを変更できるようにする開発手法です。 フラグによって機能のON/OFFが可能となります。 新しい機能を段階的にロールアウトしたり、 A/B テストを行ったりできます。 laravel.com 「最近、結構(フィーチャーフラグの話を)聞くようになった」 「 PDCAサイクル を早める手法として紹介されたり、流行っている印象です。」 などの意見が上がりました。 導入手順 下記の手順でComposer で簡単で導入することができます。 $ composer require laravel/pennant $ php artisan vendor:publish --provider= " Laravel \P ennant \P ennantServiceProvider " $ php artisan migrate 機能利用のON/OFFの定義 下記が機能利用のON/OFFの定義方法になります。 サービスプロバイダで定義する方法 <?php namespace App\Providers; use App\Models\User; use Illuminate\Support\Lottery; use Illuminate\Support\ServiceProvider; use Laravel\Pennant\Feature; // ★ class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. */ public function boot () : void { // 新しいAPIについて Feature :: define ( 'new-api' , fn ( User $ user ) => match ( true ) { // 内部メンバーは利用可能 $ user -> isInternalTeamMember () => true , // トラフィックの多いユーザは利用不可 $ user -> isHighTrafficCustomer () => false , // それ以外の場合、1/100の確率で利用可能 default => Lottery :: odds ( 1 / 100 ) , }) ; } } クラスでの定義 クラスでの定義には下記2つの特徴があります。 機能フラグをクラスで定義することも可能 artisanコマンドから雛形を作成できる $ php artisan pennant:feature NewApi <?php namespace App\Features; use Illuminate\Support\Lottery; class NewApi { /** * Resolve the feature's initial value. */ public function resolve ( User $ user ) : mixed { // 新しいAPIについて return match ( true ) { // 内部メンバーは利用可能 $ user -> isInternalTeamMember () => true , // トラフィックの多いユーザは利用不可 $ user -> isHighTrafficCustomer () => false , // それ以外の場合、1/100の確率で利用可能 default => Lottery :: odds ( 1 / 100 ) , } ; } } 機能フラグの利用方法 Feature::active('KEY名') で ON/OFF を取得できます。 <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; use Laravel\Pennant\Feature; class PodcastController { /** * Display a listing of the resource. */ public function index ( Request $ request ) : Response { return Feature :: active ( 'new-api' ) // 定義した条件通りにtrue/falseが返される ? $ this -> resolveNewApiResponse ( $ request ) : $ this -> resolveLegacyApiResponse ( $ request ) ; } // ... } クラスに定義している場合 フィーチャーフラグを管理しているクラス名を指定する必要があります。 <?php namespace App\Http\Controllers; use App\Features\NewApi; use Illuminate\Http\Request; use Illuminate\Http\Response; use Laravel\Pennant\Feature; class PodcastController { /** * Display a listing of the resource. */ public function index ( Request $ request ) : Response { return Feature :: active ( NewApi :: class )   // ★ ? $ this -> resolveNewApiResponse ( $ request ) : $ this -> resolveLegacyApiResponse ( $ request ) ; } // ... } Blade内での利用方法 @feature ディレクティブを使ってBladeで使用することも可能です。 @feature('new-api') <!-- 'site-redesign' is active --> @else <!-- 'site-redesign' is inactive --> @endfeature 下記のような話が上がり、今回のバージョンアップの注目の機能として紹介されました。 フィーチャーフラグは実はよく使われる機能!? フィーチャーフラグ、フィーチャートグルという言い方だとあまり使ったことないという感じます よくある課金ユーザだけ機能をONにするような機能で使えると思います フィーチャーフラグを定義しておけば可読性が上がるのではないかと考えられます オレオレ実装した時にハマりそうな罠を一通り回避してくれる フィーチャートグルは名前で損していると感じます 仕様を見ると下記のようなことが可能です フィーチャートグルの管理下の機能を一括で有効にしたりできます 処理の中で、あるユーザに対して「ルートAを通ったら有効、ルートBを通ったら無効」になる場合にキャッシュを取って整合性を取ってくれます オレオレ実装でハマりそうなこと 同じ条件のはずのif文同士なのに条件が微妙に違うといったことがあります。 今後、ブログ等で紹介されることを期待しています 複数チームでの開発でも使えます 並行開発するときにも使えます OFFになっていたらマージが簡単になります 複数人で開発していると、ブランチを切ってからブランチがどんどんかけ離れていってしまうことがあります 実装した機能がif文で囲われていて、そこがfalseになっているからマージしてもOKという話だと思います コンフリクトに時間をかけることも少なくできることが期待できます デメリット どこかでifを削除する必要があります カンファレンス等でそのifを自動削除するツールを作った発表を見ましたが、自動テストが充実していないと事故になりかねないと思われます ソーシャルゲーム の場合はよく使われている機能ではないか ソーシャルゲーム で新しい機能をリリースして何か不具合があった場合、フィーチャーフラグを利用しているとすぐに機能を閉じることができます ソーシャルゲーム はアプリという形で配信してしまっているので切り戻しが効かない世界です こういったノウハウを色々と持たれているかなと感じました Native type declarations in Laravel 10 skeleton 以前のバージョンでは新しくひな型を作ると、引数と戻り値に型宣言が追加されていました PHPDocで補足していた型ヒントが消えて、型宣言に置き換えられました 下位互換性があるので、型宣言が追加されたからといって既存プロジェクトが動かなくなることはないと記述されています。 「型のない言語はもう許されない(流行らない)のか……?」という意見もありました。 Native type declarations in Laravel 10 skeleton Invokable Validation rules are the default GitHub の pull リクエスト Laravel9で導入された Invokable 検証ルール をデフォルトにする変更です。 この変更によって得られるメリットは2点です。 コードが簡潔になる コストがシンプルになった分、学習コストが減る Laravelは php artisan make:rule [ルールのファイル名] で独自のバリデーションルールを設定できます。 makeコマンドに引数が無い場合はpassesメソッドとmessageメソッドが実装されたコードが生成されます。 下記コードは Github のものです。 <?php class Quantity implements Rule { protected $ messages = [] ; public function passes ( $ attribute , $ value ) { if ( ! is_array ( $ value )) { $ this -> messages [] = trans ( 'validation.quantity.must_be_an_object' ) ; return false ; } if ( ! array_key_exists ( 'magnitude' , $ value )) { $ this -> messages [] = trans ( 'validation.quantity.missing_magnitude' ) ; } if ( ! array_key_exists ( 'units' , $ value )) { $ this -> messages [] = trans ( 'validation.quantity.missing_units' ) ; } return $ this -> messages === [] ; } public function message () { return $ this -> messages; } } passesでバリデーションを実施してfalseであればmessageをreturnします。 上記のコードでいうと、配列のkeyに magnitude が存在しない時、 validation.quantity.missing_magnitude メッセージが返されます。 makeコマンドの引数にinvokableを指定すると invoke メソッドのみが実装されたシンプルなコードが生成されます。(invokable rule) php artisan make:rule [ルールのファイル名] --invokable 下記コードは Github のものです。 <?php class InvokableQuantity implements InvokableRule { public function __invoke ( $ attribute , $ value , $ fail ) { if ( ! is_array ( $ value )) { return $ fail ( 'validation.quantity.must_be_an_object' ) -> translate () ; } if ( ! array_key_exists ( 'magnitude' , $ value )) { $ fail ( 'validation.quantity.missing_magnitude' ) -> translate () ; } if ( ! array_key_exists ( 'units' , $ value )) { $ fail ( 'validation.quantity.missing_units' ) -> translate () ; } } } invoke メソッドの引数 $fail は失敗時に実行されるコールバック関数です。 Laravel10から make: ruleコマンドに引数invokableを渡さなくてもinvokable ruleが適用されるようになりました。 なお呼び出し方はどちらも同じなので 後方互換 性が無くなることはありません。 移行する場合もシンプルで、手順は以下になります。 Quantityクラスpasses内の下記のコードをInvokableQuantityクラスの__ invoke 関数内にコピペします。 <?php public function passes ( $ attribute , $ value ) { 中略 ... if ( ! array_key_exists ( 'units' , $ value )) { $ this -> messages [] = trans ( 'validation.quantity.missing_units' ) ; } } ↓ <?php public function __invoke ( $ attribute , $ value , $ fail ) { if ( ! array_key_exists ( 'units' , $ value )) { $ this -> messages [] = trans ( 'validation.quantity.missing_units' ) ; } } messagesに値を入れている部分を$failを使用するように修正します。 <?php public function __invoke ( $ attribute , $ value , $ fail ) { if ( ! array_key_exists ( 'units' , $ value )) { $ fail ( 'validation.quantity.missing_units' ) -> translate () ; } } Invokable Validation rules are the default 下記のような話があがりました。 invoke メソッドとは __ invoke ()を入れておいたら インスタンス 化されたものを関数みたいに呼び方すると呼ばれるものです 呼び出しは簡単になるが、処理が追いづらくなるという意見もありました。 見た目がスッキリしました 元々はオプションだった機能をデフォルトにするので、気をつけたほうがいい 旧方式のバリデートルールを使っている人が新方式に置き換える場合の修正内容 passesの中身を Invoke の中身に移植 messageのところを$failに渡す 実装時の手間が省くことができそうです。 実案件のコードだとrulesの配列に100行、messageの配列に100行書かないといけなかったです それが一気にスッキリかけるようになりました Laravel10の雛形ファイルが Invoke からvalidateに変更されています Processes 関連機能 別のプロセス呼び出しが ファサード 経由で実行可能になりました。 並行プロセスの実行と管理が容易になります。 プロセスの実行方法 Process ファサード の run メソッドで実行可能です。 プロセスは同期、非同期を選択できます。 同期実行 (処理が終わるまで待つ) <?php use Illuminate\Support\Facades\Process; $ result = Process :: run ( 'ls -la' ) ; return $ result -> output () ; 非同期実行 <?php use Illuminate\Support\Facades\Process; // 非同期実行(タイムアウトも設定) $ process = Process :: timeout ( 120 ) -> start ( 'bash import.sh' ) ; // 実行中かどうかを判定することもできる while ( $ process -> running ()) { // ... } $ result = $ process -> wait () ; // 処理が終わるまで待つことも可能 便利なメソッド群 run で実行したプロセスの結果を検査するためのメソッドです。 <?php $ result = Process :: run ( 'ls -la' ) ; $ result -> successful () ; $ result -> failed () ; $ result -> exitCode () ; $ result -> output () ; $ result -> errorOutput () ; 並行プロセスの管理 複数のプロセスをプールさせることも簡単になります。 <?php use Illuminate\Process\Pool; use Illuminate\Support\Facades\Process; $ pool = Process :: pool ( function ( Pool $ pool ) { $ pool -> path ( __DIR__ ) -> command ( 'bash import-1.sh' ) ; $ pool -> path ( __DIR__ ) -> command ( 'bash import-2.sh' ) ; $ pool -> path ( __DIR__ ) -> command ( 'bash import-3.sh' ) ; }) -> start ( function ( string $ type , string $ output , int $ key ) { // ... }) ; while ( $ pool -> running () -> isNotEmpty ()) { // ... } $ results = $ pool -> wait () ; Processes 関連機能 テストのプロファイル オプション artisan の test  コマンドに --profile オプションが追加されました。 実行が遅いテストを一覧表示できる。(最大10個) テストのプロファイル オプション Pest Scaffolding   Pest テスト フレームワーク です。 Laravelのアプリケーションを新規作成する時に Pest を利用できるようになりました。 $ laravel new example-application --pest Pest Scaffolding パスワード生成ヘルパー Str::password() <?php Str :: password () 特定の長さのランダムパスワードを生成できます。 パスワードは、文字、数字、記号、スペースの組み合わせで構成されます。 デフォルトでは、パスワードの長さは 32 文字です。 パスワード生成ヘルパー Str::password() 設定ファイルパスのカスタマイズ GitHub のプルリク <?php $ app -> configPath ( __DIR__ . '/../some/path' ) ; 設定ファイルへのパスを設定することができるようになりました。 設定ファイルパスのカスタマイズ 下記のような意見があげられました。 Laravelプロジェクト自体の話も飛び出しました。 Gitのプルリクで「なんでこれが必要なんだ」という議論がなされていました 下記の理由が記述されていました 「カスタマイズされたLaravel構造のプロジェクトに取り組んでいて、現在の構成パスはベースパスに残っているが、別 ディレクト リに移動したいと考えています」 「息の長いプロジェクトだとこういうことも考えないといけないんですね」と感想がありました Laravelのプルリクでは「なんで?」が多いです (Laravelのプロジェクトに対する)プルリクが4万件 「ユーザのメリットを教えてくれるかい?」など少し怖さを感じることもあります すごい数のプルリクが来ているから本当に必要なことを判別するため? 作者本人から即レスされたり、書いたプルリクはちゃんと読まれている印象 Symfony に支えられているのがLaravel doctrine/dbal is not needed anymore to modify columns in migrations Laravel9では、 マイグレーション にてテーブル列名を変更する場合は doctrine/dbal をインストールする必要がありました。 Laravel 10 からは doctrine/dbal がインストールは不要になります。 <?php return new class extends Migration { public function up () { Schema :: table ( 'foo' , function ( Blueprint $ table ) { $ table -> unsignedBigInteger ( 'bar' ) -> change () ; }) ; } … } 既に doctrine/dbal がインストールされている場合は、サービスプロバイダに以下の記述が必要です。 <?php use Illuminate\Support\Facades\Schema; … class AppServiceProvider extends ServiceProvider { public function boot () { Schema :: useNativeSchemaOperationsIfPossible () ; } } Laravel 10 requires at least Composer 2.2 Laravel10.xからは Composer 2.2 以上が必要となります。 非推奨となる変更点 以下が非推奨となる変更点です。 Remove various deprecations Pull Request #41136 getBaseQuery の削除 Illuminate\Database\Eloquent\Relations\Relation クラスの getBaseQuery メソッドの名前が toBase に変更されました。 MaintenanceModeException の削除 MaintenanceModeException はアプリケーションがメンテナンスモードの時に ステータスコード 503で投げられる例外です。 MocksApplicationServices https://github.com/laravel/framework/issues/41027 日本語記事 Remove deprecated dates property in Pull Request #42587 Eloquent モデルの非推奨の $dates プロパティが削除されました。 $casts プロパティを使用する必要があります。 Remove handleDeprecation method in Pull Request #42590 非推奨のログを出力するメソッドです。 handleDeprecation メソッドが削除されました。 代わりに handleDeprecationError を使います。 https://laravel.com/api/9.x/Illuminate/Foundation/Bootstrap/HandleExceptions.html#method_handleDeprecation assertTimesSent メソッドが削除された。 #42592 通知が送信された回数の合計をassertするテスト用のメソッドです。 assertSentTimes メソッドを代わりに使用します。 ScheduleListCommand.php の $defaultName プロパティが削除された。 コミットコメント スケジュールされているタスクのリストを表示するコマンドです。 $defaultName プロパティは遅延ロード中にコマンドを識別するために使用されていた模様です。( 修正コミット ) 使用する側には影響なしと思われます。 Route::home メソッドが削除された。 #42614 home として登録されているルートに遷移するメソッドのようです。 dispatchNow() が削除された。 #42591 ジョブをすぐに実行するメソッドです。 ジョブをすぐに実行したい場合は代わりに dispatchSync() を使用する必要があります。 参考資料 Laravel 公式 Laravel News まとめ 今回はLaravel10の新機能について、イベント参加者の生の声を交えてまとめてみましたがいかがでしたでしょうか? イベントでは追加される新機能の内容だけでなく、実用的な知見やノウハウなども紹介されていて、有意義なTech Cafeであったと思います。 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。
アバター
はじめに こんにちは!技術広報課の rks_daigo と申します。 今回は、久しぶりに弊社が主催したLT会イベント「"ChatGPT" をもっと使いたい!~活用事例Tips LT会~」について、まとめさせていただきました! はじめに イベント概要 発表タイトル一覧 発表の紹介 No1. OpenAI APIとDiscordを連携したQOL改善 No2. Power AppsとChatGPTの連携で出来たこと No3. 今更ながらLangChain使ってみた No4. ChatGPTのAPI No5. 良き開発パートナーChatGPT No6. ChatGPTを活用した便利ツールの紹介 おわりに イベント概要 イベント内容 "ChatGPT" をもっと使いたい!~活用事例Tips LT会~ 開催日: 2023/05/31(水) 18:30-20:30 イベントページ rakus.connpass.com 弊社主催イベントでは、LT(ライトニング トーク )形式を採用しております。 LTとは? Lightning Talks(ライトニング トーク )の略 "Lightning"は英語で"稲妻"という意味 つまり 「短いプレゼンテーション」 発表タイトル一覧 今回、LT会に参加された方々の発表タイトルは以下の通りです。 No. 登壇者 タイトル 1 しょーれー さん ※当社 OpenAI API とDiscordを連携した QOL 改善 2 nobuhiro_okamoto_73 さん Power AppsとChatGPTの連携で出来たこと 3 hachimada さん ※当社 今更ながらLangChain使ってみた 4 M-Tokky さん ChatGPTの API 5 KentaroWada さん 良き開発パートナーChatGPT 6 Hank Ehly さん ChatGPTを活用した便利ツールの紹介 発表の紹介 LT会では、弊社エンジニア2名、ゲスト4名の皆様にご登壇いただきました。 本記事では、発表内容を簡単にご紹介したいと思います。 No1. OpenAI API とDiscordを連携した QOL 改善 GitHub から最新のリリース情報を取得 → OpenAI API で翻訳・要約 → Doscordに通知というアプリを作ったお話。 コード作成もほぼChatGPTにお任せ。楽するために頑張るのっていいですよね。 最初のLT登壇という大役でしたが、さらっとこなしていました! No2. Power AppsとChatGPTの連携で出来たこと マイクロソフト のPowerAppsとPowerAutomateで簡単にアプリ作れちゃうよ、そこにChatGPT加えたらさらに便利になるよ、というお話でした。 No3. 今更ながらLangChain使ってみた LangChainを使って独自文書の内容をChatGPTに学習させるというお話。 API 単体だと自前で色々実装しないといけないのがLangChainを使うとかなり楽できそう。まだまだ可能性を秘めてそうなのでまたお話聞きたいです。 No4. ChatGPTの API ChatGPTをもっと使いたい.pptx from TokioMiyaoka www.slideshare.net オープンデータ管理ツールの「dim」を 自然言語 で使えるようにした話と、個人開発で ドラえもん の ひみつ道具 を考えてくれるツールを作ったという2本立て。 No5. 良き開発パートナーChatGPT 開発合宿でChatGPTに本格的に触れ、ハマってしまったというお話。いくつかのアプリを披露していただけました。開発パートナーとしてどんどん活用していこうぜ!という熱が伝わってきました。(ただし、GPT-4がいいよと) No6. ChatGPTを活用した便利ツールの紹介 ラク スのLT登壇では常連のハンクさんです。久しぶりの ラク スイベント待ってましたよ~と言っていただけてとても嬉しかったです。日々増大しているChatGPT関連のブラウザ 拡張機能 の中から特におススメのものを紹介していただけました。これで業務爆速です。 おわりに 本記事では、ChatGPTの活用事例LT会の様子を紹介させていただきました。 発表資料を見たい!という方は、以下イベントページから、ご確認ください。 rakus.connpass.com ChatGPT LT会ですが、かなり盛り上がったため第2弾も開催する予定です。 もし、本記事をお読みいただき、興味関心が湧きましたら是非次回イベントに参加/登壇申し込みいただけますと幸いです。 ラク スでは、イベントを定期的に開催しております。 我々の取り組みが、皆さまにとって新しい気づきや成長につながる機会となっていますと嬉しい限りです。 今後ともよろしくお願いいたします。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非一度ご覧ください。 現役エンジニア・マネージャーのインタビュー記事がおススメ。 career-recruit.rakus.co.jp カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお気軽にお申込みください。 forms.gle イベント情報 直近のイベント情報です。 会社の雰囲気を知りたい方は、弊社主催イベントにご参加ください! rakus.connpass.com rakus.connpass.com
アバター
はじめに PHPStanとは 前提 導入戦略 運用フロー 運用に至るまでのステップ 解析対象の除外設定 カスタムオートローダーの設定 baselineの作成 CIでの解析実行設定 途中で直面した課題 PHPStan実行時のエラー プロジェクト固有のエラー パフォーマンスについて 余談:リソース消費とスペックについて 使ってみて期待できそうなこと はじめに こんにちは。楽楽販売の開発チームに所属しているyanahmです。 最近、当チームではPHPStanの導入を段階的に始めています。 この記事ではレガシーコードへの途中からの導入の際に工夫した点についてお伝えします。 したがって、スタンダードなPHPStan導入方法とは少し異なっている部分もあります。 同じような状況の方の一助になれば幸いです。 PHPStanとは 近年人気の PHP の静的解析ツールです。 PHPStanの詳細については世の中にたくさん情報があるのでここでは割愛しますが、かいつまんで言うと下記のような特徴があります。 解析レベル(0~9) を設定でき、段階的に厳しく設定することもできる 解析時に無視するエラーの定義ファイル(baseline) を作成できる 一部動的に解析を行うため、 PHP をコードとして解釈した上で問題となる所も指摘する カスタムで解析ルール を追加できる 解析対象の設定や解析レベルなど各種設定は phpstan.neon というファイルに定義できます。 前提 まず、解析対象となるアプリの現状です。 15年選手のレガシーアプリ 解析対象は約4500ファイル 名前空間 なし 諸事情により自作オートローダーあり 歴史あるアプリのため、現在のスタンダードとは離れている部分もあります。 導入戦略 静的解析を途中から導入する方針については、おおまかに下記2パターンが考えられます。 解析レベルを1番緩いものから始めて、エラーをなくしたら段階的に厳しいものに引き上げる 既存コードのエラーは無視して、新規開発の範囲だけは厳しいルールで品質を担保する 本来なら1. が望ましいですが、歴史のあるアプリの場合、既存エラーが膨大な数になってしまう/修正による影響範囲が大きいといった問題が出てくると思います。 したがって当アプリでは、まずは新規機能開発の範囲だけでも品質を担保することを目標に 2. を採用しています。 逆に全く新規開発のプロダクトや日の浅いアプリなどの場合は1. の方が全体品質を担保できるので望ましいこともあるかと思います。 運用フロー まだ現在進行形ですが、現在下記のような形でフローで回しています。 新規開発着手前に、ベースとなるブランチでの既存エラーを無視するため、baselineを作成 baseline作成済のベースブランチから開発用ブランチを作成 コードレビューが可能な段階でマージリク エス トを作成 マージリク エス トの作成をトリガーにCI上でPHPStanが実行され、指摘が出る 担当者が指摘箇所を修正&レビュワーがチェック 問題なければメインブランチへマージ ゆくゆくは各担当者の手元で事前に実行できればと考えています。 運用に至るまでのステップ 現在の運用に至るまでの工程について紹介していきます。 解析対象の除外設定 場合によっては現状では修正が難しい/いったん解析対象から外しても問題ないファイルがあるという場合もあるかと思います。 その場合は、設定ファイルの excludePaths: で除外設定を行うことができます。 parameters: paths: # 解析対象 - ../app - ../config … excludePaths: # 設定系は除外 - ../app/config* # 廃止ソースは除外 - ../app/controllers/AbondonedController.php … カスタムオートローダーの設定 composerなど便利なツールのない時代から継続しているアプリでは、クラスマップを自作していたり、 名前空間 の設定がなかったり、複数個所で定数ファイルをrequireしていることもあるのではないでしょうか。 PHPStanには bootstrap という設定があり、PHPStanが実行される前に PHP ランタイムで何かを初期化する必要がある場合 (独自のオートローダなど)、 独自のブートストラップファイルを提供できます。 parameters: bootstrapFiles: - custom-autoloader.php 当アプリでも実行に必要な定数ファイルやカスタムオートローダーがあり、これを設定しないとそもそもクラスパスを解決できませんでした。 baselineの作成 上記の設定を基に、開発着手前にベースとなるブランチで作成します。 当社ではGitlabを使用しているため、パイプラインの手動実行で実行できるようにしています。 ※後述しますが、解析対象が多くPHPStanの実行にメモリを消費するため --memory-limit=2G を設定していないと途中で失敗しました。 php vendor/bin/phpstan --no-progress --memory-limit=2G --generate-baseline=phpstan-baseline.neon CIでの解析実行設定 前述の通り、マージリク エス トをトリガーにCI上でPHPStanが実行されるようにCIに設定を行います。 弊社ではGitLabを利用しているため、GitLab CIを使用しています。 途中で直面した課題 PHPStan実行時のエラー baselineを作成するために解析対象全体に対して実行してみると、当初は実行途中で失敗していました。 解析対象が多いレガシーコードのため、最初からスムーズにはいかないことが多いです。 $ php vendor/bin/phpstan --generate-baseline … [ERROR] An internal error occurred. Baseline could not be generated. Re-run PHPStan without --generate-baseline to see what's going on. このような場合は、 --debug オプションをつけて実行すると、下記のようにエラーとなるソースのところでストップします。 $ php vendor/bin/phpstan --debug /PATH_TO_APP/app/controllers/SampleController.php ... /PATH_TO_APP/app/controllers/BadController.php # エラー原因となるソース 次に、 -v オプションをつけて実行するとエラースタックを出力してくれるので、原因が特定しやすくなります。 php vendor/bin/phpstan analyse /PATH_TO_APP/app/controllers/BadController.php -v ちなみに、 -vvv オプションをつけて実行すると、各ファイル解析時点で消費された合計メモリや解析にかかった秒数も表示されるようになるので、リソースの問題で問題が起きた時の デバッグ に役立ちます。 /PATH_TO_APP/app/controllers/Sample1Controller.php --- consumed 36 MB, total 82 MB, took 8.35 s ... /PATH_TO_APP/app/controllers/Sample2Controller.php --- consumed 0 B, total 1.25 GB, took 0.74 s /PATH_TO_APP/app/controllers/Sample3Controller.php --- consumed 0 B, total 1.29 GB, took 0.16 s プロジェクト固有のエラー 実際に解析を回し始めると、baselineで既存エラーは無視したものの、現状では修正が難しいエラーだが毎回指摘が出てしまいノイズになるというケースがあると思います。 その場合は、無視したいエラーを 正規表現 で定義しておくことができます。 parameters: ignoreErrors: - message: '#^Access to an undefined property App\\Foo\:\:\$bar\.$#' paths: - /{APP_PATH}/app/foo/* ... これらをbaselineとは別ファイルに定義して読み込ませることも可能です。 includes: - project-ignore.neon - phpstan-baseline.neon 当チームでは仮運用開始後、そういったものがないかを 継続的にマージリク エス トをモニタリングし、メンバーからも記入しもらい定期的に棚卸できるようにしています。 本運用に乗せるためにはこの作業が一番重要な作業だと考えています。 パフォーマンスについて 前述の通り当アプリは解析対象数が多く、全ファイル解析するとそれなりのマシンパワーを消費します。 現状検証中でCI用に割り当てたマシンがそれほどスペックが高いものではないため、暫定処置として解析対象を差分ファイルのみに絞り込むようにしました。 git diff コマンドを用いて差分ファイルを抽出しています。 --diff-filter オプションでA: 追加 / C: コピー / M: 変更 / R: リネームされたファイルを対象にしています。 # 差分のあるファイルだけ抽出 - cd $CI_PROJECT_DIR - git diff --name-only --diff-filter=ACMR origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...${CI_COMMIT_SHA} -- 'app/*.php' | sed -e "s|^|${APP_HOME}\/|g" | sed "s|\n| |g" > target.txt - > if [ ! -s target.txt ]; then echo "No target exists." exit 0 fi # 解析実行 - php vendor/bin/phpstan analyse --no-progress --memory-limit=2G $(cat $CI_PROJECT_DIR/target.txt) | sed -e s@$APP_HOME/@@g ※注意※ 実は git diff を使用するやり方は 公式では推奨されておらず、毎回全体を対象に解析することが望ましいです。 1回解析実行されると結果はキャッシュされるため、2回目以降の解析速度は上がるのですが、ファイル数が多いと1回目にかなり時間がかかってしまうため、暫定処置として行っています。 将来的にはCIマシンスペックの調整を行う想定です。 余談:リソース消費とスペックについて 解析対象数は約4500で実行にかかる時間は 1コア・メモリ2GB環境:16分ほど 8コア・メモリ15GB環境:2~3分ほど メモリ消費はいずれも2~2.6GBほど でした。コア数に依存していますね。 これはPHPStanが Parallel processing に対応しているためです。 設定はデフォルトで有効になっています。 実際8コア環境で実行してみると、下記のようにコア数分workerが起動されています。 $ top PID PPID USER P S %CPU %MEM TIME SWAP DATA COMMAND 7755 7736 root 7 R 100.0 4.9 0:17 0 787604 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7757 7736 root 5 R 100.0 4.9 0:17 0 791832 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7760 7736 root 1 R 100.0 4.9 0:17 0 787604 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7753 7736 root 4 R 99.7 5.0 0:17 0 797844 /usr//bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7756 7736 root 2 R 99.7 4.9 0:17 0 787604 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7759 7736 root 6 R 99.7 4.9 0:17 0 781460 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7758 7736 root 0 R 99.3 4.9 0:17 0 781460 /usr/bin/php -c /usr/lib/php.ini vendor/bin/phpstan worker --configuration /usr/… 7754 7736 root 3 R 99.0 4.9 0:17 0 783508 /usr/bin/php -c /usr/lib/php.ini 現時点では差分のみ解析対象としているためそこまで問題になっていませんが、解析対象が多いとそれだけ要求スペックや実行時間がかかってくるため、状況に応じてCI実行環境のスペックは検討したいところです。 使ってみて期待できそうなこと 日々のコードレビューについては 機械的 にチェックされるため、人の目で見るより取りこぼしが少なく、コード品質向上が期待できる 上記効果によりレビュワーの負担が軽減され、より業務ロジックに集中したレビューに専念できる また、副次的な効果として 大規模レガシーアプリの リファクタリング はどこから手をつけるかの判断が難しいが、PHPStanの解析結果を参考に徐々に改善していく一定の指標になる といった期待が持てました。 また新たに工夫や効果が出た際にはお知らせしていきます。
アバター
はじめに バグの説明 前提:楽楽販売について バグの発見経緯 バグの原因調査 バグ報告の手順 バグ報告用のテンプレート に従う 英語で書く バグの解決 おわりに はじめに 楽楽販売の開発チームに所属している kasuke18 です。担当領域はアプリケーションの運用周りです。 最近、アプリケーション開発・運用中に OSS のバグを発見し GitHub の Issue を登録しました。 この記事では、そのバグの内容や OSS への Issue の報告方法についてお伝えします。 今回バグを発見した OSS は Guzzle という、 PHP ではメジャーなHTTPクライアントライブラリです。 バグの説明 バグの内容は Guzzle の Issue に登録していますので、経緯にご興味がなければそちらをご参照ください。 前提:楽楽販売について 楽楽販売にはファイルアップロード機能があり、アップロードされたファイルは Cloudian HyperStore というオブジェクトストレージで管理しています。 Cloudian HyperStore は Amazon S3 とインタフェースの互換性がありますので、ファイルアップロードなどの操作は AWS SDK を利用することができます。 今回の主題である Guzzle は私達のアプリケーションが直接利用しているのではなく、 AWS SDK for PHP の中でHTTPクライアントとして使用されています。 参考:このあたりに触れた過去記事がありますので、ご興味があればご参照ください。 tech-blog.rakus.co.jp バグの発見経緯 開発・運用中に特定のファイルをアップロードしようとすると、エラーが発生し、アップロードができない問題に遭遇しました。エラーは PHP の処理でアップロードを試みた場合にのみ発生し、 CLI の aws コマンドでは同じファイルをアップロードできました。 特定のファイルは、中身が 0 という文字だけのファイルでした(md5sum値は cfcd208495d565ef66e7dff9f98764da )。 ※ PHP の開発者であれば、この時点で何となく原因を推測できるかもしれません。 また、エラーログには以下のような内容が出力されていました。 Error executing "PutObject" on "${アップロードURL}"; AWS HTTP error: Error creating resource: [message] fopen(${アップロードURL}): Failed to open stream: HTTP request failed! [file] /path/to/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php [line] 324 バグの原因調査 エラーメッセージを見ると、 PHP の関数 fopen の処理でエラーが発生したと書かれています。 AWS SDK for PHP では、ファイルアップロードを WebAPI(HTTP PUTリク エス ト)で行っています。 最初はファイル アップロード なのに、なぜ fopen 関数を使用してファイルを開いているのか疑問に思いました。しかし、調査を進めるうちに、実際に fopen 関数を使用して WebAPI を実行できることがわかりました。 したがって、 fopen 関数の使用自体に問題はなく、使用方法に何らかの問題があることが分かりました。 以下は、 fopen 関数を使用してPUTリク エス トを送信するサンプルですが、重要なポイントは2つあります。 stream_context_create 関数でリク エス トの内容を設定すること その設定内容を fopen 関数に渡すことで、WebAPIを実行できること ▶ fopen 関数で PUTリクエスト を送信するサンプル <?php // 送信するデータ $ data = "This is the data to be sent" ; // リクエストのURL $ url = "http://example.com/api/endpoint" ; // リクエスト内容を設定 $ options = array ( 'http' => array ( 'method' => 'PUT' , // リクエストヘッダ 'header' => "Content-type: text/plain \r\n " . "Content-length: " . strlen ( $ data ) . " \r\n " , // リクエストボディ 'content' => $ data ) ) ; // リクエスト送信 $ context = stream_context_create ( $ options ) ; $ result = fopen ( $ url , 'r' , false , $ context ) ; したがって stream_context_create 関数を呼び出している箇所のコードで何か問題が起こっていないかを確認するため、Guzzle の ソースコード を調べました。その結果、リク エス トボディを設定する部分で怪しい処理を見つけました。具体的には、リク エス トボディに設定したい内容を文字列にキャストし、 empty 関数で条件分岐していました。 デバッグ を行いながら処理を追っていくと、ファイルの中身が 0 だけの場合、 Content-Length は設定されるがリク エス トボディが設定されない、ということが確認できました。 通常、サーバーはこのようなリク エス トを受け入れることは考えられないため、これが fopen 関数でエラーが発生した原因であると判断しました。 バグ報告の手順 ここでは、バグを OSS の開発者に報告するために実施した手順について説明します。今回の場合、Guzzle のバグ報告は GitHub の Issue を通じて受け付けられているため、その内容と書き方について話をします。 Issue を作成する際に注意した点は、以下の2つです。 OSS が提供しているバグ報告用のテンプレートに従うこと 英語で頑張って書くこと バグ報告用のテンプレート に従う 多くの主要な OSS では、このようなバグ報告に使用するためのテンプレートが用意されています。これに従って記述することで、フォーマットに悩む必要がなくなりますし、テンプレートを使用していない場合は情報が不足しているとして拒否されることもあります。Guzzle のバグ報告テンプレートでは、以下の項目を可能な限り埋めるよう求められています。 使用しているバージョン(Guzzle, PHP , cURL ) 概要 再現するためのコード 解決策と考えられる方法 追加情報 その中で、今回は「再現するためのコード」に苦労しました。 まず最初の課題は、私たちのアプリケーションが直接 Guzzle を使用していないため、ほぼゼロからコードを作成する必要があったことです。Guzzle の使用方法に慣れていなかったため、再現コードの正確さに不安がありましたが、「バグだと思われるコードを通過する」ことを目的として作成し、その旨を Issue に記載するという対応をしました。 もう一つの課題は、Guzzle が HTTP クライアントライブラリであるため、動作確認にはモックサーバーや関連環境が必要になることです。再現コードの検証だけならば、ローカルにモックサーバーを立ち上げることで十分ですが、厳密に報告するならその手順を記載する必要があります。 とはいってもその手順を Guzzle の開発者に提供してもあまり意味がないため、「Guzzle の開発者なら手順が確立されているだろう」と仮定し、特に記述しませんでした。 英語で書く 一般的に、 OSS 開発者に日本語の理解を要求することはできないため、英語で記述する必要があります。 しかし、私自身は英語が得意とは言えませんので、翻訳ツールに頼りました。 手順としては「①日本語で記述する」→「②DeepLなどを使用して英語に翻訳する」だけではなく、「③再度日本語に翻訳し直す」ことで、日本語で書いた際の意図が抜け落ちていないかを確認しています。 この手順は、社内のエンジニアがオフショア先のチームとコミュニケーションを取る際に行っている方法を聞いたことがあり、それを取り入れてみました。 こうすることで成果物を日本語にすることができました。つまり得意ではない英語ではなく、日本語でレビューできるということになり、この点が大きなメリットでした(レビューといっても、誰かに見てもらうわけではなく、セルフチェック程度ですが...)。 バグの解決 原因コードや修正方法を提供したおかげか、追加情報を求められることもなく、すぐに修正されました。 7.5.2 でリリースされています。 おわりに OSS へのバグ報告は初めての経験でしたが、以下の点に従うことでスムーズに進めることができました。 提供されたIssueテンプレートに従う 提供する情報に過不足がないかを丁寧に確認する 今回のバグは限定的なケースでのみ発生する軽微なものでしたが、それでも報告することで、一人の利用者として OSS に貢献できたのではないかと思います。
アバター
こんにちは、技術広報の yayawowo です。 突然ですが、株式会社 ラク スと聞いて何を思い浮かべますでしょうか? 弊社 ラク スでは、様々なプロダクトを展開していますが正直認知度は低いと思っております。 そこで今回、弊社についてもっともっと知っていただくため・・・ ラク スが展開している全10プロダクト 全10プロダクトの技術スタック インフラ/SRE/デザイナーの技術スタック について、ご紹介させていただきます! SaaS 開発に携わる方、弊社に少しでも興味を持っている方の一助となれば幸いです! ラクスが展開している全10プロダクトとは? バックオフィス向け フロントオフィス向け 10プロダクトの技術スタック 楽楽販売 楽楽精算 楽楽明細 楽楽電子保存 楽楽勤怠 MailDealer 配配メール Curumeru チャットディーラーAI blastmail & blastengine フロントエンドの技術スタック インフラ・SREの技術スタック インフラ SRE デザイナーの利用ツール UIデザイナー エンジニア/デザイナーの募集職種 過去イベント動画をYoutubeで公開中 終わりに ラク スが展開している全10プロダクトとは? まずは、弊社が展開しているプロダクトを一覧で見てましょう。 いくつのプロダクトをご存知でしょうか? バックオフィス向け 名称 主な利用部門 提供機能 リリース年 楽楽販売 複数のスタッフでデータや 情報共有が必要な さまざまな部門 ・販売管理 ・請求管理 ・稟議申請管理  2008年 楽楽精算 交通費精算や経費精算の申請や 支払手続を行う 営業や 経理 部門 ・交通費精算 ・経費、出張精算  2009年 楽楽明細 請求書、支払明細といった 帳票を扱う 営業や 経理 部門 ・帳票:請求書、納品書、支払明細 ・発送方法:WEB、メール添付、郵送、FAX  2013年 楽楽勤怠 打刻や休暇申請を行う 全従業員 と 勤怠の締めを行う 総務人事部門 ・打刻機能 ・打刻修正、休暇、残業などの申請 ・休暇管理 2020年 楽楽電子保存 請求書、支払明細といった 帳票を扱う 営業や 経理 部門 ・帳票の電子保存・一元管理 2022年 フロントオフィス向け 名称 主な利用部門 提供機能 リリース年 MailDealer 複数名のスタッフでメール対応を している カスタマーサポート部門 ・問合せメールの返信状況管理 ・顧客との対応履歴管理 2001年 配配メール 見込客や顧客にメルマガを 配信している 営業や マーケティング 部門 ・大量高速メルマガ配信 ・メルマガ配信の効果測定 ・エラーアドレスのクリーニング 2007年 Curumeru 同じ ・大量高速メルマガ配信 ・メルマガ配信の効果測定 ・エラーアドレスのクリーニング ・メールの承認フロー 2011年 チャットディーラー ECサイト やコールセンターなどの カスタマーサポート部門 経理 、総務、人事、情報システム などの 管理部門 ・チャットボット ・顧客動向の効果測定 ・訪問者情報の取得 2017年 blastmail & blastengine 見込客や顧客にメルマガを 配信している 営業や マーケティング 部門 ・大量高速メルマガ配信 ・メルマガ配信の効果測定 ・エラーアドレスのクリーニング - 3つ以上知っていてる方は、 ラク スマニアと言っても過言ではないですね! 初めて聞いた!という方がおりましたら、是非サイトにいき、ご確認いただけますと幸いです。 10プロダクトの技術スタック では、我々 ラク ス開発本部についてご紹介していきたいと思います。 ラク ス開発本部のミッションは 「日本を代表する SaaS 開発エンジニア集団へ」 を掲げております。 また、弊社はおよそ2年おきに最新プロダクトをリリースしております プロダクトの技術選定は、「ベスト・オブ・ブリード(Best of Breed)」という考えのもとリリース時により良い最適な技術を採用しております。 そのような背景も踏まえ、 ラク ス開発本部にて扱っている10プロダクトの 技術スタック をこれからご紹介していきたいと思います! 楽楽販売 図1:楽楽販売の技術スタック 楽楽販売 は、販売管理・案件管理をはじめとした、あらゆる社内業務をシステム化することができるWebデータベースシステムです。 Excel での業務管理を卒業して、販売管理などの業務を ラク にします。 リリースは2008年10月であり、14年以上続いているプロダクトになります! また、楽楽販売の開発拠点は、 関西 となります。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP MW PostgreSQL 、 Postfix 、 Apache 、Redis FW・ライブラリ Zend Framework 、 jQuery 開発ツール PhpStorm、GitLab、 GitHub Copilot CI・テスト Selenium /Selenide、 PHPUnit 、 JMeter 、SonarQube、Jenkins ※2023/5/26時点での情報です。 楽楽販売は、 CRM (BtoB向け)のシステムであり API 連携が多いのが特徴です。 また、お客様がノンコード(UI上)で処理を作成できるといったプロダクト特性があります。 楽楽精算 図2:楽楽精算の技術スタック 楽楽精算 は、経費・交通費・出張費・旅費・交際費など、お金にかかわる全ての処理を一元管理できる クラウド 型の交通費・経費精算システムです。 リリースは、2009年7月で弊社の中でも最も勢いのあるプロダクトであるため、開発規模も最大になります。 開発拠点は、 東京・ ベトナム です。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Java 、Swift、Kotlin MW PostgreSQL 、 Postfix 、 Apache FW・ライブラリ Spring Boot、 jQuery 開発ツール eclipse 、GitLab、 IntelliJ IDEA、 Xcode 、 Android Studio 、Flyway、 Redmine 、 GitHub Copilot CI・テスト Selenide、Gradle、 JMeter 、 jUnit 、Jenkins、Bitrise ※2023/5/26時点での情報です。 技術の選定の大きな方針としては、保守性の観点で、なるべく普及しているものを選定するようにしています。 また、主要な開発言語は Java 、上記に記載したツールを利用し、開発を行っております。 楽楽明細 図3:楽楽明細の技術スタック 楽楽明細 は、請求書・納品書・支払明細・領収書などをWEB・メール・郵送で自動発行し、 印刷・封入・発送などの帳票発行の手間をゼロにする クラウド サービスです。 リリースは2013年9月、開発拠点は 東京・ ベトナム です。 市場の成熟とともに急激に伸びているプロダクトになります! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Java 、TypeScript MW PostgreSQL 、 Apache 、 Postfix 、Docker FW・ライブラリ React、Redux、SpringBoot、JasperReports、 Lombok 、Jooq、 jQuery 、GraphQL 開発ツール IntelliJ IDEA、 Redmine 、GitLab、gulp.js、webpack、Storybook、 GitHub Copilot CI・テスト Gradle、 JMeter 、 jUnit 、TestCafe、SonarQube、Jenkins、Cypress、Spock ※2023/5/26時点での情報です。 サーバーサイドを Java で、フロントエンドをTypeScriptで構築しています。 サービスローンチから8年を数え、比較的古い アーキテクチャ となっているコア部分と、新しいFWなどを用いた部分とがハイブリッドになっています。 主要な新規機能開発部はサーバーサイドとフロンドを分離されており、SpringBootやReactを用いて開発をしています。 DDDを用いた設計手法の導入、 モノリス から バッチ処理 部分を分離して アーキテクチャ を更新する、オフショア開発をスタートするなど、 サービスの成長に合わせて開発手法や組織を変化させていっています。 ◆ 技術・デザイン情報ページ フロント刷新から設計手法アップデートまでバランス感覚を活かして幅広く推進 | ストーリー | 株式会社ラクス キャリア採用 楽楽電子保存 図4:楽楽電子保存の技術スタック 楽楽明細 は、楽楽明細と連携し、電子発行された請求書・納品書・支払明細・領収書などを 保存・一元管理できるサービスです。 電子帳簿保存法 対応により、ニーズが高まっているプロダクトです! リリースは2022年、開発拠点は 東京・ ベトナム です。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Java 、TypeScript MW PostgreSQL 、 Apache 、 Tomcat 、 Postfix 、Docker FW・ライブラリ React、Redux、MUI、styled-components、Spring Boot、Jooq 開発ツール IntelliJ IDEA、GitLab、Vite、Storybook、 GitHub Copilot CI・テスト Gradle、 JUnit 、SonarQube、Jenkins、Jest、Cypress ※2023/5/26時点での情報です。 楽楽勤怠 図5:楽楽勤怠の技術スタック 楽楽勤怠 は、主に中小企業3,000社超のバックオフィスの効率化を実現した クラウド 型経費精算システム「楽楽精算」の開発・提供で培った様々なノウハウを活用し、勤怠管理業務の効率化を実現する クラウド サービスです。 リリースは2020年10月となります! 主な開発拠点は、 東京・ ベトナム です。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Java 、 Python 、TypeScript、Sass MW PostgreSQL 、 Apache 、RabbitMQ FW・ライブラリ Spring Boot、Vue.js、Resilience4j 開発ツール IntelliJ IDEA、GitLab、Docker/ k8s 、 Visual Studio Code 、 GitHub Copilot、 Figma CI・テスト Gradle、Swagger、 JUnit 、 Checkstyle 、SpotBugs、PMD、GitLab CI、 JMeter 、Cypress、Jest、Vue Testing Library、Storybook、Mock Service Worker、StepCI ※2023/5/26時点での情報です。 ラク ス社内では目新しい技術を多く利用しており、DDDを用いた設計思想を取り入れ、フロントエンドとバックエンドを切り離した開発を行っています。 開発側にPdM相当の人がいるので、開発主導でサービスをブラッシュアップしていけます。 MailDealer 図6:Maildealerの技術スタック MailDealer は、顧客からの問合せメールを共有・一元管理し、メール対応業務を効率化するツールです。 2001年4月にリリースされ、最も古いプロダクトとなります。 主要な開発拠点は 大阪・ ベトナム になりますが、フロントエンドは 東京 と拠点を分けた体制です! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP 、Node.js MW PostgreSQL 、 Apache 、 Postfix FW・ライブラリ Laravel、 jQuery 、CKEditor、Socket.IO 開発ツール PhpStorm、 Redmine 、GitLab、Trello、 GitHub Copilot CI・テスト Selenium /Selenide、 PHPUnit 、Jenkins、Ansible ※2023/5/26時点での情報です。 主要な開発言語は PHP 。 リアルタイム通信部分にはNode.js(+Socket.IO)も利用しています。 20年以上の歴史を持つサービスながら、オフショア開発、 リファクタリング や新技術のハイブリッド導入など新たな領域にもチャレンジし続けているサービスです! 配配メール 図7:配配メールの技術スタック 配配メール は、中小企業の集客・販促活動に携わる方のメール配信業務を支援するメール マーケティング の実践に最適なメール配信サービスです。 リリース日は、2007年5月になります。 なお、MailDealerと同様、開発拠点は 大阪・ ベトナム ですがフロントエンドは 東京 になります! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP MW PostgreSQL 、 Postfix 、Nginx、 Apache 、Redis FW・ライブラリ Slim、 jQuery 、Vue.js 開発ツール PhpStorm、 REDMINE 、GitLab、Docker、 GitHub Copilot CI・テスト Puppeteer、Jenkins、 JMeter 、 PHPUnit 、PHPStan、 PHP _CodeSniffer、PHPDoc、Ansible ※2023/5/26時点での情報です。 開発言語は PHP 、 フレームワーク は国産 OSS を自社拡張したものを採用しています。 大量メール配信による マーケティング ツールのサービス基盤を自社でメンテナンスしながら、初期リリースから15年以上サービスを継続させています。 また、近年需要が高まりつつあるデジタル マーケティング ツールとして、最新のWeb技術も取り入れながら機能強化を続けています。 変化の激しい マーケティング のビジネス領域に適応するため、 アジャイル 開発や プロダクトマネジメント 手法を取り入れることにもチャレンジしています! Curumeru 図8:Curumeruの技術スタック Curumeru は、低コストで導入できる大量メール配信サービスです。 2011年6月にリリースされており、開発拠点はこちらも 大阪・ ベトナム ! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP MW PostgreSQL 、 Postfix 、 Apache FW・ライブラリ jQuery 開発ツール PhpStorm、 REDMINE 、GitLab、 GitHub Copilot CI・テスト Jenkins、Ansible ※2023/5/26時点での情報です。 開発言語は PHP 、 フレームワーク は配配メールのものをベースに構築されています。 配配メールをベースにしつつ、 API 連携やメールリレーなどの大量メール配信機能をより強化した仕組みで構築されています。 初期リリースから約10年間、お客様のシステムのメール配信を支えています。 現在は ベトナム のオフショアチームが中心となって開発しています。 チャットディーラーAI 図9: チャットディーラーの技術スタック チャットディーラーAI は、チャットによる自動回答などを通じて問合せ対応を低コスト化/効率化するツールです。 リリースは2017年6月になります! 開発拠点は 大阪・ ベトナム です。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 PHP 、Node.js、 Python MW PostgreSQL 、 Apache 、Nginx、Redis、 MeCab 、Docker FW・ライブラリ Laravel、 jQuery 、Vue.js、Bootstrap、CKEditor、Socket.IO、Express、Handlebars、npm、Composer、 ImageMagick 開発ツール PhpStorm、 Redmine 、GitLab、Jupyter Notebook、 VS Code 、Webpack、 GitHub Copilot CI・テスト Pupperteer、 PHPUnit 、Mocha、Jenkins、 JMeter 、Ansible ※2023/5/26時点での情報です。 主要な開発言語は PHP 、リアルタイム通信部分はNode.js(+Socket.IO)、 自然言語処理 を行う部分では Python を採用しています。 また、 フレームワーク はLaravel, Vue.jsを使っているなど、多様な技術に触れて学ぶ機会が有ります。 2017年リリース以降、毎月リリースを継続しており、スピーディ―な開発も魅力です。 blastmail & blastengine 図10:blastmail & blastengineの技術スタック blastmail と blastengine は、独自開発した配信エンジンと大規模ネットワークにより、280万通/時の超高速配信を実現した クラウド 型メール配信サービスです。 開発拠点は 東京 で、グループ会社である ラク スライト クラウド が扱っているプロダクトになります! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 TypeScript、 JavaScript 、 PHP 、 Java 、Go MW Apache 、nginx、jetty、 PostgreSQL 、MongoDB、RabbitMQ、 Memcached 、 Postfix FW・ライブラリ React、Laravel、 CakePHP 、SpringBoot 開発ツール Docker、GitLab、 Redmine 、StoryBook、OpenAPI、GitBook、Slack、 AWS 、 GitHub Copilot CI・テスト GitLab-CI、Jenkins、SonarQube、Jest、ReactTestingLibrary、cypress、 PHPUnit 、 jUnit ※2023/5/26時点での情報です。 マルチな言語環境、マイクロサービスの最適化を目指して日々邁進中です。 直近ではフロントエンドとバックエンドの分離を進めており、より ユーザビリティ や可搬性の高いシステムとサービス品質の向上を目指しています。 フロントエンドの技術スタック ラク スには各プロダクト開発チームの他に、フロントエンド開発を専門とする「フロントエンド開発課」があります。 楽楽シリーズ・ ラク スシリーズの各 SaaS 製品における新機能追加、パフォーマンス向上、技術的改善や刷新などを行っています。 様々なサービスにおいて活躍できる横断組織として、多様な業務に挑戦しています! ◆ 技術スタック一覧 カテゴリ 内容 使用言語 HTML、 CSS 、 JavaScript 、TypeScript MW Docker FW・ライブラリ React、Redux、Recoil、MUI、RHF、zod、Tanstack Query、axios、Laravel、Vue.js、Vuetify、 jQuery 、Sass、Vite、webpack、ESLint、Prettier、Storybook、msw 開発ツール GitLab、 GitHub Copilot、 Redmine 、PhpStorm、 VSCode 、 Figma CI・テスト GitLab CI、Jenkins、Cypress、Playwright、Jest、Vitest ※2023/5/26時点での情報です。 インフラ・SREの技術スタック 前述した10プロダクトを支えているのが、インフラ部門になります。 今回はインフラの技術スタックだけでなく、社内バックオフィス業務の自動化を推進して頂くSREの技術スタックについてもご紹介したいと思います! なお、インフラ部門の開発拠点は 大阪、東京 、SRE部門は 東京 です。 インフラ まずは、インフラの技術スタックをご紹介します。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Python 、 PHP プラットフォーム On-Premise、 AWS (EC2、ECS、EKS、RDS、S3、Lambda など) ネットワーク Cisco 、 Dell 、Fortinet、F5Networks OS CentOS 、 Amazon Linux 仮想化基盤 VMware 、Nutanix MW MySQL 、 PostgreSQL 、 Apache 、 Tomcat 、Nginx など IaC Ansible、Terraform その他ツール Git、Jenkins、 Selenium 、Rundeck、Serverspec、 GitHub Copilot 運用・監視 Zabbix、Grafana、Prometheus、ElasticStack ※2023/5/26時点での情報です。 9割のサービスリソースをオンプレミスで構築しております。 オンプレミス環境でも自動化など、なるべくソフトウェア視点のアプローチが出来るようにHCIで基盤構築し運用効率化をしています。 今後のアップデートとしては、 クラウド で先行構築した クラウド ネイティブなコンテナ環境やCI/CD環境などをオンプレミス環境にフィードバックし、自動化、自立化を推進しつつもコスト優位性を出せるシステムを構築していきます。 SRE 続いて、SREの技術スタックをご紹介します。 ◆ 技術スタック一覧 カテゴリ 内容 使用言語 Go、 Python プラットフォーム On-Premise、 AWS 仮想化基盤 Kubernetes MW PostgreSQL FW・ライブラリ Gin、Flask 開発ツール GitHub 、Docker CI/CD GitHub Actions、Kustomize、Helm、ArgoCD、 GitHub Copilot 運用・監視 Datadog ※2023/5/26時点での情報です。 主にGoを利用しており、一部 Python も併用しています。今後はGoに言語は統一していく予定です。 新しい技術スタック調査などを進めながらノウハウを各サービスへ広めることで、開発部門全体の アーキテクチャ 刷新へ寄与していきます。 デザイナーの利用ツール ラク スはエンジニアだけでなく、デザイナーも10つのプロダクトを支えております。 職種としては、UIデザイナーとなりますのでこちらの利用ツールも紹介していきたいと思います! デザイナーの所属拠点は、 東京 になります。 UIデザイナー ラク スのプロダクトのUIをデザインします!利用ツールは以下の通りです。 ◆ 技術スタック一覧 カテゴリ 内容 デザインツール Figma コミュニケーションツール Mattermost、Zoom、 Google Meet ※2023/5/26時点での情報です。 ラク スのデザイナーはバックオフィスをはじめとする、業務システムの管理画面をデザインします。 業務 ドメイン の知識を学びながら、顧客の課題を理解し、業務をデザインの力で解決します。 業務 ドメイン の知識習得や、デザインの勉強会・輪読会などをおこなっています。 エンジニア/デザイナーの募集職種 前述した通り、 ラク スでは全10のプロダクトを扱っており、 「日本を代表する SaaS 開発エンジニア集団へ」 を目指し日々精進しております。 そんな弊社ですが、まだまだ人が足りておりません。 そこで、下記に 各開発拠点ごとの募集職種を関連するプロダクトとともに整理しました! 皆様のご応募、お待ちしております!! 【開発拠点:東京】 カテゴリ 募集職種 関連プロダクト マネジメント エンジニアリングマネージャー 楽楽精算・楽楽明細・楽楽勤怠 エンジニアリングマネージャー/オフショア 〃 開発 サーバサイドエンジニア/Java 楽楽精算・楽楽明細・楽楽勤怠 サーバサイドエンジニア/PHP blastmail・blastengine プロダクトマネージャー 楽楽精算・楽楽明細・楽楽勤怠 プロジェクトマネージャー 楽楽精算・楽楽明細・楽楽勤怠 プロジェクトマネージャー/PHP blastmail・blastengine プロジェクトマネージャー/フロントエンド 楽楽明細・楽楽勤怠・メールディーラー リードエンジニア/フロントエンド 楽楽精算 フロントエンドエンジニア 楽楽明細・楽楽勤怠・メールディーラー iOSエンジニア 楽楽精算 Androidエンジニア 楽楽精算 QAマネージャー 楽楽精算・楽楽明細・楽楽勤怠 QAエンジニア 楽楽精算・楽楽明細・楽楽勤怠 SETエンジニア 楽楽勤怠 PMO/品質管理 プロダクト横断 Webエンジニア プロダクト横断 デザイン デザインマネージャー/プロダクト プロダクト横断 UIデザイナー/アシスタントマネージャー 楽楽精算 UIデザイナー 楽楽明細・楽楽販売・メールディーラー・チャットディーラーAI・配配メール UIデザイナー/ポテンシャル採用 楽楽明細・楽楽販売・メールディーラー・チャットディーラーAI・配配メール ※2023/5/26時点での情報です。 【開発拠点:大阪】 カテゴリ 募集職種 関連プロダクト マネジメント エンジニアリングマネージャー 楽楽販売・メールディーラー・チャットディーラーAI・配配メール エンジニアリングマネージャー/インフラ 〃 開発 リードエンジニア/PHP 〃 サーバサイドエンジニア/PHP 〃 プロジェクトマネージャー 楽楽販売・チャットディーラーAI プロダクトマネージャー 楽楽販売 ブリッジSE MailDealer インフラ インフラエンジニア メールディーラー・チャットディーラーAI・配配メール・楽楽販売・Curumeru ※2023/5/26時点での情報です。 過去イベント動画を Youtube で公開中 弊社 ラク スでは、毎週技術イベントを開催しております。 現在 Youtube の「 ラク スチャンネル」にて、過去イベントの アーカイブ 動画を公開中です! 各プロダクトの最前線で活躍しているエンジニアたちが、社内の取り組みを発信していますので是非お時間ありましたらご確認ください。 youtube.com なお、最新の アーカイブ 動画は ラク スDevelopers会員(技術イベント時に登録できるメール会員)限定に公開しております。 もし最新 アーカイブ 動画を見てみたい!という方、まずは技術イベントにご参加いただけますと幸いです! ラク スconnpassページ ラクス - connpass 終わりに ラク スの技術スタックにご興味いただけたでしょうか? 弊社の中では大型開発の楽楽精算を初め、様々なプロダクトが存在しております。 本ブログにより、1つでも多くプロダクトの名前を覚えていただけたら幸いです。 また弊社では、前述した通り募集職種が多くあります。 もし、どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っておりますのでお気軽に以下フォームよりお申込みください。 カジュアル面談お申込みフォーム カジュアル面談について | 株式会社ラクス キャリア採用 長くなりましたら、今後とも ラク スエンジニアブログをよろしくお願いします。 エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! rakus.connpass.com
アバター
ビジネスアプリケーションとビジネスルール 用語について パターン適用前 Specification パターン Hard Coded Specification Parameterized Specification Composite Specification 条件の再利用性が向上する テスト性が向上する ルールと条件を統一したインターフェースで扱える 動的にルールを構成できる まとめ 関連するデザインパターン Strategy パターン Composite パターン Interpreter パターン 参考文献 補足 ビジネスアプリケーションとビジネスルール 楽楽精算開発部の id:smdr3s です。主に Java を使ったサーバーサイドを担当しています。 弊社のサービスである楽楽精算は、その名のとおり経費精算のサービスです。主に企業にお勤めの方が、業務での移動時ににかかった交通費や業務に必要な物品を購入した際の代金などを経費として会社に申請する際にご利用いただいております。 楽楽精算にはさまざまな機能がありますが、その一つに「申請ルール」という機能があります。 これは、経費申請時にあらかじめ設定しておいたルールで申請内容を検証し、ルール違反があればメッセージを表示して警告したり、申請を拒否したりする機能です。 例えば、交通費精算の際に「利用 交通機関 がタクシーで、料金が3,000円以上、かつ理由欄に記述がない、場合は申請できないようにする」ようなことが可能です。 申請ルールの設定 申請時にルール違反があった場合 このように、ビジネスアプリケーションには複数の条件を組み合わせたビジネスルールによる検証を行うことがよくあります。 また、ビジネスルールに従って蓄積されたデータの中から条件に合致するものを抽出したりすることもあります。 そのような検証や抽出に使用するルールや条件を実装するときに役立つのが Specification パターン です。 用語について この記事では、パターン名にも使われている Specification という用語を以下の二通りに訳して使用しています。 ルール :最終的に満たされるべき基準の総称。「タクシー利用ルール」など。 条件 :ルールに含まれ、構成する個々の基準の名称。「金額上限条件」「理由記述条件」など。 Specification をそのまま訳すと「仕様」であり、「仕様」にも上記のような「満たすべき基準」の意味はあるかと思いますが、個人的にあまりしっくりこなかったため別の言葉を使用しています。また、全体と個々の中身を区別したい場面が多かったためそれぞれに別の言葉を割り当てました。(後述しますが、それらを同一視できるのがこのパターンのメリットの一つでもあるのですが、便宜的に使い分けました。) パターン適用前 上の「申請ルール」の説明に使用したビジネスルールを基に「タクシー利用申請アプリケーション」の実装を検討します。 このアプリケーションで検証するビジネスルールは以下のとおりとします。 タクシー利用ルール 料金が3,000円以上の場合は理由欄に理由を記述すること 申請はこのようなレコードです。 public record TaxiApplication( int fee, // 料金 String description // 理由 ) { } 申請時にルールに沿っているかを検証します。 手続き的に書く場合は検証処理は Logic クラスあたりに書かれそうです。 public class TaxiApplicationLogic { public void apply(TaxiApplication application, User applicant) { // いろいろ // タクシー利用ルールのチェック // 3,000円以上かつ理由欄が空の場合は検証エラー if (application.fee() >= 3000 && application.description().length() <= 0 ) { throw new ApplicationRuleValidationException(); } // いろいろ } } ただ、このように書いてしまうと下記のような問題があります。 ルール検証のテストがしづらい ルールが変わるごとに Logic クラスに修正が入る 同じルールを使用している箇所があった場合、ロジックが重複する 全体のコードからルールを司るコードの特定が困難 この状態は、ソフトウェア品質特性で言う、保守性の副特性である試験性、安定性、解析性に影響が出ているか、出る可能性が比較的高い状態かと思います。 これは重要なビジネスルールであるはずのタクシー利用ルールのコードが手続きの中に隠され、独立していないことが最も大きな原因と考えられます。 Specification パターン これを Specification パターンを使用して改善していきたいと思います。 Specification パターンにはいくつかの方式があるのですが、基本的にはルールや条件の検証を実行する Specification インターフェースが基になります。 public interface Specification<T> { boolean isSatisfiedBy(T candidate); } Specification インターフェースには、通常 isSatisfiedBy という名前のメソッドを定義します。このメソッドは検証対象のオブジェクトをパラメータとして受け取り、実装ではそのオブジェクトが定義されたルールまたは条件を満たしているかどうかを検証し結果をブール値で返します。 Hard Coded Specification 最も単純な実装は Hard Coded Specification です。 ルールの検証ロジックをそのまま Specification インターフェースの実装クラスに実装します。 // タクシー利用ルールクラス public class TaxiUsageSpecification implements Specification<TaxiApplication> { @Override public boolean isSatisfiedBy(TaxiApplication candidate) { return candidate.fee() < 3000 || candidate.description().length() > 0 ; } } (先ほどの Logic クラス内の実装ではルールに沿わないことを判定していましたが、今回はルールに沿うことを判定するため、真偽が逆になっています。) これでルールをクラスにすることができました。 このクラスを利用するよう上記の Logic クラスのチェック部分を変更します。 // タクシー利用ルールオブジェクトの生成 public class SpecificationFactory { public static Specification<TaxiApplication> getTaxiUsageSpecification() { return new TaxiUsageSpecification(); } } public class TaxiApplicationLogic { public void apply(TaxiApplication application, User applicant) { // いろいろ // タクシー利用ルールオブジェクトの取得 Specification<TaxiApplication> taxiUsageSpecification = SpecificationFactory.getTaxiUsageSpecification(); // 申請がルールに沿っていなければエラー if (!taxiUsageSpecification.isSatisfiedBy(application)) { throw new ApplicationRuleValidationException(); } // いろいろ } } ルールのコードをクラスに切り出すことでコードの特定が容易になり、同じルールであればこのロジックを使い回せるようになりました。 このように Hard Coded Specification は実装が簡単で、ルールがコードに直接表現されているため、ルールが単純な場合には読みやすいかと思います。 しかし、ルールをコードにベタ書きしているのはパターン適用前と変わらず、少しでもルールに変更があった場合にはコードの修正が必要となります。 ルールの条件が固定されており変更の可能性も低い場合にのみ使用するのが良いかと思います。 Parameterized Specification ルールの大まかな条件は決まっているが、細かい差異や変更がある場合に使用できるのが Parameterized Specification です。 ルールの実装クラスのオブジェクトを作成するときにパラメータを渡せるようにすることで、差異のあるルールを作成することができます。 今回の例では、タクシー料金が3,000円以上の場合に理由の記述が必要なルールとなっていますが、将来的にこの金額を変更したい、という話が出てきそうですのでこれをパラメータで渡せるようにしてみます。 // タクシー利用ルールクラス public class TaxiUsageSpecification implements Specification<TaxiApplication> { private final int maxFreeFee; // 理由記述が不要な料金上限 TaxiUsageSpecification( int maxFreeFee) { this .maxFreeFee = maxFreeFee; } @Override public boolean isSatisfiedBy(TaxiApplication candidate) { return candidate.fee() < maxFreeFee || candidate.description().length() > 0 ; } } // タクシー利用ルールオブジェクトの生成 public class SpecificationFactory { public static Specification<TaxiApplication> getManagerTaxiUsageSpecification() { // 理由記述が必要な基準料金等の条件はDB等から取得できるものとします int freeThreshold = CompanyRuleRepository.getTaxiRule().freeThreshold; return getTaxiUsageSpecification(freeThreshold); } } タクシー利用ルールが Specification<TaxiApplication> のオブジェクトであることに変更はありませんので Logic クラスに修正は必要ありません。 このように Parameterized Specification ではパラメータを使用することでルールの条件を調整することができます。 今回はパラメータを一つだけしか渡していませんが、ルールに含まれる複数の条件にパラメータを渡したり、一つの条件に複数の複数のパラメータを渡したりすることも可能です。 また、オブジェクトの生成ごとに異なるパラメータを渡すことができますので、例えば従業員と管理職で制限金額が異なる場合などに、ロジックは共通のまま異なるルールオブジェクトを作成することができます。 一方、ルールに直接条件が書かれているのは変わりませんので、条件の追加や削除などの要件に対応するにはコードの修正が必要になります。 Composite Specification 最も柔軟で動的なルールを作成できるのが Composite Specification です。 いままでの方式では ルールごと に Specification インターフェースを実装したクラスを作成していましたが、 Composite Specification では 条件ごと に独立したクラスを作成し、Composite の名のとおりそれらの条件クラスを組み合わせて最終的なルールを構成できるようにします。 今回のケースでは「料金上限」と「理由記述」の2つの条件がありますので、それらの条件ごとに Specification インターフェースを実装したクラスを作成します。 // 料金上限条件クラス public class MaxFeeSpecification implements Specification<TaxiApplication> { private final int maxFee; MaxFeeSpecification( int maxFee) { this .maxFee = maxFee; } @Override public boolean isSatisfiedBy(TaxiApplication candidate) { return candidate.fee() < maxFee; } } // 理由記述条件クラス public class ReasonSpecification implements Specification<TaxiApplication> { @Override public boolean isSatisfiedBy(TaxiApplication candidate) { return candidate.description().length() > 0 ; } } 条件の組み合わせを行うクラスを実装します。これも Specification インターフェースを実装します。 Specification インターフェースの結果はブール値ですので、AND, OR, NOTそれぞれの論理演算を行う条件クラスがあればすべての組み合わせに対応できます。 // AND条件 public class AndSpecification<T> implements Specification<T> { private final Specification<T> left; private final Specification<T> right; public AndSpecification(Specification<T> left, Specification<T> right) { this .left = left; this .right = right; } @Override public boolean isSatisfiedBy(T candidate) { return left.isSatisfiedBy(candidate) && right.isSatisfiedBy(candidate); } } // OR条件 public class OrSpecification<T> implements Specification<T> { private final Specification<T> left; private final Specification<T> right; public OrSpecification(Specification<T> left, Specification<T> right) { this .left = left; this .right = right; } @Override public boolean isSatisfiedBy(T candidate) { return left.isSatisfiedBy(candidate) || right.isSatisfiedBy(candidate); } } // NOT条件 public class NotSpecification<T> implements Specification<T> { private final Specification<T> specification; public NotSpecification(Specification<T> specification) { this .specification = specification; } @Override public boolean isSatisfiedBy(T candidate) { return !specification.isSatisfiedBy(candidate); } } そして、これらの条件を組み合わせればルールを表すオブジェクトを作成できます。 // タクシー利用ルールオブジェクトの生成 public class SpecificationFactory { public static Specification<TaxiApplication> getTaxiUsageSpecification() { int freeThreshold = CompanyRuleRepository.getTaxiRule().freeThreshold; // 条件を組み合わせてルールを作成します return new OrSpecification( // 以下のいずれかの条件を満たすこと new MaxFeeSpecification(freeThreshold), // 料金が指定金額以下 new ReasonSpecification()); // 理由が記述されている } } 今回のようにルール単純なケースでは実装が複雑になっただけのように見えるかもしれませんが Composite Specification には以下のようなメリットがあります。 条件の再利用性が向上する 条件が個々のクラスに独立したことにより、条件だけを再利用することが可能になります。 例えば、タクシー利用ルールに「料金は○万円未満」の条件が追加されたとしても、(従来は理由の記述が必要な料金の条件判断に利用していた) MaxFeeSpecification クラスを使用し、以下のように実装することができます。 // タクシー利用ルールオブジェクトの生成 public class SpecificationFactory { public static Specification<TaxiApplication> getTaxiUsageSpecification() { int freeThreshold = CompanyRuleRepository.getTaxiRule().freeThreshold(); int limitFee = CompanyRuleRepository.getTaxiRule().limitFee(); return new AndSpecification( // 上限は必須条件のためAND new MaxFeeSpecification(limitFee), // 上限を超えていないこと new OrSpecification( // その他の条件は従来どおり new MaxFeeSpecification(freeThreshold), new ReasonSpecification())); } } テスト性が向上する その他の方式ではルール全体で一つのクラスであったため、テストを行う場合はルール全体を対象とするしかなく、それに含まれる条件ごとにテストを行うことはできませんでしたが、 Composite Specification では条件ごとにクラスを作成するため、それぞれの条件の独立したテストを行うことが可能になります。 ルールと条件を統一したインターフェースで扱える 最終的なルールである SpecificationFactory+getTaxiSpecification() の戻り値オブジェクトも、それを構成する条件である MaxFeeSpecification , ReasonSpecification や AND, OR, NOT の論理条件も、すべて Specification インターフェースを実装したものであるため、相互に組み合わせや代替が可能です。 (ルールと条件を区別していたのは私の都合でしたので当然ではありますが…) 例えばあるロジックでは (A && B) のルールが利用されており、他のロジックでは ((A && B) || C) のルールが利用されていた場合、 (A && B) で X というルールクラスを作成すれば、前者のロジックで X を利用できるのはもちろん、後者のロジックでも X を条件として (X || C) として利用することができます。 この場合、テストも X に対して行えるため、後者のルールのテストも単 純化 することができます。 動的にルールを構成できる 条件がオブジェクトとして独立しているため、これらを動的に生成し、組み合わせを行って自由にルールを作成することができます。 もちろん利用される個々の条件はあらかじめ実装しておく必要がありますし、それらを組み合わせてルールを構成するロジックの実装の難易度は高いかと思いますが、要求の変化があるたびにコードを変更する必要がなくなるため、さまざまなユーザの要求に迅速に対応しやすくなります。 楽楽精算の「申請ルール」機能のように、ユーザが自由にルールを設定できる要件にも対応できます。(なお、楽楽精算の実際の実装とは異なる可能性があります。) まとめ Specification パターン は、基本的にインターフェース1つ、メソッド1つで構成される単純な構成でありながら非常に強力な効果が得られるパターンです。 そして、活用がしやすく実践的でありながら、 関数プログラミング や論理プログラミングといった パラダイム にも触れることができ、興味深く勉強できる利点があります。(個人の感想です。) 参考文献には、拡張として2つのルールや条件の包摂を判断する手法 [1] や、条件判断の実装を外部に依存する(させる)ことで SQL 等の実装を使用して抽出時のパフォーマンスを上げる方法 [2] も述べられておりますので、ぜひ参考にしてみてください。 関連する デザインパターン Strategy パターン Specification パターンはルールをインターフェースに実装し、オブジェクトによってふるまいを変化させることができますので、本質的には Strategy パターン [1] であると言えます。 Composite パターン Composite Specification では、ルールと条件を同一のインターフェースで扱うことでそれらを 再帰 的に構成することを実現しています。これはその名のとおり Composite パターンを適用したものです。 Interpreter パターン Composite Specification で動的にルールを構成する場合、 Interpreter パターンで条件オブジェクトを組み合わせて最終的なルールオブジェクトを構成することが可能です。 参考文献 Eric Evans and Martin Fowler. "Specifications". https://www.martinfowler.com/apsupp/spec.pdf Elic Evans. エリック・ エヴァ ンスの ドメイン 駆動設計. 翔泳社 , 2011, 576p. Martin Fowler. エンタープライズ アプリケーション アーキテクチャパターン . 翔泳社 , 2005, 576p. 補足 上の Composite Specification の実装では AND, OR, NOT の条件を独立したクラスとして実装しましたが、 Java 8 以降の interface の default 実装を使用することも可能です。 public interface Specification<T> { boolean isSatisfiedBy(T candidate); default Specification<T> and(Specification<T> other) { return candidate -> isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate); } default Specification<T> or(Specification<T> other) { return candidate -> isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate); } default Specification<T> not() { return candidate -> !isSatisfiedBy(candidate); } } この場合のルールの組み立ては以下のように行います。 public class TaxiRule { public static Specification<TaxiApplication> getRule() { int freeThreshold = CompanyRuleRepository.getTaxiRule().freeThreshold(); int limitFee = CompanyRuleRepository.getTaxiRule().limitFee(); return new MaxFeeSpecification(limitFee) .and( new MaxFeeSpecification(freeThreshold) .or( new ReasonSpecification())); } } また、上の Specification<T> と同様の実装が Java 8 以降で関数型インターフェース Predicate<T> として標準実装されているため、こちらを使用することも可能です。
アバター
こんにちは!フロントエンド開発課の koki _matsuraです。 この記事では、僕が開発に携わっている製品のE2Eテストに取り入れたページオブジェクトモデル(POM)という実装パターンの概要と取り入れたキッカケ、POMへ リファクタリング する簡単な例をご紹介させていただきます。 僕と同じようにE2Eテストに関わっている方、E2Eテストに興味を持っている方などに読んでいただけると幸いです。 目次は下記のようになっています。 POMとは なぜPOMを使い始めたのか POMへのリファクタリング ログイン画面 テスト内容 POM導入前のテストコード ページオブジェクト作成 POM導入後のテストコード 終わりに POMとは Webアプリケーションのテスト自動化において、テストコードとWebページを分離して管理する手法です。 POMを使わない従来のテストコードはWebページと分離しないため、どうしてもDOMの構造を意識したものになってしまい、コードは長くなり読みにくくなります。大規模になると保守性なども問題になってきます。 下図はPOMを使わないテストです。同じ要素を複数回取得する必要があり、テストコードの重複もあります。 POMはこれらの問題点を解消します。具体的にはWebページごとのクラス(ページオブジェクト)を作成し、各クラス内でそのページの要素に対する操作をメソッドとして定義します。テストコード側はページオブジェクトを呼び出し、メソッドを使うだけで要素を操作できます。 下図はPOMを使ったテストです。テストコードとWebページは分離されています。テストコードはDOMを意識せずにメソッドと アサーション だけを使えば簡単にテストが書けます。 まとめるとPOMの利点は以下が考えられます。 可読性の向上 テストコードではDOMを意識しなくて済むため、簡潔なコードとなり読みやすくなると考えられます。 保守性の向上 DOMに変更があっても、ページオブジェクトを変更するだけでテストコードの変更は不要になるため、保守性は向上します。 再利用の向上 ページオブジェクトを作成しておけば、そのページの様々なテストケースにおいてコードの重複を防ぐことができます。 チーム開発の効率化 テストコードをチームメンバーで共有しやすくなります。また、テストコードを迅速に作成することができます。 なぜPOMを使い始めたのか 上記で利点をいくつか挙げましたが、その中でもPOMを使い始めた一番の理由は「保守性の向上」です。 POMを使う前は愚直に多くのページに対して テストコードを書いていました。 テストコードを書き続けるうちに1つ1つのページに対してのテストケースが多くなっていき、1つのケースに対して関係してくるページも多くなっていました。 そして、ある日、DOMを変更した瞬間、今まで問題なく通っていたE2Eテストは落ちてしまいました。1箇所の変更だけでもそのDOMが関わるテストは下図のように全て落ちてしまいます。 そこからは何か仕様変更が起こるたびに下記のようなループが起きます。 「仕様変更発生」→「DOMの変更」→「大量のテストが落ちる」→「大量のテストの修正」→「仕様変更発生」→ ...以下略 DOMの変更点が多い日には超大量のテスト修正という虚無の時間が訪れます。 このループが続いているとテストの失敗を放置してしまうようになってしまいます。 「せっかくE2Eテストを書いたのに...」「でも、毎回メンテナンスするなんて...」「もっと修正点が少なくなればいいのに...」とメンタル的にもしんどくなってきました。 同じような経験をした人や今現在している人もいるのではないでしょうか。 テストの失敗が続いてしまうと、信頼性も下がってきます。テストの意味もなくなってきます。もっと保守性の高いコードにするべきです。 そこでPlaywrightのドキュメントを読んでいたときに見つけたのが「ページオブジェクトモデル」です。 今までのWebページとテストコードの間にページオブジェクトを挟むことでページをオブジェクトとして扱える! DOMの変更が起きても、修正するのは該当のページオブジェクトでテストコードは修正しなくて済む! 操作をメソッド化すればテストコードが簡潔になり、テストコードに慣れていない人でも簡単に読める! ページオブジェクトを作る手間はかかるけど、あの虚無の日々を考えたら全然大したことない! 保守性の高いテストが書ける! と思ったため、導入するに至りました。 今では仕様変更が発生しても、下図のようにページオブジェクトのみを修正すればすぐに全てのテストが動くようになり、大幅に修正の時間も減り、保守性を高めることができました。 POMへの リファクタリング POMの概要とどのような経緯で使うことに至ったのかの説明をしてきました。 最後は実際に簡単なログイン画面をもとに普通のテストコードからどのようにPOMへ リファクタリング していくかを説明させていただきます。 ログイン画面 ログイン画面は以下のようにシンプルに「名前」と「パスワード」の入力欄があります。 テスト内容 ログイン画面へ遷移する 名前入力欄に「user」を入力 パスワード入力欄に「password」を入力 ログインボタンをクリック 一覧画面へ遷移できているかテスト POM導入前のテストコード 名前入力欄、パスワード入力欄の要素へ記入し、ログインボタン要素をクリックしています。 ログイン後、トップ画面へ遷移できているかはURLを見ることでチェックします。 ※ 下記のコードではパスワードをベタ書きしていますが、Gitなどに上げる場合にはenvファイルを経由するなどして直接は書き込まないようにしてください。 // login.spec.ts import { test , expect } from '@playwright/test' ; test ( "ログインできているか" , async ( { page } ) => { await page. goto( "/login" ); await page.getByLabel ( '名前' ) .fill ( "user" ) await page.getByLabel ( 'パスワード' ) .fill ( "password" ) await page.getByRole ( 'button' , { name: 'ログイン' } ) .click (); await expect ( page ) .toHaveURL ( "/" ) } ) ページオブジェクト作成 ページオブジェクトを作成するために適当な ディレクト リでpageObjectフォルダを作成します。 おすすめとしてはプロジェクト直下にE2Eフォルダを作成し、その中で E2E/tests/*.spec.ts や E2E/pageObject/*.ts などを管理するのが良いと思います。 では、ページオブジェクトを作成していきます。 基本は下記のような「要素の定義」「コンスト ラク タの定義」「操作の関数定義」の形で作成します。 goto関数やlogin関数はこのテストでは必須です。 waitForPageContentsのような読み込みを待つ関数は要素の読み込みを待たないことによるテストの失敗が多い場合に定義すると良いと思います。 // loginPage.ts import { expect , Locator , Page } from '@playwright/test' ; export class LoginPage { // 要素の定義 readonly page : Page readonly name : Locator readonly password : Locator readonly loginButton : Locator // コンストラクタの定義 constructor( page : Page ) { this .page = page this .name = page.getByLabel ( "名前" ) this .password = page.getByLabel ( "パスワード" ) this .loginButton = page.getByRole ( 'button' , { name: 'ログイン' } ) }   // 関数の定義 async goto() { await this .page. goto( "/login" ) } async waitForPageContents () { await this .name.waitFor () await this .password.waitFor () await this .loginButton.waitFor () } async login ( name: string , password: string ) { await this .name.fill ( name ) await this .password.fill ( password ) await this .loginButton.click () } } POM導入後のテストコード ログインページのページオブジェクトが完成したので、実際にテストコードを リファクタリング していきましょう。 やることは簡単で、テストコードの先頭で インスタンス を生成して、あとは操作を記述していくだけです。 今回の場合は下記のようなコードになります。 import { test , expect } from '@playwright/test' ; import { LoginPage } from '../pageObject/loginPage' ; test ( "ログインできているか" , async ( { page } ) => { const loginPage = new LoginPage ( page ); await loginPage. goto(); await loginPage.login ( "user" , "password" ); await expect ( page ) .toHaveURL ( "/" ); } ) ページオブジェクトを用いてログインのテストを リファクタリング ができました。 テストコードはDOMを意識したものではなくなっているので仕様変更が起きてもテストコードを変更する必要がありません。 終わりに 今回はE2Eテストにページオブジェクトモデルを導入した話をさせていただきました。 どうでしょうか。ページオブジェクトモデルの導入によりテストコードは多少見やすくなりましたが、色々実装することを考えると微妙だなと感じた人もいるのではないでしょうか。 僕も最初はそのように感じました。保守性を高めるメリットに対して、ページオブジェクト作成とテスト改修のコストがかかるというデメリットがあるため、微妙だと感じやすいです。 しかし、テストケースが増えたり、テストする画面が増えたりなど、大規模になればなる程、ページオブジェクトモデルは真価を発揮するものです。 なので、自分が開発している製品の規模感に合わせて導入するかを検討するのが良いと思います。 ここまで読んでいただきありがとうございます。この記事を機に、少しでもページオブジェクトモデルに興味を持っていただけたら幸いです。
アバター
弊社で毎月開催し、 PHP エンジニアの間で好評いただいている PHP TechCafe。 2022年7月のイベントでは「PHPDoc」について語り合いました。 弊社のメンバーが事前にまとめてきたPHPDocの情報にしたがって、他の参加者に意見を頂いて語り合いながら学びました。 今回はその内容についてレポートします。 rakus.connpass.com そもそもPHPDocとは何か 活用するポイント 課題 代表的な書き方 基本的なタグ 型の記述 PHPDocに関する質問 最低限書いておきたいコメント 必須 微妙 その他 phpDocumentor使ってますか? PHPDocの記載がすべて出力されるか phpDocumentorの使いどころは? PHP TechCafeメンバーの中で出た意見 phpDocumentorで出力されるもの WEB APIに関しての使いどころ PHPDocの記述だけで WEB APIの動作確認をする方法はないか APIドキュメントを書いておいて、フロントエンドとバックエンドを疎結合にする開発 戻り値がvoidの場合、”@return void”は書いたほうが良いか? @throws\Throwableは書くべき? 各IDEでサポートしているPHPDocの範囲が分からない レガシーシステムとPHPDocの向き合い方 他の言語のDocコメントとの違いは? まとめ 編集後記 そもそもPHPDocとは何か PHPDocについては、2021年5月に一度 【PHPDocについて語り合う】と題して、PHPTechCafeが開催されており、 その時のページを基におさらいをしました。 rakus.connpass.com PHPDocとは、関数、定数、クラス、メソッド、プロパティなどにブロックの説明として残すコメントのことです。 基本的にはただのコメントなのでプログラミングに影響はありませんが、一部のツールではPHPDocの内容によって処理を行うものもあります。 ただPHP8.0で アトリビュート が追加されたので、PHPDocとしての記述も アトリビュート 方式の記述へどんどん変わっていくのではないかというのが前回の見解でした。 活用するポイント 編集者の理解が捗る IDE で補完が効きやすくなる 静的解析がより正確に実行できる 実行時に型チェックされる 等が挙げられます 課題 明確な記述ルールは決まっていないというのが結構ネックです。 色々な書き方がありますが、 " PHP ドキュメントジェネレータであるphpDocumentorの記述が デファクトスタンダード なのか" とか、"PhpStormにあるようなものを採用すればいいのではないか" とか、色々な意見があります。 そのような点を標準化するべく、 PSR-5 や PSR-19 で議論が行われています。 今のところまだドラフト中ではありますが、PSR-5ではPHPDocのことが書いてあります。 コメントとして 「静的解析ツールでそれぞれ独自にやりたい方法でやっている感じがします。」 「PHPDocはPhpStormで生成されるものを書いているだけです」 というようなものをいただきました。 IDE で使えるものを使うという方もおられる印象です。 代表的な書き方 基本的なタグで、よく使うものをピックアップしています。 基本的なタグ @ から始まる文字列。何についてのドキュメントかを示す。 @param:関数またはメソッドの引数について記述 @return:返り値について記述 @throws:例外について記述 @var:変数について記述 @todo:開発でやるべきことがあることを示す 型の記述 前述の タグ の後に記述することで、対象の型を記述する 例 @return int 整数を返す リテラル 型 int, bool, string etc... 配列型 ただの配列(中身不明): array 数値キーの配列: int 複数の型があり得る配列: (int|string) 文字列キーの配列: array<string, int> キーごとに値の型が異なる配列: array{id: int, name: string} その他の書き方 false型 booleanではなくfalse型 property __get() , __set() を使った動的なプロパティを記述することが可能 ローカル変数の型指定 @var で指定 このようなことを記入しておくと、 IDE で補完が効いたり静的解析でチェックされるというメリットがあります。 基本的なものは上記のとおりですが、型の記述ではBoolean型ではなくfalse型があったり、 __get() , __set() を使った動的なプロパティを作れるpropertyなどがあります。 このfalse型などの変わったものについては まとめページ に書いてあるので、目を通していただければと思います。 この中でいただいたコメントでは、 「個人開発ではめっちゃ@todo使います。これを書いているとPhpStormとかでコミットする時に「まだTODO残ってるよ」って教えてくれますね。」 「@varは変数とかよりはpropertyのためのものだと思ってもらった方がいい。PHPStan対応で必要な場合を除いてあまり書かずに済ませたいですね。」 さらに興味深いところでは 「 アノテーション に進捗状況を記入して、 ガントチャート を自動生成する」 というものをいただきました。 これについては、 「その発想はなかった!」 「嫌だそれ。 アノテーション に プログレ スとか書いて、このクラスどこまで出来上がったみたいな、そこから自動的に ガントチャート に進捗率が反映されるって。中々ですね。」 「「@progress 70%」とか世知辛いですね~、どうせこれ人が書いているから99%とかで止まっているんでしょうね。」 というような、いかにも業界な感想で話が盛り上がりました。 PHPDocに関する質問 今回は ”そもそもPHPDocってどういう時に使うか” という点を念頭に議論が行われました。 最低限書いておきたいコメント 必須 @var @param @return @throws(明示的にthrowしているなら) @var,@param,@returnに関しては、ソース上型を書けば十分という意見もあります。@throwsは何かあった時にツールが補完してくれるということも考えられます。 「@returnや@paramはArrayの時だけかな」というコメントもいただきましたが、 これについては、(Arrayの)中身ということで考えれば、記載があった方が良いと思われます。 ただ、変なArrayになるのであれば、クラスで返す事などを考えて、Arrayを受け取らなければならないシーンを無くしていきたいと思います。 他にも 「型宣言と同じものをPHPDocに書き写さなくて良い」 「記載不要と思っていてもPhpStormがコメントを入れるよう求めてくる」 というコメントをいただきました。 こちらについては、型宣言しているのなら二重メンテになるので記載不要と思われますが、PhpStormは入れるよう警告してきますので「そこまで補完しなくてもいいのでは?」という意見もあがりました。 微妙 @use/@used-by @package @use/@used-byは、可変の関数を使ったときに、”ここで使っています” と、示すために使うものです。 ですが、@use/@used-by で説明しなければいけないようなロジック自体をできるだけ避ける方が望ましいと思われます。 @package は、 名前空間 の対応物または補足として使用できます。 要素を異なる階層でグループ化できる論理的な細分化を提供できます。 その他 「参考となる物が存在する場合は @link を使う場合があります」 というコメントをいただきました。 コードのコメントとしては、”意味があるものは書くべき” と思いますので、@linkの記述はあると便利です。 また、ここまでの話から、 「 PHP に ジェネリクス 導入してほしいしてほしい」 というコメントがありました。 そうなるとPHPDocのコメントを書かなくてよくなることも考えられそうですが、 半面、 「「言語組み込み ジェネリクス はいらない」という考え方もあり、実行時それまでやってしまうと重くなり、そこはPHPDocで収めておくのがいいんじゃないか 」 「パフォーマンスに影響するなら考えものですね。PHPDocで良い気がする」 というコメントもありました。 phpDocumentor使ってますか? phpDocumentor は PHP プロジェクトのドキュメントを自動で作成してくれるツール 基本的なPHPDocコメントだけでも作成してくれうのでかなり有効 CIに組み込めばドキュメントのメンテナンスコストも削減できると考えられる と紹介されました。 phpDocumentorを使うことによってプロジェクトのドキュメントを自動で作成してくれますので、 基本的にはPHPDocコメントが書かれていれば、人によっては見やすいドキュメントを生成します。 Documentation のサイトでは、出力されるドキュメントのイメージを確認できます。 右側にあるSearch検索ボックスにクラス名などを入力するとクラス一覧が表示されます。 それをクリックすると説明が表示されます。 この ページ のTable of Contentsに P と書いてあり鍵マークが外れているものがpublic、鍵が付いているものがprivate、 M がmethod、interfaceは I となっています。 PHPDocの記載がすべて出力されるか 主催者側で検証したところ、PHPDocで型を書いているところは全部出力してくれたようです。 プロダクトではまだ使っていませんが、試しに実行してみたところしっかり出力してくれたようです。 ドキュメントのメンテナンスコストが減り、CIに組み込めば開発コストも下げられる期待があります。 phpDocumentorの使いどころは? PhpStormを使っている場合、調べたいものがあればShift+Ctrlで調べられたりします。 詰まるところ ”みんながコード読める環境ならphpDocumentorによるドキュメントは無くても良い” ということなのでしょうか。 使いどころを考えてみます。 PHP TechCafeメンバーの中で出た意見 「受託開発で一部機能だけ作ってくれという案件で納品する時にあると良いのかな」 「 API のドキュメントであれば、内部実装よりは「何を受けて何を返すのか」が最低限分かればいいので、簡単にチェックできるものがあれば良いのかな」 という意見があったことが紹介されました。 参加者の方からは、 「ライブラリにはあってほしい」 というコメントをいただきました。 ライブラリ内部までは不要ですが、どう使ったら良いかはわかると嬉しいと思います。 「ライブラリも基本的にソースを読んじゃうから、別にドキュメントいらない説」 「英語を読むより ソースコード のほうが読みやすくないですか?」 そんなコメントもいただきました。 これについては PHP TechCafeのメンバーの中では、”PhpStormで検索すればだいたい賄えるのでは”とも話が挙がっていました。 phpDocumentorで出力されるもの PHPDocで型定義をしていない変数については、phpDocumentorは出力してくれるのでしょうか? この疑問については、以下の結果が紹介されました。 PHPDocに書いていなくてもコードで定義されている型が反映される 引数なども反映される PHPDocを書いていれば、関数の説明などのコメントも反映される WEB API に関しての使いどころ WEB API の仕様をコメントの中に記述していくことが考えられますが、ここからWEB API に関して話が盛り上がります。 Swagger などはその場で実行できるので便利ですが、 API のドキュメントとなると意識的に最新化し続けないといけないという意見が挙がりました。 WEB API がドキュメント化されていても、 PHP に慣れている人からすると ”ソース読めばよいのでは” という考えも出てきます。 とはいえ、ざっくりと概要を知りたい人にはドキュメントがあったほうが助かるかもしれません。 PHPDocの記述だけで WEB API の動作確認をする方法はないか PHPDocの記述の中に、 API の仕様を記載しておくだけで API の動作を試すようなことはできないのでしょうか。 実際にそのようなものが OSS に存在しているのでしょうか。 参加者の方が、見つけてくださいました。 swagger-php NelmioApiDocBundle API ドキュメントを書いておいて、フロントエンドとバックエンドを 疎結合 にする開発 先にOpenAPIを使って API 仕様を決めておけば、バックエンドとフロントエンドを分けて開発する事ができそうですが、 PHPDocの記載にによって API 仕様を定義するのであれば、先にバックエンド側のコードの方に API 仕様を書くことになるため、バックエンドとフロントエンドを同時に分けて開発することは難しいと思われます。 他にも以下のような意見が寄せられました。 Swaggerを使う場合、別環境を準備しなければいけない為、個人開発でミニマムに済ませたいときはPHPDocに書いたほうが楽なのではないか。 PHPDocはコメントとしての力が強いので、自由に書けるというのがメリット 戻り値がvoidの場合、”@return void”は書いたほうが良いか? 「書かないとPHPStanで怒られる。」 というコメントをいただきました。 「 わざわざそこまで書くのか?」 「言語の方で補えるものは書かなくても良くなってきているかもしれないです。」 という話もありましたが、 void を書くのが圧倒的多数のようでした。 @throws\Throwableは書くべき? これは「書きたい」という意見が多かったようです。 言語仕様のほうで引数の型や戻り値の型は表現できますが、@throws\Throwableの方はしっかり書いといたほうが良さそうです。 「PhpStormでWarningが出るときと出ない時がある。 」 「CatchしているときはThrowsを書かないと警告」 「逆に投げているのにCatchしていないとそれも警告」 というコメントもいただきました。 各 IDE でサポートしているPHPDocの範囲が分からない PhpStormの提言として、PHPDocにどこまで対応しているかというリストはないようです。 公式の記載 である程度情報がまとまっているようですが、特にどこまで対応しているという記載はありません。 PhpStormが公開しない理由があるのでしょうか。 「勢いよく増えていくから、書いているうちに古くなるから。」 「メンテナンスのコストがかかるんでしょうか。 」 「結局PSRの議論がクローズしないのも、こういうことがあるからなのでしょうか。 」 という意見がありました(実態は不明のようです)。 レガシーシステム とPHPDocの向き合い方 レガシーなシステムをメンテナンスする際のPHPDocの扱い方についての議論が行われました。 PHPDocはあくまでもコメントですので、型エラーになるわけでもありません。そのため、レガシーなシステムにもPHPDocを書いていくほうが良いのではないかという考えが紹介されました。 どうしてもレガシーなところでコードを直 接触 るのをためらうときには、PHPDocコメントを書いて解析に委ねるのも戦略の一つです。 レガシーな記述方法で言えば、その一つに、array shapes 1 がありますが、素直にコメントを書いていくとすれば大変です。 リファクタリング によりクラスに置き換えることできることを考えると、PHPDocの記述で補完するよりは リファクタリング することを優先したほうが良いように思います。 他の言語のDocコメントとの違いは? 参加者から以下のような意見がありました。 JavaDoc 「あまり詳しく無いんですがあまり規約とか無いんですかね。」 Python 「派閥的なものがあるみたいです。」 PHPDocも、前述の通り、 ”これは必ず書かないといけない” という決まったルールはまだありませんが、それは他の言語でも同様のようです。 とは言え、 ”これくらいは記載したいよね” という緩いルールのような共通認識はあるようです。 まとめ 「考えていることがみんな似ていてホッとする」 「PHPDocを深く考えたことがなかったので勉強になりました」 というコメントをいただきました。 明確なルールがない部分もありますが、参加者同士で疑問を共有して考えを知れたので良い機会となりました。 主催者も含めて、この機会に勉強することができたようです。 編集後記 以上、PHPerのための「PHPDoc相談会」として、PHPDocについて深く掘り下げていきました。 PHP で書くコメントは開発者が普段から当然のように利用しているものですので、主催者/参加者ともに非常に盛り上がりを見せていました。 今後も PHP に関することや、新たなニュースに着目していきたいと思います! 「 PHP TechCafe」では今後も PHP に関する様々なテーマのイベントを企画していきます。 皆さまのご参加をお待ちしております。 PHPDocの配列表記方法の一つ。キーごとに値の型が異なる配列等を表現する。 ↩
アバター
.entry-inner img{ border: 1px solid #000; } こんにちは!技術広報課の rks_daigo と申します。 コロナ禍では多くの企業でビアバッシュ等のオフラインイベントが制限されていたと思いますが、 弊社も感染拡大を防ぐためにオフラインでの開催を自粛しておりました。 そのような中、2023年に入り新規感染者が落ち着いてきたタイミングで、感染にも配慮しながらオフラインイベントを再開しておりました。 本記事では4月に開催しました『ChatGPやってみたビアバッシュ@東京』が大変盛り上がりましたので、 そちらの様子をご紹介したいと思います! ※大阪開発拠点でも毎月ビアバッシュを開催しておりますので、また別のレポートでご紹介できればと思います。 今回のビールのお供 3月に開催された デザインビアバッシュ に触発され、おしゃれなオードブルを用意してみました。 ビールのお供 みんな喜んでくれたかな?と事後アンケートで次回のリク エス トを聞いてみたところ ピザ ×4 ピザ・お寿司 ピザや寿司があると嬉しいです 前回のように寿司&ピザみたいなものだと嬉しいです 寿司、ピザが嫌いな人はいないと思います! 銀のさら どんだけピザと寿司好きやねん。 次回はピザと寿司にしたいと思います。。 LTテーマ:「ChatGPTやってみた」 旬過ぎるテーマですね。テーマのパワーもあり、多くの方に参加していただきました。 司会は新卒で2年目のだーやまさんです。上手にまわしていただきました。感謝。 会場の様子 LT発表内容 ChatGPTを見習ってChatBotを作ろう~一家に一台ChatBot~ Google Colab上でChatBotを実装した話。Dolly 2.0使ってたのがよかったです。 おまけで Stable Diffusion も Colab に実装してましたが、ヤツは時間が溶ける。 発表者:ぐっちさん(フロントエンド開発課) LLM活用事例を考える_AutoGPT,LlamaIndex 話題のAutoGPTの実装デモ見れたのがよかった! LlamaIndexでチャットボットがさくっと作れてしまう時代ですね。 発表者:だーやまさん(楽楽勤怠 開発1課) LLMのど素人がOpenAIのEmbeddingAPIを使ってChatBotを作ってみた 文字列からベクトルを取得してコサイン類似度で突合する話です。(なんそれ) スライドも自己紹介も自動生成するというAIづくしのLTでした。 発表者:えーいちさん(楽楽勤怠 開発1課) Steamレビューを解析して要約してくれるアプリ Gamer GPT を作ってみた ChatGPTのレスポンス待ちをゲームのローディング画面っぽく実装。よいア イデア です。 アプリのUIが素敵でした。Steamって API あるんですね。 発表者:とっしーさん(フロントエンド開発課) ChatGPT加熱しすぎ問題 ChatGPTの急激な盛り上がりに一石を投じるLT。ここでは何も書けないw 技術推進課では、AI関連もどんどん追っかけていくみたいです。イイネ! 発表者:いさむさん(技術推進課) ChatGPTを使っておかやま家の生産性をXX%改善した話 おかやま家のAI事情にほっこり。うちのおかんもこんな感じです。 シリーズ化を望みます。 発表者:おかやまさん(楽楽勤怠 開発2課) OpenAPIを利用した Kubernetes 診断ツールを触ってみよう(飛び込み) ビール飲みながらさくっと実装しビール片手に登壇ニキ。 さくっと実装しちゃうところが、かっこよかったですね。 発表者:MCさん(SRE課) 終わりに 今回は、弊社のビアバッシュをご紹介させていただきました! なんか楽しそうなことしてるなと思っていただけましたら幸いです。 ラク スの開発本部では、アウトプットすることを推奨しているため ビアバッシュの予算も毎月分、確保できており(感謝) エンジニアにとって「いつでも」「気軽に」LT登壇できる環境があるのは うれしいことなんじゃないかと、しみじみ感じております。 そんな ラク スでは、一緒にビアバッシュを盛り上げてくれる仲間を募集しております! 少しでも興味を持っていただけましたら、まずはカジュアルにお声掛けください。
アバター
はじめに こんにちは!フロントエンド開発課の koki _matsuraです。 この記事では、E2Eテスト フレームワーク として用いられるPlaywrightのインストールといくつか基本的なテストコード、最後に 拡張機能 についてもご紹介させていただきます。これからPlaywrightでテストを書きたい人、E2Eテストに少しでも興味を持っていただける方に読んでいただけると幸いです。 目次は次の通りになっています。 はじめに Playwrightとは インストール 簡単なテストを書いてみよう Playwrightの設定を編集しよう 別タブを開くテストを書こう 別タブをコードから開くケース 別タブをリンクから開くケース リクエスト・レスポンスをテストしよう モックを使ってテストしよう Playwright拡張機能を使おう テストの実行 実行ブラウザの変更 ヘッドレスモードの選択 要素の取得 テストコードの生成 おわりに Playwrightとは Webブラウザ の自動化テストを実行するためのライブラリであり、Node.jsアプリケーションで使用することができます。 スクレイピング やテスト自動化、UIテスト、パフォーマンステスト、 スクリーンショット 、PDFの生成などさまざまな用途で使用することができます。特徴は下記のようになっています。 Chrome ・ Firefox ・ Safari ・Edgeなど複数のブラウザをサポートしている 簡潔な API を提供しており、直感的にWebページを操作できる 非同期で動作することによる、テストの高速実行。ブラウザのクラッシュやフリーズに対する強い耐性を持っている Puppeteerと類似してることによりPuppeteerユーザーは取り入れやすい インストール Playwrightを使用するにはPlaywright Testのインストールとブラウザのインストールが必要となります。 下記のコマンドでPlaywright Testとブラウザを同時にインストールできます。 npm init playwright@latest インストールする際に次の項目について聞かれます。 TypeScriptか JavaScript のどちらを使用しますか (デフォルトはTypeScript) -> デフォルトを選択 テストフォルダーの名前 (デフォルトは tests または e2e) -> デフォルトを選択 GitHub Actions ワークフローを追加して、CI でテストをする -> falseを選択 Playwright ブラウザー をインストールします (デフォルトは true) -> デフォルトを選択 準備はこれだけです。あとはテストコードを作成するだけで簡単にテスト自動化を実現できます。 簡単なテストを書いてみよう 早速ですが、簡単なテストを書いてみましょう。テスト内容は次のようにします。 Chrome ブラウザで Google 検索エンジン のトップページを開く 検索項目に「株式会社 ラク ス」と入力し、Enterをクリック 「株式会社 ラク ス」の公式サイトのリンクをクリック ページが正しく表示されているかテスト テストのコードはデフォルトで作成されている tests/example.spec.ts に書いていきます。 例として書かれているテストを真似して、testの大枠を書いてみましょう。内容は一旦、コメントで書いておきます。 // example.spec.ts test ( 'ラクスのサイトが表示されているか' , async ( { page } ) => { // Google検索エンジンを開く // 検索欄に「株式会社ラクス」を入力 // Enterを押す // 検索結果から公式サイトをクリック // テスト:公式サイトが表示されているか } ) test関数は第一引数にテストの名前を、第二引数にテストの関数を書いていきます。テストの関数に引数として入っているpageとはブラウザで開かれたWebページを表すオブジェクトです。 テストを記述していきます。「 Google 検索エンジン を開く」の下に書きましょう。何かのページへ移動したい場合はgoto関数を使います。引数にはURLを入れます。 await page. goto( 'https://www.google.com/' ); これで Google 検索のページへ遷移するはずです。 次は、検索欄に「株式会社 ラク ス」と入力するコードを書いていきます。このコードには検索欄の要素を取得が必要です。ですが、要素を取得するのは意外と面倒です。 このような場合はPlaywrightのコードジェネレータを使いましょう。使い方は簡単です。下記のコマンドを打ちます。 npx playwright codegen https://www.google.com Playwright Inspectorと Chromium で Google 検索が開かれると思います。 Google 検索の検索欄の要素が欲しいので、そこにホバーします。すると、下記の画像のように要素が表示されます。 この要素をクリックして、検索欄に「株式会社 ラク ス」と入力し、enterキーを押します。すると、Inspectorの方に次のようなコードが生成されていると思います。 getByRoleとは名前の通り、要素を役割別に取得できるものです。今回はcomboboxというRoleを指定していますが、buttonやinputなどもあります。 await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .fill ( '株式会社ラクス' ); await page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); 最後に公式サイトを検索結果からクリックするだけです。Inspectorから生成されたコードが次のようになっていれば問題ありません。 await page.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); 生成されたコードを example.spec.ts にコピペします。 // example.spec.ts test ( 'ラクスのサイトが表示されているか' , async ( { page } ) => { // Google検索エンジンを開く await page. goto( 'https://www.google.com/' ); // 検索欄に「株式会社ラクス」を入力 await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .fill ( '株式会社ラクス' ); // Enterを押す await page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); // 検索結果から公式サイトをクリック await page.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); // テスト:公式サイトが表示されているか } ) 最後に公式サイトが表示されているかをテストします。言い換えると、現在のページのURLが ラク スの公式サイトURLと一致するかのテストです。 次のコードを「テスト:公式サイトが表示されているか」のコメント下に追記します。 expectは アサーション ライブラリの一つで期待される結果を検証するときに用いるものです。pageが https://www.rakus.co.jp/ を持っているかの検証を示しています。 await expect ( page ) .toHaveURL ( "https://www.rakus.co.jp/" ); テストを書き終えたので実行をします。実行は下記のコマンドです。"9 passed"と表示されていればテスト通過できています。 npx playwright test レポートは npx playwright show-report を実行し、該当のページを表示すると見ることができます。 テストを実行することができました。 ちなみに、テストコードを書くためには対象となる要素を取得することが基本となり、PlaywrightではLocatorというWebページの要素を検索するためのオブジェクトを使います。 CSS セレクタ 、テキスト、要素の属性など、様々な方法で要素を検索できます。 とても便利ですが、Locatorはその便利さがゆえに使いすぎてコードがわかりにくくなったり、様々な検索が可能なために複雑なHTML構造を持つ要素を見つけるのが困難な場合があります。 なので、できる限りgetByRoleなどの明示的な組み込みロケーターを使います。組み込みロケーターはLocatorと比べ 、以下のようなメリットがあります。 シンプルでわかりやすいコードが書ける ボタンを特定するためにpage.locator()を使用するよりも、page.getByRole('button')のようにgetByRoleメソッドを使用した方がわかりやすいコードになります。 アクセシビリティ の改善 getByRole()などの組み込みのロケーターは、 Webアクセシビリティ の向上を目的としています。これらのロケーターを使用することで、 視覚障害 者やその他の障害を持つユーザーがWebページを使用するのに役立ちます。 ロケーターの選択が自動化される getByRole()などの組み込みのロケーターを使用することで、Playwrightが適切なロケーターを自動的に選択してくれます。getByRole('button')が実行されると、Playwrightは自動的にrole="button"属性を持つ要素を検索します。 Playwrightの設定を編集しよう テスト実行後のレポートを見てもらうと Chromium 、 Firefox 、 Webkit の3ブラウザで各テストが実行されていることが確認できます。もちろん複数ブラウザでテストすることも大事ですが毎回は時間がかかるのでブラウザを変更したい場合もあるでしょう。また、人によっては実行中のブラウザが表示して欲しい人もいると思います。 そのようなテストに関する設定は playwright.config.ts からできます。デフォルトでは下記のような設定になっています。 export default defineConfig ( { testDir: './tests' , /* Run tests in files in parallel */ fullyParallel: true , /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !! process .env.CI , /* Retry on CI only */ retries: process .env.CI ? 2 : 0 , /* Opt out of parallel tests on CI. */ workers: process .env.CI ? 1 : undefined , /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html' , /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry' , } , /* Configure projects for major browsers */ projects: [ { name: 'chromium' , use: { ...devices [ 'Desktop Chrome' ] } , } , { name: 'firefox' , use: { ...devices [ 'Desktop Firefox' ] } , } , { name: 'webkit' , use: { ...devices [ 'Desktop Safari' ] } , } , ] } ); testDirはテストファイルを格納しておくフォルダのことで、ここに設定された ディレクト リ以外でテストファイルを作成してもPlaywrightは認識してくれないので注意です。 他はデフォルトでコメントをつけてくれているので省略します。 重要なのはprojectsで、デフォルトでは chromium 、 firefox 、 webkit が設定されています。 chromium だけで実行したい場合は、 firefox と webkit を コメントアウト します。これだけで実行ブラウザを変更することができます。 実行中にブラウザを表示するのも簡単です。reporterとprojectsの間にあるuseの中に下記を書き加えるだけです。 headless : false headlessオプションを書かない場合はデフォルトでtrueになっているので、falseを書き加えることで実行中にブラウザ表示させることができます。 保存して、もう一度テストを実行してみましょう。 すると、 chromium が3つ立ち上がり、テストがブラウザで実行されているのが確認できると思います。3つ立ち上がっているのはfullyParallelオプションがtrueになって、並列処理がされているためです。見にくい場合はfalseにすることで一つずつ実行されます。 オプションは他にも様々ありますので、詳しくは TestConfig | Playwright を見てください。 別タブを開くテストを書こう 別タブ操作機能の有無はE2Eテスト自動化ライブラリの選定基準の一つになっていることが多いと思います。Playwrightは別タブでの操作が可能となっています。 別タブを開くケースとしては「コードから自分で開くケース」と「ボタンを押すなどのトリガーにより開かれるケース」があると思われるのでその2パターンを紹介します。 別タブをコードから開くケース テストは内容は次のようにします。 ページを開く 別タブを開く 別タブで Google 検索エンジン のトップページを開く 検索項目に「株式会社 ラク ス」と入力し、Enterをクリック 「株式会社 ラク ス」の公式サイトのリンクをクリック 別タブのページが正しく表示されているかテスト 最初に書いたテストを別タブで行うだけです。テストのコードは example.spec.ts に書いていきます。 例として書かれていた二つは消してしまいましょう。 ページを生成するのは難しくありません。下記のようにBrowser型を使うだけです。 const page = await browser.newPage () これだけなので、テストも下記のように簡単に書けます。 test ( '別タブでラクスのサイトが表示されているか' , async ( { browser } ) => { // 最初のタブを開く const page = await browser.newPage () // 別タブを開く const newPage = await browser.newPage (); // 別タブでGoogle検索エンジンを開く await newPage. goto( "https://www.google.com" ); // 検索欄に「株式会社ラクス」を入力 await newPage.getByRole ( 'combobox' , { name: '検索' } ) .click (); await newPage.getByRole ( 'combobox' , { name: '検索' } ) .fill ( '株式会社ラクス' ); // 検索ボタンを押す await newPage.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); // 検索結果から公式サイトをクリック await newPage.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); // テスト:公式サイトが表示されているか await expect ( newPage ) .toHaveURL ( "https://www.rakus.co.jp/" ); } ) 実行してみると、別のタブが開くことが確認できると思います。 別タブをリンクから開くケース ラク スの公式サイトから別タブを開くリンクを押すテストにしました。内容は以下のようにします。 ラク スの公式サイトを開く ヘッダーメニューの「事業内容」をホバーする 表示される様々なリンクから「楽楽精算」をクリックする(自動で別タブで楽楽精算のページが開かれる) 別タブのURLが楽楽精算のページと一致するかをテスト example.spec.ts に書いていきましょう。 公式サイトを開くのはgoto関数で簡単にできます。 続いて、ヘッダーメニューとはサイト上部の「 ラク スの思い」「会社情報」などが並んでいる部分です。 事業内容のリンク要素を取得したいです。コードジェネレータを使えば簡単に取得できますが、下記画像のようにだいぶ長いコードになってしまいます。 コードジェネレータに頼りすぎるとわかりにくいコードになることもあるのであくまでサポートくらいにするといいと思います。 コードジェネレータでうまく要素が取り出せない時には、HTMLを確認してみましょう。IDやClassが割り振られていればかなり要素を絞れると思います。 ヘッダの各要素には p-header__menu__item というClassが割り振られていました。ClassやIDで取得する場合にはlocatorを使います。 あとは「事業内容」というテキストを取ればいいので次のようなコードで要素を取得できます。 await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) かなりシンプルに取得できます。他にも下記のような取得方法もあります。この場合は「事業内容」というリンクがいくつかあり、その最初の要素が欲しいのでfirst()をつけることで取得できます。 await page.getByRole ( "link" , { name: "事業内容" } ) .first () 要素が取得できたのでホバーをするだけです。ホバーは要素にhover関数をつけるだけなので下記のように簡単に実装できます。 await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) .hover () 別タブでページを開く際にはイベントが発生する前にイベントを待つ関数を定義しないといけません。これは下記のように書けば実装できます。 const newPagePromise = page.waitForEvent ( 'popup' ); イベントを発生させましょう。「楽楽精算」というリンクをクリックするコードです。ここは下記のようにgetByRoleでリンク要素かつ楽楽精算というテキストで要素を絞ります。exactとは完全一致を示します。exactをつけないと部分一致になってしまいます。 await page.getByRole ( 'link' , { name: '楽楽精算' , exact: true } ) .click () 別タブで「楽楽精算」のページが開くはずなので、イベントを解決しましょう。先ほどのnewPagePromiseはPromise 型になっているのでawaitをつけることでPageが取り出せます。 const rakurakuseisanPage = await newPagePromise rakurakuseisanPageは「楽楽精算」のページを表しているはずです。 最後、rakurakuseisanPageが「楽楽精算」のページURLと一致するかのテストを書きます。ここは他のテストと同じように書けます。 await expect ( rakurakuseisanPage ) .toHaveURL ( /^https:\/\/www\.rakurakuseisan\.jp\/.*$/ ); これでテストが書けました。念の為、テストコードを全て載せておきます。 test ( '別タブで楽楽精算のページが開かれているか' , async ( { page } ) => { // 最初のタブでラクスの公式サイトを開く await page. goto( 'https://www.rakus.co.jp/' ); // ヘッダーのリンクをホバーする await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) .hover (); // 新しいページを開くイベントを待つ const newPagePromise = page.waitForEvent ( 'popup' ); // 楽楽精算のリンクをクリックする(新しいページを開くイベント) await page.getByRole ( 'link' , { name: '楽楽精算' , exact: true } ) .click (); // イベントを解決する const rakurakuseisanPage = await newPagePromise ; // テスト:新しいページのURLが楽楽精算のページと一致するか await expect ( rakurakuseisanPage ) .toHaveURL ( /^https: \ / \ /www \ .rakurakuseisan \ .jp \ /.*$/ [ f:id:koki_matsura:20230505222421p:plain ] ); } ) テストを実行してみましょう。自動で開く別タブにも対応できることが確認できます。 リク エス ト・レスポンスをテストしよう 正常にリク エス トが飛んでいるか、レスポンスが想定通り返ってきているかを確かめたい時もあると思います。 そのようなテストもできるので簡単な例とともに紹介させていただきます。テスト内容は次のようになっています。 Google 検索エンジン のトップページを開く 検索項目に「Playwright」と入力し、Enterをクリック リク エス トとレスポンスを取得 リク エス トのparamに「Playwright」が含まれているか レスポンスのステータスが200で返ってきているか リク エス トとレスポンスを取得するのは新しいページを取得するのと同じでwaitoFor系の関数を使います。 具体的には次のようなコードで書くことができます。 const [ request , response ] = await Promise . all( [ page.waitForRequest ( request => request.url () .includes ( 'search?q=Playwright' )), page.waitForResponse ( response => response.url () .includes ( 'search?q=Playwright' )), // リクエストを発生させるイベント page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ) ] ) waitForRequestとwaitForResponse内の条件に当てはまった場合だけrequestとresponseが返ってきます。 なので、この時点でparamに「Playwright」が含まれていないとテストは失敗します。 リク エス トとレスポンスが取得できれば、あとは アサーション するだけです。 テストするのはリク エス トのparamに「Playwright」が含まれているかどうかとレスポンスのステータスが200で返ってきているかどうかです。 取得したリク エス トとレスポンスを使えば簡単に次のように実装できます。 // リクエストのParamに「Playwright」が含まれているか expect ( request.url ()) .toContain ( 'search?q=Playwright' ); // レスポンスのステータスが200かどうか expect ( response. status()) .toBe ( 200 ); リク エス トに関しては先ほどのwaitForRequestでparamをチェックしているのでもう一度確認する必要はないのですが一応、書いておきました。 残りの部分は他と同じなのでテストコードは次のようになります。 test ( '「Playwright」と検索されているか' , async ( { page } ) => { // Google検索エンジンを開く await page. goto( 'https://www.google.com/' ); // 検索欄に「Playwright」を入力 await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .fill ( 'Playwright' ); // リクエスト・レスポンスを取得 const [ request , response ] = await Promise . all( [ page.waitForRequest ( request => request.url () .includes ( 'search?q=Playwright' )), page.waitForResponse ( response => response.url () .includes ( 'search?q=Playwright' )), // 検索を実行 page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ) ] ) // リクエストのParamに「Playwright」が含まれているか expect ( request.url ()) .toContain ( 'search?q=Playwright' ); // レスポンスのステータスが200かどうか expect ( response. status()) .toBe ( 200 ); } ) テストを実行してみましょう。正常に検索されれば、テストは通過するはずです。 モックを使ってテストしよう モックとは、リク エス トやレスポンスをテストする際に、実際のサーバーに接続せずに、事前に準備した偽のレスポンスを返すようにする機能です。 これを使用することで以下のようなメリットがあります。 テストの信頼性向上 外部 API はテスト実行中に変更されることがあります。これにより、実行結果が変化する可能性があります。モックを使用すると、毎回同じレスポンスを返すことができ、テストの信頼性を向上させることができます。 テストの速度向上 外部 API を呼び出すと、実行に時間がかかる場合があり、 タイムアウト になる可能性もあります。モックを使用すると、 API を待つ必要がないのでテストの速度を向上させることができます。 外部との依存排除 モックを使うと外部 API への依存関係を排除することができ、テスト実行環境の制約を減らすことができます。 エラーの発見と修正の易化 例えば、外部 API がレスポンスを返さない場合、モックを使用すればその API の問題を発見することが容易にできます。 モックはE2Eテストとは切っても切り離せない機能です。ぜひ、積極的に使っていきましょう。 早速ですが、テストの内容は次のようにします。 ラク スのページを開く ラク スのページ以下( https://www.rakus.co.jp/** )をモックする 「 ラク スの思い」(/about)の場合だけレスポンスを404にする 「 ラク スの思い」をクリックする 404ページが表示されているかをテスト Playwrightでモックを実装するにはroute関数を使用します。第一引数にはモックを反映するURLを、第二引数にはハンドラーを設定します。 このテストの場合は ラク スのページ以下を全てモックするので、下記のようにします。 await page.route ( 'https://www.rakus.co.jp/**' , rakusRouteHandler ); rakusRouteHandlerではURLが /about を含む時だけレスポンスを404にするという設定にしたいので、次のように実装しました。 const rakusRouteHandler = ( route: Route , request: Request ) => { const url = request.url (); if ( url.includes ( "about" )) { // レスポンスの内容 route.fulfill ( { status : 404 , } ); } else { // about以外は通常のレスポンスを返す route. continue(); } } ; requestのURLにaboutが含まれているときだけ404を返し、それ以外はroute.continue()で通常のレスポンスを返すようにしています。 これだけで簡単にモックすることができます。 テストの全コードは以下のようになります。 test ( "ラクスのページをモックする" , async ( { page } ) => { // モックするためのルートハンドラを作成 const rakusRouteHandler = ( route: Route , request: Request ) => { const url = request.url (); if ( url.includes ( "about" )) { // レスポンスの内容 route.fulfill ( { status : 404 , } ); } else { // about以外は通常のレスポンスを返す route. continue(); } } ; // ラクスのトップページを開く await page. goto( 'https://www.rakus.co.jp/' ); // トップページ以下のリクエストをモックする await page.route ( 'https://www.rakus.co.jp/**' , rakusRouteHandler ); // 「ラクスの思い」をクリック await page.locator ( '.p-header__menu__item' ) .getByText ( "ラクスの思い" ) .first () .click (); // テスト:404ページが表示されていること await expect ( page.locator ( 'h1' )) .toHaveText ( "この www.rakus.co.jp ページが見つかりません" ) } ) アサーション の部分はページのh1に「この www.rakus.co.jp ページが見つかりません」が表示されているかどうかを見ています。 テストを実行してみると、以下のように404ページが表示されていると思います。 また、 /about 以外は通常のレスポンスが返されるかも確認してみましょう。 「 ラク スの思い」をクリックする部分のコードを以下のように変えます。 await page.locator ( '.p-header__menu__item' ) .getByText ( "事業内容" ) .click () テストを実行してみると、正常に事業内容のページが表示されていると思います。 ハンドラで実装した通りにできています。 今回は簡単に404を返す実装にしましたが、route.fulfill内を変えるだけで柔軟に様々なレスポンスを実装することができます。 Playwright 拡張機能 を使おう VSCode 使っている人専用にはなりますが、Playwrightの 拡張機能 がとても便利なのでご紹介します。 まずは VSCode の 拡張機能 から「Playwright」と検索をし、おそらく一番上に表示される「Playwright Test for VSCode 」をインストールしましょう。 インストールが完了するとエディターのサイドバーに三角ビーカーのアイコンが表示されていると思います。 テストの実行 サイドバーの三角ビーカーをクリックすると、 tests/ 以下のテストが表示されていると思います。 再生ボタンを押すことで実行することが可能です。テストファイルの方にも同様に各テストの先頭行に再生ボタンが表示されます。 成功したテストは緑のチェックマーク、失敗したテストは赤の バツ マークがつきます。 実行ブラウザの変更 拡張機能 を入れる前は playwright.config.ts のprojectsを書き加えたり、 コメントアウト したりして実行するブラウザを変えていました。 拡張機能 の場合は三角ビーカーをクリック、下画像の「Select Configuration」をクリック、「既定のプロファイルの選択」をクリックする。 下画像のようなセレクトボックスが表示されるのでここで実行したいプロジェクトを選択して、OKを押せば簡単に実行ブラウザを変えることが可能です。 ヘッドレスモードの選択 拡張機能 を入れる前はheadressをfalseにすることで実行中の画面を表示していましたが、テストの中にはわざわざ画面で見る必要のないものもあると思います。そのようなテストを実行するたびにheadressモードにしたりするのはとても面倒です。 拡張機能 の左下、「Show browser」という チェックボックス があります。 ここにチェックを入れるとheadressをfalseにしている状態と同じになります。チェックを外すとheadressモードになります。 playwright.config.ts に書いたheadressの設定は消さないとチェックしてもしなくても画面が表示されてしまうので注意です。 要素の取得 「Show browser」の下にある「Pick locator」をクリックするとブラウザが立ち上がります。そのブラウザでテストしたい画面に移り、欲しい要素をクリックすると、下画像のように VSCode 上部に要素のコードを記してくれます。 簡単に要素を取得したい分にはこれだけで十分です。 テストコードの生成 「Pick locator」の下にある「Record new」をクリックするとブラウザが立ち上がります。 ブラウザが立ち上がるとともに、 VSCode 上で新たなテストファイルも作成され、テストの雛形が書かれていると思います。 立ち上がったブラウザ上で操作をすると、全てこのファイル内にコードが生成されていきます。コードジェネレータと同じです。コマンドでジェネレータを立ち上げなくて済むのでかなり楽です。 テストの大枠をジェネレータに生成してもらい、細かい部分を自分で修正したりすることでかなり効率化できると思います。 既存のテストファイルにコードを生成して欲しい方は「Record at cursor」をクリックしてください。 コードを生成して欲しいファイルの適当な行を選択しておきましょう。 立ち上がったブラウザで操作すると、選択した行にコードが書き加えられていきます。 例えば、「簡単なテストを書こう」のテストと同じ操作をすると下記のようなコードが example.spec.ts に生成されました。 await page. goto( 'https://www.google.com/' ); await page.getByRole ( 'combobox' , { name: '検索' } ) .click (); await page.getByRole ( 'combobox' , { name: '検索' } ) .press ( 'Enter' ); await page.getByRole ( 'link' , { name: '企業の成長を支援するクラウドサービス | 株式会社ラクス ラクス https://www.rakus.co.jp' } ) .click (); 検索欄への文字入力などが認識されていませんが、そこは自分で付け足せば、簡単にテストが作れます。 とても便利なのでどんどん使ってください。 おわりに 今回はPlaywrightを初めて書く人やE2Eに興味を持っている方に向けて、簡単なテストの例と 拡張機能 についてご紹介させていただきました。 Playwrightは直感的にテストを書くことができるので初めての人でもすぐに飛びつくことができ、コードジェネレータと組み合わせることで高速にテストコードを生み出すことも可能です。 まだまだ紹介していない機能がありますので、 公式サイト を見ていただけばなと思います。 とても長い記事になりましたが、ここまで読んでくださった方、ありがとうございます。 これを機にPlaywrightを使っていただければ、とても嬉しいです。
アバター
こんにちは、新卒2年目になりました菊池(akikuchi_rks)です。 新卒1年目では開発エンジニアとして様々な経験をさせていただきましたが、その1つとしてLaravel8→9へのバージョンアップ作業を行いました。 今回はこのLaravel9へのバージョンアップにおいて自分が躓いた経験から、注意が必要だと感じた点を紹介させていただきます。 はじめに 注意すべきこと 依存パッケージの確認 Trusted Proxiesファイルの修正 app/Http/Middleware/TrustProxies.php の修正 $headersの修正 Storageファサードを利用したファイル操作の仕様変更 ファイル書き込み時の仕様 略語を使用したメソッド名の変更 Unvalidated Array Keysの仕様変更 終わりに はじめに Laravel9が2022年2月8日、正式リリースされました。 標準 メーラー の変更、Flysystem3.xへのバージョンアップなど様々な変更点がありますが、これらの変更点に関する説明は他の記事で既にまとめられているため本記事では割愛させていただきます。 弊社でも毎月開催している勉強会「 PHP TechCafe」において『PHPerのための「Laravel9について語る」』というテーマで議論した回があり、 その内容のレポート記事で、Laravel9の変更点について簡潔にまとめられているので、良ければ参考にしていただければと思います。 tech-blog.rakus.co.jp Laravelのバージョンアップを行う際は、基本的に Laravel公式のアップグレードガイド でアップグレード手順を確認しながら作業を行うことになると思います。 手動で変更する必要がある修正内容については、ほとんどこちらの公式ドキュメントで説明されているのでこちらを参考に修正を行えば問題ありませんが、私が実際にバージョンアップ作業を行う中で 公式ドキュメントに書かれておらず修正が漏れていた 公式ドキュメントの説明を正確に理解出来ていなかった など、いくつか躓いた点がありました。 本記事ではこのような躓いた経験をもとに、注意すべきだと感じた点を紹介させていただきます。 注意すべきこと 依存パッケージの確認 バージョンアップ作業に入る前の話になりますが、アプリケーションの依存パッケージを事前に確認する必要があります。 特に、Laravel9ではSymfony6.x系に依存していますがSymfony6.x系をまだサポートしていないcomposerパッケージが意外とあります。 このような場合は、 別のパッケージを使用する パッケージを自力でカスタムする などの対応が必要になります。 Trusted Proxiesファイルの修正 Laravel9へのバージョンアップ対応において、Composerの依存パッケージの変更以外で手作業での修正が必須となるのはこちらの項目くらいだと思います。 そのため、Laravel9へバージョンアップ後にアプリケーションが動かなくなったという場合はまずはこちらの修正が正しく行えているかを確認すると良いと思います。 app/Http/Middleware/TrustProxies. php の修正 変更前 use Fideveloper\Proxy\TrustProxies as Middleware 変更後 use Illuminate\Http\Middleware\TrustProxies as Middleware $headersの修正 変更前 protected $headers = Request::HEADER_X_FORWARDED_ALL; 変更後 protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_POST | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB ; 因みに TrustProxies.php 以外のファイルでも HEADER_X_FORWARDED_ALL の定数が使われている場合は修正が必要です。 私の作業を行ったプロジェクトでも他のファイルで HEADER_X_FORWARDED_ALL が使われていましたが、そのことに気づかず修正が漏れていたことが原因でサービスが動きませんでした。 Storage ファサード を利用したファイル操作の仕様変更 Laravel9ではFlysystem 1.x から 3.xへの移行を行っています。 それに伴い、Storage ファサード を利用したファイル操作の仕様が一部変わっています。 ※参考:Flysystem公式ドキュメント flysystem.thephpleague.com ファイル書き込み時の仕様 特に注意すべきなのは、 write() 、 writeStream() でのファイルの書き込み時の仕様変更です。 Flysystemの公式ドキュメントにも書かれているように、以下の表のように書き込み関連のメソッドの仕様が変更されました。 Flysystemの書き込みメソッドの仕様変更表 Flysystem 1.xでは書き込み用のメソッドと更新用のメソッドが別で用意されており、 write() が上書き処理をすることはありませんでした。 また、書き込みと更新を自動で使い分けてくれるメソッドとして put() が用意されていました。 Flysystem 3.xでは以前の put() の機能を write() が担うようになり、 wirte() と update() は削除されました。 この仕様変更に伴い、Laravel9のStorage ファサード でput()やwrite()を使用した際は、どちらもFlysystemの write() が呼び出され、自動的に既存ファイルを上書きするようになりました。 もし、既存のファイルを上書きしたくない場合は、手動でファイルの存在確認をする必要があります。 この対応を怠ると、意図せず既存ファイルを上書きしてしまう、という事象が発生してしまうため注意が必要です。 略語を使用したメソッド名の変更 こちらはLaravel公式のアップグレー ドガ イドに載っていない内容ですが、該当のメソッドを使用している場合は対応が必要です。 Flysystem 1.x から 3.xへの移行の影響で省略された単語を含むメソッド名が省略なしの単語を使ったメソッド名に変更されました。 Laravelでは Illuminate\Filesystem\FilesystemAdapter.php に定義されていないメソッドをStorage ファサード で呼び出した際、Flysystemのメソッドを直接呼び出す仕組みがありますが、 この仕組みを利用して、今回変更があったメソッドを直接呼び出している場合は、変更後のメソッド名に修正する必要があります。 以下が変更されたメソッド名の例です。 変更前 $filesystem- > createDir($path); $filesystem- > deleteDir($path); 変更後 $filesystem->createDirectory($path); $filesystem->deleteDirectory($path); Unvalidated Array Keysの仕様変更 配列に対してバリデーションを行った際の挙動が変わりました。 具体的にはLaravelのバリデータが返すバリデーション済みデータにバリデーションしていない配列キーを含むかどうかが変わっています。 このあたりの仕様変更については、ドキュメントを読んだだけではいまいちピンと来なかったため、具体例を用いて説明しようと思います。 例えば 'staff' => ['name' => '田中太郎', 'age' => 26] のような配列のバリデーションをする場合を考えてみます。 <?php // ①バリデーションしていない配列キーを含む設定がされている場合 $ validated = $ request -> validate ([ 'staff' => [ 'array' ] , 'staff.name' => [ 'string' ] , ]) ; var_dump ( $ validated ) ; // array(1) { // ['staff']=> // array(2) { // ["name"]=> // string(12) "田中太郎" // ["age"]=> // int(23) // } //} // ②バリデーションしていない配列キーを除外する設定がされている場合 $ validated = $ request -> validate ([ 'staff' => [ 'array' ] , 'staff.name' => [ 'string' ] , ]) ; var_dump ( $ validated ) ; // array(1) { // ['staff']=> // array(1) { // ["name"]=> // string(12) "田中太郎" // } //} 上記の例のように配列の特定のキーに対してバリデーションを行わなかった場合に、 validate() で返されるデータがそのキーを含むかどうかに違いがあります。 以前のリリースのLaravelではデフォルトで①、アプリケーションのサービスプロバイダの boot() 内で excludeUnvalidatedArrayKeys() を呼び出している場合は②の挙動となる仕様でした。 一方で、Laravel9ではデフォルトで②、アプリケーションのサービスプロバイダの boot() 内で includeUnvalidatedArrayKeys() を呼び出している場合は①の挙動となる仕様に変更されています。 そのため、推奨はされていませんが、以前のリリースの動作を保つためには includeUnvalidatedArrayKeys() を呼び出す必要があります。 因みに、配列の要素に対してバリデーションを全く設定していない場合は②のようなバリデーションしていない配列キーを除外する設定にしていても、 validate() で返されるデータに全てのキーが含まれる挙動になっているようなので注意が必要です。 <?php $ validated = $ request -> validate ([ 'staff' => [ 'array' ] , ]) ; var_dump ( $ validated ) ; // array(1) { // ['staff']=> // array(2) { // ["name"]=> // string(12) "田中太郎" // ["age"]=> // int(23) // } //} 終わりに 今回は、Laravel8からLaravel9へバージョンアップ作業を行う際の注意点について紹介させていただきました。 Laravel9は残念ながらLTSではなくなったので、Laravel10へのバージョンアップ作業も早々に取り組む必要がありそうですね、、 Laravel10へのバージョンアップを行う際は、今回の経験を生かしてよりスムーズに作業を終えられるよう頑張りたいです。 以上です。 本記事が少しでもLaravelバージョンアップ時の手助けになれば嬉しいです。
アバター
こんにちは。配配メール開発課のmoryosukeです。 最新のLaravelではデフォルトのフロントエンドビルドツールがLaravel MixからLaravel Viteへと移行しました。 そこでTailwind CSS をビルドする手順を追いながらLaravel Viteに慣れていこうと思います。 Laravel Viteとは 事前準備 プロジェクト作成 Tailwind CSSを導入する 補足 最後に Laravel Viteとは Laravel ViteはLaravel用の高速なフロントエンド・ビルドツールであり、 JavaScript のパッケージマネージャである Vite を使用しています。Vite は、フロントエンドのアプリケーションを高速に開発するためのツールであり、Webpack よりも高速で、リアルタイムのホットリロードや開発時の最適化機能を提供しています。 Laravelのバージョンが9.18以前はLaravel Mixがデフォルトで使用されていました。Laravel ViteとLaravel Mixの性能比較は以下の資料が参考になります。 speakerdeck.com readouble.com 事前準備 プロジェクト作成 今回はLaravel Sailを使ってプロジェクトを作成していきます。 Docker Desktopをインストール後、プロジェクトを作成する ディレクト リ下で以下のコマンドを実行します。 curl -s https://laravel.build/vite-sample | bash プロジェクト作成が完了するとvite.config.jsという設定ファイルが追加されていることが確認できます。 ここで以下のような設定が可能です。 root:プロジェクトのルート ディレクト リを指定する。 base:公開されるアセットのベースパスを指定する。 build:ビルドに関する設定を指定する。target、minify、outDirなどのプロパティがあります。 plugins:Vite プラグイン を追加するための設定です。例えば、PWA プラグイン やSass プラグイン などがあります。 server:開発サーバーの設定を指定する。port、host、proxy、 https などのプロパティがあります。 resolve:importやrequireで解決するファイルの拡張子などの設定を指定する。 ここでは、開発サーバの設定だけを追加で行います。 vite.config.js import { defineConfig } from 'vite' ; import laravel from 'laravel-vite-plugin' ; export default defineConfig( { ///ここから server: { hmr: { host: 'localhost' , } , } , ///ここまで追加 plugins: [ laravel( { input: [ 'resources/css/app.css' , 'resources/js/app.js' , ] , refresh: true , } ), ] , } ); Tailwind CSS を導入する 以下のコマンドでTailwind CSS をインストールします。 ./vendor/bin/sail up -d sail npm install -D tailwindcss postcss autoprefixer sail npx tailwindcss init -p すると、tailwind.config.jsという設定ファイルが作成されます。 ここで以下のような設定が可能です。 theme:主に色、フォントサイズ、余白などの設定を変更するために使用されます。例えば、ヘッダーの高さやフォントファミリー、リストアイテムのインデント、ボックスシャドウのカラーやサイズなどを指定できます。 variants:各クラスに適用される状態を指定するために使用されます。例えば、hoverやfocus、activeなどの状態に応じたスタイルを定義することができます。 plugins:Tailwind CSS に新しい機能や変数を追加するために使用されます。例えば、フォントサイズを変数にまとめたり、新しい テキストエフェクト を追加することができます。 ここでは、デフォルトのままとします。 tailwind.config.js /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./resources/**/*.blade.php" , "./resources/**/*.js" , "./resources/**/*.vue" , ] , theme: { extend: {} , } , plugins: [] , } 次にresources/ css /app. css に以下のコードを追加し、Tailwindのプリセットを使用できるようにします。 resources/ css /app. css @tailwind base ; @tailwind components; @tailwind utilities; これらのプリセットを使用することで、プロジェクトにTailwind CSS を適用するために必要な基本スタイル、 コンポーネント 、およびユーティリティクラスがインポートされます。 これでビルド時にTailwind CSS が読み込まれるようになりました。 bladeから CSS を呼び出すと完成です。 <!doctype html> < html > < head > < meta charset = "utf-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > @vite('resources/css/app.css') </ head > < body class = "bg-gray-400" > < h1 class = "text-3xl bg-red-500 font-bold underline" > Hello world! </ h1 > </ body > </ html > 以下のコマンドを入力すると開発サーバに CSS が配置されます。 sail npm run dev アクセスすると以下のような画面が表示されます。 補足 上記で実行した sail npm run dev は、開発サーバーを起動するためのコマンドであり、プロジェクトのファイルを監視し、変更があった場合に自動的に再ビルドして更新されたコードを提供します。このコマンドは、開発中に使用され、 ソースコード の変更を即座に確認することができます。 本番環境用にビルドする際は、 sail npm run build を使用します。これにより、public ディレクト リにファイルが出力され、本番環境にデプロイできる状態になります。 最後に いかがだったでしょうか。今回はTailwind CSS の導入でしたが、Vue.jsやReactも同じように簡単な手順で導入することができます。 また、Laravel Mixからの移行もできるため、ぜひ試してみてください。
アバター