CakePHP3を触ってみました 〜アソシエーション〜

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
最後に人と手をつないだのはもう何年前になるのか……

この記事を三行にまとめると

belongsTo
hasMany
belognsToMany
CakePHPを使っているとアソシエーションを使うことが多々あると思うんですが、このアソシエーションに関しても2系と3系では違う点があるので、ちょっとそれを見ていきたいと思います。



belongsTo

僕の中では一番簡単で一番よく使うのがbelongsToです。

ここではブログの記事が入っているpostsというテーブルと、記事につけるタグが入っているtagsというテーブルがある場合を考えます。postsとtagsはbelongsToの関係とします。

タグに紐づく記事のデータを取って来る場合、CakePHP2だとモデルごとにデータが分かれて取得されます。

//Tag.php
class Tag extends AppModel {
  public $belongsTo = array('Post');
}

//TagsController.php
class TagsController extends AppController {
  public function index() {
    $params = array(
      'conditions' => array('Tag.id' => 1),
      'contain' => array('Post'),
    );
    $data = $this->Tag->find('first', $params);
  }
}

//$data
Array
(
  [Tag] => Array
  (
    [id] => 1
    [post_id] => 1
    [name] => タグ
  )

  [Post] => Array
  (
    [id] => 1
    [title] => タイトル
    [text] => 本文
  )
)

もんのすごいざっくり書くとこんな感じ。idが1のタグに紐づく記事のデータを一緒に取ってきています。

TagとPostのそれぞれに配列が分かれているので、$dataを使ってタグの名前や記事のタイトルを表示する場合はこうなりますね。

echo $data['Tag']['name'];//タグ名
echo $data['Post']['title'];//記事のタイトル

CakePHP3だと、テーブルごとに分かれずにデータが取得されます。

//TagsTable.php
class TagsTable extends AppTable {
  public function initialize(array $config) {
    $this->belongsTo('Posts');
  }
}

//TagsController.php
class TagsController extends AppController {
  public function index() {
    $query = TableRegistry::get('Tags')->find();
    $query->where(['Tags.id' => 1]);
    $query->contain(['Posts']);
    $data = $query->first();
  }
}

//$data
App\Model\Entity\Tag Object
(
  [_properties:protected] => Array
  (
    [id] => 1
    [post_id] => 1
    [name] => タグ
    [post] => App\Model\Entity\Post Object
    (
      [_properties:protected] => Array
      (
        [id] => 1
        [title] => タイトル
        [text] => 本文
      )
    )
  )
)

配列ではなくエンティティオブジェクトで取得されるところも違いますが、TagとPostのそれぞれのオブジェクトに分かれるのではなく、Tagオブジェクトの中に「post」というキーがあって、その中にPostオブジェクトが入っています。

したがって$dataを使ってタグ名と記事のタイトルを表示するのもCakePHP2とは異なります。

echo $data->name;//タグ名
echo $data->post->title;//記事タイトル



「確かにちょっとは違うけど、でも一回見れば分かるしたいしたことねーじゃん」って思うじゃん?

確かにそうなんだけど、でもCakePHP3の場合は、データの取り方によってここが変わる場合があんのよ。

例えば上記の条件で、記事のタイトルだけ取ってくる場合。

CakePHP2の場合は単にTagの方の配列がなくなるだけで済むんですよ。だから$dataを使って表示する場合も何ら変わりはない。

//TagsController.php
class TagsController extends AppController {
  public function index() {
    $params = array(
      'conditions' => array('Tag.id' => 1),
      'fields' => array('Post.title'),
      'contain' => array('Post'),
    );
    $data = $this->Tag->find('first', $params);
  }
}

//$data
Array
(
  [Post] => Array
  (
    [id] => 1
    [title] => タイトル
  )
)

//タイトルの表示
echo $data['Post']['title'];

でもCakePHP3だと、アソシエーション先のフィールドしか取得しない場合は、「$data->post」みたいな形にならない。

//TagsController.php
class TagsController extends AppController {
  public function index() {
    $query = TableRegistry::get('Tags')->find();
    $query->where(['Tags.id' => 1]);
    $query->select(['Posts.title']);
    $query->contain(['Posts']);
    $data = $query->first();
  }
}

//$data
App\Model\Entity\Tag Object
(
  [_properties:protected] => Array
  (
    [Posts] => App\Model\Entity\Post Object
    (
      [_properties:protected] => Array
      (
        [title] => タイトル
      )
    )
  )
)

「post」だったのが「Posts」に変わります。そして「$data->Posts」では参照できないことにも注意。

この形で記事のタイトルを表示する場合はこうなります。

echo $data['Posts']->title;

ちょびっと変則的な感じがしますね。

個人的には2系の形の方がいろいろと使いやすいんですが……まあ慣れればそんなこともないのかな。



hasOne

hasOneはbelongsToとほとんど一緒です。CakePHP3でアソシエーション先のフィールドだけ取る場合も同様。だからここでは省略しちゃいます。



HasMany

では今度は、postsとtagsがhasManyの関係を考えます。一つの記事に複数のタグが紐づくイメージね。

まずはCakePHP2の場合。

//Post.php
class Post extends AppModel {
  public $hasMany = array('Tag');
}

//PostsController.php
class PostsController extends AppController {
  public function index() {
    $params = array(
      'conditions' => array('Post.id' => 1),
      'contain' => array('Tag');
    );
    $data = $this->Post->find('first', $params);
  }
}

//$data
Array
(
  [Post] => Array
  (
    [id] => 1
    [title] => タイトル
    [text] => 本文
  )

  [Tag] => Array
  (
    [0] => Array
    (
      [id] => 1
      [post_id] => 1
      [name] => タグ1
    )

    [1] => Array
    (
      [id] => 2
      [post_id] => 1
      [name] => タグ2
    )
  )
)

