場合分けロジックの整理

場合分けロジックの望ましい書き方

  • 判断や処理のロジックをメソッドとして独立させる
  • elseをなくす(早期リターンする=ガード節にする)とコードが簡潔になる
  • インターフェースを使って異なるクラスを同じクラスとして扱う
    • クラスを区別するための場合分けの必要がなくなる

区分オブジェクト

インターフェイスによるポリモーフィズムは区分の一覧がわかりにくいという問題がある(個々のクラスを確認するしかない)。

enum
  • enumによって区分ごとのクラス一覧を明示することができる
  • 各区分ごとの値をインスタンス変数として保持できる
  • 各区分ごとのロジックをメソッドとして記述できる
  • 状態遷移などを表現すると吉
<?php
interface Colorful
{
    public function color(): string;
}

enum Suit: string implements Colorful
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';

    // インターフェイスの規約を満たすための実装
    public function color(): string
    {
        return match($this) {
            Suit::Hearts, Suit::Diamonds => 'Red',
            Suit::Clubs, Suit::Spades => 'Black',
        };
    }
}

このようにインターフェイスenumを駆使して区分ごとのロジックをわかりやすく整理する方法を区分オブジェクトという。

  • インターフェイスを使うことで区分ごとのクラスを使う側は条件分岐の必要性から解放される
  • enumを使うことで区分の一覧性が高まり、コードの見通しが良くなる

ソースコードの整理

