データの削除

エンティティの削除

deleteメソッドでエンティティを削除する。

<?php
$entity = $this->Articles->get(2);
$result = $this->Articles->delete($entity);
?>

連鎖削除

エンティティーを削除するとき関連データを削除することもできる。テーブルの初期化メソッドにおいてHasOne や HasMany が dependent として設定されている場合、削除処理はそれらのエンティティーにも連鎖適用される。

<?php
// テーブル内の初期化メソッド
$this->hasMany('Comments', [
    'dependent' => true,
    'cascadeCallbacks' => true,
]);
?>

一括削除

deleteAll()で、引数の条件を満たすレコードを一括削除できる。

<?php
// 全てのスパムを削除する
function destroySpam()
{
    return $this->deleteAll(['is_spam' => true]);
}
?>

厳密な削除

deleteOrFail()は、次の条件で Cake\ORM\Exception\PersistenceFailedException を投げます。

  • エンティティーが新しい場合
  • エンティティーが主キーの値を持たない場合
  • アプリケーションルールのチェックが失敗した場合
  • 削除コールバックによって中断された場合
<?php
try {
    $table->deleteOrFail($entity);
} catch (\Cake\ORM\Exception\PersistenceFailedException $e) {
    echo $e->getEntity(); // エラー内容を表示
}
?>

データの保存 ~リクエストデータからアソシエーションを保存する~

リクエストデータのエンティティへの変換

getData()で取得したリクエストデータをnewEntity()に渡し、エンティティに変換。

<?php
$entity = $articles->newEntity($this->request->getData());
?>

複数のエンティティを変換する場合、newEntities()を用いる。

<?php
// リクエストデータ
[
    [
        'title' => '一番目の投稿',
        'published' => 1
    ],
    [
        'title' => '二番目の投稿',
        'published' => 1
    ],
];

$entity = $articles->newEntities($this->request->getData());
?>

変換するアソシエーションを定義

リクエストデータに含まれているアソシエーションを保存する場合、associatedオプションでどのアソシエーションが変換されるべきか定義する必要がある。'associated' => ['モデル']で変換するアソシエーションを定義する。

<?php
// Tags, Comments, Commentに紐付いたUsersも含めてエンティティに変換する
$entity = $articles->newEntity($this->request->getData(), [
    'associated' => [
        'Tags', 'Comments' => ['associated' => ['Users']] // 入れ子になっている
    ]
]);

// より簡潔な記法
$entity = $articles->newEntity($this->request->getData(), [
    'associated' => ['Tags', 'Comments.Users']
]);
?>

リクエストデータの形式

belongsToMany

アソシエーションのデータを新規作成する場合

<?php
$data = [
    'title' => '私のタイトル',
    'body' => '本文',
    'user_id' => 1,
    'tags' => [
        ['name' => 'CakePHP'],
        ['name' => 'インターネット'],
    ]
];
?>

既存のアソシエーションと紐付ける場合

<?php
$data = [
    'title' => '私のタイトル',
    'body' => '本文',
    'user_id' => 1,
    'tags' => [
        '_ids' => [1, 2, 3, 4]
    ]
];
?>

新規と既存のレコードを両方関連付ける場合

<?php
$data = [
    'title' => '私のタイトル',
    'body' => '本文',
    'user_id' => 1,
    'tags' => [
        ['name' => '新しいタグ'],
        ['name' => '別の新しいタグ'],
        ['id' => 5],
        ['id' => 21]
    ]
];
?>
hasMany

アソシエーションを新規作成したり、それらのプロパティを更新する場合

<?php
$data = [
    'title' => 'My Title',
    'body' => 'The text',
    'comments' => [
        ['id' => 1, 'comment' => 'Update the first comment'], // 既に紐付いているアソシエーションのプロパティを更新
        ['id' => 2, 'comment' => 'Update the second comment'],
        ['comment' => 'Create a new comment'], // アソシエーションを新規作成
    ]
];
?>

既存のレコードと紐付ける場合

<?php
$data = [
    'title' => '私の新しい記事',
    'body' => '本文',
    'user_id' => 1,
    'comments' => [
        '_ids' => [1, 2, 3, 4]
    ]
];
?>

データの保存

データの新規作成

データの新規作成は以下の流れで行われる。

  • newEntity()でエンティティをビルド
  • エンティティのプロパティに値をセット
  • save()でエンティティを保存
<?php
use Cake\ORM\TableRegistry;

$articlesTable = TableRegistry::getTableLocator()->get('Articles');
$article = $articlesTable->newEntity();

$article->title = '新しい記事';
$article->body = 'これは記事の本文です';

$articlesTable->save($article);
?>

データの更新

データの更新も新規作成と同様にsave()で行われる。

