CakePHP3.0のブックマークチュートリアル2

さて、今日は、ブックマークチュートリアルの続きを勉強してみたいと思います。

チュートリアル1では、下記を作成しました。

  • データベースの作成
  • Bakeでスキャフォールドを作成
  • パスワードハッシャーの作成
  • 特定のタグを使用してブックマークの取得

前回作成した、ブックマークは、他人が作成したブックマークも修正、削除することができました。今回は、自分が所有するブックマークだけを修正、削除できるようにアクセス制限をします。

ログイン機能の追加

CakePHPは、Authコンポーネントによって認証処理が行われます。コンポーネントは、コントローラのイベントのライフサイクルの中にフックし、アプリケーションと対話することができます。

Authコンポーネントを、Bookmarkアプリケーションの全体で使用するために、AppControllerのinitialize()メソッドに、追加します。

src/Controller/ApppController.php

<?php
namespace App\Controller;
use Cake\Controller\Controller;
class AppController extends Controller
{
public function initialize()
 {
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
 'authenticate' => [
 'Form' => [
 'fields' => [
 'username' => 'email',
 'password' => 'password'
 ]
 ]
 ],
 'loginAction' => [
 'controller' => 'Users',
 'action' => 'login'
 ]
 ]);

$this->Auth->allow(['display']);
 }

8行目:フラッシュコンポーネントをロードします。

9-22行目:Authコンポーネントをロードします。

10-17行目:フォームのメールアドレスとパスワードで、認証処理をします。

18-21行目:ログインアクションは、Usersコントローラのloginアクションで処理します。

次に、上記で指定したUsersコントローラで、ログインアクションとログアウトアクションを作成します。

src/Controller/UsersController.php

//ログインアクションの追加
 public function login()
 {
 if ($this->request->is('post')) {
 $user = $this->Auth->identify();
 if ($user) {
 $this->Auth->setUser($user);
 return $this->redirect($this->Auth->redirectUrl());
 }
 $this->Flash->error('ユーザー名かパスワードが違います');
 }
 }
 //ログアウトアクションの追加
 public function logout()
 {
 $this->Flash->success('ログアウトしました');
 return $this->redirect($this->Auth->logout());
 }

ログインするためのフォームを下記の様に作成します。

src/Template/Users/login.ctp

<h1>Login</h1>
 <?= $this->Form->create() ?>
 <?= $this->Form->input('email') ?>
 <?= $this->Form->input('password') ?>
 <?= $this->Form->button('Login') ?>
 <?= $this->Form->end() ?>

ログインの例外処理

上記の様にログイン機能をBookmarkerアプリケーション全体に、設定しましたので、新規ユーザーを作成しようとしても、ログインページへ移動してしまい、新規ユーザーを作成することができません。

そこで、新規ユーザーの作成は、ログインしなくてもできるように、ログインの例外処理をUsersコントローラに追加します。

src/Controller/UsersController.php

//ログインの例外処理
 public function beforeFilter(\Cake\Event\Event $event)
 {
 $this->Auth->allow(['add']);
 }

Authコンポーネントで、addアクションに関しては、認証していなくても、許可するように設定しています。

ブックマークアクセスの制限

上記では、登録しているユーザーのみが、Bookmarkerアプリケーションにアクセスできるようになりました(※UsersControllerのaddアクションを除く)。

しかし、現状では、登録しているユーザーならば、他のユーザーのブックマークにもアクセスでき、修正や削除を行うことができるので、ブックマークにアクセス制限をしたいと思います。

我々の要求は、非常に単純なので、BookmarssControllerにいくつかの簡単なコードを記述できます。

その前に、アプリケーションが、どのようにアクションを認証するのかをAuthComponentに伝えたいと思います。

そこで、AppControllerに下記を追加します。

src/Controller/AppController.php

public function isAuthorized($user)
{
 return false;
}

そして、同じくAppControllerのinitialize()メソッドを下記の様に修正します。

