初心者用CakePHPでブログサイト⑥1対多アソシエーションと多対多アソシエーション

初心者用CakePHPでブログサイト⑥1対多アソシエーションと多対多アソシエーション

128 回閲覧されました

みなさんこんにちは、jonioです。

今の状態は誰が記事を投稿したか分からない状態ですが今回は1対多アソシエーションを使って誰が記事を投稿したか分かるようにします。

また多対多アソシエーションを使って記事とタグを紐付けします。

アソシエーションはモデルとモデルを繋ぐ機能のことです。

まずは1対多アソシエーションから説明します。

ユーザーと記事を紐づける

一人のユーザーに記事を結びつけるのでUserモデルとPostモデルを繋ぎます。

ユーザーに記事が結びつくのでUserモデルが親にあたります。

マイグレーションファイルの編集

postsテーブルを作成した時に使ったマイグレーションファイルを編集します。

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class CreatePosts extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change(): void
    {
        $table = $this->table('posts');
        $table->addColumn('title', 'string', [
            'limit' => 150,
            'null' => false
        ])
              ->addColumn('description', 'text', [
                  'limit' => 255
              ])
              ->addColumn('body', 'text')
              ->addColumn('published', 'boolean', [
                  'default' => false
              ])
              
              
              //ここから追加
              ->addColumn('user_id', 'integer', [
                  'default' => 1,
                  'limit' => 11,
                  'null' => false
              ])
              //ここまで追加
              
              
              ->addColumn('created', 'datetime')
              ->addColumn('modified', 'datetime')
              ->create();
    }
}

32行目の「user_id」ですがアソシエーションをする時は子にあたるテーブルに「親モデル名_id」カラムを作らないといけないルールがあります。

Seederの編集

PostsSeed.phpを編集します。

<?php
declare(strict_types=1);

use Migrations\AbstractSeed;

/**
 * Posts seed.
 */
class PostsSeed extends AbstractSeed
{
    /**
     * Run Method.
     *
     * Write your database seeder using this method.
     *
     * More information on writing seeds is available here:
     * https://book.cakephp.org/phinx/0/en/seeding.html
     *
     * @return void
     */
    public function run(): void
    {
        $data = [
            // 各レコードにuser_idを追加
            [
                'title' => '最初の投稿',
                'description' => '最初の投稿の概要',
                'body' => '最初の投稿の内容',
                'published' => 1,
                'user_id' => 1,                           //この行を追加
                'created' => '2020-05-02 10:00:00',
                'modified' => '2020-05-02 10:00:00',
            ],
            [
                'title' => '2番目の投稿',
                'description' => '2番目の投稿の概要',
                'body' => '2番目の投稿の内容',
                'published' => 1,
                'user_id' => 1,                           //この行を追加
                'created' => '2020-05-02 10:00:00',
                'modified' => '2020-05-02 10:00:00',
            ],
        ];

        $table = $this->table('posts');
        $table->insert($data)->save();
    }
}

コマンド

下記のコマンドを叩きます。

rm database/product.sqlite
bin/cake migrations migrate
bin/cake migrations seed

これでpostsテーブルが更新されました。

モデルの編集

Entityフォルダの下の階層にあるPost.phpを編集します。

<?php
declare(strict_types=1);

namespace App\Model\Entity;

use Cake\ORM\Entity;

/**
 * Post Entity
 *
 * @property int $id
 * @property string $title
 * @property string $description
 * @property string $body
 * @property bool $published
 * @property \Cake\I18n\FrozenTime $created
 * @property \Cake\I18n\FrozenTime $modified
 */
class Post extends Entity
{
    /**
     * Fields that can be mass assigned using newEntity() or patchEntity().
     *
     * Note that when '*' is set to true, this allows all unspecified fields to
     * be mass assigned. For security purposes, it is advised to set '*' to false
     * (or remove it), and explicitly make individual fields accessible as needed.
     *
     * @var array<string, bool>
     */
    protected $_accessible = [
        'title' => true,
        'description' => true,
        'body' => true,
        'published' => true,
        'user_id' => true,                //この行を追加
        'created' => true,
        'modified' => true,
    ];
}

