TECH LEAD Blog

IT・WEBエンジニアのキャリア相談/転職支援サービス【TECH LEAD(テックリード)】が、エンジニアキャリアに関する記事やtwitterアンケート企画「エンジニア世論調査」の結果を投稿しています。

【サーバーサイド(内部API層)編】TECH LEADの技術を惜しげも無く公開します!React × PHP Laravel × BEAR.Sunday × SSR

こんにちは、@TECH LEADです。

第4回となる今回は、TECH LEADのサーバーサイド(内部API層)の技術をご紹介します。

  1. アーキテクチャー編
  2. フロントエンド編
  3. サーバーサイド(アプリケーション層)編
  4. サーバーサイド(内部API層)編 ←いまここ
  5. インフラ編

目次

はじめに

内部API層は、データベース・Elasticsearchの操作、メール送信処理などに直接アクセスし、それらの処理をラップ(抽象化)しています。

各アプリケーションからのリクエストを元に、データベースなどを操作し、データを作成・取得・更新をしています。

TECH LEADのアーキテクチャー
TECH LEADのアーキテクチャ

内部API層では、Bear.Sundayというフレームワークを使用しています。あまり聞いたことがないという方もいると思いますので、Bear.SundayというPHPフレームワークの紹介と内部API層で実装されているコードを紹介したいと思います。

主な技術 

PHP 7.2

フレームワーク

BEAR.Sunday

BEAR.Sundayの紹介

BEAR.Sundayは、リソース指向のPHPアプリケーションフレームワークです。

リソース指向とは、RESTリソースを元にした考え方です。

MVCのようにControllerがModelからデータを受け取り、Viewへ渡すのではなく、各リソースがレンダラーを持ちリソースを表現します。

Resource-Method-Representation
Resource-Method-Representation

BEAR.Sundayは3つのオブジェクトフレームワークで構成されています。

今回は、一番特徴的なBEAR.Resourceについて少し紹介したいと思います

BEAR.Resouceは、REST(Hypermedia)のような振る舞いを可能にするハイパーメディアフレームワークです。

1つのURIのリソースは、1つのクラス(リソースオブジェクト)でマッピングされます。 リクエストによってリソースの状態をつくり、リソース自身が持っているレンダラーによって、表現、アウトプットします。

また、<a>タグの様にリソースに関連するリソースを紐づけたり、<img>タグの様にリソースに別のリソースを埋め込むが事ができます。

これからTECH LEADのコードを見ながら説明していきたいと思います。

参考

https://github.com/bearsunday/BEAR.Sunday

http://bearsunday.github.io/manuals/1.0/ja/index.html

https://ja.wikipedia.org/wiki/Representational_State_Transfer

※今回、説明から省かせていただきましたが、Ray.DIと Ray.AOPも興味のある方は、是非調べてみてください!

内部API層の実装

ディレクトリ構成

.api/
├── bootstrap/ 
├── src/Resource/
|  └── App/
|    |                     # データベース
|    ├── Common/           # サービス共通のリソース(ユーザー情報・マスターデータなど)
|    ├── Agent/            # TECH LEAD Agentのリソース
|    ├── Job/              # TECH LEAD Jobのリソース
|    ├── Resume/           # TECH LEAD Resumeのリソース
|    |
|    ├── Searcher/         # Elasticsearchのリソース
|    └── Mail/             # メール送信のリソース
├── var/
├── vender/
├── api.php                # アプリケーション層で読込用のエントリポイント
|                          # 内部API層をインスタンス化しものを返す
├── autoload.php
├── composer.json
└── composer.lock

今回は、データベースのリソースを説明します。

TECH LEADサービスでは、1つのデータベース上に全てのサービスのデータが入っています。 サービス固有のデータに関しては、テーブル名にuser_resume_(TECH LEAD Resume)のようなプレフィックスをつけ、どのサービスで使用しているテーブルか識別しやすくしています。 また、リソースはテーブル名と対応させ、テーブル毎にネームスペースを用意してリソースオブジェクト(クラス)を作成しています。

TECH LEAD Resumeのプロジェクト取得

今回は例として、TECH LEAD Resumeのプロジェクト(user_resume_projectsテーブル)取得の部分を紹介します。

<?php
// .api/src/Resource/App/Resume/UserResumeProject/Index.php
namespace GlobalShift\TechLead\Api\Resource\App\Resume\UserResumeProject;

use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
use Koriym\HttpConstants\StatusCode;
use Ray\AuraSqlModule\AuraSqlDeleteInject;
use Ray\AuraSqlModule\AuraSqlInject;
use Ray\AuraSqlModule\AuraSqlInsertInject;
use Ray\AuraSqlModule\AuraSqlSelectInject;
use Ray\AuraSqlModule\AuraSqlUpdateInject;
use Ray\Validation\Annotation\OnFailure;
use Ray\Validation\Annotation\OnValidate;
use Ray\Validation\Annotation\Valid;
use Ray\Validation\FailureInterface;
use Ray\Validation\Validation;

class Index extends ResourceObject
{
    use AuraSqlInject;
    use AuraSqlDeleteInject;
    use AuraSqlInsertInject;
    use AuraSqlSelectInject;
    use AuraSqlUpdateInject;
    use ResourceInject;

    public function onGet(int $user_account_id) : ResourceObject
    {...}

    /**
     * @Valid("put")
     */
    public function onPut(
        int $id,
        int $user_account_id,
        string $name,
        string $start_date,
        int $user_resume_office_position_id,
        int $user_resume_business_work_id = null,
        string $company_name = null,
        bool $is_secret = null,
        string $end_date = null,
        string $note = null,
        int $headcount = null,
        string $reference_url = null,
        int $user_resume_business_field_id = null
    ) {...}

    /**
     * @OnValidate("put")
     */
    public function onPutValidate(
        int $id,
        int $user_account_id,
        string $name,
        string $start_date,
        int $user_resume_office_position_id,
        int $user_resume_business_work_id = null,
        string $company_name = null,
        bool $is_secret = null,
        string $end_date = null,
        string $note = null,
        int $headcount = null,
        string $reference_url = null,
        int $user_resume_business_field_id = null
    ) {....}

    /**
     * @OnFailure("put")
     */
    public function onPutFailure(FailureInterface $failure)
    {...}

    /**
     * @Valid("post")
     */
    public function onPost(
        int $user_account_id,
        string $name,
        string $start_date,
        int $user_resume_office_position_id,
        int $user_resume_business_work_id = null,
        string $company_name = null,
        bool $is_secret = null,
        string $end_date = null,
        string $note = null,
        int $headcount = null,
        string $reference_url = null,
        int $user_resume_business_field_id = null
    ) {...}

    /**
     * @OnValidate("post")
     */
    public function onValidate(
        int $user_account_id,
        string $name,
        string $start_date,
        int $user_resume_office_position_id,
        int $user_resume_business_work_id = null,
        string $company_name = null,
        bool $is_secret = null,
        string $end_date = null,
        string $note = null,
        int $headcount = null,
        string $reference_url = null,
        int $user_resume_business_field_id = null
    ) {...}

    /**
     * @OnFailure("post")
     */
    public function onFailure(FailureInterface $failure)
    {...}

    /**
     * @Valid("delete")
     */
    public function onDelete(int $id, int $user_account_id)
    {...}

    /**
     * @OnValidate("delete")
     */
    public function onDeleteValidate(
        int $id,
        int $user_account_id
    ) {...}

    /**
     * @OnFailure("delete")
     */
    public function onDeleteFailure(FailureInterface $failure)
    {...}

    private function deleteProjectsTags(int $user_resume_project_id, int $user_account_id)
    {...}

    private function deleteProjectsSkills(int $user_resume_project_id, int $user_account_id)
    {...}
}

リソースオブジェクトのonGet() onPost() onPut() onDelete()の各メソッドは、それぞれHTTPメソッドに対応しています。

/resume/user-resume-project/indexにGETリクエストをするとIndexクラスのonGet()メソッドが呼び出されます。ここでは、引数に$user_account_idが必要なので、/resume/user-resume-project/index?user_account_id=1の様にパラメータをつけてリクエストします。

IndexクラスのonGet()内を詳しく見ていきます。

<?php

