TECH PLAY

株式会社ラクス

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

941

はじめに こんにちは akihiyo76 です。Swift Concurrency が WWDC で発表されてから 2 年になりました。各プロダクトではサポートバージョンがアップデートされ、実際に導入が進み始めているプロダクトも多いのではないでしょうか。一方で新規で開発する場合は、前提となる技術だと考えています。弊社でも Swift Concurrency への移行対応を行いましたが、今回は実際に行った導入戦略を紹介したいと思います。 はじめに 導入するメリット 1. 並行処理を簡潔・安全に記述できる 2. データの競合やデッドロックを回避(品質向上) async / await Sendable Actor Task 既存プロジェクトへの導入 1. PoCコードの実装 Strict Concurrency Checking の設定 実装方針の決定 2. スコープ分割 3. 横展開・テスト まとめ 参考 導入するメリット では、動いている既存コードを修正して Concurrency を導入するメリットは何でしょうか。 実際に対応を進める場合、実装コストだけではなく、品質を担保するためのテストコストも必要になります。更にプロダクトによってはリリースコストが必要になるため、そのコストに合うメリットが要求されます。 そこで、実際に Concurrency を導入するメリットについて考えてみたいと思います。 1. 並行処理を簡潔・安全に記述できる まず、Concurrency のメリットとして「並行処理を簡潔・安全に記述できる」という点が挙げられます。 実際のコードを比較してみましょう。以下は従来の Block 構文で実装した通信周りのコード例です。 func fetchImageData (request : URLRequest , completionHandler : @escaping ( UIImage? ) -> () ) { self .session.dataTask(with : request ) { data, response, error in // Image をロードする self .loadImage(data : data ) { image in // Image サイズが適切かチェックする self .checkImage(image : image ) { completionHandler(image) } } }.resume() } この実装例では completionHandler() でコールバックを繋げる実装になっており、それぞれの処理がネスト構造を形成しています。これによりコードの複雑性が増し、分岐処理やエラー処理が追加されると更に実装が複雑になり、可読性も低下し、品質にも影響を及ぼす可能性があります。複雑性のために completionHandler() の記述を忘れた場合、特定の処理でコールバックが得られずアプリの処理が止まるリスクも考えられます。 それでは、この Block 構文で記述されたコードを Concurrency を使用して書き換えてみましょう。 func request (url : URL ) async throws -> UIImage? { let (data, response) = try await URLSession.shared.data(from : url , delegate : nil ) let image = try await loadImage(data : data ) let result = try await checkImageSize(image : image ) return result } async / await を使って書き換えることで、Block 構文の多段ネストが解消され、非同期の実装を簡潔に記述できるようになります。 ただし、コードスタイルや可読性の向上だけで Concurrency 移行のコストを検討するのは、割に合わないかもしれません。 2. データの競合や デッドロック を回避(品質向上) Concurrency 導入のメリットは、データ競合や デッドロック を回避できることです。具体的には Sendable や Actor といった機能の恩恵によるものですが、これについては後ほど具体的に説明します。Concurrency を導入し、これらの機能に準拠することで、品質の確保・改善に期待できる点が大きなメリットだと考えています。 async / await 関数の先頭に async(async throws) を定義することで、その関数を非同期関数として定義できます。定義した非同期関数を実行するためには、 await を使用する必要があります。 func execute () { Task.detached { do { let url = URL(string : "https://api.example.com" ) ! // 実行完了まで待機する let response = try await self .request(url : url ) // 後続処理 } catch { print(error.localizedDescription) } } } // 非同期関数として定義する func request (url : URL ) async throws -> HTTPURLResponse { // 通信処理 return result } このように、 async / await を使って定義することで、非同期関数の定義と実行が可能になります。 Sendable Sendable はデータ競合が起こらないことを コンパイル 時に保証してくれる型で、データ競合を避けて安全に渡せるデータを表す概念として導入されました。Sendable プロトコル に準拠することによって、その型が Sendable であることが コンパイラ に伝えられます。 final class Valid : Sendable { // 定数定義 let name : String init (name : String ) { self .name = name } } final class Invalid : Sendable { // 変数定義 var name : String init (name : String ) { self .name = name } } Valid と Invalid は共に Sendable プロトコル に準拠していますが、name については定数と変数で定義しており、Invalid の name は公開されているため変更が可能であり、データ競合が生じる可能性があります。この状態で コンパイル すると、 Stored property 'name' of 'Sendable'-conforming class 'Invalid' is mutable という コンパイラ のデータ競合警告が発生します。 Actor Swift 5.5 から導入された Actor は Concurrency の一部として、データ競合を防ぐ型です。Actor により作成された インスタンス は、一つのデータへのアクセスが同時に行われないように制御されます。これを Actor isolated と呼び、複数のタスクがデータにアクセスする際にもデータ競合を防ぐことができます。 actor MainActor { // 変数定義 var number : Int = 1 } func increment () { let act = MainActor() Task.detached { // number を更新 await act.number = 100 } } このように Actor で生成された インスタンス の number を変更しようとすると、 Actor-isolated property 'number' cannot be mutated from a Sendable closure という コンパイル エラーが発生します。 Task Task は並行処理を実行する単位で、複数のタスクの構造化(Task Tree)も可能になります。以下のコードのように、 withTaskGroup を使用してタスクグループ(親タスク)を作成し、要素となる子タスクを追加・実行することができます。 func getUser () async -> UserInfo { // 親タスク await withTaskGroup(of : DataType.self ) { group in // 子タスク A group.addTask { let friends = await self .getFriends() return DataType.friends(friends) } // 子タスク B group.addTask { let notes = await self .getNotes() return DataType.notes(notes) } } return UserInfo(friends : friends , notes : notes ) } こうした async / await 、 Sendable 、 Actor 、 Task といった機能を使用することで、Swift Concurrency を活用した効果的な非同期処理を実現できるようになります。 既存プロジェクトへの導入 ここからは既存プロジェクトへの導入方法について紹介します。導入の主な流れとしては、以下のようになります。 PoCコードの実装(Concurrency 設定と実装方針の決定) スコープ分割 各スコープごとの対応 リグレッション テスト 大まかな対応方法としては、実装方針をFIXしてチーム全体で一気に対応していくという流れです。 1. PoCコードの実装 Strict Concurrency Checking の設定 導入に際しては、実装方針(Concurrency の記述方法)を決定する必要があります。まず最初にプロジェクトの設定として、 Strict Concurrency Checking の設定を行います。 設定には Minimal、Targeted、Complete の 3 種類がありますが、 Targeted に設定します。 Minimal Sendable の制約を、明示的に採用された場所にのみ強制し、コードが並行性を採用した場所では actor-isolated チェックを実行します Targeted Sendable の制約を強制し、コードが並行性を採用した場所( Sendable を明示的に採用した場所も含む) actor-isolated チェックを実行します Complete モジュール全体で Sendable の制約と actor-isolated の分離チェックを強制します Complete はモジュール全体に大きな影響を及ぼすため、導入時の制約としては厳しすぎると判断しました。 参考: https://developer.apple.com/documentation/xcode/build-settings-reference#Strict-Concurrency-Checking 実装方針の決定 Strict Concurrency Checking の設定後は、基本的な Concurrency の実装パターンを作成します。 通信実装などのコアな部分が多く対象になると考えられるため、同じ責務の Concurrency 関数を準備します。最終的には既存の実装を削除するため、 @available で deprecated を指定しておきます。以下は withCheckedThrowingContinuation を使用して Concurrency に変換していますが、async / await を使用しても同様に変換可能です。 @available ( * , deprecated , message: "This function is deprecated. Use the requestAsync () instead." ) func request (request : URLRequest , completionHandler : @escaping ( Data? ) -> () ) { self .session.dataTask(with : request ) { data, response, error in completionHandler(data) }.resume() } func requestAsync (request : URLRequest ) async throws -> Data? { return try await withCheckedThrowingContinuation { continuation in self .session.dataTask(with : request ) { data, response, error in if let error = error { continuation.resume(throwing : self.handleNSURLError (error : error as NSError )) return } continuation.resume(returning : data ) }.resume() } } また、requestAsync() の呼び出し元クラスも変更します。以下の fetchUserData() は View 側から実行される想定なので、 @MainActor を付けて Main Thread から実行できるようにし、また nonisolated を指定して非分離メソッドとして指定します。 @MainActor class ViewModel { private let apiService = ApiService() nonisolated func fetchUserData () async throws -> Data? { do { let urlRequest = await self .createUrlRequest() return try await apiService.requestAsync(request : urlRequest ) } catch { throw await self .handleError(error) } } } 最後に fetchUserData() の呼び出しを Task 内で行います。Task の定義は、プロジェクトの アーキテクチャ に合わせて行うのが良いでしょう。 override func viewDidLoad () { super .viewDidLoad() Task { do { let userData = try await viewModel.fetchUserData() self .updateUser(userData) } catch { // エラー処理 } } } } 2. スコープ分割 導入の基本方針が確定したら、具体的な進行方法を検討します。大規模なプロジェクトでは修正が多岐にわたることが予想されますが、一度に行う変更が大きいと、レビューや品質管理が難しくなる可能性があります。そのため、一定の粒度で分割し、対応を進めることで迅速な対処が可能です。チーム全体で進行する場合、各メンバーが実装とレビューを同時に行うことが望ましいでしょう。もし修正すべき箇所が多い場合は、 スプレッドシート で一覧化しておき、ファイルごとやクラスごとなど明確な区分け方法にすることで、作業の分担が容易になります。 3. 横展開・テスト 修正すべきスコープが確定したら、あとは一気に修正を進めます。チームの状況によっては、徐々に移行する方法ありますが、他の機能の実装も進行中の場合、コンフリクトが発生する可能性があるため、一気に対応する方法をオススメします。最後に、@available deprecated を指定した関数を削除することを忘れずに行いましょう。導入実装が完了したら、 リグレッション テストを実施して機能 デグレ がないことを確認します。 まとめ Swift Concurrency の主要な機能に焦点を当てながら、既存のプロジェクトにおける導入戦略を紹介しました。今回紹介した導入戦略は、私自身が実践した方法ですが、既存の実装を保ちながら進行したため、多少冗長な箇所があるかもしれません。これはあくまで一つの戦略として、Swift Concurrency の導入を支援するための手助けとなることを願っております。 参考 developer.apple.com
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ 弊社では先日 テスト駆動開発 (以降、TDDと呼ぶ)ハンズオン勉強会を開催しました! 今回の記事の内容はズバリ2つ 誤解してる!? テスト駆動開発 の良さ!学ぶ事の意味! TDDハンズオン勉強会を開催する意図や実施内容、感想! 読者のターゲットは TDDを誤解している人 TDDハンズオン勉強会を弊社でもやろう!とか思ってる人 を想定していますっ。 誤解されがちなTDD、記事にするには書ききれないTDD...なるべく小難しい内容は省いて興味を持ってもらうための記事を書いてみようと思います! テスト駆動開発(TDD)は良い物だ! テスト駆動開発(TDD)とは何か? TDDに対する誤解 TDDハンズオンについて TDDハンズオンの趣旨 TDDハンズオンの計画 事前準備 スケジュールと概要 TDDハンズオンの感想(反省点や振り返り) 動画は事前に見てもらうべきだった TODOリストの作成にもう少しゆとりを持つと良い まとめ 最後に 参考文献等 テスト駆動開発 (TDD)は良い物だ! テスト駆動開発 (TDD)とは何か? プログラム開発手法の一種で、プログラムに必要な各機能について、最初にテストを書き、そのテストが 動作する必要最低限な実装をとりあえず行なった後、コードを洗練させる、という短い工程を繰り返すス タイルである (Wikipediaから抜粋) よくTDDについて調べてみると、だいたい上のような説明に始まり、Red/Green/RefactorのサイクルがTDDであると学んで終わってしまう。でも、この記事に辿り着く読者は「そんな事はわかっている」という人が大半だと思うので、あえてここではこう説明しておきます。 「動作するきれいなコード」。Ron Jeffriesのこの簡潔な言葉が、テスト駆動開発(TDD)のゴールだ。 動作するきれいなコードはあらゆる意味で価値がある。 (「テスト駆動開発」著 Kent Beck 訳 和田卓人のまえがきから抜粋) TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング 2020年にあった和田さんによる基調講演も「TDDとは何か?」という話はここから始まります。是非この動画は見て頂きたいのですが、抜粋して言うと、 「動作するきれいなコードを書く事は、品質・コスト・開発速度どの側面から見ても重要な要素であり、それを実現するのにTDDは1つの解である」 といった内容に感じました。 このゴールから始まる「まえがき」の書き方も、よくよく考えるとTDDっぽくて感慨深いですね!グレイトっ(笑) では、「そんなにTDDが良いのならなぜもっと普及しないのか?」少しだけTDDに対する誤解について話しておこうと思います。 TDDに対する誤解 テストコードを書く事が一般的になった昨今ですが、そんな中で 「TDDは先にテストコードを書く事」という偏った認知 が広がっていったようです。これは誤った認識だという事は声を大にしてお伝えしたいです。 TDDが テストファースト である事には複数の意味がありますが、私が一番しっくりきた解釈としては、 詳細設計と実装の狭間に「どんな風にクラスを分けて、どんなメソッドを定義してあげると良いだろうか?」というのをテストコードという形で箇条書きしている開発フロー だと感じました。 思いつきで書いた動作するコードは、得てしてテストコードが書きづらく、既存実装に対して書きやすさで実装が進んでしまいます。なので、先にどのクラスにどんなメソッドが存在していてほしいかをテストコードとして記載していく事で、本来あるべき実装・振る舞いを実装できる。 また、 「TDDはテストコードの開発に時間がかかる」 というのもよく聞く話ですが、先ほど紹介した動画の中でも最後に解説されていますが、テストの経済性というのは実際そんなに悪くはなく、少なからず担当者が離職する間隔よりも短いタイミングで元は取れるようです。 また、ここであえて「離職する間隔よりも短い」という言い回しをさせてもらったのは、実体験的に当事者意識を持てるか?というのが案外大事なのではないかと思ったから補足させてもらいました。内部品質の高いコードを書く事の見返りは自分にもある事を知っていてほしいです。 xUnit Test Patternsより抜粋 ※上の画像は、先ほど解説した動画の最後の方で出てくるグラフです。 無駄にメンテナンスが困難なテストコードを量産しているとメンテナンスコストが高くなる事もあるので注意です。 ※本来の テスト駆動開発 をやればこうはなりませんよ!深く考えずテストを後から書くような開発してるとこうなっちゃう現実が待ってるという注意喚起です。 TDDハンズオンについて ある意味、ここからが本題ですねっ! TDDハンズオンの趣旨 「TDDの良さを知ってもらって、普段の開発に活かしてほしい。」という事で開催されました。 先にも記載した通り、TDDというのは名前 からし ても誤解されがちな傾向にあり、本当の良さって伝わりにくいものです。 実際に手を動かしてはじめて「あ、確かに開発の流れがしっくりくる!安心感がある!」そんなTDDだからこそハンズオンをやる意味があるのでは? と考えました。 TDDハンズオンの計画 事前準備 TDDを実施する開発環境として Java が動く開発環境が必要になりますが、手間であれば Codinggame.com のようなサイトでやってもらっても良いかと思います。 www.codingame.com ※弊社も一部の社員はこちらを使いました。普段 PHP で開発していて Java 環境がない社員等。 スケジュールと概要 計画 1日目: 弊社では1日目を TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング を一緒に視聴する時間に費やしました。ハンズオン本編以外の部分をスキップしたり、1.25倍速で再生して1時間半に無理矢理短縮しました^^; まずは基礎知識としてTDDの流れについてちゃんと理解してもらいたかったためです。 2日目: うるう年問題は以下としました。 入力した整数がうるう年かどうかを判定するプログラムを書け。 うるう年のときは「true」、それ以外は「false」とプリントすること。 うるう年の定義 ・西暦年号が4で割り切れる年をうるう年とする ・ただし、西暦年号が100で割り切れ、かつ400で割り切れない年は平年とする。 私の所感としては、1時間半でやるハンズオンであれば、丁度いい難易度かなと感じました。 タイムスケジュールとしては以下としました。 うるう年についてのTODOリストの整理(10分) 2チームからTODOリストを発表(6分) ※これを参考に後半タスクを進めてもらう テストコードのサイクル開始(Red/Green/Refactor)(30分) 2チームからTDDの結果を発表(6分) バッファ(20分) TDDハンズオンの感想(反省点や振り返り) 動画は事前に見てもらうべきだった 参加者から頂いたコメントにも 「開催回数が増えてもよいのでビデオは早回し再生ではなく通常再生が良かったです。」 といったご意見がありました。 確かに1.25倍速で抜粋して見てもらったのは苦肉の策で、計画段階でも悩んだポイントでした。 「参加者は事前にこれを見て来てください(倍速で見るとかはお任せ)」とした上で、重要ポイントだけピックアップして解説してから始めるぐらいにした方が、円滑に進んだかも! TODOリストの作成にもう少しゆとりを持つと良い 動画を見てもらうとわかるかとは思いますが、TODOリストとは「どんな振る舞いをしてほしいのかを箇条書きにした仕様書のようなもの」です。 TODO作成時間に時間を割いても良かったように感じました。 TODOがしっかりできていれば後はそれ通りに実装するだけなので、 割合的にはTODO作成時間が多くなるイメージだと個人的にはもう少しうまくできたのかなと思いました。 上記のような意見も後日アンケートで頂きました。 確かに10分なんかじゃ全然足りなかった!初回のTODOリスト作成は20分ぐらいはせめてあるべき、30分でも良いと感じました。 3分クッキングみたいに「出来上がったTODOリストはこちらです」って提示するのは一案かもしれないんですが、ハンズオンの意味が色褪せちゃいますよねOrz せっかくのハンズオンなのでもう少し時間にゆとりがあれば良かったです!反省! まとめ 反省すべき点はありますが、30名程参加頂き、総じてTDDハンズオンの実施で新しい学びがあったという意見は多く、有意義な時間だったなと思います! TDDハンズオン風景(大阪) TDDハンズオン風景(東京) エンジニアは万能ではないので(少なからず私は)ミスする事は多々あります。TDDはそういった普通の人でも、正しい実装を安心して開発していける手法です。 是非、ブログ記事1つで理解したつもりにならず、本を読んだり、TDDハンズオンを実際に体感して、一考してもらえればなと思います。 最後に 和田さんに感謝!TDDブートキャンプの基調講演の動画とても参考になりました。 和田さんの前で「ちゃんとテスト書いてます!」って言えるようにがんばるます! 参考文献等 「テスト駆動開発」著 Kent Beck 訳 和田卓人 TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング xUnit Test Patternsから学ぶユニットテストの6つの目指すべきゴール Codinggame.com
こんにちはサッカー大好き@neroblubrosです。 ラク スでは月に一度ですが、定時後にビアバッシュを行っています。 開発部隊は東京オフィスと大阪オフィスにいますが、それぞれでビアバッシュを開催しています。 今回は大阪オフィスで開催しているビアバッシュについて紹介いたします。 ビアバッシュとは 6月レポート 1人目「社会人1年目を振り返ろう」 2人目「社会人1年目の振り返り」 3人目「やらかし事例集」 4人目「PHPMDでコード品質を計測する」 5人目「RSSで手軽に更新通知」 6人目「ChatGPTでブログを書いてみた」 7人目「おもしろいDB設計本ミッケ!」 7月レポート テーマ枠は「新卒に伝えたいこと」 4人目「新卒の方々にお伝えしたいこと」 5人目「君たちはどう生きるか」 6人目「新卒に伝えたいことLT」 7人目「最年長のプロダクトに居座り続ける新卒入社のパイセンから伝えたい ”これだけはやっておけ”」。 8人目「新卒の皆さんに知ってほしい!チャットディーラー開発課」 9人目「PHPを支えるコミュニティたち」 次回の8月ビアバッシュの予告 ビアバッシュとは まずはここで軽くビアバッシュの説明をします。ビアバッシュとは、 シリコンバレー が発祥で ビア(beer):ビール バッシュ( bash ): どんちゃん 騒ぎ を組み合わせた造語で、ビールと軽食を食べつつアウトプットする場です。 ラク スの大阪オフィスでは2015年から始め、途中コロナ禍では開催できなかったときやオンラインで開催していましたが、毎月継続して開催しています。 コロナが5種になったことから今年度からアルコール+オフラインでの開催が再開しました。 軽食とアルコールとソフトドリンクが振る舞われますが、それらの費用は会社持ちです! 出席は自由で発表や出席をすると人事評価につながります。 大阪オフィスでは各課の代表者がビアバッシュ推進委員を構成してビアバッシュの運営しています。 以下、 ラク ス大阪オフィスのビアバッシュの内容です。 みんなで語り合うのではなく、希望者が発表をして聞く、質問あり。 発表内容はテーマ枠と自由枠がある。 テーマ枠は新入社員の自己紹介、トラブル事例など 自由枠はITや仕事に関することならなんでもOK。仕事に関係のない趣味の発表とかはNG 発表時間はテーマ枠と自由枠ともに5分~10分 ここから大阪オフィスで6月と7月に開催したビアバッシュの内容をざっと紹介します。 6月レポート 軽食はピザ+ナゲット+ポテトフライ。 最初の3名はテーマ枠の「社会人1年目の振り返り」です 1人目「社会人1年目を振り返ろう」 1人目は「社会人1年目を振り返ろう」です。 コロナ禍に入社した新卒3年目の発表で、新卒研修を終えて、配属部署での研修の話でした。 緊急事態宣言下に研修をすることの苦労話ややらかしたこと、逆に良かったことの発表でした。 2人目「社会人1年目の振り返り」 2人目は「社会人1年目の振り返り」で、前職で社会人1年目に経験したことの発表です。 スマホ アプリの機能追加で最新版をインストールすると起動できなくなり、リリース直後にユーザからのクレームが殺到したなど、いくつかの苦い経験を暗くなりすぎないように、面白おかしく発表してくれました。 個人的には 東日本大震災 の 計画停電 で、フロアの照明が定時で消灯されるため、デスクライトをつけて残業したことが印象に残りました。 3人目「やらかし事例集」 3人目は「やらかし事例集」で、2人目と同じような内容ですが、タイトル通り「自分がやらかしたこと」の発表です。 検証環境のつもりが接続先を間違えて本番環境を変更した 本番環境にテストデータを登録してしまった など、やらかし事例集をクイズ形式での発表でした。 不謹慎ですが、他人の不幸は蜜の味ということで、一番盛り上がった発表となりました。 次からは自由枠です。 4人目「PHPMDでコード品質を計測する」 4人目は「PHPMDでコード品質を計測する」です。 ソースコード の分岐や繰り返し処理がどれくらいあるかを表した数値が循環的複雑度といい、 オープンソース の静的解析ツールで配配メールの循環的複雑度を計測した結果の発表でした。 5人目「 RSS で手軽に更新通知」 5人目は「 RSS で手軽に更新通知」。 RSS を題材にシステム間連携で扱い易いフォーマットを考える発表でした。 6人目「ChatGPTでブログを書いてみた」 6人目は「ChatGPTでブログを書いてみた」で私の発表です。 ChatGPTを使った ライフハック に興味を持っていて、ChatGPTを使ってブログを書く方法を発表しました。 ブログの記事をChatGPTに生成させるにはどのようなプロンプトを作成すればいいかを、実際に使ったプロンプトと完成したブログを用いて説明しました。 7人目「おもしろいDB設計本ミッケ!」 7人目は「おもしろいDB設計本ミッケ!」でERDの本の紹介で、大阪オフィスのテッ クリード による発表です。 単なる本の紹介ではなく、本に書かれている方法で実際に領収書をつかってテーブル設計を行うという、さすがテッ クリード という発表で、参加者はうなずきながら聞いていました。 6月はテーマ枠が「社会人を1年目の振り返り」でしたが、自由枠も実体験に基づいたリアリティに溢れた発表内容でした。 7月レポート 軽食はサンドウィッチ(3種類から選択)+ポテチ。 テーマ枠は「新卒に伝えたいこと」 7月から新卒で入社した新人が配属されたため、新卒の自己紹介とテーマ枠が「新卒に伝えたいこと」でした。 最初は新卒で入社した3名の自己紹介で、それぞれ工夫をこらした発表でした。 以降4人目からの発表を紹介します。 4人目「新卒の方々にお伝えしたいこと」 4人目は「新卒の方々にお伝えしたいこと」で私の発表です。 現在は退任しましたが、取締役のメールを紹介しました。 内容は紹介できませんが、 ラク スの文化を象徴するメールの内容だったので新卒に皆さんに紹介しました。 5人目「 君たちはどう生きるか 」 5人目は「 君たちはどう生きるか 」というタイトルでちょっと身構えてしまいそうですが、自分を守る仕事の仕方の紹介です。 ブラックな環境で働いてきた経験から ラク スがいかに恵まれた環境か、そしてその環境を生かして「楽しく健やかなエンジニアライフ」を過ごす方法の説明で、 ラク ス愛あふれた発表でした。 私も経験がありますが、修羅場をくぐり抜けてきた人の「人生は長い。心身の健康が一番大事。」という言葉に重みがあります。 6人目「新卒に伝えたいことLT」 6人目は「新卒に伝えたいことLT」で「みなさんはプロですか?」という問いかけから仕事に対する姿勢を新卒入社に説明。 「プロの仕事とは?」という問いかけから「プロフェッショナル」な仕事の仕方についての発表でした。 7人目「最年長のプロダクトに居座り続ける新卒入社のパイセンから伝えたい ”これだけはやっておけ”」。 自己研鑽をやっておけという当たり前のような内容からなぜ必要なのか?という説明です。 メールディーラー開発の実装チームのリーダの発表で、説得力があり大阪オフィスのテッ クリード から「すごくいい発表だった」と称賛のことばがもらえました。 8人目「新卒の皆さんに知ってほしい!チャットディーラー開発課」 8人目は「新卒の皆さんに知ってほしい!チャットディーラー開発課」で、チャットディーラー開発課から異動になり、チャットディーラー開発課のいいところの紹介です。 タイトルからもわかるようにチャットディーラー愛にあふれる発表でした。 9人目「 PHP を支えるコミュニティたち」 9人目は「 PHP を支えるコミュニティたち」ということで、タイトル通り PHP の歴史とそのコミュニティの紹介です 7月の発表テーマは「新卒に伝えたいこと」だったので、仕事に対することや 自己啓発 的な発表が多かったです。 次回の8月ビアバッシュの予告 大阪オフィスの8月のビアバッシュのテーマは「納涼! ヒヤリハット 特集」です。 暑い夏を吹き飛ばすようなヒヤリとしたエピソードを共有することで、同じようなミスをしないようにする内容ですね。 次回のビアバッシュレポートもお楽しみにしててください!
はじめに こんにちは akihiyo76 です。先日 Android 14 Beta 5 がリリースされ、最終リリースまであと僅かとなりました。そこで、今回は Android 14 で提供される新機能の概要をまとめてみました。 はじめに 機能と API の概要 国際化(Internationalization) アプリ固有の言語設定 Grammatical Inflection API grammatical gender の設定方法 ユーザー補正(Accessibility) ユーザー体験(User experience) 共有シートのカスタムアクションの追加 アプリストアの改善 スクリーンショットの検知 予測型「戻る」アプリ内アニメーション まとめ 参考 機能と API の概要 Android 14 の「新機能と API の概要」は、 公式ページ に記載されており、日本語の翻訳も徐々に進んでおりますが、ニュアンスが微妙なところもあるので、可能であれば 英語版 で読むとより理解が進むかと思います。 Android 14 の新機能を大別すると、以下の 3 つに分類されるので、それぞれを順に紹介したいと思います。 国際化(Internationalization) ユーザー補正(Accessibility) ユーザー体験(User experience) API の追加、変更、削除の一覧については、 API 差分レポート でご確認ください。また、新しい API についての詳細は、 Android API リファレンス でご確認ください。 国際化(Internationalization) 国際化の新機能としては、 アプリ固有の言語設定の自動化 と Grammatical Inflection API(文法的な語形変化) が追加になります。 アプリ固有の言語設定 Android 13( API レベル 33)で導入されたアプリ別の言語機能が拡張され、以下の機能が追加されます。 アプリの localeConfig の自動生成 アプリの localeConfig の動的アップデート インプット メソッド エディタ( IME )によるアプリの言語の確認 Android 14では、locales_config. xml の生成を以下の設定を追加することで、自動生成にすることが可能になりました。自動生成には、 Android Studio Giraffe Canary 7 および AGP 8.1.0-alpha07 以降が必要となっています。 1. androidResources > generateLocaleConfig を定義する。 gradle:build.gradle.kts android { androidResources { generateLocaleConfig true } } 2. resources.properties を作成し、デフォルトの言語を設定する。 res/resources.properties unqualifiedResLocale=en-US このように設定するだけで、locales_config. xml の生成を自動生成にすることができます。 Grammatical Inflection API Android 14 では、性別で文法が変わる言語に合わせてユーザー中心の UI を構築するため、アプリを リファクタリング せずに文法上の性別への対応を追加できる Grammatical Inflection API が導入されています。 日本語の場合、文法的な語形変化はなかなかイメージしにくいと思いますが、フランス語での一例を紹介します。 🇫🇷 冠詞の性別 ( 英語 : the ) 男性形単数 : le 女性形単数 : la 🇫🇷 動詞の過去分詞 ( 英語 : spoken ) 男性形単数 : parlé 女性形単数 : parlée Grammatical Inflection API では、このような文法的言語変化に対応することができます。 grammatical gender の設定方法 Grammatical Inflection API で設定できる文法上の性別の設定は以下の 3 パターンになります。 Configration. java public static final int GRAMMATICAL_GENDER_NEUTRAL = 1; // 中性的 public static final int GRAMMATICAL_GENDER_FEMININE = 2; // 女性的 public static final int GRAMMATICAL_GENDER_MASCULINE = 3; // 男性的 アプリで Grammatical Inflection API を利用するには、対象となる Activity に configChanges の指定します。 AndroidManifest. xml <activity android:name=".MainActivity" android:configChanges="grammaticalGender" android:exported="true"> </activity> 実際に grammatical gender を GRAMMATICAL_GENDER_FEMININE に設定する場合は、 setRequestedApplicationGrammaticalGender() API を利用して設定します。 val gIM = context.getSystemService(GrammaticalInflectionManager :: class .java) gIM.setRequestedApplicationGrammaticalGender(Configuration.GRAMMATICAL_GENDER_FEMININE) また、設定された grammatical gender を取得するには、 getApplicationGrammaticalGender() API を利用します。 val gIM = context.getSystemService(GrammaticalInflectionManager :: class .java) val grammaticalGender = gIM.getApplicationGrammaticalGender() このように設定した grammatical gender によって、 -neuter -feminine -masculine の suffix を付けた resources ファイルでリソースを使い分けることができるようになります。 res/values-fr-neuter/strings. xml <resources> <string name="example_string">Abonnement à...activé</string> </resources> res/values-fr-feminine/strings. xml <resources> <string name="example_string">Vous êtes abonnée à...</string> </resources> values-fr-masculine/strings. xml <resources> <string name="example_string">Vous êtes abonné à...</string> </resources> このように Grammatical Inflection API では文法上の性別によって表現を変えることができるため、性別を扱うことが多い SNS アプリなどで活用できる場面もあるかと思いますが、ローカルでのリソース設定に止まるため使える場面は限定的になりそうです。 ユーザー補正(Accessibility) Android 14 では、 非線形 フォントスケーリングが 200% までサポートされます。SP 指定の場合、追加のオプションとスケーリングの改善がアプリのテキストに自動的に適用されるようになります。 (引用: The first developer preview of Android 14 ) このようにスタンダードスケーリングだと表示崩れが生じるため、 200%のフォントサイズで UI テストで確認すること が重要となります。 200% のフォントサイズを有効にする手順は以下の通りです。 設定アプリを開き、[ユーザー補助] > [表示サイズとテキスト] に移動] [フォントサイズ] オプションで、最大フォントサイズの設定が有効になるまで、プラス(+)アイコンをタップ また、アプリで sp 単位を使用している場合、 Android はユーザーが選択するテキストサイズを適用して適切にスケーリングできるよう、テキストサイズは必ず sp 単位 で指定しましょう。 ユーザー体験(User experience) ユーザー体験では、以下の機能が追加になりました。 共有シートのカスタムアクションの追加 アプリストアの改善 スクリーンショット の検知 予測型「戻る」アプリ内アニメーション 共有シートのカスタムアクションの追加 共有シートのカスタムアクションを追加するには、カスタム ChooserAction から ChooserActions のリストを作成します。 MainActivity.kt fun buildCustomShareSheetActions(context: Context): Array<ChooserAction> { val pendingIntent = PendingIntent.getActivity( context, 0, Intent(Intent.ACTION_WEB_SEARCH).apply { putExtra(SearchManager.QUERY, "Search on browser") }, PendingIntent.FLAG_IMMUTABLE ) val actions = mutableListOf<ChooserAction>() for (i in 0 until 5) { val customAction = ChooserAction.Builder( Icon.createWithResource(context, R.drawable.ic_launcher_foreground), "Action${i+1}", pendingIntent ).build() actions.add(customAction) } return actions.toTypedArray() } 次に、作成した ChooserActions のリストを Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS として指定し、共有シートを開きます。 MainActivity.kt fun buildChooserIntent(chooserActions: Array<ChooserAction>): Intent { val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, "dummy text") } val chooserIntent = Intent.createChooser(intent, "Android 14").apply { putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, chooserActions) } return chooserIntent } fun showActionSheet() { val chooserActions = buildCustomShareSheetActions(this) val chooserIntent = buildChooserIntent(chooserActions) startActivity(chooserIntent) } このように複数のカスタムアクションを設定することができます。 アプリストアの改善 Android 14では、ユーザの操作を中断せずにアプリのアップデートが可能になります。具体的には、 以下の操作を API で実現できるようになります。 ダウンロードする前にインストールの承認をリク エス ト 今後の更新に責任を移譲 影響が少ないタイミングでアプリを更新 アプリインストール操作の事前承認では、targetSdk 34 で PackageInstaller が追加され、 requestUserPreapproval() を実行することで、事前承認を行うことができます。 アプリストアの改善機能は、スムーズなアプリインストールを促すことが期待できるため、ユーザーにとっても嬉しい機能になるでしょう。 スクリーンショット の検知 API level 17 以降では、 FLAG_SECURE を設定することで スクリーンショット を無効にすることができましたが、 Android 14 では スクリーンショット の検知ができるようになります。 スクリーショットの検知をするには、 DETECT_SCREEN_CAPTURE の permission を定義します。DETECT_SCREEN_CAPTURE の Protection level は normal です。 AndroidManifest. xml <uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" /> 検知用の Callback は ScreenCaptureCallback を作成して、ライフサイクルに合わせて登録・登録解除を行います。 MainActivity.kt val screenCaptureCallback = ScreenCaptureCallback { // Add logic to take action in your app. } override fun onStart() { super.onStart() registerScreenCaptureCallback(mainExecutor, screenCaptureCallback) } override fun onStop() { super.onStop() unregisterScreenCaptureCallback(screenCaptureCallback) } このように Android 14 では スクリーンショット の検知が非常に簡単に実現できます。アプリ内で スクリーンショット の検知したら、安易に共有させないような注意喚起を行うケースなどで有用かと思います。 予測型「戻る」アプリ内アニメーション バックアクションに対して、事前検知することでカスタムアニメーションなどの処理を追加することが可能になりました。 @RequiresApi(34) で指定している Implement Menbers が Android 14 で追加になります。 MainFragment.kt val callback = object: OnBackPressedCallback(enabled = false) { @RequiresApi(34) override fun handleOnBackStarted(backEvent: BackEvent) { // Create the transition } @RequiresApi(34) override fun handleOnBackProgressed(backEvent: BackEvent) { // Play the transition as the user swipes back } override fun handleOnBackPressed() { // Finish playing the transition when the user commits back } @RequiresApi(34) override fun handleOnBackCancelled() { // If the user cancels the back gesture, reset the state } } this.requireActivity().onBackPressedDispatcher.addCallback(callback) 実際にアニメーションを追加実装サンプルは 公式ページ にあるので、実際の実装例を見てみたい方はそちらをご覧ください。 まとめ Android 14 の新機能概要を紹介させて頂きました。アプリによっては有用な機能もあると思うので、targetSdk 34 対応時に合わせて、アプリの新機能として検討してみてはいかがでしょうか。 参考 Features and APIs Overview Android Developers Blog
はじめに はじめまして、新卒1年目のTKDSです! 先日、Spring Bootの入力値チェックについて触れる機会があったため、入力値チェックの使い方について調べてました。 今回は、調べた内容と簡単な使いかたについてご紹介したいと思います。 はじめに 入力値チェック アノテーションの実践 ネストされたformの入力値チェック まとめ 参考文献 入力値チェック Spring Bootでは、入力値の検証を行うための便利な機能が提供されています。 これにより、入力データがアプリケーションの要件を満たしているかどうかを確認できます。 入力値チェックはフォームクラスにチェックする内容を示す アノテーション をつけると行えます。 フォームクラスにつける アノテーション にはさまざまなものがあります。 一部を記載します。 アノテーション チェック内容 @NotNull Nullでないか @Max() 最大値以下か @Min 最小値以上か @Pattern 正規表現 に一致するか @Size 要 素数 が最小以上かつ最大以下であるか アノテーション の実践 サンプルアプリを用いて、実際に アノテーション を行ってみます。 検証用にPOSTリク エス トをおくると json を返すアプリを定義します。 まだこのサンプルアプリでは入力値チェックは行われていません。 コントローラー // 一部抜粋 @PostMapping ( "todo-list/{id}" ) public ResponseEntity<Object> post( @PathVariable Integer id, @RequestBody Todo todo,) { List<Todo> todoList = new ArrayList<>(); todo.setId( 1L ); todo.setUsrName( "test001" ); todoList.add(todo); return ResponseEntity.status(HttpStatus.OK).body(todoList); } レスポンスボディを受け取る兼返信用クラス public class Todo { private Long id; private String usrName; private String taskName; @JsonSerialize (using = LocalDateTimeSerializer. class ) @JsonDeserialize (using = LocalDateTimeDeserializer. class ) @JsonFormat (pattern = "yyyy-MM-dd'T'HH:mm:ss" ) private LocalDateTime date; // Getter, Setter, コンストラクタ省略 } このサンプルアプリはタスク名と時刻を JSON に含めてリク エス トを送信するとTodo型のオブジェクトに当てはめて、 JSON 形式で返却します。 リク エス トの送信には、 VS Code 拡張のREST Clientを使用しています。 リク エス ト POST http://localhost:8000/todo-app/todo-list/ 1 Content-Type: application/json { " taskName " : " 読書 " , " date " : " 2023-08-02T22:20:00 " } レスポンス HTTP/ 1 . 1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 02 Aug 2023 15:20:54 GMT Connection: close [ { " id " : 1 , " usrName " : " test001 " , " taskName " : " 読書 " , " date " : " 2023-08-02T22:20:00 " } ] 現状のコードではバリデーションチェックがなにも行われていないため、Nullなどの値を送ってもそのままエラーチェックをすり抜けます。 例として、taskNameをリク エス トボディに含めずリク エス トを送信します。 リク エス ト POST http://localhost:8000/todo-app/todo-list/ 1 Content-Type: application/json { " date " : " 2023-08-02T22:20:00 " } レスポンス HTTP/ 1 . 1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 02 Aug 2023 15:11:12 GMT Connection: close [ { " id " : 1 , " usrName " : " test001 " , " taskName " : null, " date " : " 2023-08-02T22:20:00 " } ] リク エス トに含まれていない "taskName" が null になっているのが確認できます。 では、入力値のバリデーションチェックを行い、taskNameが指定されていない場合にエラーを返すようにします。 TodoクラスのtaskNameに @NotNull アノテーション を付けます。 @NotNull private String taskName; これで、Null値を許容しない設定にすることができました。 次にエラーハンドリングができるようにします。 コントローラーを次のように変更します。 @PostMapping ( "todo-list/{id}" ) public ResponseEntity<Object> post( @PathVariable Integer id, @RequestBody @Validated Todo todo, BindingResult result) { if (result.hasErrors()) { return new ResponseEntity<Object>( "bad request" , HttpStatus.BAD_REQUEST); } List<Todo> todoList = new ArrayList<>(); todo.setId( 1L ); todo.setUsrName( "test001" ); todoList.add(todo); return ResponseEntity.status(HttpStatus.OK).body(todoList); } 引数の部分に アノテーション を追加し、エラーハンドリングの処理を追加します。 先ほどと同じ、taskNameがないリク エス トを送信します。 結果は次の通りです。 HTTP/ 1 . 1 400 Content-Type: text/plain; charset =UTF-8 Content-Length: 11 Date: Wed, 02 Aug 2023 15:33:01 GMT Connection: close bad request エラーハンドリングの部分で設定した bad request が返ってきたのが確認できました。 ネストされたformの入力値チェック 前項では、入力値を受け取るクラスのフィールド変数に対して簡単にバリデーションを行うことができることが確認できました。 しかし、フォームクラス内に自作クラスを使用してフィールド変数を持つ場合、従来の方法だとバリデーションチェックが正常に機能しないケースがあります。 具体的には、フォームクラスの変数自体は アノテーション でチェックできるものの、そのフィールド変数が自作クラスであり、かつ空の配列が入力された場合、入力値のチェックがうまく行われないことがあります。 実際に要素を追加して確認してみましょう。 public class Todo { private Long id; private String usrName; @NotNull private String taskName; @JsonSerialize (using = LocalDateTimeSerializer. class ) @JsonDeserialize (using = LocalDateTimeDeserializer. class ) @JsonFormat (pattern = "yyyy-MM-dd'T'HH:mm:ss" ) private LocalDateTime date; // 追加部分 private List<SubTask> subTasks; public static class SubTask { @NotNull private String taskName; // Getter Setter コンストラクタ省略 } // Getter Setter コンストラクタ省略 リク エス ト POST http://localhost:8000/todo-app/todo-list/ 1 Content-Type: application/json { " taskName " : "" , " date " : " 2023-08-02T22:20:00 " , " subTasks " : [ {} ] } レスポンス HTTP/ 1 . 1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Sun, 06 Aug 2023 15:34:33 GMT Connection: close [ { " id " : 1 , " usrName " : " test001 " , " taskName " : "" , " date " : " 2023-08-02T22:20:00 " , " subTasks " : [ { " taskName " : null } ] } ] subTaskのtaskNameが指定されていないにもかかわらず、バリデーションエラーが返ってきません。このことから、SubTask内のtaskNameに対して、 @NotNull は働いていないことがわかります。 この問題は、親クラスでフィールド変数を宣言する際に@Valid アノテーション を付与することで解決できます。 @Valid private List<SubTask> subTasks; これにより、オブジェクトのフィールド変数に対しても アノテーション チェックが行われるようになります。 リク エス ト POST http: //localhost:8000/todo-app/todo-list/1 Content-Type: application/json { "taskName" : "" , "date" : "2023-08-02T22:20:00" , "subTasks" : [ {} ] } レスポンス HTTP/ 1.1 400 Content-Type: text/plain;charset=UTF- 8 Content-Length: 11 Date : Wed, 02 Aug 2023 15 : 33 : 01 GMT Connection : close bad request バリデーションチェックが行われていることが確認できました。 まとめ この記事では、Spring bootにおける入力値のバリデーションについて、調べて結果を紹介しました。 また、サンプルアプリを用いて、入力値のバリデーションを実践しました。 Spring Bootのバリデーションの理解の一助になれば幸いです。 参考文献 https://access.redhat.com/documentation/ja-jp/red_hat_jboss_enterprise_application_platform/7.4-beta/html/development_guide/jakarta_bean_validation https://spring.pleiades.io/specifications/platform/10/apidocs/jakarta/validation/package-summary.html https://jakarta.ee/specifications/bean-validation/3.0/jakarta-bean-validation-spec-3.0.html https://spring.pleiades.io/specifications/platform/10/apidocs/jakarta/validation/constraints/package-summary.html
こんにちは。 ラク ス インフラチーム所属のas119119です。 今回は、タイトルの通りビジネスの場面で重要なスキルといわれている ロジカルシンキング 研修を開催した件について取り上げてみたいと思います。 今回のブログ構成については以下の通りとなります。 ロジカルシンキングとは 研修実施の背景 研修の内容 研修を実施してみて ロジカルシンキング とは 世間でよく聞く ロジカルシンキング とはそもそもどのようなものなのでしょうか。 日本語をそのまま当てはめると「論理的思考」となるようです。 しかし、「論理的思考」といってもあまりにも漠然としていますね。 少なくとも個人的にはそう思います。 巷にあふれる情報の中から論理的思考とは何かと調べてみると、 概ね次にあるような類似の定義にたどり着きます。 「論理的思考」とは、つまるところ直感や感覚的なところで物事を捉えるのではなく、筋道を立てて内容に矛盾や破綻がないように理屈が合うように考え、 結論を導くことという定義に集約できると思います。 研修実施の背景 部署内には新卒や中途、ベテランなど多種多様なメンバーがいる中で課題設定や課題解決能力にばらつきがあり、メンバー間の連携共有をよりスムーズに実施するためにはどうすればよいかという点に端を発しています。 そこでビジネスをする上で重要なスキルである ロジカルシンキング 研修をメンバー向けに実施することで、メンバー間のビジネススキルの底上げを図るとともに、チーム内部で共通のスキルを身に着けることで情報連携力の向上は図ろうとしたのが主催の背景となります。 その意味では、このようなインフラに関連していない試みでもチャレンジ(スモールスタートと言えるでしょう)できる土壌があるという点は ラク スの強みでもあると言えます。 研修の内容 さて、何を研修で扱ったかについて、ここでは取り上げていきたいと思います。 研修としては、業務時間も有効に活用しつつ、1回あたり90分を全3回、2週間から3週間おきに実施し、おおよそ2ヵ月以内で全内容を消化した形となります。 毎回、研修中にハンズオン形式の課題に取り組みつつ、各回の終了後、次回までに必須課題があるため、参加者にとっては通常業務等々をこなしつつ、なかなかヘビーな内容であったと思います。 イメージしやすいように、参考までに使用したテキストのサンプルをご紹介したいと思います。 ロジカルシンキング を検討する上で、様々なツールを使用すると便利であることがわかっているので、研修では一般的なビジネス概念である MECE やロジックツリーなどのツールを用いて、様々はハンズオンを実施しました。 今回は、ベテランから若手まで多様なメンバーが参加していたこともあり、ハンズオンにおける意見交換は活発に実施され、とりわけ若手にとっては色々な意見に触れるよい機会になったのではないかと思います。 研修を実施してみて 研修を ファシリテーター として実施するのも初めてで、研修を実施するための準備について、それなりに別のベテランから指導を受けたつもりでしたが、時間管理やハンズオンのコン トロール などはなかなかスムーズにいかないところも多々ありました。 しかし、全体を通しては実施する価値はあったと確信しています。 実際、参加者に対して毎回、研修の最後にアンケートを依頼していたのですが、概ね好評で、他のエンジニアにも受講を薦めるという意見が多く出ていました。 ある程度、 定量 化できる効果も出せたのではないかと感じています。 というのも、研修の成果物の一環として、参加者に「前後確認シート」と呼ばれるものを研修前と研修後に作成を依頼したところ、当研修で学んだことを活用して業務内における半年先までの目標設定に活用している成果が見受けられたためとなります。 今回、 ロジカルシンキング 研修を内製化するような形で実施したことを踏まえ、比較的習得したいスキルや知識はやりかた次第でできる環境が整備されていると思いました。 他方で新卒などの若手やベテランの分け隔てなく必要な研修を提供できた点は、個人的にも貴重な経験を積む機会となったのではと感じました。 ここまで目を通して頂き、ありがとうございました。
ユーザーによるカスタマイズ 型と実体(クラスとインスタンス) 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> として標準実装されているため、こちらを使用することも可能です。