ブログ開発 

CakePHPの学習を目的に、一からブログアプリケーションを開発します。 開発するブログの機能を、実際にはてなブログというブログサービスを使用することを通して考えました。

優先度: 高

1. 記事の一覧

記事の一覧表示機能(画面)。

2. 記事の閲覧

記事の内容を閲覧する機能(画面)。

3. 記事の投稿

記事の投稿を行う機能(画面)。

4. 記事の編集

記事の編集を行う機能(画面)。

5. 記事の削除

記事の削除を行う機能。

6. 管理者ログイン機能

管理者ログイン機能を設け、ログイン状態でのみ、記事の投稿・編集・削除を行えないように制限する。

7. 画像の掲載

記事内にローカルの画像を掲載できるように、画像のアップロード機能を設ける。 (アップロード先は imgur などの無料サービスを使用することを想定。)

8. マークダウン対応

記事をマークダウンで記載できるように、対応できるようにする。 (マークダウン -> HTML変換のプラグインを探す想定。)

9. プログラムコードのハイライト機能

記事内にプログラムコードを記載する際に、ハイライトされるようにする。 (もしかしたら、マークダウン対応と同時に対応可能かもしれない。)

10. カテゴリー付け機能

記事ごとにカテゴリーを設定できるようにする。

優先度: 低

記事の下書き保存

記事の途中までの状態を保持できるようにする。

カテゴリー付けの際のカテゴリー検索機能

カテゴリー付けの際に、既に設定されているカテゴリーのリストの表示を行えるようにする。

コメント機能

記事へのコメントを受け付ける機能

API機能

ブログ内の記事の操作を行うためのAPIを実装する。 例えば、ビューを他技術で構築する場合に、既存の記事の活用を見込めるため。

ブログチュートリアル Part.2

CakePHP4のブログチュートリアルを進める。Part.2 book.cakephp.org

シンプルな認証と認可のアプリケーション

ユーザーに関連するコードを作成する

ログインユーザーを保持するためにUsersテーブルを作成する。 下記SQLSQLite内で実行。

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255),
    password VARCHAR(255),
    role VARCHAR(20),
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL
);

Usersテーブルを作成したので、Bake allする。

bin/cake bake all Users

認証の追加

認証用のプラグインをインストールする。

composer require "cakephp/authentication:^2.0"

パスワードハッシュの追加

パスワードを自動的にハッシュ化するためのメソッドをUserエンティティに追加する。

<?php
protected function _setPassword($password)
{
    if (strlen($password) > 0) {
        return (new DefaultPasswordHasher)->hash($password);
    }
}

認証の設定

認証を有効にするため、下記二つのプラグインをApplication.php内で有効にする。

  • AuthenticationService
  • AuthenticationMiddleware
<?php
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Psr\Http\Message\ServerRequestInterface;

アプリケーションクラスに認証インターフェースを実装する。

<?php
class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{

インターフェースのメソッドを実装する。

<?php
// src/Application.php
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
        // ... other middleware added before
        ->add(new RoutingMiddleware($this))
        // add Authentication after RoutingMiddleware
        ->add(new AuthenticationMiddleware($this));

    return $middlewareQueue;
}

public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
    $authenticationService = new AuthenticationService([
        'unauthenticatedRedirect' => '/users/login',
        'queryParam' => 'redirect',
    ]);

    // 識別子をロードして、電子メールとパスワードのフィールドを確認します
    $authenticationService->loadIdentifier('Authentication.Password', [
        'fields' => [
            'username' => 'email',
            'password' => 'password',
        ]
    ]);

    // 認証子をロードするには、最初にセッションを実行する必要があります
    $authenticationService->loadAuthenticator('Authentication.Session');
    // メールとパスワードを選択するためのフォームデータチェックの設定
    $authenticationService->loadAuthenticator('Authentication.Form', [
        'fields' => [
            'username' => 'email',
            'password' => 'password',
        ],
        'loginUrl' => '/users/login',
    ]);

    return $authenticationService;
}

AppController内で認証用のコンポーネントをロードする。