Tableフォルダの下の階層にあるPostsTable.phpを編集します、ここでアソシエーションの設定をします。

<?php
declare(strict_types=1);

namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

/**
 * Posts Model
 *
 * @method \App\Model\Entity\Post newEmptyEntity()
 * @method \App\Model\Entity\Post newEntity(array $data, array $options = [])
 * @method \App\Model\Entity\Post[] newEntities(array $data, array $options = [])
 * @method \App\Model\Entity\Post get($primaryKey, $options = [])
 * @method \App\Model\Entity\Post findOrCreate($search, ?callable $callback = null, $options = [])
 * @method \App\Model\Entity\Post patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
 * @method \App\Model\Entity\Post[] patchEntities(iterable $entities, array $data, array $options = [])
 * @method \App\Model\Entity\Post|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
 * @method \App\Model\Entity\Post saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
 * @method \App\Model\Entity\Post[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable $entities, $options = [])
 * @method \App\Model\Entity\Post[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable $entities, $options = [])
 * @method \App\Model\Entity\Post[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable $entities, $options = [])
 * @method \App\Model\Entity\Post[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable $entities, $options = [])
 *
 * @mixin \Cake\ORM\Behavior\TimestampBehavior
 */
class PostsTable extends Table
{
    /**
     * Initialize method
     *
     * @param array $config The configuration for the Table.
     * @return void
     */
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('posts');
        $this->setDisplayField('title');
        $this->setPrimaryKey('id');

        $this->addBehavior('Timestamp');

        $this->belongsTo('Users');           //この行を追加
    }

    /**
     * Default validation rules.
     *
     * @param \Cake\Validation\Validator $validator Validator instance.
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->scalar('title')
            ->maxLength('title', 150)
            ->requirePresence('title', 'create')
            ->notEmptyString('title');

        $validator
            ->scalar('description')
            ->requirePresence('description', 'create')
            ->allowEmptyString('description');

        $validator
            ->scalar('body')
            ->requirePresence('body', 'create')
            ->allowEmptyString('body');

        $validator
            ->boolean('published')
            ->notEmptyString('published');


        //ここから追加
        $validator
            ->boolean('user_id')
            ->notEmptyString('user_id');
        //ここまで追加
        

        return $validator;
    }
}

48行目がアソシエーションをする為の記述です。

またTableフォルダの下の階層にあるUsersTable.phpも編集します。

<?php
declare(strict_types=1);

namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

/**
 * Users Model
 *
 * @method \App\Model\Entity\User newEmptyEntity()
 * @method \App\Model\Entity\User newEntity(array $data, array $options = [])
 * @method \App\Model\Entity\User[] newEntities(array $data, array $options = [])
 * @method \App\Model\Entity\User get($primaryKey, $options = [])
 * @method \App\Model\Entity\User findOrCreate($search, ?callable $callback = null, $options = [])
 * @method \App\Model\Entity\User patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
 * @method \App\Model\Entity\User[] patchEntities(iterable $entities, array $data, array $options = [])
 * @method \App\Model\Entity\User|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
 * @method \App\Model\Entity\User saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable $entities, $options = [])
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable $entities, $options = [])
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable $entities, $options = [])
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable $entities, $options = [])
 *
 * @mixin \Cake\ORM\Behavior\TimestampBehavior
 */
class UsersTable extends Table
{
    /**
     * Initialize method
     *
     * @param array $config The configuration for the Table.
     * @return void
     */
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('users');
        $this->setDisplayField('username');
        $this->setPrimaryKey('id');

        $this->addBehavior('Timestamp');