//記事タイトルやタグの表示
echo $data['Post']['title'];
echo $data['Tag'][0]['name'];

idが1の記事にタグが2つ紐づいているという結果です。

続いてCakePHP3。

//PostsTable.php
class PostsTable extends AppTable {
  public function initialize(array $config) {
    $this->hasMany('Tags');
  }
}

//PostsController.php
class PostsController extends AppController {
  public function index() {
    $query = TableRegistry::get('Posts')->find();
    $query->where(['Posts.id' => 1]);
    $query->contain(['Tags']);
    $data = $query->first();    
  }
}

//$data
App\Model\Entity\Post Object
(
  [_properties:protected] => Array
  (
    [id] => 1
    [title] => タイトル
    [text] => 本文
    [tags] => Array
    (
      [0] => App\Model\Entity Object
      (
        [_properties:protected] => Array
        (
          [id] => 1
          [post_id] => 1
          [name] => タグ1
        )
      )

      [1] => App\Model\Entity Object
      (
        [_properties:protected] => Array
        (
          [id] => 2
          [post_id] => 1
          [name] => タグ2
        )
      )
    )
  )
)

//記事タイトルやタグの表示
echo $data->title;
echo $data->tags[0]->name;

hasManyの場合は複数のデータを取ってくるので、「tags」というキーの中に一つずつエンティティができます。さっきのbelongsToの時は「post」のように単数形でしたが、今回は「tags」と複数形になってます。必ずしもこの形になるかは分かりませんが、CakePHPの命名規約に従った時はこの形になると思って良いでしょう。



HABTM

最後にHABTMです。記事とタグが多対多で紐づいている場合を考えてみましょう。postsのtagsの中間テーブルとしてposts_tagsテーブルがあると仮定してください。

ではCakePHP2から見ていきましょー。

//Post.php
class Post extends AppModel {
  public $hasAndBelongsToMany = array('Tag');
}

//PostsController.php
class PostsController extends AppController {
  public function index() {
    $params = array(
      'conditions' => array('Post.id' => 1),
      'contain' => array('Tag');
    );
    $data = $this->Post->find('first', $params);    
  }
}

//$data
Array
(
  [Post] => Array
  (
    [id] => 1
    [title] => タイトル
    [text] => 本文
  )

  [Tag] => Array
  (
    [0] => Array
    (
      [id] => 1
      [name] => タグ1
      [PostsTag] => Array
      (
        [id] => 1
        [post_id] => 1
        [tag_id] => 1
      )
    )

    [1] => Array
    (
      [id] => 2
      [name] => タグ2
      [PostsTag] => Array
      (
        [id] => 1
        [post_id] => 1
        [tag_id] => 2
      )
    )
  )
)

//記事タイトルやタグの表示
echo $data['Post']['title'];
echo $data['Tag'][0]['name'];

形としてはhasManyに近いですかね。タグの方に「PostsTag」という中間テーブルのデータが一緒に入っています。

そしてCakePHP3。

//PostsTable.php
class PostsTable extends AppTable {
  public function initialize(array $config) {
    $this->hasMany('Tags');
  }
}

//PostsController.php
class PostsController extends AppController {
  public function index() {
    $query = TableRegistry::get('Posts')->find();
    $query->where(['Posts.id' => 1]);
    $query->contain(['Tags']);
    $data = $query->first();    
  }
}

//$data
App\Model\Entity\Post Object
(
  [_properties:protected] => Array
  (
    [id] => 1
    [title] => タイトル
    [text] => 本文
    [tags] => Array
    (
      [0] => App\Model\Entity Object
      (
        [_properties:protected] => Array
        (
          [id] => 1
          [name] => タグ1
          [_joinData] => Cake\ORM\Entity Object
          (
            [_properties:protected] => Array
            (
              [id] => 1
              [post_id] => 1
              [tag_id] => 1
            )
          )
        )
      )

      [1] => App\Model\Entity Object
      (
        [_properties:protected] => Array
        (
          [id] => 2
          [name] => タグ2
          [_joinData] => Cake\ORM\Entity Object
          (
            [_properties:protected] => Array
            (
              [id] => 1
              [post_id] => 1
              [tag_id] => 2
            )
          )
        )
      )
    )
  )
)

//記事タイトルやタグの表示
echo $data->title;
echo $data->tags[0]->name;

こっちもhasManyに近いですね。中間テーブルのデータは「_joinData」というところに入っています。

hasManyやHABTMに関しては、2と3でそんなに使い勝手の差はないですかね。僕はそんな気がしました。



おまけ

CakePHP3で、hasManyやbelongsToManyにwhereやselectなどの指定をしたい場合。

$query->contain([
  'Tag' => function($q) {
    return $q->select(['Tag.name']);
  }
]);

こんな感じでメソッドを書けばオッケーです。






そんなに難しいことはないんですけど、belongsToとhasManyの時だけフィールドの取り方で微妙に返って来るデータが違うのに気づかなくてしばらく迷走したので、一応記事にしとこうかなと。

そういえばHABTMは、CakePHP2と3で言い方も違ってるんですね。2の時は「hasAndBelongsToMany」でしたが、3だと「belongsToMany」に変わりました。ってことは略称も「BTM」になるんですかね。サンドイッチみたいだな。BTMサンド。Bはベーコン、Tはタマゴで、Mは……ミートか? 完全にベーコンエッグバーガーですやん。

データの中身を見ればすぐに分かることなんですけど、もし同じように「あれ? データが取れねえ」ってなった時には、この記事のことを思い出してみてください。



その他のCakePHP3を触ってみましたの記事はこちら
まとめという名の箸休め
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください