こんにちは、@TECH LEAD です。
第4回となる今回は、TECH LEADのサーバーサイド(内部API 層)の技術をご紹介します。
アーキテクチャー編
フロントエンド編
サーバーサイド(アプリケーション層)編
サーバーサイド(内部API 層)編 ←いまここ
インフラ編
目次
はじめに
内部API 層は、データベース・Elasticsearchの操作、メール送信処理などに直接アクセスし、それらの処理をラップ(抽象化)しています。
各アプリケーションからのリクエス トを元に、データベースなどを操作し、データを作成・取得・更新をしています。
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
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 /
├── 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
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
{ ... }
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
) { ... }
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
) { .... }
public function onPutFailure( FailureInterface $ failure )
{ ... }
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
) { ... }
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
) { ... }
public function onFailure( FailureInterface $ failure )
{ ... }
public function onDelete( int $ id , int $ user_account_id )
{ ... }
public function onDeleteValidate(
int $ id ,
int $ user_account_id
) { ... }
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
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
@Link
@Link
@Link
@Link
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