<?php
$article = $articlesTable->get(12); // id 12 の記事を返します

$article->title = 'CakePHP は最高のフレームワークです!';
$articlesTable->save($article)
?>

アソシエーションの保存

デフォルトではsaveでアソシエーションの一階層目を保存できる。

<?php
$author = $articlesTable->Authors->findByUserName('mark')->first(); // 関連のあるテーブルからレコードを取得

$article = $articlesTable->newEntity(); // エンティティを生成
$article->title = 'mark の記事';
$article->author = $author; // 関連付け

$articlesTable->save($article); // 保存
?>

save()でアソシエーションのレコードを保存することもできる。

<?php
$Comment = $articlesTable->Comments->newEntity(); // アソシエーションのエンティティを生成
$Comment->body = 'CakePHP の機能は傑出しています';

$article = $articlesTable->get(12);
$article->comments = [$Comment]; // 関連付け

$articlesTable->save($article); // 保存
?>

関連付けにlink()を用いても可。

<?php
$articlesTable->Tags->link($article, [$tag1, $tag2]); // 関連付け
$articlesTable->save($article);

$articlesTable->Tags->unlink($article, $tags); // 関連付け解除
?>

リクエストデータをもとにエンティティを保存

getData()で取得したリクエストデータをnewEntity()に渡し、エンティティに変換。saveで保存する。

<?php
$entity = $articles->newEntity($this->request->getData());
$articlesTable->save($entity);
?>
エンティティ構築前のリクエストデータ変更

beforeMarshalイベントの中でエンティティ構築前のリクエストデータ変更処理を行う。

<?php
// ファイルの先頭に use ステートメントを入れること。
use Cake\Event\Event;
use ArrayObject;

// テーブルまたはビヘイビアークラスの中で
public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
{
    if (isset($data['username'])) {
        $data['username'] = mb_strtolower($data['username']);
    }
}
?>

マスアサインメント攻撃の回避

マスアサインメント攻撃を回避するには、エンティティの一括代入機能を使う。編集可能なプロパティの配列を$_accessibleとして定義する。

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

class Article extends Entity
{
    protected $_accessible = [
        'title' => true,
        'body' => true,
    ];
}
?>

または、newEntity()あるいはpatchEntity()でfieldListオプションを使用する。

<?php
$data = $this->request->getData();

// タイトルのみ変更することを許します
$entity = $this->patchEntity($entity, $data, [
    'fieldList' => ['title']
]);
$this->save($entity);
?>

厳密な保存

saveOrFail()を使うと、次の条件でCake\ORM\Exception\PersistenceFailedExceptionを投げる。

  • アプリケーションルールのチェックに失敗した場合
  • エンティティーにエラーが含まれている場合
  • 保存がコールバックによって中断された場合
<?php
try {
    $table->saveOrFail($entity);
} catch (\Cake\ORM\Exception\PersistenceFailedException $e) {
    echo $e->getEntity(); // エラーの内容を表示
}
?>

バリデーションとルール

バリデーション

リクエストデータがエンティティにコンバートされる前に、データ型や書式のルールが適用される。

<?php
$article = $articles->newEntity($this->request->getData());
if ($article->errors()) {
    // エンティティー検証失敗。
}
?>
エンティティ構築時のバリデーションの流れ
  1. バリデータオブジェクトが作成される
  2. table および default バリデーションプロバイダーが追加される
  3. 命名に沿ったバリデーションメソッドが呼び出される。たとえば validationDefault 。
  4. Model.buildValidator イベントが発動する
  5. リクエストデータが検証される
  6. リクエストデータがそのカラム型に対応する型に変換される
  7. エラーがエンティティーにセットされる
  8. 正しいデータはエンティティーに設定されるが、 検証を通らなかったフィールドは除外される

バリデータオブジェクトの作成

デフォルトのバリデータオブジェクト

デフォルトのバリデータオブジェクトはテーブル中でvalidationDefault()で作成される。

<?php
use Cake\ORM\Table;
use Cake\Validation\Validator;

class ArticlesTable extends Table
{
    public function validationDefault(Validator $validator)
    {
        $validator
            ->requirePresence('title', 'create')
            ->notEmpty('title');

        $validator
            ->allowEmpty('link')
            ->add('link', 'valid-url', ['rule' => 'url']);

        ...

        return $validator;
    }
}
?>
カスタムバリデータオブジェクト

カスタムバリデータ'update'の使用例。validationUpdate()メソッドで作成する。

<?php
class ArticlesTable extends Table
{
    public function validationUpdate($validator)
    {
        $validator
            ->add('title', 'notEmpty', [
                'rule' => 'notEmpty',
                'message' => __('タイトルを設定してください'),
            ])
            ->add('body', 'notEmpty', [
                'rule' => 'notEmpty',
                'message' => __('本文は必須です')
            ]);
        return $validator;
    }
}
?>