        $this->hasMany('Posts');              //この行を追加
    }

    /**
     * Default validation rules.
     *
     * @param \Cake\Validation\Validator $validator Validator instance.
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->scalar('username')
            ->maxLength('username', 50)
            ->requirePresence('username', 'create')
            ->notEmptyString('username');

        $validator
            ->scalar('password')
            ->maxLength('password', 255)
            ->requirePresence('password', 'create')
            ->notEmptyString('password');

        return $validator;
    }

    /**
     * Returns a rules checker object that will be used for validating
     * application integrity.
     *
     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
     * @return \Cake\ORM\RulesChecker
     */
    public function buildRules(RulesChecker $rules): RulesChecker
    {
        $rules->add($rules->isUnique(['username']), ['errorField' => 'username']);

        return $rules;
    }
}

48行目がアソシエーションをする為の設定です。

次は投稿者を表示します。

コントローラーの編集

「cakePHPのプロジェクト > src > Controller」の下の階層にあるPostsController.phpのviewメソッドを修正します。

public function view($id = null)
{
  $post = $this->Posts->get($id, [
    'contain' => 'Users'
  ]);

  $this->set(compact('post'));
}

今PostモデルとUserモデルのアソシエーションをしていますがpostsテーブルに関連するusersテーブルの情報を使う為に4行目があります。

getメソッドの第二引数の連想配列の部分はkeyにcontainを指定してvalueにモデル名s(今はUsers)をしています。

ビューの編集

templatesフォルダの直下にあるPostsフォルダの下の階層にあるview.phpを修正します。

<div class="content">
    <p><?= $post->created->i18nFormat('YYYY年MM月dd日 HH:mm') ?></p>
    <h1><?= h($post->title) ?></h1>
    <?= $this->Text->autoParagraph(h($post->body)) ?>
    <p><small>投稿者 : <?= h($post->user->username) ?></small></p>
    <hr>
    <?= $this->Html->link('一覧へ戻る', [
        'action' => 'index'
    ], ['class' => 'button']) ?>
</div>

5行目にアソシエーションを使っています。

これで「http://localhost:8888/cakephp/CakeBlog1/Posts/view/2」にアクセスすると表示が下記になります。

次は記事一覧のページにもユーザー名を表示します。

PostsController.phpのindexメソッドを修正します。

public function index()
{
  $posts = $this->paginate($this->Posts->find()->contain(['Users']));           //この行を修正

  $this->set(compact('posts'));
}

「->contain([‘Users’])」を追加しました。

そして「cakePHPのプロジェクト > templates > Posts」の下の階層にあるindex.phpを修正します。

<div class="content">
    <?php foreach($posts as $post): ?>
        <h3><?= h($post->title) ?></h3>
        <p><?= $post->created->i18nFormat('YYYY年MM月dd日 HH:mm') ?></p>
        <p><?= h($post->description) ?></p>
        <p><small>投稿者 : <?= h($post->user->username) ?></small></p>        //この行を追加

これで「http://localhost:8888/cakephp/CakeBlog1/Posts/」にアクセスすると下記の表示になります。

次は管理画面でもユーザー名を表示させます。

Adminフォルダの下の階層にあるPostsController.phpのindexアクションを修正します、ページネーションを一緒に追加します。

public function index()
{
  $posts = $this->paginate($this->Posts->find()->contain(['Users']));       //この行を修正

  $this->set(compact('posts'));
}

次はtemplates > Admin > Postsフォルダの下の階層のindex.phpを修正します。

<?php
/**
 * @var \App\View\AppView $this
 * @var iterable<\App\Model\Entity\Post> $posts
 */
?>
<div class="posts index content">
    <?= $this->Html->link(__('New Post'), ['action' => 'add'], ['class' => 'button float-right']) ?>
    <h3><?= __('Posts') ?></h3>
    <div class="table-responsive">
        <table>
            <thead>
                <tr>
                    <th><?= $this->Paginator->sort('id') ?></th>
                    <th><?= $this->Paginator->sort('title') ?></th>
                    <th><?= $this->Paginator->sort('user_id') ?></th>          //この行を追加    
                    <th><?= $this->Paginator->sort('published') ?></th>
                    <th><?= $this->Paginator->sort('created') ?></th>
                    <th><?= $this->Paginator->sort('modified') ?></th>
                    <th class="actions"><?= __('Actions') ?></th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($posts as $post): ?>
                <tr>
                    <td><?= $this->Number->format($post->id) ?></td>            
                    <td><?= h($post->title) ?></td>
                    <td><?= h($post->user->username) ?></td>                  //この行を追加
                    <td><?= h($post->published) ?></td>

