【新人エンジニア】MVCモデルの進化版!? ADRが使いやすかったお話

こんにちは!
7月からインターン生として株式会社Hajimariに入社した、難波 慧人です。

現在は、TUKURÜS事業部で受託開発の業務を行っています! 今行っている案件では、サブスクリプション型動画配信サイトの、新規機能開発・運用保守を担当しています!

開発言語に関しては、 バックエンドはPHP(laravelフレームワーク)を用いており、アーキテクチャADR(Action Domain Responder)を採用しています。

案件にジョインした当初、MVCアーキテクチャしか知らない私でしたが、ADRの有用性が少しずつ理解できてきました!

そこで今回は、MVCアーキテクチャと、ADRアーキテクチャの違いについてご紹介したいと思います!!

また、各項目にサンプルコード(ユーザーの一覧、詳細機能)を示していきます!!

MVC(Model View Controller)とは??

引用元( https://white-t007.com/tech/it%E5%9F%BA%E7%A4%8E/mvc/

ユーザーインターフェイスを持つアプリケーションを、モデル(Model)、ビュー(View)、コントローラー(Controller)の3つの責務に分割して、実装していく手法です。

qiita.com

モデル(Model)

モデルとは、アプリケーションの中で、ビジネスロジック、データベースとのアクセスを担っています。例えば、計算、データ取得、データ管理などが挙げられます。

Laravelフレームワークでは、Eloquent ORMと呼ばれる、モデルとデータベースの紐付けを行う機能があります。
Eloquent ORMにより、アプリケーションとデータベース間のデータのやりとりをより円滑にすることができます!! readouble.com

  • サンプルコード
Models/User.php

class User extends Model
{
    /**
     * usersテーブルから全てのユーザーを取得
     * 
     * @return Collection
     */
    public function fetchAllUsers(): Collection
    {
        return User::all();
    }

    /**
     * usersテーブルから指定ユーザーを取得
     * 
     * @return ?User
     */
    public function fetchUser($userId): ?User
    {
        return User::where('id', $userId)->first();
    }
}

ビュー(View)

ビューとは、webサイト上のレイアウトを作成する役割を持っています。主にHTMLで記述されます。また、ビューから生成されたレイアウトはアプリケーションとユーザーのインターフェイスになっています。

ユーザーはこのweb上のレイアウトを通して、アプリケーションにリクエストを送信、アプリケーションからのレスポンスを閲覧することができます。

コントローラー(Controller)

コントローラーとは、ユーザーからのリクエストに応じて、モデルとビューの制御を担う部分です。 実際のユーザーインターフェイスはこのコントローラーになります。アプリケーションとユーザー間のやり取りは全てここを経由します。

ユーザーがwebサイト上で商品一覧をみたい時を例にします。 ユーザーから「商品一覧がみたい」というリクエストが来たら、モデルに商品データを取得するように依頼します。次に取得データをビューに渡し、ビューが作成したレイアウトを、webサイト上に表示しています。これによって、ユーザーがweb上で、商品一覧を閲覧することができます。

  • サンプルコード
Controllers/UserController.php

class UserController extends Controller
{
    /**
     * ユーザー一覧
     *
     * @return \Illuminate\View\View
     */
    public function index(): View
    {
        // Userクラス(モデル)のインスタンス作成
        $userModel = new User();
        // Userクラス(モデル)のメソッドを使って、ユーザーを取得
        $users = $userModel->fetchAllUsers();

        return view('users.index', ['users' => $users]);
    }

    /**
     * ユーザー詳細
     *
     * @param  int  $userId
     * 
     * @return \Illuminate\View\View
     */
    public function show(int $userId): View
    {
        // Userクラス(モデル)のインスタンス作成
        $userModel = new User();
        // Userクラス(モデル)のメソッドを使って、ユーザーを取得
        $user = $userModel->fetchUser($userId);

        // 対象ユーザーが存在しなかったら、404ページを返す
        if(is_null($user)) {
            abort(404); 
        } else {
            return view('users.show', ['user' => $user]);
        }
    }
}

MVCの問題点

  • コントローラーの肥大化
    コントローラーは基本的に、ユーザーインターフェイスとしてアプリケーションの入出力のみを担う責務ですが、コントローラーにビジネスロジックやデーターベースのやり取りを記述される場合があります。

  • モデルの肥大化
    モデルとは、ビジネスロジック・データベースアクセスを書く場所である。しかし、アプリケーションの規模が大きくなると、ビジネスロジック・データベースアクセスも必然的に増えていき、結果モデルは肥大化していきます。。。

ADRの導入

以上の問題点を踏まえ、MVCの改良版でもあるADR(Action Domain Responder)を導入しています!!

ADR(Action Domain Responder)とは??

上記のMVCモデルの改良版として、Paul M. Jonesによって提案されたソフトウェアアーキテクチャパターンです。

引用元:( https://en.wikipedia.org/wiki/Action%E2%80%93domain%E2%80%93responder

ADRでは、アクション(Action)、ドメイン(Domain)、レスポンダー(Responder)の3つの責務に分割して、実装していく手法です!

ADR:wikipedia

アクション(Action)

ユーザーからのHTTPリクエストを受け取り、ドメインの処理結果をレスポンダーに渡す役割です。MVCでいうコントローラー的な位置付けです。 全てのHTTPリクエストはここで処理されます。
HTTPリクエストとは

  • サンプルコード
Actions/User/UserIndexAction.php

class UserIndexAction extends Controller
{
    /**
     * Instance of the Responder property.
     *
     * @var UserRepository
     * @var UserIndexResponder
     */
    protected $userRepository;
    protected $userIndexResponder;

    /**
     * Instantiate the class.
     *
     * @param UserRepository $userRepository
     * @param UserIndexResponder $userIndexResponder
     */
    public function __construct(
        UserRepository $userRepository,
        UserIndexResponder $userIndexResponder
    )
    {
        $this->userRepository = $userRepository;
        $this->responder = $userIndexResponder;
    }

    /**
     * Invoke our action, handle domain, respond.
     *
     *
     * @return View
     */
    public function __invoke(): View
    {
        $users = $this->userRepository->fetchAllUsers();

        return $this->responder->response($users);
    }
}
Actions/User/UserShowAction.php

class UserShowAction extends Controller
{
    /**
     * Instance of the Responder property.
     *
     * @var UserRepository
     * @var UserShowResponder
     */
    protected $userRepository;
    protected $userShowResponder;

    /**
     * Instantiate the class.
     *
     * @param UserRepository $userRepository
     * @param UserShowResponder $userShowResponder
     */
    public function __construct(
        UserRepository $userRepository,
        UserShowResponder $userShowResponder
    )
    {
        $this->userRepository = $userRepository;
        $this->responder = $userShowResponder;
    }

    /**
     * Invoke our action, handle domain, respond.
     * 
     * @param Request $request
     * 
     * @return View
     */
    public function __invoke(Request $request): View
    {
        $user = $this->userRepository->fetchUser($request->user_id);
        
        return $this->responder->response($user);
    }
}

ドメイン(Domain)

アクションから受け取った依頼をもとに、ビジネスロジック、データベースアクセスを担当する役割です。MVCでいうモデル的な位置付けです。
ビジネスロジックとは

今回は、Domainをさらにサービス(Service)、リポジトリ(Repository)、モデル(Model)に分けています。

  • サービス(Service)
    主にアプリケーションビジネスルールを記述する。
    (アプリケーションビジネスルール = システムであるがゆえに発生するビジネスルール)
    例)データに対する複合処理、データ加工など

  • リポジトリ(Repository)
    データベースとのインターフェイスとして使用。
    例)データベースアクセスのみ(加工はサービスの責務)

  • モデル(Model)
    主にエンタープライズビジネスルールを記述する。
    エンタープライズビジネスルール = アプリケーション都合でないビジネスルール)
    例)野球  勝敗:点数が多い方が勝ち 時間:9回で終了 試合人数:各チーム9人ずつ

  • サンプルコード (今回はビジネスロジックがない為、リポジトリのみ)

