$yuzu->log();

技術ネタなど。

【チュートリアル】最新フレームワークCakePHP3でブックマーカーを作ってみる その2

yuzurus.hatenablog.jp

前回の続きになります。

ログイン機能の実装

ログイン機能はCakePHP2同様AuthComponentを利用します。

AppContorollerに追加しましょう

// In src/Controller/AppController.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'
            ]
        ]);
        
        // Allow the display action so our pages controller
        // continues to work.
        $this->Auth->allow(['display']);
    }
}

ここではflashとauthコンポーネントをロードしています。

usernameにemail,passwordにpasswordを使用します。このへんはCakePHP2と同様ですね。

現在ログインしていないので、どのページにアクセスしても/users/loginにリダイレクトされます。

では/users/loginページを実装しましょう。

// In 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('Your username or password is incorrect.');
    }
}
// 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() ?>

これでログイン機能ができました。 f:id:yuzurus:20150609230203p:plain

ログアウト機能の実装

UsersController.phpにlogoutメソッドを追加します。

public function logout()
{
    $this->Flash->success('You are now logged out.');
    return $this->redirect($this->Auth->logout());
}

ここもCakePHP2と同様ですね。

ユーザ登録の有効化

現在ユーザ登録しようとしてもログイン画面へリダイレクトされるので、それをさせないようにします。

// In src/Controller/UsersController.php
public function beforeFilter(\Cake\Event\Event $event)
{
    $this->Auth->allow(['add']);
}

addメソッドが認証を必要としないことを示します。

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

現在ログインしさえすればすべてのユーザのブックマークにアクセスできてしまします。

ですので、その人が登録したブックマークのみアクセス出来るようにしましょう。

authorizationアダプターを使用します。

// In src/Controller/AppController.php
public function initialize()
{
    $this->loadComponent('Flash');
    $this->loadComponent('Auth', [
        'authorize'=> 'Controller',//added this line
        'authenticate' => [
            'Form' => [
                'fields' => [
                    'username' => 'email',
                    'password' => 'password'
                ]
            ]
        ],
        'loginAction' => [
            'controller' => 'Users',
            'action' => 'login'
        ],
        'unauthorizedRedirect' => $this->referer()
    ]);
 
    // Allow the display action so our pages controller
    // continues to work.
    $this->Auth->allow(['display']);
}

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

基本はアクセス禁止で、ホワイトリスト方式が良いでしょう。

// In src/Controller/BookmarksController.php
public function isAuthorized($user)
{
    $action = $this->request->params['action'];
 
    // The add and index actions are always allowed.
    if (in_array($action, ['index', 'add', 'tags'])) {
        return true;
    }
    // All other actions require an id.
    if (empty($this->request->params['pass'][0])) {
        return false;
    }
 
    // Check that the bookmark belongs to the current user.
    $id = $this->request->params['pass'][0];
    $bookmark = $this->Bookmarks->get($id);
    if ($bookmark->user_id == $user['id']) {
        return true;
    }
    return parent::isAuthorized($user);
}

これで表示、編集、削除しても元のページに戻ります。 エラーページも用意しましょう

// In src/Template/Layout/default.ctp
// Under the existing flash message.
<?= $this->Flash->render('auth') ?>

一覧ページとフォームの修正

追加機能と一覧ページには以下の問題があります。 1. ブックマークを追加や編集する場合、ユーザを選択出来てしまう 2. 一覧ページでは他のユーザのブックマークまで閲覧できてしまう

これらを修正していきます。

フォームの修正

Bookmarks/add.ctpから input('user_id') を削除してユーザIDを編集できないようにしましょう。

またBookmarksController.phpをaddメソッドとeditメソッドを以下のように修正しましょう。

public function add()
{
    $bookmark = $this->Bookmarks->newEntity();
    if ($this->request->is('post')) {
        $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->data);
        $bookmark->user_id = $this->Auth->user('id');
        if ($this->Bookmarks->save($bookmark)) {
            $this->Flash->success('The bookmark has been saved.');
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error('The bookmark could not be saved. Please, try again.');
    }
    $tags = $this->Bookmarks->Tags->find('list');
    $this->set(compact('bookmark', 'tags'));
}
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);
        $bookmark->user_id = $this->Auth->user('id');
        if ($this->Bookmarks->save($bookmark)) {
            $this->Flash->success('The bookmark has been saved.');
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error('The bookmark could not be saved. Please, try again.');
    }
    $tags = $this->Bookmarks->Tags->find('list');
    $this->set(compact('bookmark', 'tags'));
}

一覧画面の修正

続いて一覧画面でログインしているユーザーのブックマークだけを表示するようにします。 pagenate()に条件を加えて以下の様に修正してください。

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

タグ機能の修正

現状ではTagsControllerがすべてのアクセスを拒否するため、新しいタグの登録ができません。 アクセスを許可する代わりに、区切りのテキストで入力するUIを開発しましょう。 これにより良いユーザユーザーエクスペリエンスを手に入れられるだけでなく、ORMのさらなる機能を使ってみることができます。

処理済フィールドの追加

エンティティへのアクセスをシンプルな方法にするために、エンティティに仮想的で処理済なフィールドを追加しましょう。src/Model/Entity/Bookmark.phpに以下を追加してください。

use Cake\Collection\Collection;

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, ', ');
}

これは私たちが$bookmark->tag_stringとすれば処理済のプロパティにアクセスすることを可能にします。 後で、このプロパティをフォームのinputで使います。 また保存に必要なのでtag_stringプロパティをエンティティの_accessibleリストに追加してください。

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

ビューの更新

エンティティが更新されたので、新しいinput要素を追加することができます。add、editの、既存のtags._idsinputを以下のように変更してください。

<?= $this->Form->input('tag_string', ['type' => 'text']) ?>

タグ文字列の永続化

既存のタグを文字列として表示できるようになりました。同様にタグ文字列を保存できるようにしてみましょう。

tag_stringを$_accessibleに追加したので、ORMはこのデータをリクエストからエンティティにコピーします。

beforeSave()フックメソッドを使用して、エンティティに対してタグ文字列のパースとタグリストの検索/追加ができます。以下のコードをBookmarksTable.phpに追加してください。

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]);

    // Remove existing tags from the list of new tags.
    foreach ($query->extract('title') as $existing) {
        $index = array_search($existing, $new);
        if ($index !== false) {
            unset($new[$index]);
        }
    }
    // Add existing tags.
    foreach ($query as $tag) {
        $out[] = $tag;
    }
    // Add new tags.
    foreach ($new as $tag) {
        $out[] = $this->Tags->newEntity(['title' => $tag]);
    }
    return $out;
}

まとめ

ブックマークアプリケーションにおいて、簡単なログイン認証、アクセスコントロールなどを実装できました。 これを機会にCakePHP3に触れる人が増えてくればなによりです。