これで「http://localhost:8888/cakephp/CakeBlog1/admin/posts」にアクセスするとユーザー名が表示されます。

次は記事の詳細画面です。

さっきのPostsController.phpのviewメソッドを修正します。

public function view($id = null)
{
  $post = $this->Posts->get($id, [
    'contain' => ['Users'],              //この行を修正
  ]);

  $this->set(compact('post'));
}

そして「Admin > Posts > view.php」を修正します。


  <tr>
    <th><?= __('Modified') ?></th>
    <td><?= h($post->modified) ?></td>
  </tr>
  <tr>
    <th><?= __('Published') ?></th>
    <td><?= $post->published ? __('Yes') : __('No'); ?></td>
  </tr>
  
  
  //ここから追加
  <tr>
    <th><?= __('User') ?></th>
    <td><?= h($post->user->username) ?></td>
  </tr>
  //ここまで追加
  
  
</table>

これで「http://localhost:8888/cakephp/CakeBlog1/admin/posts/view/1」にアクセスすると下記の表示になります。

次は記事の編集画面でユーザー名を編集できるのをselectタグでします。

さっきのPostsController.phpのeditメソッドを修正します。

<?php
declare(strict_types=1);

namespace App\Controller\Admin;

use App\Controller\Admin\AdminController;

/**
 * Posts Controller
 *
 * @property \App\Model\Table\PostsTable $Posts
 * @method \App\Model\Entity\Post[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class PostsController extends AdminController
{
    public $Users = null;             //この行を追加
    
    public $paginate = [
        'limit' => 3,
        'order' => [
            'Posts.created' => 'desc'
        ],              
    ];


    //ここから追加
    public function initialize(): void{
        parent::initialize();

        $this->loadModel('Users');
    }
    //ここまで追加






    /**
     * Edit method
     *
     * @param string|null $id Post id.
     * @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function edit($id = null)
    {
        $post = $this->Posts->get($id, [
            'contain' => [],
        ]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $post = $this->Posts->patchEntity($post, $this->request->getData());
            if ($this->Posts->save($post)) {
                $this->Flash->success(__('The post has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The post could not be saved. Please, try again.'));
        }

        $users = $this->Users->find('list');            //この行を追加

        $this->set(compact('post', 'users'));           //この行を編集
    }

このコントローラーでは60行目でUsersを使っていますが同じ名称以外のモデル(今回はUser)を使う場合は29行目の記述をします、引数は「モデル名s」です。

26行目〜30行目のintializeメソッドの中に記述します、これはコントローラーが呼び出された時に最初に実行するメソッドです。

27行目は必ず記述します。

次に「Admin > Posts」の下の階層にあるedit.phpを修正します。

<?php
  echo $this->Form->control('title');
  echo $this->Form->control('description');
  echo $this->Form->control('body');
  echo $this->Form->control('published');
  echo $this->Form->control('user_id');             //この行を追加
?>

そして「http://localhost:8888/cakephp/CakeBlog1/admin/posts/edit/1」にアクセスすると下記のようにユーザーを選択できる形で編集ができるようになります。

次は多対多アソシエーションです。

タグと記事を紐づける

タグと記事ですがお互いに複数紐づきます、こんな時に多対多アソシエーションを使います。

Tag用のテーブルの作成

マイグレーションファイルを作成する為に下記のコマンドを叩きます。

bin/cake bake migration CreateTags

作成したマイグレーションファイルに下記の記述をします。

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class CreateTags extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change(): void
    {
        $table = $this->table('tags');
        $table
              ->addColumn('title', 'text', [
                'limit' => 100,
                'null' => false
              ])
              ->addColumn('created', 'datetime')
              ->addColumn('modified', 'datetime')
              ->create();
        $table = $this->table('posts_tags');
        $table
              ->addColumn('post_id', 'integer', [
                'null' => false
              ])
              ->addColumn('tag_id', 'integer', [
                'null' => false
              ])
              ->create();
    }
}

多対多アソシエーションをする場合は中間テーブルというテーブルを作成しないといけません、26行目〜34行目に当たります。

今回はpostsテーブルとtagsテーブルを紐づけますが「テーブル名_id」カラムを作成しないといけません。

その為に28行目(post_id)と31行目(tag_id)があります。

記述が終わったら下記のコマンドを叩きます。

 bin/cake migrations migrate

これでpostsテーブルとposts_tagsテーブルが作成されます。

次はテーブルにデータを挿入する為のSeederを作成します。

Seederの作成

下記のコマンドを叩きます。

bin/cake bake seed Tags

そしてSeederファイルの中身を下記にします。

<?php
declare(strict_types=1);

use Migrations\AbstractSeed;

/**
 * Tags seed.
 */