Domain/User/UserRepository.php

class UserRepository
{
    /**
     * usersテーブルから全てのユーザーを取得
     * 
     * @return Collection
     */
    public function fetchAllUsers(): Collection
    {
        return User::all();
    }

    /**
     * usersテーブルから指定ユーザーを取得
     *
     * @param int  $userId
     *
     * @return ?User
     */
    public function fetchUser($userId): ?User
    {
        return User::where('id', $userId)->first();
    }
}

レスポンダー(Responder)

ドメインの処理結果を受け取り、HTTPレスポンスを作成、処理を担当します。MVC モデルのビューとは異なり、httpステータスの変更、クッキー操作も行います。

  • サンプルコード
Responders/User/UserIndexResponder.php

class UserIndexResponder
{
    protected $view;

    public function __construct(ViewFactory $view)
    {
        $this->view = $view;
    }

    /**
     * 単体動画の表示
     *
     * @param Collection $users
     * 
     * @return View
     */
    public function response(Collection $users): View
    {
        return $this->view->make(
            'users.index',
            compact('users')
        );
    }
}
Responders/User/UserShowResponder.php

class UserShowResponder
{
    protected $view;

    public function __construct(ViewFactory $view)
    {
        $this->view = $view;
    }

    /**
     * 単体動画の表示
     *
     * @param ?User $user
     * 
     * @return View
     */
    public function response(User $user): View
    {
        if (is_null($user)) {
            return \App::abort(404);
        } else
        {
            return $this->view->make(
                'users.index',
                compact('users')
            );
        }
    }
}

MVCからの改善点

  • コントローラーの肥大化
    MVCのコントローラーでは、ユーザーインターフェイスとして、全てのリクエスト、レスポンスを処理していました。
    しかし、ADRではリクエストをアクション、レスポンスをレスポンダーといったように、責務を分けることで改善されています。
    また、ADRでは、1アクションに対し1クラスしか作成しません。これにより、1つのコントローラーにメソッドが集中することを避けれます!!

  • モデルの肥大化
    MVCのモデルでは、全てのビジネスロジックの責務を担当していましたが、サービス(Service)、リポジトリ(Repository)、モデル(Model)という責務に細分化することで、モデルの肥大化を防ぎます。

  • 保守性、可読性の向上
    実際、コードリーディングを行う際に、処理の流れが明確なので追いやすいです。
    また、データーベースアクセスが一目でわかる、追いたい処理を探す時にもファイル特定がしやすい(サービス層の処理ならDomain配下のserviceファイルを探せばいい)などの利点がありました!

■まとめ

以上が、MVCADRの違いでした!!

最初は、「ADRってなんぞや??」と戸惑いましたが、

MVCをある程度理解していれば、ADRのキャッチアップもやりやすいと思います!

是非ともADR(Action Domain Responder)を導入してみてはいかがでしょうか?

Fatなコントローラー 、モデル内で、無限スクロールする開発生活から解放されるはずです。。。


株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、
一緒に開発を行なっていただけるエンジニア募集しています! 長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com www.wantedly.com

エンジニア交流会やってみた!!

こんにちは!

2022年5月から株式会社Hajimariでエンジニアをしています、植本です。 現在は、TUKURÜS事業部で主に受託開発の業務を行っています。

今回は先日行った、弊社の23卒内定者を含めた全エンジニアでの「エンジニア交流会」について書いていきます!

というのも、、、

  • 他事業部の社員と話すことがあんまりない

  • (内定者から)オンラインだと距離感が掴めなくて気軽に話せる人は実は少ない・・・

という声を聞いていました…!

リモ―トワークが普及しつつある今、会社によって社員同士の交流の頻度が減っているところもあると思います。

確かにこれじゃ何かあっても殆ど話したことない人に気軽には相談しにくいですよね…。(そりゃそう)

ということで(?)

エンジニア交流会を開催したので、その様子を一挙大公開します!!!

※社員同士の交流は最低限でいい、という方もいると思います。 その考え方も尊重しているので、今回の交流会は「強制」ではなく「自由参加」です!