<?php
// src/Controller/AppController.php
public function initialize(): void
{
    parent::initialize();
    $this->loadComponent('RequestHandler');
    $this->loadComponent('Flash');

    // Add this line to check authentication result and lock your site
    $this->loadComponent('Authentication.Authentication');

UsersController に以下のメソッドを実装する。

<?php
public function beforeFilter(\Cake\Event\EventInterface $event)
{
    parent::beforeFilter($event);
    // ログインアクションを認証を必要としないように設定することで、
    // 無限リダイレクトループの問題を防ぐことができます
    $this->Authentication->addUnauthenticatedActions(['login']);
}

public function login()
{
    $this->request->allowMethod(['get', 'post']);
    $result = $this->Authentication->getResult();
    // POSTやGETに関係なく、ユーザーがログインしていればリダイレクトします
    if ($result->isValid()) {
        // ログイン成功後に /article にリダイレクトします
        $redirect = $this->request->getQuery('redirect', [
            'controller' => 'Articles',
            'action' => 'index',
        ]);

        return $this->redirect($redirect);
    }
    // ユーザーの送信と認証に失敗した場合にエラーを表示します
    if ($this->request->is('post') && !$result->isValid()) {
        $this->Flash->error(__('Invalid email or password'));
    }
}

ログイン用のtemplate(login.php)を実装する。

<?php
<div class="users form">
    <?= $this->Flash->render() ?>
    <h3>Login</h3>
    <?= $this->Form->create() ?>
    <fieldset>
        <legend><?= __('ユーザー名とパスワードを入力してください') ?></legend>
        <?= $this->Form->control('email', ['required' => true]) ?>
        <?= $this->Form->control('password', ['required' => true]) ?>
    </fieldset>
    <?= $this->Form->submit(__('Login')); ?>
    <?= $this->Form->end() ?>

    <?= $this->Html->link("Add User", ['action' => 'add']) ?>
</div>

認証が不要なアクションを定義する。

<?php
// src/Controller/AppController.php で
public function beforeFilter(\Cake\Event\EventInterface $event)
{
    parent::beforeFilter($event);
    $this->Authentication->addUnauthenticatedActions(['index', 'view']);
}

ログアウト

ログアウト用のアクションを Usersコントローラーに追加する。

<?php
// src/Controller/UsersController.php で
public function logout()
{
    $result = $this->Authentication->getResult();
    // POSTやGETに関係なく、ユーザーがログインしていればリダイレクトします
    if ($result->isValid()) {
        $this->Authentication->logout();
        return $this->redirect(['controller' => 'Users', 'action' => 'login']);
    }
}

こちらで、 /users/logout にアクセスするとログアウトが完了し、ログイン画面へリダイレクトされるようになる。

ブログチュートリアル Part.1

CakePHP4のブログチュートリアルを進める。 book.cakephp.org

インストール

Composerでローカルにインストールする。

composer self-update && composer create-project --prefer-dist cakephp/app:4.* blog

初期設定

tmpとlogsディレクトリのパーミッションを変更する。

chown -R www-data tmp
chown -R www-data logs

データベースの設定を行う。今回はSQLiteを使用する。 Aritclesテーブルを作成するために下記SQLを実行。

sqlite> CREATE TABLE articles (
   ...>     id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
   ...>     title VARCHAR(50),
   ...>     body TEXT,
   ...>     created DATETIME DEFAULT NULL,
   ...>     modified DATETIME DEFAULT NULL
   ...> );
sqlite>
sqlite> ;

config/app.php にデータベースの設定を追加する。

<?php
'Datasources' => [
    'default' => [
        'driver' => Sqlite::class,
        'database' => ROOT . DS . 'db',
    ],
]

Article モデルの作成

モデルクラスを作成します。 モデルクラスは、データベースと直接やりとりを行う場所で、基本的なCRUD操作の役割を担います。

下記コマンドでArticlesTable モデルを作成する。

bin/cake bake model Articles

Articles コントローラーの作成

コントローラーを作成します。 コントローラーは、処理対象を操作するロジック全てが発生する場所で、モデルクラスに命令を与えるのもコントローラーの役割です。

下記コマンドでArticlesController クラスを作成する。

bin/cake bake controller Articles

Article ビューの作成

ビューを作成します。 ビューは、クライアントへ表示する画面の構成を担います。 コントローラーからセットされたビュー変数で画面をレンダリングします。

下記コマンドでArticle ビューを作成する。

bin/cake bake template all Articles

データのバリデーション

デフォルトのバリデーションロジックを ArticlesTable クラスに実装します。

<?php
namespace App\Model\Table;

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

class ArticlesTable extends Table
{
    public function initialize(array $config): void
    {
        $this->addBehavior('Timestamp');
    }

    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->notEmptyString('title')
            ->requirePresence('title', 'create')
            ->notEmptyString('body')
            ->requirePresence('body', 'create');

        return $validator;
    }
}

ルーティング

config/routes.php を修正し、トップ画面にArticlesのindexページを表示するように修正します。

<?php
$builder->connect('/', ['controller' => 'Articles', 'action' => 'index']);

ツリーカテゴリーの作成

投稿記事をカテゴリ作成のために Tree behavior を使用します。

