【サーバーサイド(アプリケーション層)編】TECH LEADの技術を惜しげも無く公開します!React × PHP Laravel × BEAR.Sunday × SSR
こんにちは、@TECH LEADです。
第3回となる今回は、TECH LEADのサーバーサイド(アプリケーション層)の技術をご紹介します。
目次
はじめに
アーキテクチャー編でも少し説明させて頂きましたが、 TECH LEADサービスのサーバーサイドプログラム(PHP)は、大きく内部API層とアプリケーション層の2つに分かれています。
アプリケーション層は、フロントからのリクエストされたデータや処理を内部API層へリクエストし、結果をフロントへ返却する役割をしています。
今回は、前回の続きでSSR(サーバーサイドレンダリング)の実装部分と内部API層との連携部分に関して説明していきたいと思います。
主な技術
PHP 7.2
フレームワーク
Laravel 5.6
ディレクトリ構成
. | # 内部API層 ├── api/ # [BEAR.Sunday]ミドルウェアの操作・メール送信 ├── db/ # [Laravel] データベースのマイグレーション・シードを管理 └── web/ # アプリケーション層 ├── site-common/ # 共通で利用できるものを配置 | # シンボリックリンクで各アプリケーションで使用 ├── site-mypage/ # [Laravel] TECH LEAD共通部分 | # アカウント設定・パスワード変更など ├── site-agent/ # [Laravel]TECH LEAD Agent ├── site-job/ # [Laravel]TECH LEAD Job └── site-resume/ # [Laravel]TECH LEAD Resume
サーバーサイドレンダリングに関して
TECH LEADでは、PHPのエクステンションのV8jsを使用してSSRを行なっています。
TECH LEAD Jobのトップページを例に説明していきたいと思います。
※フロントエンドの実装は、前回の記事をご確認ください。
<?php namespace App\Http\Controllers\Site; use App\Models\Job\Posting; use App\Models\UserAccount; use GlobalShift\TechLead\Models\Api\Searcher\JobPosting as JobPostingSearcher; class IndexController extends Controller { public function index() { /* @var UserAccount $userAccount */ $userAccount = $this->loginUserAccount(); if ($userAccount) { return redirect()->route('site/postings/index'); } // 内部API層へリクエスト $result = (new JobPostingSearcher())->cachedSearch( ['public_status' => Posting::PUBLIC_STATUS_OPEN], 1, Posting::NEWER_SORT ); if ($result->code !== 200) { abort(404); } $searchResult = collect($result->body); // SSR用のHTML作成 $html = $this->getSsrHtml('index', [ 'postings' => $searchResult->get('sources', []), ]); return view('site.ssr', [ 'html' => $html, ]); } }
<?php namespace App\Http\Controllers\Site; use App\Http\Controllers\Controller as BaseController; use App\Models\UserJob\Information; use GlobalShift\TechLead\Models\Api\UserJob\Information as ApiInformation; class Controller extends BaseController { // 省略 protected function getSsrHtml(string $page, array $payload = []) { $v8js = new \V8Js(); // 共通して必要な環境変数・パスなど $variables = [ 'env' => config('app.env'), 'flash_message' => htmlspecialchars(json_encode($this->flashMessage())), 'path' => [ 'index_js_path' => route('/js/site_csr.js'), 'index_css_path' => route('/css/site.css'), ], ]; // V8jsで実行させるJavascript(文字列) $code = <<<EOT %s VARIABLES=%s global.render(%s, %s); EOT; $code = sprintf( $code, file_get_contents(__DIR__.'/../../../../public/js/site_ssr.js'), json_encode($variables), '"'.$page.'"', json_encode($payload) ); return $v8js->executeString($code); } private function flashMessage() { return [ 'success' => implode(',', session()->pull('success', [])), 'error' => implode(',', session()->pull('error', [])), ]; } }
<!-- web/site-job/resources/views/site/ssr.twig --> {{ html|raw }}
まず、必要なデータを内部API層へリクエストし、収集します。次にgetSsrHtml
メソッドにレンダリングしたいページ名とページの生成に必要なデータを渡し、HTMLを生成します。
getSsrHtml
メソッドでは、フロントエンドでSSR用に作成したエントリポイント(site_ssr.js)を読み込み、global.render
を実行するJavascriptのコードを文字列で作成します。
V8jsのexecuteString
を使用して、上記のJavascriptコードを実行し、結果(HTML)を返します。
ssr.twig
は、PHPのviewテンプレートでSSRするページ共通で使用していて、コントローラから渡されたHTMLをそのまま表示しています。
※SSRには、まだ課題があり対応を検討中です。(参照:フロントエンド編)
内部APIとの連携に関して
アーキテクチャー編で少し説明させて頂きましたが、現状、同一サーバ上で全てのサービスが動いてHTTPでリスエスト処理をするには、まだサービスの規模が小さく、HTTP通信にすることによるオーバーヘッドが大きくなってしまうので、現在は内部API層(BEAR.Sunday)をインスタンス化したものをアプリケーション層で読み込み、内部API層の機能を利用してデータの取得などを行なっています。
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. */ public function boot() { // 内部API層のインスタンスを注入 $this->app['api'] = require dirname(__DIR__, 4).'/api/api.php'; } /** * Register any application services. */ public function register() { require dirname(__DIR__).'/helpers.php'; } }
<?php namespace App\Traits; trait DBApiTrait { /** * @return \BEAR\Sunday\Extension\Application\AbstractApp */ protected function dbApi() { // 内部API層のインスタンスを呼び出す return app()['api']; } }
アプリケーション層の実際にデータを取得するための部分は、./web/site-common/app/Models/Api/
以下に定義し、composerのautoload機能を利用してアプリケーション層(各サービス)で利用できるようにしています。
"autoload": { "psr-4": { "App\\": "app/", "GlobalShift\\TechLead\\": "../site-common/app" } },
実際に都道府県データを取得するためのコードを紹介します。
<?php use GlobalShift\TechLead\Models\Api\Prefecture; $result = (new Prefecture())->all(); // body内に内部API層で取得したデータ入っている $result->body;
// ./web/site-common/app/Models/Api/Prefecture.php <?php namespace GlobalShift\TechLead\Models\Api; class Prefecture extends Base { public function all() { // 内部API層の都道府県取得部分にリクエスト $result = $this->get('app://self/common/prefecture/index'); return $result; } public function findByCode(string $code) { $result = $this->get('app://self/common/prefecture/detail', ['code' => $code]); return $result; } public function findByUrlWord(string $urlWord) { $result = $this->get('app://self/common/prefecture/detail', ['url_word' => $urlWord]); return $result; } }
// ./web/site-common/app/Models/Api/Base.php <?php namespace GlobalShift\TechLead\Models\Api; use App\Traits\DBApiTrait; abstract class Base { use DBApiTrait; /** * @return \BEAR\Resource\ResourceObject | \BEAR\Resource\Request */ protected function cloneGet($uri, $queries = [], $crawls = []) { // `$this->dbApi()`は内部API層のインスタンス $resource = $this->dbApi()->resource; $request = $resource->get->uri($uri)->withQuery($queries); foreach ($crawls as $crawl) { $request->linkCrawl($crawl); } return $request->request(); } /** * @return \BEAR\Resource\ResourceObject | \BEAR\Resource\Request */ protected function get($uri, $queries = [], $crawls = []) { $request = $this->dbApi()->resource->get->uri($uri)->withQuery($queries); foreach ($crawls as $crawl) { $request->linkCrawl($crawl); } return $request->request(); } /** * @return \BEAR\Resource\ResourceObject | \BEAR\Resource\Request */ protected function post($uri, $data) { return $this->dbApi()->resource->post->uri($uri)->withQuery($data)->request(); } /** * @return \BEAR\Resource\ResourceObject | \BEAR\Resource\Request */ protected function put($uri, $data) { return $this->dbApi()->resource->put->uri($uri)->withQuery($data)->request(); } /** * @return \BEAR\Resource\ResourceObject | \BEAR\Resource\Request */ protected function delete($uri, $data) { return $this->dbApi()->resource->delete->uri($uri)->withQuery($data)->request(); } }
※内部API層(BEAR.Sunday)の詳細は次回のブログで紹介します。
まとめ
ここまで、サーバーサイドのアプリケーション層に関して、一部になりますが説明させて頂きました。
弊社のエンジニアチームでは、このような複数サービスを横断的に利用できるWEBサービスの開発をした経験がなかったため、どのように実装するかとても悩みました。いざ実装していくと想定外の問題にぶつかったり、改善すべき点が見つかったりまだまだ試行錯誤しながら開発しています。
是非、同じようなサービスを開発している方や同じような課題を持っている方とお話ししてみたいです。興味がある方は是非お声がけください!
エンジニアの皆さんへお願い
もし、「ここをもっと聞きたい」「うちではこうやっている」と言ったご意見・アドバイスなどありましたら、@TECH LEADまでリプライやDMにてお気軽にお知らせください!
よろしくお願いします!
次回予告
次回は【サーバーサイド(内部API層)編】で、BEAR.Sundayの紹介と内部API層の実装ついて話したいと思います。
最後まで読んでいただきありがとうございました!
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