※でも、都合が合わない方以外はほぼ全員参加してくれました、本当にありがとうございます(歓喜

さてさて前置きが長くなりましたが、、、

  • 株式会社Hajimariのエンジニア組織がどんな雰囲気か知りたい!

  • 弊社エンジニアのリアルな様子が知りたい!

  • 弊社の個や組織へのこだわり/想いが知りたい!

こんなことが気になる方は是非読んでみてください!!

※今回の記事は技術的なことは書いてありませんのでご了承ください、、、

Hajimariの社員ってどんな人がいる?

そもそも弊社やその社員についてどのような印象でしょうか。

自分と仲間の決断と行動、その結果を受け入れて前に進んでいくことのできる人ばかりです!本当に人の良さは声を大にできるのですが、、、

ここで語っても中々イメージ湧かないと思うので最近の記事を紹介しておきますね!(是非覗いてみてください!)

www.wantedly.com

www.wantedly.com

なぜエンジニア交流会を行ったのか

嗚呼、また前置きが長くなってしまったorz

端的に言うと、、、

新しいメンバーも増えてきたので、自己理解と相互理解を深めて自分の目指すべき方向性を再確認してもらうため

にエンジニア交流会を開催することにしました!

弊社は2015年設立のベンチャー企業で、社員数は127名(2022年9月時点)です。新卒採用では23卒を26人採用しておりどんどん大きくなっている会社になります。長野拠点も今年度から本格的に活動をしています!

note.com

そしてエンジニアという括りでいうと、社員数は32人、そのうち23卒内定者9人、インターン生は4人です。

こういった背景を踏まえると、今回の交流会は

  1. 長野拠点/リモートワークで、物理的に普段話せない仲間との交流の場になる点

  2. 全国にいる23卒内定者に、弊社のエンジニア組織や雰囲気を感じてもらえる点

で大きな意義があるものだと思います!

と言っても、会社全体として危機感が募った結果、交流会をしなきゃ!となったわけではありませんでした。

急激に人数が増えたことでコミュニケーションが疎になりつつあることを感じていて、「一回交流の場をあったら良いね」という話から、「じゃ、エンジニア交流会やろっか!」と自然にスタートしました!

自分と仲間の「自立」に向き合える環境を創ることも弊社が大切にしていることなのです。

エンジニア交流会の内容

今回のエンジニア交流会はオンライン開催、3部構成でした!!

  • 一致するまで終われまテン

  • wevox value card

  • Will語り会

1. 一致するまで終われまテン

1つ目の終われまテンは、zoomを使ったアイスブレイクです! 準備の段階では、オンラインだし初対面の人同士もいるしでやや心配していました。。。

でもゲーム後には打ち解けた様子でメインルームへ帰ってきてくれました! ゲームの様子と伴に各ルームで珍回答があったようなので、一部晒しておきます! 知らない観光名所で勉強になりました。

「川」のつく名字で、まず西川が出てくるのか・・・

2. wevox value card

2つ目からは、自分と仲間の価値観や大切にしていることを相互理解をする催しです!

wevox.io

このゲームも今回の趣旨にぴったりのもので、ゲーム中「そんなこと思ってたんだ!」とか「あ〜、どれも捨てがたいいい」などの声が終始続いていましたようです!

終わったあとに少し雑談(いつの間にか人以外が紛れ込んでいました・・・)

3. Will語り会

場も温まってきて、自分の価値観も言語化できたところで、最後の催しです!

できるだけ①事業部が異なる、②年代が異なるを意識してペアを作り、1on1形式でWill語り会をしました。

  1. 自分のWillとそのきっかけ
  2. Hajimariに入社(内定承諾)した理由
  3. 今やっている業務内容や今後やってみたいことについて

過去〜未来の時間軸で自分の価値観や想いを語ってもらいました! アンケートの結果では一番評価が高く、むしろ時間が足りなかったという声が多かったです!

飲みに行くことが決まったメンバーもいたとか笑

(因みに話に夢中になって写真撮るの忘れてしまいました泣)

終わりに ~弊社が大切にしている自己理解と他者理解、組織理解~

終わった後に、アンケートを見ながら運営陣で振り返りをしました!

「終始笑顔が溢れていた空間だった」「内定者が打ち解けた様子で嬉しかった」「交流なので、いろんな他事業の人と話せるコンテンツでも良かったかもしれない」などなど色んな意見をいただきました!!

ただ、アンケート上で良い評価をもらえましたが鵜呑みにするべきではありません。今回参加できなかった人との間に溝が生まれてないか、参加はしていたけど消極的だった人はいなかったか。それに、交流会などのコミュニケーションの場は1回行えば大丈夫というものではありません。かと言って1回1回が長時間となると負担が大きくなるのでそれもまた良くはないように思えます。

反省点や考えることは尽きませんが、、、

こういうたくさんの言葉をいただけただけでも、やって良かったなと素直に感じます!

まだまだ組織には課題が山のようにあります(汗)が、交流会を通して気付けたこと/良くなったこともあります!

それらに対して今後どうするか、続・エンジニア交流会をするのか、、、勉強会をやってみるのか、、、もくもく会をするのか、、、

まぁとにかく色んなことをやってみるので今後の記事に乞うご期待ください!!

こんな感じで(急にまとめに入る)

以上、Hajimariのエンジニア交流会のお話でした〜!


株式会社Hajimariでは、一緒に働く仲間を募集しています!
長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
そして気軽にお話しましょう!! 
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com

【Raycast】心地の良いPCライフを送るためのランチャーアプリ

【Raycast】心地の良いPCライフを送るためのランチャーアプリ

こんにちは!
6月から23卒内定者インターン生として株式会社Hajimariに入社した、江端 凌です。

普段は、TUKURÜS事業部で受託開発の業務に携わっています。

今回は普段の業務を効率化するためにインストールしたランチャーアプリであるRaycastが便利すぎたので、この場を借りてご紹介したいと思います。

ランチャーアプリとは

ランチャーアプリとは、ショートカットなどの簡単操作でファイルやフォルダーの呼び出し、起動などが行えるアプリです。

Macをお持ちの方は、⌘ + spaceキーで既存のランチャーアプリである「Spotlight」が使えます。実際に、このSpotlightにアプリケーションの名前入力すると起動してくれます。

support.apple.com

もちろん、検索したいキーワードを入力するとGoogle Chromeで検索結果を表示してくれますし、PCを使う上で痒いところに手が届くアプリなのです。

Raycastをインストールする

では、RaycastがどのようにSpotlightより優れているのかを紹介する前に、実際に使いながら実感してもらうためにインストール方法から説明していきます。

まずはRaycastのサイトに飛んで、「Download for Mac」を押下してください。

www.raycast.com

ダウンロードが開始され、完了するとRaycast.dmgがダウンロードディレクトリ配下に入っています。

クリックして開くと以下のような画面になっていると思うので、

Raycastのインストール画面

左側のRaycastのアイコンをドラッグして、右側のApplicationsディレクトリにドロップします。これでMacのLaunchpadなどからRaycastを探せるようになります。

Raycastを起動したら以下のような表示になります(初回起動は確認事項があるので、全て右下のボタンをクリックでOKです)。

Raycastの初期画面

Raycastのここがすごい

個人的にRaycastがすごいと思う箇所は、その万能さです。

例として、コピーした履歴を表示させる「Clipy」という有名なアプリがありますが、似たようなことがRaycastの「Clipboard History」という機能でも可能です。
もちろん「Clipy」とは使い勝手も違うので、この場でどちらが便利だよというわけではありませんが、よく使われるアプリの機能も包括的にサポートしているという点がRaycastの大きなメリットです。

Raycastの基本機能

ではどのように「Clipboard History」を利用するかご紹介していきます。

まずはoption + spaceキーでRaycastを起動して、検索窓に「Clipboard History」と入力します。

ClipboardHistory

すると、一番上に「Clipboard History」が表示されているので、選択してください。
そうするとコピー履歴が表示されます。

ただ、呼び出すのが面倒ですよね。 Raycastにはわざわざoption + spaceキーで起動して、検索欄で検索をかけなくても良い方法がありますが、それは後述します。

前述した通り、Raycastのすごいところはその万能さです。
Raycastは「Clipboard History」以外に、多くの機能を備えています。

Window Management

WindowManagement

Window Management」は、ウインドウサイズを自在に操作できる機能です。
主に左右に画面分割するときに重宝します。

Snippets

Snippets

Create Snippet」、「Search Snippet」、「Import Snippet」はスニペットを作成、検索できる機能です。
スニペットに登録した内容は、「Keyword」を入力したら呼び出せます。

Quit All Applications

QuitAllApplications

全アプリケーションを落とせます。
PCを使い続けていると、いろんなアプリを開きまくって負荷もかけまくっちゃいがち。
このコマンドで定期的にアプリを落としてあげましょう。

ショートカット

では、そんな便利なコマンドたちをショートカットで呼び出せるようにしていきましょう。

Raycastには「Hotkey」と呼ばれる機能があり、これを使うことでRaycastの機能をショートカットに登録できます。

例として、僕はshift + control + Hに「Clipboard History」登録しています。

登録の方法としては、

  1. option + spaceキーでRaycastを起動。
  2. ⌘ + ,(カンマ)で設定画面を表示。
  3. Extensions から、「Clipboard History」を検索。
  4. Clipboard History」のHotkeyの欄をクリックして、画像のように「Recording...」と表示されたら、登録したいキーを押してください。 ExtensionsHotkey
  5. 一度option + spaceキーでRaycastを閉じて、登録したショートカットで「Clipboard History」が起動したら成功です。

こうすることで、Raycastを開かなくても使いたい機能を使うことができます。
ちなみに、Raycastの他の機能も同様に登録することが可能です。

※ おすすめはshift + control + ⇦ | ⇨に「Window Management」の左半分(Left Half) と右半分(Right Half)。

これでRaycastの体験が爆上がりします。

Raycastの拡張機能

ここまでご紹介した、「Window Management」や「Clipboard History」はRaycast以外でも賄えてしまいます。
全部Raycastで済むというメリットはありますが、同じようなランチャーアプリである「Alfred」でも可能です。

Alfred

Alfred

  • Running with Crayons Ltd
  • 仕事効率化
  • 無料
apps.apple.com

ですが、RaycastとAlfledでは「拡張機能」が大きく違います。
既存のランチャーアプリとしての機能を超えて、便利なアプリが多くリリースされているのですが、Raycastでは基本的に無料で拡張機能が提供されており、対するAlfredは有料です。

では、どんな拡張機能がRaycastにあるのかを少しだけご紹介します。

Brew

www.raycast.com

HomebrewをRaycastで操作できます。
何がインストールされているか確認できたり、アップグレードをすることができます。

Github

www.raycast.com

RaycastでIssueの作成やPRの作成が行えます。
ワークフローの実行まで可能なので、サクッとやりたいときに便利。

Docker

www.raycast.com

Dockerのコンテナを起動・停止できたり、Imageの確認・削除もできます。
自分はDocker for Desktopの挙動がたまにおかしくなるので、重宝しています。

その他Raycastの拡張機能は、Raycast内の「Store」から探すことができるので、気になったものを探してみるもよし、なんなら作っちゃうのもありですね。

Store

zenn.dev


株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、
一緒に開発を行なっていただけるエンジニア募集しています! 長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com www.wantedly.com

【PHP】Carbon::now()は誰のnow?

こんにちは、毎日暑いですね。エンジニアの三宅です。

最近、仕事でPHPのコードリーディングを少ししたので、その過程を記載します!

Carbon::now()はOSの現在のシステム時刻

結論はこれです。OSのシステム時刻を持ってきています。

これ以降の内容は、コードを見ながら本当にシステム時刻から取ってきているのかを確認するためのものです。

Carbonとは

今回扱っているCarbonはPHPのDateTimeクラスを継承し、時間操作を簡易化したり便利化したものを提供する拡張クラスになります!

carbon.nesbot.com

これが所謂Carbon::now()の使用例

<?php
require 'vendor/autoload.php';
use Carbon\Carbon;

$now = Carbon::now();
$now->format('Y-m-d H:i:s'); // 2022-08-24 19:31:22

PHPの時間操作

DateTimeはPHP標準クラスです!

www.php.net

Carbonはこれを継承しています。

コードリーディング方針

今回は如何にWEB完結でコードリーディングしていくかの手順を書いていきますが、本格的に処理を追うのであれば debug実行出来る環境を手元に用意すると良いと思います。

WEBでコードリーディング実践

Carbonクラスのコードリーディング

では早速Carbonから追っていきましょう

まずはCarbonのgithubからCarbon::now();の処理を確認します。 github.com

GitHub上で検索する際には"function now"とダブルクォーテーションで括ります こうすると目指す関数に当たりやすいです!

ちなみに対象関数を探すのは、GitHub上よりもgit cloneしてローカルファイルをエディターで検索した方が楽です

ただ、GitHub検索するとissueやcommitメッセージなども対象になるため色んな情報を得る可能性があるというメリットもあります

検索の結果、どうやらここにあるようです!! github.com

nowメソッド

/**
     * Get a Carbon instance for the current date and time.
     *
     * @param DateTimeZone|string|null $tz
     *
     * @return static
     */
    public static function now($tz = null)
    {
        return new static(null, $tz); // ← ここでインスタンス生成して返している
    }

newしたものを返しているようです! new staticは実行されるクラスのインスタンスを生成します。

実行クラスのコンストラクタが第一引数nullで呼ばれますので、そちらの処理を確認

public function __construct($time = null, $tz = null)
    {
        if ($time instanceof DateTimeInterface) {
            $time = $this->constructTimezoneFromDateTime($time, $tz)->format('Y-m-d H:i:s.u');
        }

        if (is_numeric($time) && (!\is_string($time) || !preg_match('/^\d{1,14}$/', $time))) {
            $time = static::createFromTimestampUTC($time)->format('Y-m-d\TH:i:s.uP');
        }

        // If the class has a test now set and we are trying to create a now()
        // instance then override as required
        $isNow = empty($time) || $time === 'now';

        if (method_exists(static::class, 'hasTestNow') &&
            method_exists(static::class, 'getTestNow') &&
            static::hasTestNow() &&
            ($isNow || static::hasRelativeKeywords($time))
        ) {
            static::mockConstructorParameters($time, $tz);
        }

        // Work-around for PHP bug https://bugs.php.net/bug.php?id=67127
        if (!str_contains((string) .1, '.')) {
            $locale = setlocale(LC_NUMERIC, '0');
            setlocale(LC_NUMERIC, 'C');
        }

        try {

            // ここで親コンストラクタを呼んでいる!!!!!!
            parent::__construct($time ?: 'now', static::safeCreateDateTimeZone($tz) ?: null); 
        } catch (Exception $exception) {
            throw new InvalidFormatException($exception->getMessage(), 0, $exception);
        }

        $this->constructedObjectId = spl_object_hash($this);

        if (isset($locale)) {
            setlocale(LC_NUMERIC, $locale);
        }

        self::setLastErrors(parent::getLastErrors());
    }

親のコンストラクタを呼んでいます。

parent::__construct($time ?: 'now', static::safeCreateDateTimeZone($tz) ?: null); *

Carbonの親クラスは前述通りDateTimeクラスになります。 そもそも元のコンストラクタの第一引数である$timeはnullできているので、このCarbon→DateTimeのコンストラクタへ移行するタイミングで ’now’という引数が設定されることが読み取れます!

そうすると今度はDateTimeクラスになっていきます

DateTimeクラスのコードリーディング

DateTimeクラスのコンストラクタには説明ドキュメントがありました

www.php.net

PHPの元コードはC言語で書かれています!

GitHubで開発はされていないのですが、ミラーされたコードが上がっています。

github.com

まずは"class DateTime"で検索してみます。

C言語ではなくPHPのstabファイルで引っ掛かりました。

/**
 * Representation of date and time.
 * @link https://php.net/manual/en/class.datetime.php
 */
class DateTime implements DateTimeInterface
{
  ...
}

ここでファイルのディレクトリに注目!! dateを扱ってそうなディレクトリです。

php_date.cが怪しいなといって見ていきます。

今度はブラウザ検索で"PHP_FUNCTION"を見ていきます。

PHP_FUNCTIONC言語のマクロで定義されています。 機能は引数でもらった名前の関数をPHPで動作させるものです。

DateTime classのコンストラクタに当たりそうな処理が見つかりました!

github.com

コメントにReturns new DateTime objectとありますね!

/* {{{ Returns new DateTime object */
PHP_FUNCTION(date_create)
{
    zval           *timezone_object = NULL;
    char           *time_str = NULL;
    size_t          time_str_len = 0;

    ZEND_PARSE_PARAMETERS_START(0, 2)
        Z_PARAM_OPTIONAL
        Z_PARAM_STRING(time_str, time_str_len)
        Z_PARAM_OBJECT_OF_CLASS_OR_NULL(timezone_object, date_ce_timezone)
    ZEND_PARSE_PARAMETERS_END();

    php_date_instantiate(date_ce_date, return_value);
    if (!php_date_initialize(Z_PHPDATE_P(return_value), time_str, time_str_len, NULL, timezone_object, 0)) {
        zval_ptr_dtor(return_value);
        RETURN_FALSE;
    }
}
/* }}} */

上記コードはdate_create()というPHP関数の実行処理内容です!

www.php.net

date_create()を知っていれば、最初からdate_createで検索すればよかったのでは?と思うかと思います。

その通りです!

PHP関数名が分かっていればストレートに検索していけば問題なくたどり着けます!

さて、PHP_FUNCTION(date_create)の中を見てきます。。 php_date_initialize関数が正にその実行処理のようです! コードを追っていきます。

PHPAPI bool php_date_initialize(php_date_obj *dateobj, const char *time_str, size_t time_str_len, const char *format, zval *timezone_object, int flags) /* {{{ */
{
    timelib_time   *now;
    timelib_tzinfo *tzi = NULL;
    timelib_error_container *err = NULL;
    int type = TIMELIB_ZONETYPE_ID, new_dst = 0;
    char *new_abbr = NULL;
    timelib_sll new_offset = 0;
    time_t sec;
    suseconds_t usec;
    int options = 0;

    if (dateobj->time) {
        timelib_time_dtor(dateobj->time);
    }
    if (format) {
        if (time_str_len == 0) {
            time_str = "";
        }
        dateobj->time = timelib_parse_from_format(format, time_str, time_str_len, &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper);
    } else {
        if (time_str_len == 0) {
            time_str = "now";
            time_str_len = sizeof("now") - 1;
        }
        dateobj->time = timelib_strtotime(time_str, time_str_len, &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper);
    }

んー、まだnowの時間を取ってはなさそうですね..!

 /* update last errors and warnings */
    update_errors_warnings(err);

    /* If called from a constructor throw an exception */
    if ((flags & PHP_DATE_INIT_CTOR) && err && err->error_count) {
        /* spit out the first library error message, at least */
        zend_throw_exception_ex(NULL, 0, "Failed to parse time string (%s) at position %d (%c): %s", time_str,
            err->error_messages[0].position, err->error_messages[0].character, err->error_messages[0].message);
    }
    if (err && err->error_count) {
        timelib_time_dtor(dateobj->time);
        dateobj->time = 0;
        return 0;
    }

    if (timezone_object) {
        php_timezone_obj *tzobj;

        tzobj = Z_PHPTIMEZONE_P(timezone_object);
        switch (tzobj->type) {
            case TIMELIB_ZONETYPE_ID:
                tzi = tzobj->tzi.tz;
                break;
            case TIMELIB_ZONETYPE_OFFSET:
                new_offset = tzobj->tzi.utc_offset;
                break;
            case TIMELIB_ZONETYPE_ABBR:
                new_offset = tzobj->tzi.z.utc_offset;
                new_dst    = tzobj->tzi.z.dst;
                new_abbr   = timelib_strdup(tzobj->tzi.z.abbr);
                break;
        }
        type = tzobj->type;
    } else if (dateobj->time->tz_info) {
        tzi = dateobj->time->tz_info;
    } else {
        tzi = get_timezone_info();
        if (!tzi) {
            return 0;
        }
    }

    now = timelib_time_ctor();
    now->zone_type = type;
    switch (type) {
        case TIMELIB_ZONETYPE_ID:
            now->tz_info = tzi;
            break;
        case TIMELIB_ZONETYPE_OFFSET:
            now->z = new_offset;
            break;
        case TIMELIB_ZONETYPE_ABBR:
            now->z = new_offset;
            now->dst = new_dst;
            now->tz_abbr = new_abbr;
            break;
    }

timezone関連の処理が続いてますね。。。

    php_date_get_current_time_with_fraction(&sec, &usec);
    timelib_unixtime2local(now, (timelib_sll) sec);
    php_date_set_time_fraction(now, usec);

きた!

遂に見つけました...!!

php_date_get_current_time_with_fraction

正にというような名前ですね。

static void php_date_get_current_time_with_fraction(time_t *sec, suseconds_t *usec)
{
#if HAVE_GETTIMEOFDAY
    struct timeval tp = {0}; /* For setting microseconds */

    gettimeofday(&tp, NULL);
    *sec = tp.tv_sec;
    *usec = tp.tv_usec;
#else
    *sec = time(NULL);
    *usec = 0;
#endif
}

#if HAVE_GETTIMEOFDAYこの記述はC言語ifdef機能で、条件コンパイラのための機能です。 HAVE_GETTIMEOFDAY0 or 1で定義することで、どちらのコードが取り込まれるかが決まります。

C言語の現在時間取得メソッドである gettimeofday(&tp, NULL);time(NULL); 処理があります。

gettimeofday は POSIX に準拠した関数で、マイクロ秒単位の精度で現在の時刻を取得します。 linuxjm.osdn.jp

timeはC言語の時間ライブラリの関数です。 www.geeksforgeeks.org

ちなみにWindowsのようなOSの場合はどうなるのか??

windows環境ではHAVE_GETTIMEOFDAY1です github.com

windows環境でのgettimeofdayはここにある処理が走ります github.com

結果としてここのコードでwindows OSの時間を取得しています https://github.com/php/php-src/blob/5b01c4863fe9e4bc2702b2bbf66d292d23001a18/win32/time.c#L31

static zend_always_inline MyGetSystemTimeAsFileTime get_time_func(void)
{/*{{{*/
    MyGetSystemTimeAsFileTime timefunc = NULL;
    HMODULE hMod = GetModuleHandle("kernel32.dll");

    if (hMod) {
        /* Max possible resolution <1us, win8/server2012 */
        timefunc = (MyGetSystemTimeAsFileTime)GetProcAddress(hMod, "GetSystemTimePreciseAsFileTime");
    }

    if(!timefunc) {
        /* 100ns blocks since 01-Jan-1641 */
        timefunc = (MyGetSystemTimeAsFileTime) GetSystemTimeAsFileTime;
    }

    return timefunc;
}/*}}}*/

GetSystemTimePreciseAsFileTimeGetSystemTimeAsFileTimeという関数がwindowsの関数ですね。

Windows8以降でより高精度の時間を取得する関数が増えたために分岐があるようです。 metatrading.hatenablog.com

まとめ

ということで、PHPでCarbon::nowをコールするとPHP内部のC言語でOSのシステム時刻を取得しています!

Carbon::now(Carbonクラス) → DateTime(PHP) →gettimeofday/ time (C言語)→ OS ...

OSがどの様に時刻を刻んでいるか、その仕組みなど興味があれば色々調べてみると面白いです。 ja.wikipedia.org

何気なく使っている関数の裏側にはどんな処理が走っているか、WEB上や自分の環境で確認できる手段を持つことは理解を深める一手になるのではないかと思います。

【テックブログ】新卒エンジニアが入社2ヶ月で新規サービスをリリースした話。

こんにちは! 株式会社Hajimari22卒エンジニアの神野 凌太郎です。

普段は、事業部づけのエンジニアとして人事プロパートナーズの開発業務を担当しています。 内定者インターン〜新卒入社後の期間、人事プロパートナーズの開発と並行して 新規サービス「アミーチ」の開発・立ち上げを1人で行いました。

今回は、実際に新規事業の開発・立ち上げを行なって、学んだこと・反省点をまとめてお伝えできればと思います!

■新規サービス「アミーチ」概要

『人事専門型の求人サイト』として立ち上げたサービスです。 もともと、人事プロパートナーズ内で人材紹介を行なっており、 その専用サイトを立ち上げようというところからスタートしています。

人事を扱う求人サイトだからこそ、学校の保健室のように、 気軽にキャリアを考えられる、そんな求人サイトを作りたいという思いから、 「アミーチ」の立ち上げがスタートしました。

現在、リリースから約2ヶ月たち、 ユーザー数、求人数ともに右肩上がりで増えていますので、 正社員人事を採用したい企業様、人事職にチャレンジしたい、キャリアアップをしたいとお考えの求職者様、ぜひご登録をお待ちしております!

biz.ami-chi.com

ami-chi.com

■開発に着手するまで

開発をするにあたって、まずざっくりとした要件定義を行いました。 後述しますが、当時は何よりも早く開発することを優先していたため、 開発の段階では「ざっくりと要件を詰めれば良い」という認識でいました。

サイトに関しては、先に求人を募るために TO B → TO Cサイトの順でリリースを目指すことになりました。

具体的なスケジュールとしては、 TO B向けサイトは5月末、TO C向けサイトは6月中旬のリリースを目指しました。

技術選定は、Laravelとvue.jsを利用し、デザイン部分はbootstrapを利用することにしました。

サービスの立ち上げ・開発自体が初めての経験であったかつ、 1人で開発を行う(LPは外注しました)という状況だったので、 不安を抱えながら開発をすることになりました。

■開発過程〜リリースに至るまで

実際、TO B向けサイトは5月末、TO C向けサイトは6月中旬と スケジュール通りにリリースまで持っていくことができました。 しかし、リリースに至るまでいくつものハードルがありました。

要件が途中でいくつも変更があったこと

最も大変だったハードルは、「途中で要件が変わること」でした。

もともとフワッと始まったプロジェクトだったので、 要件をガチガチに決めていなかったこともあり、ビジネスサイドとの認識齟齬がある状態で 開発を進めてしまったためです。

特に、リリース直前にいくつか改善案や意見をもらったときは、 「本当にリリースできるのか?」と思ったくらい絶望的な状態でした。

ただ、もともとひいていたスケジュールより、 前倒しして開発を進めていたので、優先順位をつけて対応することで リリース時期がズレることなく、リリースすることができました。

学んだこととしては、

  • 時間がかかっても、要件定義はガチガチに固めてから開発をすること
  • キャパを越えそうなときは、優先順位を目に見える形で洗い出してから対応すること

です。

要件定義は、開発スピードを優先していたとしても、 きちんと行なったほうがスムーズに開発できることを身を持って知りました。(当たり前のことかもしれませんが) 最低でも、「テーブル定義」「機能設計」「画面設計」「URI設計」「モデル図」くらいは行なったほうが良いと思います。 アウトプットをあらかじめ行なっておくことで、プロジェクトメンバーとコンセンサスが取れている状態で認識齟齬なく開発を行うことができます。

メインサービス「人事プロ」との開発両立

2つ目は、「サービス開発の両立」です。 弊事業部では、エンジニア2人チームで行っているため、 新規サービスの開発だけに専念できるわけではありません。

別で、先輩エンジニアが大規模の新規サービス開発を行なっていたため、 そちらの開発にかかっきりな状態ということもあり、 メインサービスの「人事プロ」の開発業務も兼任していました。

新規サービスを立ち上げをするにあたって、 大前提メインの事業の収支を成り立たせないといけません。

また事業自体が少数精鋭で運営しているので、 エンジニア観点から常にインパクトのある改善、開発を行なっていく必要があります。

新規プロダクトの開発は楽しいし、やりがいもある一方で、 会社や事業部の誰かがメインの事業にコミットして数字を成り立たせてくれるからこそ、 チャレンジができます。

両立はすごく大変でしたが、改めて数字を成り立たせる重要性を身を持って体感しました。

不安との葛藤

1人で開発していたということもあり、常に不安との葛藤がありました。 特に、技術が成熟しているわけでもなく、「このコード設計で問題なく動くのかな…」といった不安は常につきまとっていました。

加えて、リソースもギリギリな状態で開発を行なっていたので、 納期へのプレッシャーもたくさんありました。

不安を解消するために行なったことは、 開発後の「セルフレビュー」と「テスト」です。

自分が書いたコードが動くわけがないという視点で セルフレビューとテストを行うことによって、サービスのクオリティを維持することができました。

学んだこととしては、開発をひと段落した後に安堵、過信をしないこと。 繰り返し確認・テストすることで、サービスクオリティ維持、不安の解消に繋がることを身を持って知ることができました。

■まとめ

新卒入社でいきなり新規サービスの開発を任される経験は滅多にないと思うので、 かなり良い経験でした!

実際に開発が終わり、リリースした時の感動は凄まじかった…!

まだサービスの改善をするところはたくさんあるので、 より良いサービスを目指して、日々開発に勤しもうと思います!

最後まで読んでいただきありがとうございました!


株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、
一緒に開発を行なっていただけるエンジニア募集しています! 長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com

puppeteerでbootstrap導入時の影響調査をやってみた!

puppeteerを使ってみた!

こんにちは!
6月から23卒内定者インターン生として株式会社Hajimariに入社した、江端 凌です。

普段は、TUKURÜS事業部で受託開発の業務に携わっています。

今回は受託開発している既存サイトに、bootstrapを導入することになったので、どのような影響が出るのかを調査することになりました。

背景としては、既存サイトのCSSの複雑化と適切なブレークポイントの設置が出来ていなかったことから導入に至りました。

そこでbootstrapの導入前と導入後を比較するために、node.js製のライブラリであるpuppeteerを利用して全ページのスクリーンショットと差分画像を生成しました!

今回はpuppeteerの概要から、実装までの流れを紹介させていただきたいと思います!

puppeteerとは?

puppeteerとは、自動でブラウザ操作を行えるnode.jsのライブラリです。

npmでインストールすると、操作するchromiumも一緒についてきます。
puppeteerは、このchromiumでブラウザを操作していきます。

e-words.jp

puppeteerの概要としては、

  1. ヘッドレスで操作できる。
  2. 動的に生成されるページ(SPAなど)でも操作ができる。

といった特徴があります。

qiita.com

puppeteerを使ってやったこと

では、今回puppeteerを使ってやりたいことを確認していきます。

bootstrapを既存のサイトに導入した場合、すでに存在しているCSSと競合してデザイン崩れを起こす可能性があります。

その時、どのような影響が出るのかを調査したいため、

  • テスト環境(bootstrap未実装)
  • local環境(bootstrap導入済み)

の二つを利用して、puppeteerを使って両方の環境でスクリーンショットを撮りました。

その後、looks-sameというライブラリで差分画像を生成をします。

looks-sameもnode.jsのライブラリですので、puppeteerと一緒にインストールできますが、長くなるので今回は割愛させていただきます。

例として今回は、弊社のホームページのスクリーンショットを自動で撮影するプログラムを作りましょう!

puppeteerを導入してみる

では早速、npmを使ってpuppeteerをインストールしていきます!

node.jsをインストールをしていない場合は、node.jsのインストールを行なってください。

node.jsがインストールされているかどうか*1は、node -vコマンドで確認できます。

&#x60;node -v&#x60;コマンドの実行画面

qiita.com

npmでインストールする

まず、puppeteerを使うフォルダを作ります。

適当にデスクトップなど(どこでもいいです!)に移動して、以下のコマンドを実行してください。

$ mkdir puppeteer && cd puppeteer

そうしたらフォルダが作成されていると思います。

続いて、npmでpuppeteerをインストールしていきます!

$ npm init && npm install puppeteer

そうするとREPLで色々聞かれるので、全部Enterキーで答えましょう。

使うバージョンなどを聞かれています。

npm init &amp;&amp; npm install puppeteerの実行

画像のような表示になったらインストールは完了です。

puppeteer単体をインストールしたい方は、

$ npm install puppeteer-core

で単体でインストールすることが可能です。*2

実際にコードを書いてみる

今回は弊社のホームページスクリーンショットしてみます。

先ほど作成したpuppeteerフォルダ配下にindex.jsと画像を保存するdistフォルダを作成しましょう。

$ touch index.js && mkdir dist

このindex.jsの中に処理を書いていきます。

index.jsファイルがエントリーポイントになります。

e-words.jp

本当は全ページ撮影するなら、ConstsファイルにURLやログイン情報などを保存しておく方が便利ですが、今回は簡易的にindex.jsだけで書いていきます。

index.js

const puppeteer = require('puppeteer');

/**
 * 引数に渡した数 * ミリ秒待機
 * @param {int} msec
 * @returns setTimeout
 */
function sleep(msec) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
    }, msec);
  });
}