public function onGet(int $user_account_id) : ResourceObject
    {
        $this->select->cols(['*'])
            ->from('user_resume_projects');
            ->where('user_account_id = ?', $user_account_id)
            ->orderBy(['start_date DESC']);

        // ユーザーに紐づく全プロジェクトを取得
        $userResumeProjects = $this->pdo->fetchAll($this->select->getStatement(), $this->select->getBindValues());

        $data = [];

        // プロジェクトの関連情報も取得する
        foreach ($userResumeProjects as $key => $project) {
            $data[$key] = [];

            $request = $this
                ->resource
                ->get
                ->uri('app://self/resume/user-resume-project/detail')
                ->withQuery(['user_account_id' => $user_account_id, 'id' => $project['id']]);

            $request->linkCrawl('user_resume_projects_tags');

            // 外部キーがない場合は、クロールさせない
            // キーがない状態でクロールすると落ちる
            if ($project['user_resume_office_position_id']) {
                $request->linkCrawl('user_resume_office_position');
            }
            if ($project['user_resume_business_work_id']) {
                $request->linkCrawl('user_resume_business_work');
            }
            if ($project['user_resume_business_field_id']) {
                $request->linkCrawl('user_resume_business_field');
            }

            $request->linkCrawl('user_resume_projects_skills');

            $request->request();

            $data[$key] = $request->body;
        }

        $this->body = $data;

        return $this;
    }

IndexクラスのonGet()では、まずユーザーに紐づく全てのプロジェクト情報を取得しています。

ユーザーに紐づくプロジェクトを取得したら、プロジェクト情報に紐づくポジションや企業情報などを取得するために、/resume/user-resume-project/detailへGETリクエストし、プロジェクトのリレーションデータを取得しています。

linkCrawl()は、リソースオブジェクトに設定されている@Linkアノテーションクロールを実行し、リレーションデータを収集してくれます。

DetailクラスのonGet()をみて見ましょう。

<?php
// .api/src/Resource/App/Resume/UserResumeProject/Detail.php
namespace GlobalShift\TechLead\Api\Resource\App\Resume\UserResumeProject;

use BEAR\Resource\Annotation\Link;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
use Koriym\HttpConstants\StatusCode;
use Ray\AuraSqlModule\AuraSqlInject;
use Ray\AuraSqlModule\AuraSqlSelectInject;

class Detail extends ResourceObject
{
    use AuraSqlInject;
    use AuraSqlSelectInject;
    use ResourceInject;

    /**
     * @Link(crawl="user_resume_projects_tags", rel="user_resume_projects_tags", href="app://self/resume/user-resume-projects-tag/index?user_resume_project_id={id}&user_account_id={user_account_id}")
     * @Link(crawl="user_resume_projects_skills", rel="user_resume_projects_skills", href="app://self/resume/user-resume-projects-skill/index?user_resume_project_id={id}&user_account_id={user_account_id}")
     * @Link(crawl="user_resume_business_work", rel="user_resume_business_work", href="app://self/resume/user-resume-business-work/detail?id={user_resume_business_work_id}&user_account_id={user_account_id}")
     * @Link(crawl="user_resume_business_field", rel="user_resume_business_field", href="app://self/resume/user-resume-business-field/detail?id={user_resume_business_field_id}")
     * @Link(crawl="user_resume_office_position", rel="user_resume_office_position", href="app://self/resume/user-resume-office-position/detail?id={user_resume_office_position_id}")
     */
    public function onGet(int $id, int $user_account_id) : ResourceObject
    {
        $this->select->cols(['*'])
            ->from('user_resume_projects')
            ->where('id = ?', $id)
            ->where('user_account_id = ?', $user_account_id);

        $userResumeProject = $this->pdo->fetchOne($this->select->getStatement(), $this->select->getBindValues());

        if (!$userResumeProject) {
            $this->code = StatusCode::BAD_REQUEST;
            $this->body = [];

            return $this;
        }

        $this->body = $userResumeProject;

        return $this;
    }
}

@Link(crawl="クロール名", rel="クロール結果がここで指定したkeyでbodyに入る", href="リクエストするリソースを指定")

Detailクラスでは、@Linkアノテーションのクロールを設定していて、プロジェクトのリレーションデータを収集できる様になっています。

クロールは、リソースの$body内の値を使用して、hrefに指定しているリソースへリクエストします。クロールした結果は、リクエストしたリソースオブジェクトのbody内にrelで指定したキーで入ります。

例:@Link(crawl="user_resume_projects_tags", rel="user_resume_projects_tags", href="app://self/resume/user-resume-projects-tag/index?user_resume_project_id={id}&user_account_id={user_account_id}")の場合、{id}{user_account_id}それぞれに$this->body['id']$this->body['user_account_id']の値が入ります。クロール結果は、$this->body['user_resume_projects_tags']に入ります。

/resume/user-resume-project/index?user_account_id=1にGETリクエストをした場合、レスポンスは下記の様なツリー構造のデータになります。