class TagsSeed extends AbstractSeed
{
    /**
     * Run Method.
     *
     * Write your database seeder using this method.
     *
     * More information on writing seeds is available here:
     * https://book.cakephp.org/phinx/0/en/seeding.html
     *
     * @return void
     */
    public function run(): void
    {
        $data = [
            [
                'title' => 'タグ1',
                'created' => '2020-05-02 10:00:00',
                'modified' => '2020-05-02 10:00:00',
            ],
            [
                'title' => 'タグ2',
                'created' => '2020-05-03 10:00:00',
                'modified' => '2020-05-03 10:00:00',
            ],
            [
                'title' => 'タグ3',
                'created' => '2020-05-04 10:00:00',
                'modified' => '2020-05-04 10:00:00',
            ],
        ];

        $table = $this->table('tags');
        $table->insert($data)->save();

        $data = [
            [
                'post_id' => 1,
                'tag_id' => 1,
            ],
            [
                'post_id' => 1,
                'tag_id' => 2,
            ],
            [
                'post_id' => 2,
                'tag_id' => 3,
            ],
            [
                'post_id' => 3,
                'tag_id' =>1,
            ],
        ];

        $table = $this->table('posts_tags');
        $table->insert($data)->save();
    }
}

41行目〜64行目は中間テーブル用のデータです。

記述が終わったら下記のコマンドを叩きます。

bin/cake migrations seed --seed TagsSeed

これでSeederの情報がテーブルに追加されました。

次はモデルを作成します。

モデルの作成

下記のコマンドを叩きます。

CakeBlog3 % bin/cake bake model tags

そしてEntityフォルダの下の階層にあるPost.phpを修正します。

protected $_accessible = [
  'title' => true,
  'description' => true,
  'body' => true,
  'published' => true,
  'user_id' => true,
  'tags' => true,                //この行を追加
  'created' => true,
  'modified' => true,
];

そしてTableフォルダの下の階層にあるPostsTable.phpを修正します。

public function initialize(array $config): void
{
  parent::initialize($config);

  $this->setTable('posts');
  $this->setDisplayField('title');
  $this->setPrimaryKey('id');

  $this->addBehavior('Timestamp');

  $this->belongsTo('Users');
  
  
  //ここから追加
  $this->belongsToMany('Tags', [
    'foreignKey' => 'post_id',         
    'targetForeignKey' => 'tag_id',    
    'joinTable' => 'posts_tags'      
  ]);
  //ここまで追加
  
  
}

15行目〜19行目ですが第2引数に連想配列の形でキーの指定をします。

16行目は自分のキーで17行目は相手のキーで18行目は中間テーブル名です。

これでモデルの設定が終わったので次はコントローラーの作成です。