// puppeteerの設定
(async () => {
  const browser = await puppeteer.launch({
    // ヘッドレスかどうか
    headless: false,  
    // 10000ミリ秒応答が無かったら終了する
    timeout: 10000,  
  });

  const page = await browser.newPage();
  await page.setViewport({ width: 1920, height: 1080 });

  // ページに移動
  await page.goto('https://www.hajimari.inc/');

  // 画面遷移のアニメーションが終わるまで待機
  await sleep(8000);

  // スクリーンショットを取ってdistフォルダに保存する。
  await page.screenshot({ path: 'dist/hajimari.png', fullPage: false });

  // ブラウザを閉じる
  await browser.close();
})();

上記のコードを書いて保存したら、node index.jsをターミナルで実行しましょう。

$ node index.js

distフォルダ配下にhajimari.pngが保存されているはずです。

こんな感じで ↓

Hajimariのトップページ

それぞれのコードについて

それぞれのコードを見ていきましょう。

puppeteerは基本的に非同期で動作するので、asyncawaitで処理を書いていきます。

qiita.com

puppeteerの設定

(async () => {
  const browser = await puppeteer.launch({
    headless: false,  
    timeout: 10000,  
  });

  const page = await browser.newPage();
  await page.setViewport({ width: 1920, height: 1080 });

ここでは、puppeteerの設定ができます。

3行目のheadless: falseは、ヘッドレスにするかどうかを決めています。

今回はわかりやすく、falseにすることでchromiumの動きを見えるようにしました。必要なければtrueにすることで見えなくなります。

8行目で表示するサイズを変えられます。

await page.setViewport({ width: 1920, height: 1080 });

今回はPC表示にしていますが、スマホサイズで撮影したい場合は、
widthheightスマホのサイズにすることで撮影可能です。

スクリーンショットの処理

// ページに移動
await page.goto('https://www.hajimari.inc/');

// 画面遷移のアニメーションが終わるまで待機
await sleep(8000);

// スクリーンショットを取ってdistフォルダに保存する。
await page.screenshot({ path: 'dist/hajimari.png', fullPage: false });

// ブラウザを閉じる
await browser.close();

1行目で目的のページに移動します。

page.goto()に目的のURLを渡してあげましょう。ここを変更すると別のページにも飛ぶことができます。

2行目のsleep()は上の方で作った関数です。

function sleep(msec) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
    }, msec);
  });
}