エンティティ作成時のオプションで'update'を指定。これでデフォルトのバリデータではなくupdateバリデータが適用される。

<?php
$article = $articles->newEntity(
    $this->request->getData(),
    ['validate' => 'update'] // バリデーションのオプション
);
?>
バリデータの組み合わせ

バリデータ作成メソッドの中で異なるバリデータを作成することで、バリデーションを組み合わせることができる。

<?php
public function validationHardened(Validator $validator)
{
    $validator = $this->validationDefault($validator); // デフォルトバリデータオブジェクトの作成

    $validator->add('password', 'length', ['rule' => ['lengthBetween', 8, 100]]);
    return $validator;
}
?>

ルール

データが保存される前に、ドメインルールまたはアプリケーションルールが適用される。
バリデーションはデータの構文や形式が正しいことを保証するのに対し、ルールはアプリケーションやネットワークの既存の状態に対してデータを比較することに焦点を当て、データの一貫性を保証する。

ルールチェッカーの作成

ルールチェッカーはテーブルクラスのbuildRules()で定義される。

<?php
use Cake\ORM\RulesChecker;

// テーブルクラスの中で
public function buildRules(RulesChecker $rules)
{
    // 作成および更新操作に提供されるルールを追加
    $rules->add(function ($entity, $options) {
        // 失敗/成功を示す真偽値を返す
    }, 'ruleName');

    // 作成のルールを追加
    $rules->addCreate(function ($entity, $options) {
        // 失敗/成功を示す真偽値を返す
    }, 'ruleName');

    // 更新のルールを追加
    $rules->addUpdate(function ($entity, $options) {
        // 失敗/成功を示す真偽値を返す
    }, 'ruleName');

    // 削除のルールを追加
    $rules->addDelete(function ($entity, $options) {
        // 失敗/成功を示す真偽値を返す
    }, 'ruleName');

    return $rules;
}
?>
一意性ルール

isUnique(['検証するフィールド'])でデータの一意性を検証する。

<?php
use Cake\ORM\Rule\IsUnique;

// 一つのフィールド
$rules->add($rules->isUnique(['email']));

// フィールドのリスト
$rules->add($rules->isUnique(
    ['username', 'account_id'],
));
?>
外部キールール

existIn('検証するフィールド', '参照先テーブル')でほかのリソースと関連づいているかどうか(外部キーに参照先テーブルのキーが入っているか)を検証する。

<?php
// 一つのフィールド
$rules->add($rules->existsIn('article_id', 'Articles'));

// 複数キー。複合主キーに役立ちます。
$rules->add($rules->existsIn(['site_id', 'article_id'], 'Articles'));
?>
アソシエーションカウントルール

一対多、多対多の関連があるとき、validCount('関連データ名', '件数', '比較演算子', 'エラーメッセージ')で関連データの件数を検証する。

<?php
// ArticlesTable.php ファイルの中で
// 記事にタグは5つ以内。
$rules->add($rules->validCount('tags', 5, '<=', 'タグは 5 つまで持てます'));
?>
ルールの無効化

エンティティ保存時にオプションでルールを無効化できる。

<?php
$articles->save($article, ['checkRules' => false]);
?>

キャッシュ

FileCache

ローカルファイルを使用するキャッシュ。tmp/cache配下にキャッシュファイルを作成し、そこにデータを保存する。

クエリ結果をキャッシュする

cacheメソッドでクエリ結果をキャッシュする。第一引数にキャッシュキーを渡す。

<?php
$query->cache('recent_articles');
$query->cache('recent_articles', 'dbResults'); // 第二引数にキャッシュ設定を渡す
?>

この場合、デフォルトではキャッシュファイルのパスはtmp/cache/cake_recent_articlesになる。

キャッシュへの書き込み

Cache::write(key, value)でキャッシュに値を書き込む。またCache::writeManyでは複数のキーを書き込める。

<?php
Cache::write('recent_articles', $articles);

Cache::writeMany([       // 連想配列を渡す
    'recent_articles' => $article,
    'recent_comments' => $comments
]);
?>

キャッシュへの書き込み

Cache::read(key)でキャッシュから値を読み込む。またCache::readManyでは複数のキーを読み込める。

<?php
$results = Cache::read('recent_articles');

$results = Cache::readMany([       // 配列を渡す
    'recent_articles' ,
    'recent_comments'
]);
// [
//    'recent_articles' => '...',
//    'recent_comments' => '...'
// ]
?>

キャッシュからの削除

Cache::delete(key, value)でキャッシュから値を削除する。またCache::deleteManyでは複数のキーを削除できる。

<?php
Cache::delete('recent_articles');

