OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

レイヤー間の依存関係の静的解析 - PHP deptrac ~ 導入編

はじめに

今日のPHPでのウェブアプリケーションでは、MVCなどのレイヤー分割を気を付けて構造の開発を行われているかと思います。はたして気を付けてるだけでよいのでしょうか? ・・・ということで、"アーキテクチャテスト" とも称されるレイヤーへの静的解析による検証について記事をお送りします。

PHPプロジェクトにおけるソフトウェブレイヤー間の依存関係の静的解析ツールである deptrac のセットアップまでを行う本エントリー「導入編」と、実際に弊社でのバックエンドプロジェクトにCIに導入している現状を踏まえた上での「実践編」とに分けてお送りしたいと思います。

なぜ、レイヤー間の依存関係のチェックをツールで行えるようにするのか

上述した通り、気を付けるだけでは気づかぬうちに違反が起きていく可能性があります。また、レビューで担保しようとするとそのチェックのためのコストがかかり、チェック項目として用意しなければいけません。また、チェック項目として用意するとしても文章では曖昧で判定がしにくい箇所がでてきます。 それならば、ユニットテストのテイスティングツールと同じように反復してテストするためのツールを用意できればということになります。

パッケージやクラスの依存関係をテストするツールは、アーキテクチャテストともよばれているようで、『ドメイン駆動設計を支えるアーキテクチャテスト』という講演資料では、Javaの ArchUnitを取り上げているほか、PHPのツールについても紹介がありました。

https://speakerdeck.com/kawanamiyuu/object-oriented-conference-2020

現在のPHPにおけるレイヤーとは

ここまで前提なしに"レイヤー"という用語を出していましたが、改めてPHPにおけるレイヤーをざっくり考えてみます。

  • PHPはクラスベースの言語ではない。
  • 組み込みで用意されている I/O(ストリーム, DBアクセス, サーバーパラメータなど) はグローバルな関数・変数でのアクセスである。
  • 00年代後半からのOOP啓蒙~2010年代前半頃でのオニオンアーキテクチャ・DDDムーブメント、psr-0/4の普及によりクラスベースでの開発が一般的に
    • クラス定義・利用の手間を考えて配列が多用される場面も多かったが、昨今では配列の代わりでのクラス作成は抵抗がなくなってきた。
  • またDIの考え方の普及とともに疎結合なコンポーネント/オブジェクトの作成も受け入れやすくなった。

ということを踏まえてPHPにおける現在のレイヤーとは、「自前かライブラリのクラスに対して定義する」というように捉えられると思います。

PHP での レイヤー間の依存関係チェックツール

2016年ごろ以降は、 ast/PHP-Parserをベースとした静的解析ツールの隆盛により、依存関係のチェックに特化してチェックするツールも出回るようになりました。 後述する deptracのほかには以下のツールがあります。

deptrac について

deptracの開発リポジトリは以下で、READMEに詳細な利用法も記述されています。

deptracを開発/メンテナンスしているのは、Symfony で知られる Sensio Labsのドイツ支社のメンバーです。 2016年に開発が始まり現在まで継続的に開発され、2020年の10月には 0.10.0 がリリースされました。

現時点での特徴としては、

  • yamlでのルールセット定義
    • 他のプログラム記述型のものと違い、プロジェクト全体でのレイヤーとその従属関係のルールが捉えやすくなります。
  • collector と呼ぶ正規表現などでレイヤー群を定義できます。
    • スカラー値やresourceと言ったシンボルを定義はできなさそうです。
    • ※ 配列は・・・ psalm-type のような概念を導入しないといけなさそうです。
  • Graphviz経由によるクラス間の依存関係の可視化
  • Allowed数ならびに Uncovered 数の算出
  • (0.10 から) baselineサポートによるviolationなクラスをスキップすることが可能に
    • ※ このbaselineとは psalmやPHPStan のと同様の考えのものです。

deptracのインストール・セットアップ

READMEでのインストール方法には、まずpharのダウンロードでの利用が案内されていますが、私はバージョンアップの手間などを考えてphive でのグローバルインストールを利用しています。

sudo phive --global install deptrac