細かい説明は省きますが、引数に渡した数字 × ミリ秒だけ待機します。

なので、await sleep(8000);は8秒待機させています。

3行目の処理はスクリーンショットをして、引数に渡した保存先に保存しています。

4行目でブラウザを閉じて、puppeteerの処理を終了しています。

まとめ

自分はこのpuppeteerアプリを作成したことで、何度も確認したりスマホ画面に切り替えたりできて効率化につながりました!

またヘッドレスでも動くので、puppeteerを動かしながら別の業務もできます。

今回は紹介しきれませんでしたが、差分画像を生成するlooks-sameというライブラリを使ったり、ベーシック認証を突破したり、Xpathで要素を取得してボタンを押したりもできたりして、とても幅広いです。

今後もE2Eテスト用に使ったりできそうなので、より良いものにできるように保守していきたいと思っています!


株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、
一緒に開発を行なっていただけるエンジニア募集しています! 長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com

*1:puppeteer@3.0.0以降は、Node v10.18.1以降の環境が必要になるので、適宜アップデートしてください。

*2:puppeteer単体でインストールする場合は、chromiumはインストールされないので、注意してください。

PHPStan導入のすすめ

こんにちは!
株式会社Hajimari21卒エンジニアの古田 鏡です。

普段は、TUKURUS事業部(旧PIECE事業部)で
自社プロダクトであるスタートアップ向けマッチングサイト構築パッケージPIECE
https://crowd.itpropartners.com/piece/)の開発や受託開発を行っています!
※2022年4月から事業部の方針変更により事業名が変更となりました!