プログラムの変更が楽になる書き方

  • 変数名・関数名にはわかりやすい名前を使う
  • 長いメソッドは段落を分ける
  • 段落をメソッドとして独立(抽出)させてもよい
    • 変更箇所をそのメソッド内に限定できる(そのロジックを他の場所でも使うことになると、変更箇所がその分だけ増える
    • 抽出元のメソッドが読みやすくなる
    • メソッド名からコードの意図を読み取りやすくなる
    • 関連性が強いデータとロジックを抽出することでロジックを再利用しやすくなる
  • 目的ごとに変数を用意する
    • 一つの変数を複数の目的に使い回さない
  • 異なるクラスの重複したコードをなくす
    • それぞれのクラスで共通のコードをそれぞれメソッドに抽出する
    • 参照側で抽出したメソッド呼び出しを、参照先のメソッドのオブジェクトの呼び出しに変更する(2つのクラスに参照関係がある場合)
    • 共通のメソッドの置き場所として別のクラスを作る(2つのクラスに参照関係がない場合)
  • 狭い関心事に特化したクラスにする

安全性を高める書き方

値オブジェクト
  • 値の範囲を制限して安全性を高める(値を扱うためのクラスをつくる)
    • intは、マイナス21億からプラス21億の範囲の整数
    • BigDecimalは、実質的に無限の範囲の数
    • 新しいクラスを定義し、実際の業務ロジックで使う範囲に値の範囲を限定する
    • 値オブジェクトは不変にする
      • 値が異なれば別のオブジェクトにする
      • 変数を変更するセッターを作らない
class Quantity {
    static final int MIN = 1;
    static final int MAX = 100;

    int value;
  • 型を使って意図を明らかにする
    • intではなく値オブジェクトを渡すことによって引数の渡し間違えを防ぐ
    • 業務の関心事を直接的に表現できる
    // Bad: int amount(int unitPrice, int quantity) {
    Money amount(Money unitPrice, Quantity quantity) {
        if(quantity.isDiscountable)
            return discount(unitPrice, quantity)

        return unitPrice.multiply(quantity.value()) ; 
    }
値オブジェクトの例
  • Quantity: 数量
  • Unit: 単位
  • Amount: 金額
  • Currency: 通貨
  • Days: 日数
  • Period: 期間
  • Telephone: 電話番号
  • Email: メアド
  • Url: URL
  • LIne: 一行のテキスト
  • Description: 説明文
コレクションオブジェクト
  • コレクションを扱うコードがあちこちに散らばるとコードが読みにくくなり変更作業が煩雑になる
    • forやifなど複雑な処理を行うため
  • コレクション型を扱うロジックを専用クラスに閉じ込める
    • クラスでは目的のコレクション以外の変数を持たせない(意図を明確にしコードを簡潔にするため)
class Customers { 
    List<Customer> customers;

    void add(Customer customer) { ... }
    void removeIfExist(Customer customer) { ... }

    int count() { ... }

    Customers importantCustomers() { ... } 
}
  • コレクションオブジェクトを安定させる
    • 値オブジェクトと同じ理由
    • コレクション操作のロジックをコレクションオブジェクトに移動させる
    • コレクション操作の結果も同じ型のコレクションオブジェクトとして返す
      • コレクションの要素を変化させる操作では、捜査の結果として別のオブジェクトを作って返す
      • 個々のコレクションオブジェクトは内部の状態が変わらない不変スタイルのオブジェクトになる


値オブジェクトやコレクションオブジェクトのように、あるクラスに データとロジックを閉じ込めると、そのオブジェクトを使う側のロジッ クが単純になる。使う側のプログラムの記述が簡単になるように、使われる側のクラスに便利なメソッドを用意するのが オブジェクト指向設計のコツ。

コントローラのユニットテスト

コントローラテストの概要

tests/TestCase/Controller 配下に テストクラスの ファイルを作成する。以下はArticlesコントローラに対するテストクラスArticlesControllerTestクラスの作成例。

<?php
namespace App\Test\TestCase\Controller;

use Cake\ORM\TableRegistry;
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;

class ArticlesControllerTest extends TestCase
{
    use IntegrationTestTrait;

    public $fixtures = ['app.Articles'];

    public function testIndex()
    {
        $this->get('/articles'); // GETリクエストを送信

        $this->assertResponseOk();
        // 他のアサート
    }

    public function testIndexQueryData()
    {
        $this->get('/articles?page=1');

        $this->assertResponseOk();
        // 他のアサート
    }

    public function testIndexPostData()
    {
        $data = [
            'user_id' => 1,
            'published' => 1,
            'slug' => 'new-article',
            'title' => 'New Article',
            'body' => 'New Body'
        ];
        $this->post('/articles', $data); // POSTリクエストを送信

        $this->assertResponseSuccess();
        $articles = TableRegistry::getTableLocator()->get('Articles');
        $query = $articles->find()->where(['title' => $data['title']]);
        $this->assertEquals(1, $query->count()); // 投稿できているかを検証
    }
}

get(), post(), put(), delete(), patch(), head(), options()メソッドによって、それぞれに対応するHTTPリクエストを送信することができる。

アサーションメソッド

以下のアサーションメソッドでレスポンスの検証を行うことができる。

<?php
// 2xx レスポンスコードをチェック
$this->assertResponseOk();

// 2xx/3xx レスポンスコードをチェック
$this->assertResponseSuccess();

// 4xx レスポンスコードをチェック
$this->assertResponseError();

// 5xx レスポンスコードをチェック
$this->assertResponseFailure();

// 指定したレスポンスコードをチェック。例: 200
$this->assertResponseCode(200);

// Location ヘッダーをチェック
$this->assertRedirect(['controller' => 'Articles', 'action' => 'index']);

// Location ヘッダーが設定されていないことをチェック
$this->assertNoRedirect();

// Location ヘッダーの一部をチェック
$this->assertRedirectContains('/articles/edit/');

// 3.7.0 で追加
$this->assertRedirectNotContains('/articles/edit/');

// レスポンスが空ではないことをアサート
$this->assertResponseNotEmpty();

// レスポンス内容が空であることをアサート
$this->assertResponseEmpty();

// レスポンス内容をアサート
$this->assertResponseEquals('Yeah!');

// レスポンス内容が等しくないことをアサート
$this->assertResponseNotEquals('No!');

// レスポンス内容の一部をアサート
$this->assertResponseContains('You won!');
$this->assertResponseNotContains('You lost!');

// 返されたファイルをアサート
$this->assertFileResponse('/absolute/path/to/file.ext');

// レイアウトをアサート
$this->assertLayout('default');

// テンプレートが表示されたかどうかをアサート
$this->assertTemplate('index');

// セッション内のデータをアサート
$this->assertSession(1, 'Auth.User.id');

// レスポンスヘッダーをアサート
$this->assertHeader('Content-Type', 'application/json');

// 3.7.0 で追加
$this->assertHeaderNotContains('Content-Type', 'xml');

// ビュー変数をアサート
$user =  $this->viewVariable('user');
$this->assertEquals('jose', $user->username);

// レスポンス内のクッキーをアサート
$this->assertCookie('1', 'thingid');

// コンテンツタイプをチェック
$this->assertContentType('application/json');

テーブルクラスのテスト

Articleテーブルのテスト。公開済みの記事を取得するfindPublished()メソッドを持つ。

<?php
namespace App\Model\Table;

use Cake\ORM\Table;
use Cake\ORM\Query;

class ArticlesTable extends Table
{
    public function findPublished(Query $query, array $options)
    {
        $query->where([
            $this->alias() . '.published' => 1
        ]);
        return $query;
    }
}

tests/TestCase/Table配下にテーブルテストクラスのファイルを作成する。

<?php
namespace App\Test\TestCase\Model\Table;

use App\Model\Table\ArticlesTable;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\TestCase;

class ArticlesTableTest extends TestCase
{
    public $fixtures = ['app.Articles'];
}

テストメソッドの作成

テストメソッドtestFindPublished()でArticleTableのfindPublished()関数をテストする。assertEquals($expected, $result)メソッドで取得結果が期待される値と合致するか検証する。

<?php
class ArticlesTableTest extends TestCase
{
    public $fixtures = ['app.Articles'];

    public function setUp()
    {
        parent::setUp();
        $this->Articles = TableRegistry::getTableLocator()->get('Articles');
    }

    public function testFindPublished()
    {
        $query = $this->Articles->find('published'); // 公開済みの記事を取得する
        $this->assertInstanceOf('Cake\ORM\Query', $query); // Queryクラスのインスタンスであることを検証
        $result = $query->enableHydration(false)->toArray();
        //  期待される値
        $expected = [
            ['id' => 1, 'title' => 'First Article'], 
            ['id' => 2, 'title' => 'Second Article'],
            ['id' => 3, 'title' => 'Third Article']
        ];

        $this->assertEquals($expected, $result); // 期待される値と合致するか検証
    }
}

モデルメソッドのモック化

getMockForModel('モデル名', ['メソッド'])でテーブルクラスのモックを作成できる。

<?php
public function testFindPublished()
{
    $mock = $this->getMockForModel('Article', ['findPublished']);
    $mock->expects($this->once()) // 呼ばれる回数
        ->method('findPublished') // 対象のメソッド
        ->will($this->returnValue($expected)); // 期待される戻り値

    // 実行
    $result = $mock->findPublished(array('id', 'title'));
}

findPublishedメソッドを呼び出す対象のモックを作成し、期待される戻り値等を設定してから実行している。

フィクスチャ

フィクスチャ

テストコードの挙動がデータベースやモデルに依存するとき、テストに使うためのテーブルを生成し、 一時的なデータをロードするために フィクスチャを使うことができる。

  • フィクスチャを使うことにより、 実際のアプリケーションに使われているデータを破壊することなくテストができる。
  • アプリケーションのためのコンテンツを実際に用意するより先にコードをテストすることができる。

CakePHP はフィクスチャーに基づいたテストケースを実行するにあたり、以下の動作をする。

  1. 各フィクスチャーで必要なテーブルを作成する
  2. フィクスチャーにデータが存在すれば、それをテーブルに投入する
  3. テストメソッドを実行
  4. フィクスチャーのテーブルを空にする
  5. データベースからフィクスチャーのテーブルを削除する

フィクスチャの作成

Articleモデルのフィクスチャを作成する場合、tests/FixtureディレクトリにArticlesFixture.phpという名前のファイルを作成する。
フィクスチャクラスにはCake\TestSuite\Fixture\TestFixtureを継承させる。

<?php
namespace App\Test\Fixture;

use Cake\TestSuite\Fixture\TestFixture;

class ArticlesFixture extends TestFixture
{
      // オプション。異なるテストデータソースにフィクスチャーをロードするために、このプロパティーを設定
      public $connection = 'test';

      public $fields = [
          'id' => ['type' => 'integer'],
          'title' => ['type' => 'string', 'length' => 255, 'null' => false],
          'body' => 'text',
          'published' => ['type' => 'integer', 'default' => '0', 'null' => false],
          'created' => 'datetime',
          'modified' => 'datetime',
          '_constraints' => [
            'primary' => ['type' => 'primary', 'columns' => ['id']]
          ]
      ];
      public $records = [
          [
              'title' => 'First Article',
              'body' => 'First Article Body',
              'published' => '1',
              'created' => '2007-03-18 10:39:23',
              'modified' => '2007-03-18 10:41:31'
          ],
          [
              'title' => 'Second Article',
              'body' => 'Second Article Body',
              'published' => '1',
              'created' => '2007-03-18 10:41:23',
              'modified' => '2007-03-18 10:43:31'
          ]
      ];
 }
$connection

フィクスチャーが使用するデータソースを定義する。上の例ではデフォルトの test データソースを使用している。

$fields

テーブルを構成するフィールドと、その定義を記述する。
オプション

  • type: データ型を指定
  • length: 文字列の長さを指定
  • null: null許容(true or false)を指定
  • default: デフォルト値を指定
  • precision: 小数点以下の桁数を指定

フィクスチャのロード

<?php
class ArticlesTest extends TestCase
{
    public $fixtures = ['app.Articles', 'app.Comments'];
}

上の例ではArticlesフィクスチャとCommentsフィクスチャをロードしている。

テスト

テストケースの規約

  1. テストを含むPHPファイルは、 tests/TestCase/[Type] ディレクトリーに置く
  2. ファイル名はFooBarTest.php とする
  3. テストを含むクラスは Cake\TestSuite\TestCase 、 Cake\TestSuite\IntegrationTestCase または \PHPUnit\Framework\TestCase を継承する
  4. 他のクラス名と同様に、テストケースのクラス名はファイル名と一致させる
  5. テストを含むメソッド (つまり、アサーションを含むメソッド) の名前は testPublished() のように test で始める。 @test というアノテーションをメソッドに マークすることでテストメソッドとすることもできる。

テストの作成

テストコードの例

ヘルパーを作成。以下のヘルパーに対するテストを作成する。

<?php
namespace App\View\Helper;

use Cake\View\Helper;

class ProgressHelper extends Helper
{
    public function bar($value)
    {
        $width = round($value / 100, 2) * 100;
        return sprintf(
            '<div class="progress-container">
                <div class="progress-bar" style="width: %s%%"></div>
            </div>', $width);
    }
}

テストコード。

<?php
namespace App\Test\TestCase\View\Helper;

use App\View\Helper\ProgressHelper;
use Cake\TestSuite\TestCase;
use Cake\View\View;

class ProgressHelperTest extends TestCase
{
    public function setUp()
    {
        parent::setUp();
        $View = new View();
        $this->Progress = new ProgressHelper($View);
    }

    public function testBar()
    {
        $result = $this->Progress->bar(90);
        $this->assertContains('width: 90%', $result);
        $this->assertContains('progress-bar', $result);

        $result = $this->Progress->bar(33.3333333);
        $this->assertContains('width: 33%', $result);
    }
}
setUp

setUpメソッドはこのテストケースクラスのメソッドが呼び出される前に毎回呼び出される。テストに必要なオブジェクトの初期化や設定を行う。

parent::setUp

TestCase::setUp() は、 Core\Configure の値をバックアップしたり、 Core\App にパスを保存したりといったいくつかの作業をしている。

アサーション

期待した結果が出力されるかどうかを検証する。assertContains()は、ヘルパーが返した値に期待した文字列が含まれているかどうかを検証する。 TestSuitePHPUnit で多数のアサーションメソッドが用意されている。

PSR-12

Declare Statements, Namespace, and Import Statements

各ブロックは以下の順序で記述する。

  • 開始タグ
  • ファイルレベルの DocBlock
  • 1つ以上の宣言ステートメント
  • ファイルの名前空間宣言
  • 1つ以上のクラスベースの use インポート文
  • 1つ以上の関数ベースの use インポート文
  • 1つ以上の定数ベースの use インポート文
  • ファイル内の残りのコード
<?php

/**
 * This file contains an example of coding styles.
 */

declare(strict_types=1);

namespace Vendor\Package;

use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
use Vendor\Package\SomeNamespace\ClassD as D;
use Vendor\Package\AnotherNamespace\ClassE as E;

use function Vendor\Package\{functionA, functionB, functionC};
use function Another\Vendor\functionD;

use const Vendor\Package\{CONSTANT_A, CONSTANT_B, CONSTANT_C};
use const Another\Vendor\CONSTANT_D;

/**
 * FooBar is an example class.
 */
class FooBar
{
    // ... additional PHP code ...
}

2階層以上の名前空間は使用しない。

<?php
// 許可されない
use Vendor\Package\SomeNamespace\{
    SubnamespaceOne\AnotherNamespace\ClassA,
    SubnamespaceOne\ClassB,
    ClassZ,
};

Classes, Properties, and Methods

Extends and Implements
  • extendやimplementはクラス名と同じ行で宣言する
  • クラスの開始波括弧は独自の行に配置し、前後に空白行があってはいけない
  • クラスの終了波括弧は独自の行に配置し、前に空白行があってはいけない
<?php

namespace Vendor\Package;

use FooClass;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class ClassName extends ParentClass implements \ArrayAccess, \Countable
{
    // constants, properties, methods
}
Using Traits
  • useは開始波括弧の次の行で宣言する
  • クラスにインポートされる個々のトレイトはそれぞれ独自のuse宣言が必要
  • use宣言のあとにコードが続く場合、use宣言のあとに空白行が必要
<?php

namespace Vendor\Package;

use Vendor\Package\FirstTrait;
use Vendor\Package\SecondTrait;
use Vendor\Package\ThirdTrait;

class ClassName
{
    use FirstTrait;
    use SecondTrait;
    use ThirdTrait;

    private $property;
}
Properties and Constants
  • すべてのプロパティで可視性(private, public)を宣言する
  • PHP7.1以降、すべての定数で可視性を宣言する
  • プロパティの宣言にvarを使用しない
  • 一つのステートメントで宣言できるプロパティは一つだけ
<?php

namespace Vendor\Package;

class ClassName
{
    public $foo = null;
    public static int $bar = 0;
}
Methods and Functions
  • すべてのメソッドで可視性を宣言する
  • 開始波括弧は独自の行に配置する
  • 終了波括弧の前に空白行があってはいけない
  • 左括弧の後、右括弧の前に空白があってはいけない
Method and Function Arguments
  • 引数リストではカンマの前にスペースがあってはならず、後にスペースがなければいけない
  • デフォルト値を持つ引数はリストの最後でなければならない
  • 戻り値の型宣言がある場合、コロンと型宣言の間にスペースが必要
  • null許容型宣言では疑問符と型の間にスペースがあってはならない
  • 参照演算子と型の間にスペースが有ってはならない
  • 可変長引数のドット演算子と型の間にスペースがあってはならない
<?php

namespace Vendor\Package;

class ClassName
{
    public function foo(?int $arg1, &$arg2, $arg3 = []): string
    {
        // method body
    }
}
  • 引数リストは複数行に分割でき、後続の各行は一回インデントされる
  • その場合、リストの最初の引数は次の行にある必要がある
<?php

namespace Vendor\Package;

class ClassName
{
    public function aVeryLongMethodName(
        ClassTypeHint $arg1,
        &$arg2,
        array $arg3 = []
    ) {
        // method body
    }
}
abstract, final, and static
  • abstractおよびfinal宣言がある場合、それらは可視性宣言の前になければならない
  • static宣言がある場合、それらは可視性宣言のあとになければならない
<?php

namespace Vendor\Package;

abstract class ClassName
{
    protected static $foo;

    abstract protected function zim();

    final public static function bar()
    {
        // method body
    }
}
Method and Function Calls

メソッドまたは関数を呼び出す場合 - 括弧の前後にスペースがあってはならない - 引数リストの記法については定義するときと同じ

<?php

bar();
$foo->bar($arg1);
Foo::bar($arg2, $arg3);

Control Structures

  • 制御構造キーワードの後にスペースが必要
  • 右括弧と左波括弧の間にスペースが必要
  • 波括弧内の文は一段インデントする
    if, elseif, else
<?php

if ($expr1) {
    // if body
} elseif ($expr2) {
    // elseif body
} else {
    // else body;
}
switch
<?php

switch ($expr) {
    case 0:
        echo 'First case, with a break';
        break;
    case 1:
        echo 'Second case, which falls through';
    // no break
    case 2:
    case 3:
    case 4:
        echo 'Third case, return instead of break';
        return;
    default:
        echo 'Default case';
        break;
}
while, do while
<?php

while ($expr) {
    // structure body
}
<?php

do {
    // structure body;
} while ($expr);
for
<?php

for ($i = 0; $i < 10; $i++) {
    // for body
}
foreach
<?php
foreach ($iterable as $key => $value) {
    // foreach body
}
try, catch, finally
<?php

try {
    // try body
} catch (FirstThrowableType $e) {
    // catch body
} catch (OtherThrowableType | AnotherThrowableType $e) {
    // catch body
} finally {
    // finally body
}

Operator

単項演算子
<?php

$i++;

++$j;
二項演算子
<?php
if ($a === $b) {
    $foo = $bar ?? $a ?? $b;
} elseif ($a > $b) {
    $foo = $a + $b * $c;
}
三項演算子
  • 「?」文字と「:」文字の両方の前後に少なくとも1つのスペースが必要
<?php
$variable = $foo ? 'foo' : 'bar';

Closures

  • 左波括弧は同じ行に配置する
  • 右波括弧は本文に続く次の行に配置する
  • 引数リスト・変数リストでは各コンマの前にスペースがあってはならず、後にスペースがなければならない
  • デフォルト値を持つ引数はリストの最後に配置する
<?php

$closureWithArgs = function ($arg1, $arg2) {
    // body
};

$closureWithArgsAndVars = function ($arg1, $arg2) use ($var1, $var2) {
    // body
};

$closureWithArgsVarsAndReturn = function ($arg1, $arg2) use ($var1, $var2): bool {
    // body
};

クロージャーが関数またはメソッドの呼び出しで引数として直接使用される場合にも、フォーマット規則が適用される。

<?php

$foo->bar(
    $arg1,
    function ($arg2) use ($var1) {
        // body
    },
    $arg3
);