また、READMEに案内されているようにレイヤー間の図示出力 (--formatter=graphviz ) にはgraphvizのインストールも必要です。

バージョンは、--version で確認できます。

$ deptrac --version
deptrac 0.10.2

deptrac の実行例

deptracのREADMEでは、「Ruleset (Allowing Dependencies)」の項に Controller-Services-Respositories を例にとり説明がありますが、ここからはフレームワーク・ライブラリとの関係を考えてみたいので psr/http-server-handler(psr-15) をベースとしたプロジェクトを例にとってみます。

サンプルのプロジェクト構造

Psr\Http\Server\RequestHandlerInterface を実装したものを Actionとして、Action-Repository-Entityをレイヤー構造に位置づけたとします。

$ tree src/
src/
├── Action
│   └── UserShowAction.php
├── Entity
│   └── User.php
└── Repository
    └── UserRepository.php

3 directories, 3 files

各クラスの中身は以下の通りです。

<?php
namespace Foo\Action;

use Foo\Repository\UserRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class UserShowAction implements RequestHandlerInterface
{
    /**
     * @var UserRepository
     */
    private $userRepository;  

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {}
}
<?php
namespace Foo\Entity;

class User
{}
<?php
namespace Foo\Repository;
use Foo\Entity\User;

class UserRepository
{
    public function findOneById(int $id) : User
    {}
}

ActionがRepository に依存するをルールセットに定義する

ActionがRepositoryを呼ぶ。言い換えると、ActionとRespotiroyというレイヤーがあり、ActionがRepositoryのクラスを呼び出すのを許容する というのをルールセットとして定義したい場合は以下の通りとなります。

paths:
  - ./src
exclude_files:
layers:
  - name: Action
    collectors:
      - type: implements
        implements: Psr\Http\Server\RequestHandlerInterface
  - name: Repository
    collectors:
      - type: className
        regex: Foo\\Repository\\.*Repository

ruleset:
  Action:
    - Repository
  Repository: ~

これを action-repository.yamlとして保存します。

実行とエラー検出

解析は analyse コマンドとともに引数としてルールセット定義yamlを渡して実行します。

$ deptrac analyse action-repository.yaml
 3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


Report:
Violations: 0
Skipped violations: 0
Uncovered: 8
Allowed: 3

RepositoryがActionから呼び出されるのを許容するのをコメントアウトした場合は、

ruleset:
  Action:
#    - Repository
  Repository: ~
$ deptrac analyse action-repository.yaml
 3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

Foo\Action\UserShowAction must not depend on Foo\Repository\UserRepository (Action on Repository)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::4
Foo\Action\UserShowAction must not depend on Foo\Repository\UserRepository (Action on Repository)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::11
Foo\Action\UserShowAction must not depend on Foo\Repository\UserRepository (Action on Repository)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::16

Report:
Violations: 3
Skipped violations: 0
Uncovered: 8
Allowed: 0

と Violations が検出されます。この3か所は use句での利用とプロパティでのアノテーションでの利用とコンストラクタでの型宣言での利用のものがそれぞれ検出されています。 「定義されたレイヤーは、ruleset で従属関係を定義しないと呼び出し違反となる」がdeptracでの基本の考え方です。

カバーされなかった箇所の検出 ~ --report-uncovered

さきほどの出力では、「Uncovered: 8」となっていました。カバーされてなかった箇所の検出には --report-uncovered オプションで確認できます。

$ deptrac analyse --report-uncovered action-repository.yaml
 3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

Uncovered dependencies:
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Server\RequestHandlerInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::9
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Message\ResponseInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::5
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Message\ServerRequestInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::6
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Server\RequestHandlerInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::7
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Message\ServerRequestInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::21
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Message\ResponseInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::21
Foo\Repository\UserRepository has uncovered dependency on Foo\Entity\User (Repository)
/mnt/d/dev/sandbox-deptrac/src/Repository/UserRepository.php::3
Foo\Repository\UserRepository has uncovered dependency on Foo\Entity\User (Repository)
/mnt/d/dev/sandbox-deptrac/src/Repository/UserRepository.php::7