crowd.itpropartners.com

現在ジョインさせていただいている案件では、
組織のオンボーディングシステムの新規機能開発・システム統合等を担当させていただいております。

開発言語に関してはバックエンドでPHPフレームワークはLaravel)を使っていて、
静的解析ツールPHPStanを導入しているのですが、
このツールが便利だったので今回はご紹介していきたいと思います!

PHPの特徴

そもそもPHPは「動的型付け言語」に分類され、
基本的にはデータ型の宣言がいらないプログラミング言語です。

※参考 動的言語と静的言語の違い

型の宣言がいらないことで得られるメリットとしては、

  • どのような型の値でも代入でき、型(文字列・数値・配列・オブジェクト等)を意識する必要がない
  • (型の宣言がいらない分)記述量が少なくすむ
  • ソースコードの変更(修正)に強い

この辺が挙げられるのではないかと思います。

私もこれまで実際PHPの案件をやっている中で、
型を意識せずに面倒から解放される点でPHP最高ー!!と思っていました。

ですが、現在携わっている案件で、複数人での開発・システム統合をする中で、
コードの統合・不要コード削除に伴い、影響範囲の大きな修正が増えたことで、
静的解析の重要性を身に染みて感じました、、、
(修正がバグを生む可能性大、、、テストコードを全て書くのは時間がかかりますし、、、)