Migrations プラグイン

migrations プラグイン をインストールするため、下記コマンドを実行します。

composer require cakephp/migrations:~1.0

articles テーブルを作成するマイグレーションファイルを作成します。 下記コマンドでマイグレーションファイルを生成します。

bin/cake bake migration CreateArticles title:string body:text category_id:integer created modified

次に、categories テーブルのマイグレーションファイルを作成します。 下記コマンドでマイグレーションファイルを生成します。

bin/cake bake migration CreateCategories parent_id:integer lft:integer[10] rght:integer[10] name:string[100] description:string created modified

最後に、マイグレーションコマンドを実行し、データベース内にテーブルを作成します。

bin/cake migrations migrate

テーブルの編集

Articles と Categories テーブルとを結びつけるため、ArticlesTable に下記のようにアソシエーションの設定を追加します。

<?php
$this->addBehavior('Timestamp');
// Just add the belongsTo relation with CategoriesTable
$this->belongsTo('Categories', [
    'foreignKey' => 'category_id',
]);

Categories のスケルトンコードを作成する

bake コマンドで、Categoriesのコードを作成します。

bin/cake bake model Categories
bin/cake bake controller Categories
bin/cake bake template Categories

templateのカテゴリのオプションの修正を行います。

<?php
echo $this->Form->control('parent_id', [
    'options' => $parentCategories,
    'empty' => 'No parent category'
]);

CategoriesController.php のindexアクションを編集して、ツリーでカテゴリーを並べ替えるために moveUp() および moveDown() メソッドを追加する。

<?php
class CategoriesController extends AppController
{
    public function index()
    {
        $categories = $this->Categories->find()
            ->order(['lft' => 'ASC'])
            ->all();
        $this->set(compact('categories'));
        $this->viewBuilder()->setOption('serialize', ['categories']);
    }

    public function moveUp($id = null)
    {
        $this->request->allowMethod(['post', 'put']);
        $category = $this->Categories->get($id);
        if ($this->Categories->moveUp($category)) {
            $this->Flash->success('The category has been moved Up.');
        } else {
            $this->Flash->error('The category could not be moved up. Please, try again.');
        }
        return $this->redirect($this->referer(['action' => 'index']));
    }

    public function moveDown($id = null)
    {
        $this->request->allowMethod(['post', 'put']);
        $category = $this->Categories->get($id);
        if ($this->Categories->moveDown($category)) {
            $this->Flash->success('The category has been moved down.');
        } else {
            $this->Flash->error('The category could not be moved down. Please, try again.');
        }
        return $this->redirect($this->referer(['action' => 'index']));
    }
}

templates/Categories/index.php を下記のように変更する。

<?php
<div class="actions large-2 medium-3 columns">
    <h3><?= __('Actions') ?></h3>
    <ul class="side-nav">
        <li><?= $this->Html->link(__('New Category'), ['action' => 'add']) ?></li>
    </ul>
</div>
<div class="categories index large-10 medium-9 columns">
    <table cellpadding="0" cellspacing="0">
    <thead>
        <tr>
            <th>Id</th>
            <th>Parent Id</th>
            <th>Lft</th>
            <th>Rght</th>
            <th>Name</th>
            <th>Description</th>
            <th>Created</th>
            <th class="actions"><?= __('Actions') ?></th>
        </tr>
    </thead>
    <tbody>
    <?php foreach ($categories as $category): ?>
        <tr>
            <td><?= $category->id ?></td>
            <td><?= $category->parent_id ?></td>
            <td><?= $category->lft ?></td>
            <td><?= $category->rght ?></td>
            <td><?= h($category->name) ?></td>
            <td><?= h($category->description) ?></td>
            <td><?= h($category->created) ?></td>
            <td class="actions">
                <?= $this->Html->link(__('View'), ['action' => 'view', $category->id]) ?>
                <?= $this->Html->link(__('Edit'), ['action' => 'edit', $category->id]) ?>
                <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $category->id], ['confirm' => __('Are you sure you want to delete # {0}?', $category->id)]) ?>
                <?= $this->Form->postLink(__('Move down'), ['action' => 'moveDown', $category->id], ['confirm' => __('Are you sure you want to move down # {0}?', $category->id)]) ?>
                <?= $this->Form->postLink(__('Move up'), ['action' => 'moveUp', $category->id], ['confirm' => __('Are you sure you want to move up # {0}?', $category->id)]) ?>
            </td>
        </tr>
    <?php endforeach; ?>
    </tbody>
    </table>
</div>

こちらで、/yoursite/categories/add へアクセスすることで、カテゴリー一覧に遷移することでできる。