コントローラーとビューファイルの作成

「src > Controller」の下の階層にTagsController.phpを作成して中身を下記にします。

<?php
declare(strict_types=1);

namespace App\Controller;

/**
 * Posts Controller
 *
 * @method \App\Model\Entity\Post[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class TagsController extends AppController
{
    /**
     * Index method
     *
     * @return \Cake\Http\Response|null|void Renders view
     */
    public function index()
    {
        $this->paginate = [
            'limit' => 30,
            'order' => [
                'created' => 'desc'
            ],
        ];

        $tags = $this->paginate($this->Tags->find());

        $this->set(compact('tags'));
    }

    /**
     * View method
     *
     * @param string|null $id Post id.
     * @return \Cake\Http\Response|null|void Renders view
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function view($id = null)
    {
        $post = $this->Posts->get($id, [
            'contain' => 'Users'
        ]);

        $this->set(compact('post'));
    }

    /**
     * Add method
     *
     * @return \Cake\Http\Response|null|void Redirects on successful add, renders view otherwise.
     */
    public function add()
    {
        $post = $this->Posts->newEmptyEntity();
        if ($this->request->is('post')) {
            $post = $this->Posts->patchEntity($post, $this->request->getData());
            if ($this->Posts->save($post)) {
                $this->Flash->success(__('The post has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The post could not be saved. Please, try again.'));
            $this->log(print_r($article->errors(),true),LOG_DEBUG);
        }
        $this->set(compact('post'));
    }

    /**
     * Edit method
     *
     * @param string|null $id Post id.
     * @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function edit($id = null)
    {
        $post = $this->Posts->get($id, [
            'contain' => [],
        ]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $post = $this->Posts->patchEntity($post, $this->request->getData());
            if ($this->Posts->save($post)) {
                $this->Flash->success(__('The post has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The post could not be saved. Please, try again.'));
        }
        $this->set(compact('post'));
    }

    /**
     * Delete method
     *
     * @param string|null $id Post id.
     * @return \Cake\Http\Response|null|void Redirects to index.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function delete($id = null)
    {
        $this->request->allowMethod(['post', 'delete']);
        $post = $this->Posts->get($id);
        if ($this->Posts->delete($post)) {
            $this->Flash->success(__('The post has been deleted.'));
        } else {
            $this->Flash->error(__('The post could not be deleted. Please, try again.'));
        }

        return $this->redirect(['action' => 'index']);
    }
}

templatesフォルダの下の階層にTagsフォルダを作成してPostsフォルダの中にあるindex.phpとview.phpをコピーしてTagsフォルダの中に設置します。

Tagsフォルダの中にあるindex.phpを下記に変更します。

<div class="content">
    <ul>
        <?php foreach($tags as $tag) :?>
            <li>
                <time><?= $tag->created->i18nFormat('YYYY年MM月dd日 HH:mm') ?></time>
                <?= $this->Html->link($tag->title, ['controller' => 'tags', 'action' => 'view', $tag->id]) ?>
            </li>
        <?php endforeach ?>
    </ul>

    <?php if($this->Paginator->total() > 1) :?>
        <div class="paginator">
            <ul class="pagination">
                <?= $this->Paginator->first('<< 最初') ?>
                <?= $this->Paginator->prev('< 前へ') ?>
                <?= $this->Paginator->numbers() ?>
                <?= $this->Paginator->next('次へ >') ?>
                <?= $this->Paginator->last('最後 >>') ?>
            </ul>
        </div>
    <?php endif ?>
</div>

そして「http://localhost:8888/cakephp/CakeBlog1/tags」にアクセスすると下記の表示になります。

次は詳細ページを作成します、タグに関連した記事の内容を表示します。

TagsController.phpのviewメソッドを下記にします。

public function view($id = null)
{
  $tag = $this->Tags->get($id, [
    'contain' => 'Posts'
  ]);

  $this->paginate = [
    'limit' => 10,
    'order' => [
      'Posts.created' => 'desc'
    ],
    'contain' => ['Users', 'Tags']
  ];

  $posts = $this->Posts->find()->matching('Tags', function(Query $q) use($id){
    return $q->where(['Tags.id' => $id]);
  });
  
  $posts = $this->paginate($posts);           

  $this->set(compact('tag', 'posts'));
}

15行目〜17行目で記事の絞り込みをしています。

15行目の「matching(‘Tags〜」の「Tags」が絞り込みに使うモデル名で「function」の中が検索条件です。

15行目の「use($id)」でPostsテーブルのidカラムを「matching」の中で使うことができるようにして16行目の「’Tags.id’ => $id」でPostsテーブルのidカラムとTagsテーブルのidカラムが一致するデータを取得しています。

ただ、TagsController.phpではPostモデルが使えないのでTagsController.phpでPostモデルを使うことができるようにします。

TagsController.phpに追記します。

<?php
declare(strict_types=1);

namespace App\Controller;

use Cake\ORM\Query;              //この行を追加

/**
 * Posts Controller
 *
 * @method \App\Model\Entity\Post[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class TagsController extends AppController
{


    //ここから追加
    public $Posts = null;

    public function initialize(): void{
        parent::initialize();

        $this->loadModel('Posts');
    }
    //ここまで追加
    
    

18行目〜24行目でPostモデルが使えるようになります。

「’contain’ => [‘Users’, ‘Tags’]」に「Users」があるのでUserモデルを使っていますが「$this->set(compact(‘tag’, ‘posts’));」の所でUserモデルを使っていないので「$this->loadModel(‘Users’)」は必要ありません。

そしてTagsフォルダの下の階層にあるview.phpを下記に変更します。

<div class="content">
    <h1>「<?= h($tag->title) ?>」の投稿一覧</h1>
    <?php foreach($posts as $post) :?>
        <h3><?= h($post->title) ?></h3>
        <p><?= $post->created->i18nFormat('YYYY年MM月dd日 HH:mm') ?></p>
        <p><?= h($post->description) ?></p>
        <p><small>投稿者 : <?= h($post->user->username) ?></small></p>
        <?= $this->Html->link('記事を読む', ['controller' => 'Posts', 'action' => 'view', $post->id], ['class' => 'button']) ?>
        <hr>
    <?php endforeach ?>
    <?= $this->Html->link('一覧へ戻る', ['action' => 'index'], ['class' => 'button']) ?>
</div>

そしてタグの詳細を見ると下記の警告が表示されます。

警告内容は下記になります。

Deprecated: Passing query options as paginator settings is deprecated. Use a custom finder through `finder` config instead. Extra keys found are: contain /Applications/MAMP/htdocs/cakephp/CakeBlog3/vendor/cakephp/cakephp/src/Datasource/Paging/NumericPaginator.php, line: 189 You can disable all deprecation warnings by setting `Error.errorLevel` to `E_ALL & ~E_USER_DEPRECATED`. Adding `vendor/cakephp/cakephp/src/Datasource/Paging/NumericPaginator.php` to `Error.ignoredDeprecationPaths` in your `config/app.php` config will mute deprecations from that file only. in /Applications/MAMP/htdocs/cakephp/CakeBlog3/vendor/cakephp/cakephp/src/Core/functions.php on line 318

ネットで調べても解決方法を見つけることができなかったのですが ChatGTPに質問してとりあえず対処に近い解決方法を見つけました。

「CakePHPのプロジェクト > config > app.php」の「Error」の項目に追記すればいいです。

'Error' => [
  'errorLevel' => E_ALL,
  'exceptionRenderer' => ExceptionRenderer::class,
  'skipLog' => [],
  'log' => true,
  'trace' => true,
  
  
  //ここから追加
  'ignoredDeprecationPaths' => [
      'vendor/cakephp/cakephp/src/Datasource/Paging/NumericPaginator.php'
  ],
  //ここまで追加
  
  
]

これで警告はなくなります。