そこでPHPStanの出番です!!

●PHPStanとは?

PHPStan は、
PHP コードの静的解析ツールです。 (PHP Static Analysis Toolの略)
https://phpstan.org/

MIT ライセンスで公開されており、
Composer でインストールして利用できます。
また、Docker イメージも公式で公開されております。

github.com

phpstan.org

概要
  • 動作にはPHP 7.2以降が必要
  • 未定義の変数・メソッド・Class・プロパティなどを検出
  • PHPDocの構文チェックが可能

●ルールレベル設定

PHPStanはルールレベル設定があり、
レベルは0~9の10段階で設定できます!
レベル9ともなると結構厳しめのチェックとなるようです、、、!

phpstan.org

Lev 内容
0 基本的なチェック、未知のクラス、未知の関数、$this上で呼び出された未知のメソッド、それらのメソッドや関数に渡された引数の数が間違っている、常に未定義の変数をチェック
1 未定義の変数、call と get を持つクラスの未知のマジックメソッドとプロパティがある可能性がある
2 ($this だけでなく)すべての式で未知のメソッドをチェックし、PHPDocs を検証する
3 戻り値の型、プロパティに割り当てられた型の確認
4 基本的なデッドコードチェック - instanceofやその他の型チェックが常にfalse、到達しないelse文、return後の到達不能コードなど
5 メソッドや関数に渡される引数の型チェック
6 タイプヒントの欠落を報告する
7 部分的に間違っている論理和型の報告 - 論理和型の一部の型にしか存在しないメソッドを呼び出した場合、レベル7はそのことを報告し始めます(その他の不正確な状況も)
8 null可能な型に対するメソッド呼び出しとプロパティへのアクセスを報告する
9 混合型に厳密であること - この型で唯一許される操作は、この型を別の混合型に渡すことである

