はじめに
僕がプログラマ駆け出しの時に必死で書いた奴です。自分用メモです。
Qiitaの下書きがいっぱいになってしまったので仕方なく公開します。。
cakePHP3系です。
独自の機能
ログアウト画面の実装
navbarがlogin、logout時で変わるようにした
logoutの転移先変更(ログイン画面からホームへ)
チュートリアル通りにやったのに何故かバグが多かったのでかなりデバッグした
気付いた暗黙の機能
Controller::render() メソッドは各アクションの最後に自動的に呼ばれる。
もっと言えばアクションと同じ名前のviewを勝手に描画する。
https://book.cakephp.org/3.0/ja/controllers.html#id7
HTTP requestを受けた時の処理
ルーターが対応するコントローラーに処理を渡す。
Router::scope('/', function (RouteBuilder $routes) {
///使用したいミドルウェアを登録して
$routes->registerMiddleware('csrf', new CsrfProtectionMiddleware([
'httpOnly' => true
]));
///適用する
$routes->applyMiddleware('csrf');
///「http://myapp.jp/」(ルート)にアクセスしたらpageコントローラーのdisplayメソッドを呼び出す。第三引数の「home」は表示させたいview(templateファイル)の名前をコントローラーのメソッドに引数として渡している。
$routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
}
疑問:使うミドルウェアを変えたい時以外はこのスコープの中に書き込んでいくので良いのか?
一応routesには、
* If you need a different set of middleware or none at all,
* open new scope and define routes there.
と書かれている。
public function index()
{ ///Usersの情報とアソシエーションを追加するというオプションをインスタンス変数として設定している。(多分users情報も一緒に持たせてる感じだと思う)
$this->paginate = [
'contain' => ['Users']
];
///paginateメソッドは上で設定したオプションをもとにしつつページ分けされた検索結果を返す。
$articles = $this->paginate($this->Articles);
///$articlesをviewに渡す。
$this->set(compact('articles'));
}
この$this->Articlesがモデルで
EntityとTableがあってDBと直接やり取りをしてバリデーションとかまでしちゃうのがTableそれ以外(データの整形とか?)をEntityが引き受ける。
各コントローラーは同名のモデルをインスタンス変数として持っているらしい。
なので
$this->モデル
$this->モデル->メソッド
$this->モデル->プロパティ
でコントローラー内でモデル自身やモデルが持っているメソッド、プロパティも使えるということなのか。
paginateメソッドは色々引数にできるよ
https://book.cakephp.org/3.0/ja/controllers/components/pagination.html
それで表示されるviewがこちら↓
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\Article[]|\Cake\Collection\CollectionInterface $articles
*/
///部分的な抜粋です
<?php foreach ($articles as $article): ?>
<tr>
<td><?= $this->Number->format($article->id) ?></td>
///hasメソッドはプロパティが定義されててnullじゃなかったらtrue
<td><?= $article->has('user') ? $this->Html->link($article->user->id, ['controller' => 'Users', 'action' => 'view', $article->user->id]) : '' ?></td>
<td><?= h($article->title) ?></td>
<td><?= h($article->slug) ?></td>
<td><?= h($article->published) ?></td>
<td><?= h($article->created) ?></td>
<td><?= h($article->modified) ?></td>
<td class="actions">
///value'view'で/articles/view/idというhrefのaタグが生成される
<?= $this->Html->link(__('View'), ['action' => 'view', $article->id]) ?>
<?= $this->Html->link(__('Edit'), ['action' => 'edit', $article->id]) ?>
///このpostLinkを使わないとブラウザでjsが使えないらしい。(第三引数がjsのっぽい)
<?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $article->id], ['confirm' => __('Are you sure you want to delete # {0}?', $article->id)]) ?>
</td>
</tr>
<?php endforeach; ?>
$thisにはViewを継承してるAppViewが入っていてヘルパ関数を使えるようにしてくれてるみたい。
ほんで(__)というのがなにかわからん。。
あとコントローラーで紐づけたUsersはやっぱりUsersのモデルが入ってたみたいで、hasでそれがarticlesと紐づいてるか確認して紐づいてたらarticles->user->idでプロパティを参照してる。わざわざ頑張ってDB上の二つのテーブルにアクセスするクエリをそれぞれ書いて2つのモデルそれぞれを単体で処理しなくても、1つのモデルにもう1つのモデルを紐づけることでデータの扱いを楽にしているというわけか。
これなんかまさにいい例
public function edit($slug)
{
$article = $this->Articles
->findBySlug($slug)
->contain('Tags') // 関連づけられた Tags を読み込む
->firstOrFail();
if ($this->request->is(['post', 'put'])) {
$this->Articles->patchEntity($article, $this->request->getData());
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been updated.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to update your article.'));
}
// listなので配列形式でタグデータを取得してる
$tags = $this->Articles->Tags->find('list');
// ビューコンテキストに tags をセット
$this->set('tags', $tags);
$this->set('article', $article);
}
が、これはわざわざcontainしている。ほかの方法として
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('articles');
$this->setDisplayField('title');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
'joinType' => 'INNER'
]);
$this->belongsToMany('Tags', [
'foreignKey' => 'article_id',
'targetForeignKey' => 'tag_id',
'joinTable' => 'articles_tags'
]);
}
このようにtableですでに関連付けていると、なんと!
containしなくても
public function edit($slug = null)
{
///こういう検索結果に関連づいたデータが欲しい時はcontainしないとだめ。
$article = $this->Articles->findBySlug($slug)->contain('Tags')->firstOrFail();
if ($this->request->is(['patch', 'post', 'put'])) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
if ($this->Articles->save($article)) {
$this->Flash->success(__('The article has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The article could not be saved. Please, try again.'));
}
///articleにユーザーが紐づけられてる!!
$users = $this->Articles->Users->find('list', ['limit' => 200]);
$tags = $this->Articles->Tags->find('list', ['limit' => 200]);
$this->set(compact('article', 'users', 'tags'));
}
ということです。
全部取ってくるときはcontainしなくてもいいけど検索して取ってくるときはしないとだめだよ!!!
ただこれだとarticleのデータ単体で取りに行ったときにも無条件で毎回ユーザーとタグがくっついて来ちゃうんじゃないかという疑問。まぁ実際単体で取りに行くことなんてないか。どうなんだろう。そんなことないか。
あとは、これが面白くて
$routes->connect('/articles/tagged/*',[
'controller' => 'Articles',
'action' => 'Tags'
]);
ってルート定義すると「*」の部分がTagsメソッドの引数に送られるので、
public function tags()
{
$tags = $this->request->getParam('pass');
// ArticlesTable を使用してタグ付きの記事を検索します。
$articles = $this->Articles->find('tagged', [
'tags' => $tags
]);
// 変数をビューテンプレートのコンテキストに渡します。
$this->set(compact('articles','tags'));
}
これが
public function tags(...$tags)
{
$articles = $this->Articles->find('tagged', [
'tags' => $tags
]);
$this->set(compact('articles','tags'));
}
こうです。
あと、
find()に'tagged'というなぞの使い方をしてる。
これはそのモデル(table)で定義している(もちろん自分で定義する)独自のfinderメソッドを使っている。
public function findTagged(Query $query, array $options)
{
$columns = [
'Articles.id', 'Articles.user_id', 'Articles.title',
'Articles.body', 'Articles.published', 'Articles.created',
'Articles.slug',
];
$query = $query
->select($columns)
->distinct($columns);
if (empty($options['tags'])) {
// タグが指定されていない場合は、タグのない記事を検索します。
$query->leftJoinWith('Tags')
->where(['Tags.title IS' => null]);
} else {
// 提供されたタグが1つ以上ある記事を検索します。
$query->innerJoinWith('Tags')
->where(['Tags.title IN' => $options['tags']]);
}
return $query->group(['Articles.id']);
}
こんな感じ。findMethodって感じで定義すればその名前でつかえるようだ。
クエリオブジェクトは多分色々メソッドとかあるから都度調べよう。。
ほんで認証はコンポーネントで行う。コンポーネントはまぁ便利パッケージみたいなかんじか。
この辺はまた、コンポーネントごとに色んなオプションとかメソッドとかあるだろうから、省略したいけどざっくりやる。
認証コンポーネントはアプリケーション全体(全MVC)に適用させたいからAppControllerにloadComponentする。
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'fields' => [
'username' => 'email',
'password' => 'password'
]
]
],
'loginAction' => [
'controller' => 'Users',
'action' => 'login'
],
// コントローラーで isAuthorized を使用します
'authorize' => ['Controller'],
// 未認証の場合、直前のページに戻します
'unauthorizedRedirect' => $this->referer()
]);
// display アクションを許可して、PagesController が引き続き
// 動作するようにします。また、読み取り専用のアクションを有効にします。
$this->Auth->allow(['display', 'view', 'index']);
多分ここで大事なのはallow。ここで設定したということは全コントローラーのdisplay,view,indexアクションが未承認のユーザーに許可されるということ。それ以外のアクションを未承認で実行しようとした場合は前のページに飛ばされる。
public function initialize()
{
parent::initialize();
$this->Auth->allow(['logout','add']);
}
public function logout()
{
$this->Flash->success('ログアウトしました。');
return $this->redirect($this->Auth->logout());
}
特定のコントローラーの特定のアクションだけ許可したい場合はこのようにそのコントローラーでallowするっぽい。
この場合はユーザー登録ができるようにaddもいれてる。
はい、またハマった点。
initializeでAuthコンポーネントを使う場合、親であるAppControllerのinitializeでコンポーネントを読み込んでいるということは子であるArticlesControllerのinitializeで親のinitializeを呼び出してあげないと使えないよねという話。
他のメソッドないならなくてもいけるっぽいがinitializeはないとだめっぽい。
public function initialize()
{
///これがないとエラー
parent::initialize();
$this->Auth->allow(['tags']);
}
基本的にはコンポーネントを使って色々何とかできないか探っていくのが良いと思う。
で、アプリ全体に適用したいときはAppControllerでロードするとか。