public function initialize()
{
 $this->loadComponent('Flash');
 $this->loadComponent('Auth', [
 'authorize'=> 'Controller', //追加
 'authenticate' => [
 'Form' => [
 'fields' => [
 'username' => 'email',
 'password' => 'password'
 ]
 ]
 ],
 'loginAction' => [
 'controller' => 'Users',
 'action' => 'login'
 ],
 'unauthorizedRedirect' => $this->referer() //追加
 ]);

 $this->Auth->allow(['display']);
}

次に、Bookmarksコントローラに下記のisAuthorized()メソッドを追加します。

src/Controller/BookmarksController.php

public function isAuthorized($user)
 {
 $action = $this->request->params['action'];
// 常時許可するアクション
 if (in_array($action, ['index', 'add', 'tags'])) {
 return true;
 }
 // 上記以外のアクションはIDを必要とします.
 if (empty($this->request->params['pass'][0])) {
 return false;
 }
// ブックマークが現在のユーザーに所属しているかをチェックします
 $id = $this->request->params['pass'][0];
 $bookmark = $this->Bookmarks->get($id);
 if ($bookmark->user_id == $user['id']) {
 return true;
 }
 return parent::isAuthorized($user);
 }

そして、許可されていないアクションを実行しようとした場合、元のページへリダイレクトして、エラーメッセージを表示するようにdefaultビューの適当な個所に下記を追加します。

src/Template/Layout/default.ctp

<?= $this->Flash->render('auth') ?>

リストビューとフォームの修正

上記には、まだ以下のような問題があります。

  1. ブックマークを追加するときに、ユーザーを選択することができる
  2. ブックマークを編集するときに、ユーザーを選択することができる
  3. リストページに、他のユーザーからのブックマークを表示している

それでは、まず最初にBookmarksコントローラのaddアクションを修正します。

src/Controller/BookmarksController.php

public function add()
{
 $bookmark = $this->Bookmarks->newEntity();
 if ($this->request->is('post')) {
 $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->data);
 //下記の1行追加(ブックマークのuser_idに現在のアクセスユーザーidを代入)
 $bookmark->user_id = $this->Auth->user('id');
 if ($this->Bookmarks->save($bookmark)) {
 $this->Flash->success('ブックマークは保存されました');
 return $this->redirect(['action' => 'index']);
 } else {
 $this->Flash->error('ブックマークは保存されません。もう一度やり直してください。');
 }
 }
 //下記の2行を修正
 $tags = $this->Bookmarks->Tags->find('list');
 $this->set(compact('bookmark', 'tags'));
}

Bookmarksコントローラのeditアクションを下記の様に修正します。

src/Controller/BookmarksController.php

public function edit($id = null)
 {
 $bookmark = $this->Bookmarks->get($id, [
 'contain' => ['Tags']
 ]);
 if ($this->request->is(['patch', 'post', 'put'])) {
 $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->data);
 //下記の1行追(ブックマークのuser_idに現在のアクセスユーザーidを代入)
 $bookmark->user_id = $this->Auth->user('id');
 if ($this->Bookmarks->save($bookmark)) {
 $this->Flash->success('ブックマークは保存されました');
 return $this->redirect(['action' => 'index']);
 } else {
 $this->Flash->error('ブックマークは保存されません。もう一度やり直してください。');
 }
 }
 //下記の2行を修正
 $tags = $this->Bookmarks->Tags->find('list');
 $this->set(compact('bookmark', 'tags'));
 }

現在ログインしているユーザーのブックマークを表示するために、Bookmarksコントローラのindexアクションを下記の様に修正します。

src/Controller/BookmarksController.php

public function index()
 {
 $this -> paginate = [
 'conditions' => [
 'Bookmarks.user_id' => $this -> Auth -> user ( 'id' ),
 ]
 ];
 $this -> set ( 'bookmarks' , $this -> paginate ( $this -> Bookmarks ));
 }

タグ付けの経験を改善

今、TagsControllerは、すべてのアクセスを禁止していますので、新しいタグは追加できません。代わりに、アクセスを許可するのは、カンマ区切りのテキストフィールドを使用することで、タグ選択UIを改善することができます。これは、私たちのユーザーに、より良い経験を与え、ORMでいくつかのより多くの素晴らしい機能を使用します。

計算フィールドの追加