下記に例題が載っているのでチェックしてみてください!

phpstan.org

●インストール方法

インストールする方法としては

phpstan.org

公式にインストールする方法が記載してあるので、それに従いインストールします。

Composerを使い下記のコマンドを実行します。

composer require --dev phpstan/phpstan

基本的に開発環境で使用するものだと思いますので、
--devオプションをつけるのが良いと思います。

インストールできているか確認

./vendor/bin/phpstan analyse --version
PHPStan - PHP Static Analysis Tool 0.12.99⇦インストールされているバージョンを表示

●実行方法

実行コマンド
./vendor/bin/phpstan analyse

ルールレベルを変更する場合には、 レベルを変更するには --level (-l )オプションで実行します。
↓ではレベル4で設定しています。

.\vendor\bin\phpstan analyse -l 4
設定ファイル(phpstan.neon 一部抜粋)
includes:
    - ./vendor/nunomaduro/larastan/extension.neon

parameters:

    paths:
        - app

    level: 4
実行結果

エラーありの場合

エラーなしの場合

こんな感じで結果が出ます。

●実際に使ってみて得られたこと

  • 導入のしやすさ
    基本的に公式通りでインストールでき、導入のハードルが低いこと。

  • 解析が高速にできる
    状況に応じて、ルールレベル・実行範囲を変えられ、必要部分に対して解析が高速にできること

  • レビューの負担軽減
    システム統合など大きな改修を行う場合、コードレビューの量が多くなりがちですが、
    事前に静的解析を行うことでレビューの量を減らせること
    (個人的にはこの効果が一番大きいと思いました。)

  • ある程度システムの安全性・整合性を担保できる
    型レベルの安全性・整合性が保たれること
    しかしながら、無限ループや関数の型がmixed・null許容の場合、解析をすり抜けてしまうことがあること

●まとめ

大前提として、別途ユニットテスト等は必要だと思いますが
導入と実行の容易さ、時間短縮の観点から非常にコスパの良いツールだと思いました!
プロジェクトが大きくなればなるほど、効果を発揮するツールだと思いますので、是非導入してみてはいかがでしょうか!


株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、
一緒に開発を行なっていただけるエンジニア募集しています! 長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com