Report:
Violations: 0
Skipped violations: 0
Uncovered: 8
Allowed: 3

もし全てのクラスをカバーしようとするならば

仮に Uncoverdなしにしようとした場合、以下の通りルールセットが必要になります。

paths:
  - ./src
exclude_files:
layers:
  - name: Psr\Http\Message
    collectors:
      - type: className
        regex: Psr\\Http\\Message\\.*
  - name: Psr\Http\Server\RequestHandlerInterface
    collectors:
      - type: className
        regex: Psr\\Http\\Server\\RequestHandlerInterface

  - name: Action
    collectors:
      - type: implements
        implements: Psr\Http\Server\RequestHandlerInterface
  - name: Repository
    collectors:
      - type: className
        regex: Foo\\Repository\\.*Repository
  - name: Entity
    collectors:
      - type: className
        regex: Foo\\Entity\\.*

ruleset:
  Action:
    - Psr\Http\Message
    - Psr\Http\Server\RequestHandlerInterface
    - Entity
    - Repository
  Repository:
    - Entity
$ deptrac analyse all.yaml
 3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


Report:
Violations: 0
Skipped violations: 0
Uncovered: 0
Allowed: 11

以上の通り、すべてのクラスに対するチェックが行える訳ですが、はじめのうちは Uncoveredの数に捕らわれず、チェックしたいレイヤー関係が充足できているか?を重点に導入を始めたほうがよいと思います。


ここまでで、PHPにおけるレイヤーの俯瞰とdeptrac導入まで紹介しました。次回は実際にプロジェクトに導入したうえでの発見点などについて「実践編」としてお送りしたいと思います。

React Native の ScrollView でスクロール時に child を上部に貼り付けたい

こんにちは、アプリ開発担当のエモトです。本業は稲作で、合間に横スクロールのダンジョン攻略や、アプリ開発を行っています。

React Native で画面を作る時、ScrollView を利用する方は多いと思います。その画面でスクロールするとき、ある child だけ上部に固定させたいというシーンが出てくるかもしれません。iOS で言うと UITableView におけるセクションヘッダーに近い動きです。さて、それを React Native で実装したい場合、どうすれば良いでしょうか?

f:id:mitsuharu_e:20201128141318g:plain

上部固定させたい

まず最初にネイティブ開発で考えると、iOS の UIScrollView にはそのような機能がないので、UITableView のセクションヘッダーをカスタマイズする方法が考えられます。

それに従うと React Native なら ScrollView から SectionList に置き換えと、手間のかかる改修が予想されました・・・が!、なんと、React Native の ScrollView 自体にその機能が既に実装してありました。stickyHeaderIndices に固定したい child の index を指定するだけです。簡単でした。

const Component: React.FC<Props> = () => {
  const stickyHeaderIndices = [1]
  return (
    <ScrollView stickyHeaderIndices={stickyHeaderIndices} >
      <Header />
      <Sticky />
      <Contents />
    </ScrollView>
  )
}

なぜ上部固定できるのか?

すでに ScrollView で組んでるコードをバラすことなく、パラメーター1つで実現できました。ただ、ここで1つの疑問が生まれます。本来ネイティブの UIScrollView ではそのよう機能がないのに、なぜ React Native の ScrollView だとできるのか?。

ここで私は、「ScrollView はセクションヘッダーを利用するため、実は UITableView なのでは?」と考えましたが、ネイティブ側に対応する RCTScrollView.h を見る限り、UIView を継承しいて、その考えは誤りでした。

次に、stickyHeaderIndices を設定すると呼ばれる ScrollViewStickyHeader.js を見ると、その答えが分かりました。

style={[..., {transform: [{translateY: this._translateY}]}, ...]}

このコンポーネントは AnimatedView で作られて、style をアニメーション制御していました。あの React Native でも力技的な・地味なコードを書いているんだなーという印象を受けました。

まとめ

私のバックボーンは iOS アプリのネイティブ開発なので、何かを作る際はまずネイティブの知識や経験を持ってきて、それを React Native に当てはめることをやるので、今回のようなネイティブではできないことが React Native では簡単にできるのは驚かされます。日々勉強です。