実体のためにフォーマットされたタグにアクセスする単純な方法が欲しいので、我々は仮想的な/計算されたフィールドを実体に加えることができます。

Bookmarskモデルに下記を追加します。

src/Model/Entity/Bookmarks.php

<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\Collection\Collection; //追加

 class Bookmark extends Entity
 {
 //追加
 protected function _getTagString()
 {
 if (isset($this->_properties['tag_string'])) {
 return $this->_properties['tag_string'];
 }
 if (empty($this->tags)) {
 return '';
 }
 $tags = new Collection($this->tags);
 $str = $tags->reduce(function ($string, $tag) {
 return $string . $tag->title . ', ';
 }, '');
 return trim($str, ', ');
 }

 protected $_accessible = [
 'user_id' => true,
 'title' => true,
 'description' => true,
 'url' => true,
 'user' => true,
 'tags' => true,
 'tag_string' => true, //追加
 ];
 }

これは、我々にその算出されたプロパティの$bookmark->tag_stringにアクセスするための簡単な方法でしょう。我々は、後々、入力でこのプロパティを使用します。我々が後ほどそれを『保存』したいように、あなたのEntityモデルでtag_stringプロパティを_accessibleリストに加えるのを忘れないでください。

ビューの更新

ビューファイルのadd.ctpとedit.ctpの入力フォーム部分を下記の様に修正します。

src/Template/Bookmarks/add.ctp

<?php
 echo $this->Form->input('user_id', ['options' => $users]); //削除
 echo $this->Form->input('title');
 echo $this->Form->input('description');
 echo $this->Form->input('url');
 echo $this->Form->input('tag_string', ['type' => 'text']); //修正
 ?>

2行目を削除して、6行目を修正します。

6行目は、セレクトタグから、inputタグに修正しています。

src/Template/Bookmarks/edit.ctp

<?php
 echo $this->Form->input('user_id', ['options' => $users]); //削除
 echo $this->Form->input('title');
 echo $this->Form->input('description');
 echo $this->Form->input('url');
 echo $this->Form->input('tag_string', ['type' => 'text']); //修正
 ?>

上記と同様、2行目を削除して、6行目を修正します。

タグ文字列の永続化

私たちは既存のタグを文字列として見ることができる今、そのデータも保存したいと思うでしょう。なぜなら、我々はそのtag_stringをアクセス可能なようにマークしたので、ORMは私たちの実体にそのデータをリクエストからコピーします。我々はタグ文字列と関連エンティティのfind/buildを解析し、検索するためのbeforeSave()フックメソッドを使用することができます。

BookmarksTableモデルに以下を追加します。

src/Model/Table/ BookmarksTable.php

public function findTagged(Query $query, array $options)
 {
 $fields = [
 'Bookmarks.id',
 'Bookmarks.title',
 'Bookmarks.url',
 ];
 return $this->find()
 ->distinct($fields)
 ->matching('Tags', function ($q) use ($options) {
 return $q->where(['Tags.title IN' => $options['tags']]);
 });
 }
 //追加
 public function beforeSave($event, $entity, $options)
 {
 if ($entity->tag_string) {
 $entity->tags = $this->_buildTags($entity->tag_string);
 }
 }
protected function _buildTags($tagString)
 {
 $new = array_unique(array_map('trim', explode(',', $tagString)));
 $out = [];
 $query = $this->Tags->find()
 ->where(['Tags.title IN' => $new]);
// 新しいタグリストから既存のタグを除外
 foreach ($query->extract('title') as $existing) {
 $index = array_search($existing, $new);
 if ($index !== false) {
 unset($new[$index]);
 }
 }
 // 既存のタグを追加
 foreach ($query as $tag) {
 $out[] = $tag;
 }
 // 新しいタグを追加
 foreach ($new as $tag) {
 $out[] = $this->Tags->newEntity(['title' => $tag]);
 }
 return $out;
 }

今日は、ここまでです。

このエントリーを含むはてなブックマーク Buzzurlにブックマーク livedoorクリップ Yahoo!ブックマークに登録

トラックバック&コメント

この投稿のトラックバックURL:

コメントをどうぞ

このページの先頭へ