Cache::deleteMany([       // 連想配列を渡す
    'recent_articles',
    'recent_comments'
]);
?>

キャッシュデータのクリア

<?php
// 有効期限切れのキーのみをクリアする。
Cache::clear(true);

// すべてのキーをクリアする。
Cache::clear(false);
?>

【Table】関連データの取得

関連データの取得

containメソッド

containで関連データをeagar loadして取得する。

<?php
$articles->find('all')->contain(['Authors', 'Comments']);
?>

ネストされた関連データを取得するときはドット記法を用いる。

<?php
$articles->find()->contain([
    'Authors.Addresses',
    'Comments.Authors'
]);
?>

関連データを条件によってフィルターすることができる。条件を指定するには、第二引数としてクエリーオブジェクトを受け取る無名関数を渡す。

<?php
$articles->find()->contain('Comments', function (Query $q) {
    return $q
        ->select(['body', 'author_id'])
        ->where(['Comments.approved' => true]);
});
?>
matchingメソッド

matchingメソッドで、関連データの条件にマッチするレコードを取得する。

<?php
$articles->find()->matching('Tags', function ($q) {
    return $q->where(['Tags.name' => 'CakePHP']);
});
?>

このメソッドはINNER JOIN句を生成するので、レコードが重複する可能性がある。distinctメソッドでプライマリーキーなど(ここではid)で重複行をまとめる。

<?php
$articles->find()->distinct('id')->matching(...);
?>
innerJoinWithメソッド

matchingと同様に関連データの条件でレコードを絞り込むが、matchingと違い関連データを取得しない。

<?php
$articles->find()->innerJoinWith('Tags', function ($q) {
    return $q->where(['Tags.name' => 'CakePHP']);
});
?>
notMatchingメソッド

matchingメソッドの逆。関連データの条件にマッチしないレコードを取得する。

<?php
$articles->find()->notMatching('Tags', function ($q) {
    return $q->where(['Tags.name' => 'CakePHP']);
});
?>

このメソッドはLEFT JOIN句を生成するので、レコードが重複する可能性がある。distinctメソッドでプライマリーキーなど(ここではid)で重複行をまとめる。

<?php
$articles->find()->distinct('id')->notMatching(...);
?>
leftJoinWithメソッド

すべての関連データをロードしたくはないが、関連データに基いて結果を計算したいときはleftJoinWithを使う。以下は記事 (Article) の全データと一緒に、記事ごとのコメント (Comment) 数をロードするときの例。

<?php
$articlesTable->find()->select(['total_comments' => $query->func()->count('Comments.id')])
    ->leftJoinWith('Comments')
    ->group(['Articles.id'])
    ->enableAutoFields(true);
?>

【Table】データの取り出し

クエリの生成

findメソッド

findメソッドでクエリを生成。

<?php
$query = $articles->find('all'); // この時点ではクエリーは走らない。

$results = $query->all(); // all() の呼び出しはクエリーを実行し、結果セットを返す
foreach ($query as $row) {...} // イテレーションはクエリーを実行する
?>

第一引数

<?php
// 全件取得
$query = $articles->find('all'); 

// データを連想配列として取得
$query = $articles->find('list', [
    'keyField' => 'slug', // キーを指定 デフォルトではid
    'valueField' => 'title' // 値を指定 デフォルトではdisplayFieldで指定された列
]);
// 取得結果
// $query->toArray() = [
//    'first-post' => '最初の投稿',
//    'second-article-i-wrote' => '私が書いた2つ目の記事',
// ];

// スレッド状のデータを取得
$query = $articles->find('thread');

?>
カスタムFinder

findFooBarメソッドによって、fooBarという名前のカスタムfinderが作れる。

<?php
public function findOwnedBy(Query $query, array $options)
{
    $user = $options['user'];
    return $query->where(['author_id' => $user->id]);
}

$query = $articles->find('ownedBy', ['user' => $userEntity]); // 呼び出し
?>

クエリの実行

クエリ実行パターン① findの第二引数にオプションを指定する
<?php
$query = $articles->find('all', [
    'conditions' => ['Articles.created >' => new DateTime('-10 days')],
    'contain' => ['Authors', 'Comments'],
    'limit' => 10
]);
?>

findで使えるオプション

  • conditions WHERE
  • limit LIMIT
  • offset 指定した整数番目からデータを取得する
  • contain 関連をイーガーロード (eager load) する
  • fields 列を指定する
  • group GROUP BY
  • having HAVING
  • join JOIN
  • order ORDER BY
クエリ実行パターン② クエリオブジェクトにメソッドを呼び出す
<?php
$articles->find('all')->first(); // 最初のレコードを取得
$articles->find('all')->count(); // レコードの件数を取得
$articles->find('list')->toArray(); // レコードを連想配列として取得
?>