また、毎日使っている、便利でスマートなフレームワークは、OSS活動される皆様方たちの努力の上でなりたっているんだなと、改めて実感しました。そのフレームワークを使って、よいアプリを作っていこうと思う、良い機会でした。

audiobook.jp作品のサンプルコンテンツを埋め込める様になりました

こんにちは、普段サーバーサイドやWeb開発をやりつつフロントエンド入門中の岩Dです。
コロナも怖いですが、そろそろインフルエンザも怖くなってくる時期ですね。皆様インフルエンザの予防接種はお済みですか?私は午前に予防接種を受けてきて左腕が重い今日この頃です。

さて、今回のネタは「Embed (埋め込みコンテンツ)を作ってみた」です。

Embed って?


鈴村健一・堀江由衣共演!『君の膵臓をたべたい』

この様な感じで、ブログやWebページ内にYoutubeなどのコンテンツを埋め込んで表示ができるもので、埋め込んだページ内で動画や音声の再生などが出来たり、ページが華やかになったりと様々な良いことがあります。

参考 oEmbed

実装した Embed

今回実装したのがこの Embed です。この様にオーディオブックの書籍画像とサンプル音源の再生画面が表示され、audiobook.jp の商品ページへ行かずともサンプル音源が再生できる様になっています。
※サンプル音源が備わっていない作品につきましては、Embedを埋め込んでも再生画面が表示されません。

Embed 埋め込みサンプル

<iframe
  width="100%"
  height="253"
  src="https://audiobook.jp/embed/product/234391"
  frameborder="0"
  scrolling="no">
</iframe>

実装要件

この Embed は以下の様な要件で実装されております。

  • PC/SP で縦幅が同じになる様にする
  • 様々な端末で表示ができる様レスポンシブ対応にする
  • 社内デザイナーがデザインした様な見た目のサンプル音源再生を実現する
  • サンプル音源がない場合はサンプル音源再生部分に商品概要を表示させる
  • タイトル、著者などの表示について、PCでは1行で、SPでは2行で表示させて Embed の横幅を超える場合は省略表示させる
  • 販売されていないオーディオブックが埋め込まれた場合、コンテンツが表示できない事を表示させる

気を遣った点

縦幅について

PC/SP で表示が若干異なるものの、縦幅を同じにしなければいけない要件がありました。著者の情報がないもの、サンプル音源がないものなど表示内容に欠損があったとしても縦幅が変わる事なく表示できる様にするため、各項目の表示領域に対して親要素の幅から計算し height を適切な値で設定する事で Embed の高さが変わらない様にしています。また、SP表示の時はサンプル音源再生の部分が書籍画像の下に配置されますが、書籍画像のサイズをPC表示より少し小さくし、PC表示と同じ高さになる様に色々計算をしています。

PC表示
f:id:siwadate:20201106154752p:plain
SP表示
f:id:siwadate:20201106154843p:plain

サンプル音源の再生について

サンプル音源の再生について、今回は <audio> タグと MediaElement.js を使い、css で見た目の最終調整をして表示させています。

before
f:id:siwadate:20201106144817p:plain
after
f:id:siwadate:20201106144954p:plain

これは、audiobook.jp の商品ページ(君の膵臓をたべたい by audiobook.jp)などで表示しているサンプル音源再生の部分でもほぼ同じ仕組みが使われていますが、 css で見た目を調整するだけで随分と変わるものですね。

終わりに

PC/SPで Embed の縦幅が同じになる様にし、様々な端末で表示できる様レスポンシブ対応させつつ、サンプル音源再生を表示させるなど様々な苦労点がありました。一番こだわったサンプル音源再生の部分については、再生・停止ボタンを変更した以外は css で見た目の調整をしたのみで、さほど手間をかけずに良いものができたと思います。

今回の様な見た目として反映されるものを作るのが好きなので、今後もこの様な見た目を良くする仕事をしていきたいです。

また、この Embed について社内 note にも記載があるのでこちらも併せてどうぞ。 note.com

末筆になりますが、水樹奈々さん妊娠おめでとうございます。お子様にはぜひ「おやすみ、ロジャー」をお読み聞かせください。