ブログ開発
CakePHPの学習を目的に、一からブログアプリケーションを開発します。 開発するブログの機能を、実際にはてなブログというブログサービスを使用することを通して考えました。
優先度: 高
1. 記事の一覧
記事の一覧表示機能(画面)。
2. 記事の閲覧
記事の内容を閲覧する機能(画面)。
3. 記事の投稿
記事の投稿を行う機能(画面)。
4. 記事の編集
記事の編集を行う機能(画面)。
5. 記事の削除
記事の削除を行う機能。
6. 管理者ログイン機能
管理者ログイン機能を設け、ログイン状態でのみ、記事の投稿・編集・削除を行えないように制限する。
7. 画像の掲載
記事内にローカルの画像を掲載できるように、画像のアップロード機能を設ける。 (アップロード先は imgur などの無料サービスを使用することを想定。)
8. マークダウン対応
記事をマークダウンで記載できるように、対応できるようにする。 (マークダウン -> HTML変換のプラグインを探す想定。)
9. プログラムコードのハイライト機能
記事内にプログラムコードを記載する際に、ハイライトされるようにする。 (もしかしたら、マークダウン対応と同時に対応可能かもしれない。)
10. カテゴリー付け機能
記事ごとにカテゴリーを設定できるようにする。
優先度: 低
記事の下書き保存
記事の途中までの状態を保持できるようにする。
カテゴリー付けの際のカテゴリー検索機能
カテゴリー付けの際に、既に設定されているカテゴリーのリストの表示を行えるようにする。
コメント機能
記事へのコメントを受け付ける機能
API機能
ブログ内の記事の操作を行うためのAPIを実装する。 例えば、ビューを他技術で構築する場合に、既存の記事の活用を見込めるため。
ブログチュートリアル Part.2
CakePHP4のブログチュートリアルを進める。Part.2 book.cakephp.org
シンプルな認証と認可のアプリケーション
ユーザーに関連するコードを作成する
ログインユーザーを保持するためにUsersテーブルを作成する。 下記SQLをSQLite内で実行。
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
初期設定
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 へアクセスすることで、カテゴリー一覧に遷移することでできる。