200 OK
content-type: application/hal+json
{
    "0": {
        "id": "1",
        "user_account_id": "1",
        "user_resume_business_work_id": "1",
        "name": "新規プロジェクト",
        "start_date": "2019-04-01",
        "end_date": null,
        "headcount": 10,
        "user_resume_business_field_id": 1,
        "reference_url": null,
        "note": null,
        "created_at": "2019-04-01 00:00:00",
        "updated_at": "2019-04-01 00:00:00",
        "user_resume_office_position_id": "1",
        "company_name": null,
        "is_secret": "0",
        "user_resume_projects_tags": [
            {
                "user_resume_project_id": "1",
                "user_resume_project_tag_id": "1"
            }
        ],
        "user_resume_office_position": {
            "id": "1",
            "name": "サーバーサイド",
            "sort_no": "1",
            "url_word": "sever-side"
        },
        "user_resume_business_work": {
            "id": "27",
            "name": "人材・HR",
            "sort_no": "5",
        },
        "user_resume_business_work": {
            "id": "1",
            "user_account_id": "1",
            "name": "グローバルシフト株式会社",
            "start_date": "2019-04-01",
            "end_date": null,
            "employment_pattern": "regular",
            "note": "",
            "created_at": "2019-04-01 00:00:00",
            "updated_at": "2019-04-01 00:00:00"
        },
        "user_resume_projects_skills": [
            {
                "id": "1156",
                "user_resume_project_id": "59",
                "skill_category_id": "1",
                "skill_id": "6",
                "name": null,
                "version": null,
                "skill_name": "PHP",
                "start_date": "2014-04-01",
                "end_date": null,
                "skill_category": {
                    "id": "1",
                    "name": "言語",
                    "sort_no": "1"
                },
                "skill": {
                    "id": "6",
                    "skill_category_id": "1",
                    "name": "PHP",
                    "sort_no": "6",
                    "group_type": null
                }
            }
        ]
    },
    "_links": {
        "self": {
            "href": "/resume/user-resume-project/index?user_account_id=1"
        }
    }
}

実装時に悩んだ事

開発時、内部API層はなるべくシンプルにデータの取得・更新をだけを行うように実装しようと考えていました。

しかし、実際にコーディングしていくとリレーションデータを含んだリストデータが必要になりましたが、クロールリンクはリストデータに対して、それぞれのリレーションデータを取得する事ができませんでした。

アプリケーション側で、まずリストデータを取得してからループでそれぞれのデータの関連データを取得するのが綺麗に書けると思いましたが、データが多いと何度も内部API層にリクエストをしなければならなくなり、重くなってしまいます。

上記のプロジェクト取得の場合、アプリケーション側で必ずリレーションデータを使用してページを描画していたため、onGet()内でlinkCrawl()を使ってリレーションデータを取得することにしました。※この方法だとリレーションデータが必要なくても収集してしまうデメリットもあります。

現状、データによって内部API層でリレーションデータを含んだリストデータを作成するパターンとアプリケーション側でリレーションデータを再度取得しているパターンの両方を場合によって使い分けています。

まとめ

ここまでBEAR.Sundayの紹介とTECH LEADでの実装を紹介しました。

BEAR.Sundayは、RESTをアプリケーションのフレームワークとしてコンポーネントを作成し、HTTPをアプリケーションプロトコルとして扱う新しいパターンのフレームワークです。

今までMVCパターンのフレームワークしか使った事がなく、初めてBEAR.Sundayを知った時は、「こんな考え方があるんだ!」「すごい!」「面白い!」と感じました。

DI(依存性の注入)やAOP(アスペクト指向プログラミング)もBEAR.Sundayを使って初めて知った技術だったのでエンジニアとして、とても勉強になるフレームワークだと思います。

機会があればAPIサーバなどに使ってみてください!

エンジニアの皆さんへお願い

もし、「ここをもっと聞きたい」「うちではこうやっている」と言ったご意見・アドバイスなどありましたら、@TECH LEADまでリプライやDMにてお気軽にお知らせください!

よろしくお願いします!

次回予告

次回は【インフラ編】で、AWSの設定・Terraform・Ansibleついて全体的に話したいと思います。

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

PR

「今は具体的な転職を考えていないよ」と言う方も、是非一度TECH LEADの各サービスを触ってみていただけると嬉しいです。

TECH LEAD Job

IT/WEBエンジニア専門の求人サービス|TECH LEAD Job

TECH LEAD Resume

TECH LEAD Resume|IT・WEBエンジニア向けレジュメ管理サービス

TECH LEAD Agent

IT/WEBエンジニア専門の転職支援サービス|TECH LEAD Agent