cakephp4.xに慣れよう3 Authentication

Installing Authentication Plugin
$ php composer.phar require “cakephp/authentication:^2.0”

Adding Password Hashing
$ bin/cake bake all users

src/Model/Entity/User.php

namespace App\Model\Entity;

use Authentication\PasswordHasher\DefaultPasswordHasher;
use Cake\ORM\Entity;

class User extends Entity {
    protected function _setPassword(string): ?string {
        if(strlen($password) > 0 ){
            return (new DefaultPasswordHasher())->hash($password);
        }
    }
}

Adding Login
// src/Application.php

    public function getAuthenticationService(ServiceRequestInterface $request){
        $authenticationService = new AuthenticationService([
            'unauthenticatedRedirect' => Router::url('/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' => Router::url('/users/login'),
        ]);
        return $authenticationService;
    }

// src/Controller/AppController.php

    public function initialize(): void
    {
        parent::initialize();
        $this->loadComponent('Flash');
        $this->loadComponent('Authentication.Authentication');

UsersController

    public function beforeFilter(\Cake\Event\EventInterface $event){
        parent::beforeFilter($event);
        $this->Authentication->addUnauthenticatedAction(['login']);
    }

    public function login(){
        $this->request->allowMethod(['get', 'post']);
        $request = $this->Authentication->getResult();
        if($result && $result->isValid()){
            $redirect = $this->request->getQuery('redirect', [
                'controller' => 'Articles',
                'action' => 'index',
            ]);
            return $this->redirect($redirect);
        }
        if($this->request->is('post') && !$result->isValid()){
            $this->Flash->error(__('Invalid username or password'));
        }
    }

// /templates/Users/login.php

<div class="users form">
	<?= $this->Flash->render() ?>
	<h3>Login</h3>
	<?= $this->Form->create() ?>
	<fieldset>
		<legend><?= __('Please enter your username and password') ?></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>

// in src/Controller/AppController.php

    public function beforeFilter(\Cake\Event\EventInterface $event){
        parent::beforeFilter($event);
        $this->Authentication->addUnauthenticatedActions(['index', 'view']);
    }

Logout

    public function logout(){
        $result  = $this->Authentication->getResult();
        if($result && $result->isValid()){
            $this->Authentication->logout();
            return $this->redirect(['controller'=> 'Users', 'action'=> 'login']);
        }
    }

なるほど、CakePHPは人気に翳りがあるので舐めてましてが、自分がアホでしたね。かなり色々勉強しないと追いつかないほど先を行ってます。

cakephp4.xに慣れよう2

Update Validation Rules for Articles: src/Model/Table/ArticlesTable.php

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

    public function beforeSave(EventInterface $event, $entity, $options)
    {
        if ($entity->isNew() && !$entity->slug) {
            $sluggedTitle = Text::slug($entity->title);
            // trim slug to maximum length defined in schema
            $entity->slug = substr($sluggedTitle, 0, 191);
        }
    }

    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->notEmptyString('title')
            ->minLength('title', 10)
            ->maxLength('title', 255)

            ->notEmptyString('body')
            ->minLength('body', 10);

        return $validator;
    }
}

Add Delete Action
L src/Controller/ArticlesController.php

	public function delete($slug){
		$this->request->allowMethod(['post', 'delete']);

		$article = $this->Articles->findBySlug($slug)->firstOrFail();
		if($this->Articles->delete($article)){
			$this->Flash->success(__('The {0} article has been deleted.', $article->title));
			return $this->redirect(['action'=> 'index']);
		}
	}

templates/Articles/index.php

		<td>
			<?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?>
			<?= $this->Form->postLink(
				'Delete',
				['action' => 'delete', $article->slug],
				['confirm' => 'Are you sure?'])
			?>
		</td>

### Tags and User
$ bin/cake bake model users
$ bin/cake bake controller users
$ bin/cake bake template users

$ bin/cake bake all tags

Updating Articles to enable Tagging
// in src/Controller/ArticlesController.php

    public function add()
    {
        $article = $this->Articles->newEmptyEntity();
        if ($this->request->is('post')) {
            $article = $this->Articles->patchEntity($article, $this->request->getData());

            // Hardcoding the user_id is temporary, and will be removed later
            // when we build authentication out.
            $article->user_id = 1;

            if ($this->Articles->save($article)) {
                $this->Flash->success(__('Your article has been saved.'));
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('Unable to add your article.'));
        }
        $tags = $this->Articles->Tags->find('list')->all();
        $this->set('tags', $tags);

        $this->set('article', $article);
    }

templates/Articles/add.php:

	echo $this->Form->control('tags._ids', ['options' => $tags]);
	public function edit($slug) {
		$article = $this->Articles
			->findBySlug($slug)
			->contain('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.'));
		}
		$tags = $this->Articles->Tags->find('list')->all();
        $this->set('tags', $tags);
		$this->set('article', $article);
	}

Finding Articles by Tags
// config/routes.php

        $builder->scope('/articles', function(RouteBuilder $builder){
            $builder->connect('/tagged/*', ['controller' => 'Articles', 'action'=>'tags']);
        });

src/Controller/ArticlesController.php

	public function tags(){
		$tags = $this->request->getParam('pass');

		$articles = $this->Articles->find('tagged', [
				'tags' => $tag
			])
			->all();

		$this->set([
			'articles' => $articles,
			'tags' => $tags
		]):
	}

Creating the Finder Method
// src/Model/Table/ArticlesTable.php

    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'])){
            $qeury->leftJoinWith('Tags')
                ->where(['Tags.title IS' => null]);
        } else {
            $query->innerJoinWith('Tags')
                ->where(['Tags.title IN' => $options['tags']]);
        }
        return $query->group(['Articles.id']);
    }

Creating the View
// templates/Articles/tags.php

<h1>
	Articles tagged with
	<?= $this->Text->toList(h($tags), 'or') ?>
</h1>

<section>
	<?php foreach($articles as $article): ?>
		<h4><?= $this->Html->link(
			$article->title,
			['controller' => 'Articles', 'action' => 'view', $article->slug]
		) ?></h4>
		<span><?= h($article->created) ?></span>
	<?php endforeach; ?>
</section>

Improving the Tagging Experience
– Adding a computed field
// src/Model/Entity/Article.php

    protected function _getTagString(){

    	if(isset($this->_fields['tag_string'])){
    		return $this->_fields['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, ', ')
    }

Updating the views
// templates/Articles/add.php

	echo $this->Form->control('tag_string', ['type' => 'text']);

// templates/Articles/view.php

<h1><?= h($article->title) ?></h1>
<p><?= h($article->body) ?></p>
<p><b>Tags:</b><?= h($article->tag_string) ?></p>
    public function view($slug)
    {
        $article = $this->Articles->findBySlug($slug)->contain('Tags')->firstOrFail();
        $this->set(compact('article'));
    }

Persisting the Tag String
// src/Model/Table/ArticlesTable.php

    public function beforeSave(EventInterface $event, $entity, $options)
    {
        if($entity->tag_string) {
            $entity->tags = $this->_buildTags($entity->tag_string);
        }

        if ($entity->isNew() && !$entity->slug) {
            $sluggedTitle = Text::slug($entity->title);
            // trim slug to maximum length defined in schema
            $entity->slug = substr($sluggedTitle, 0, 191);
        }
    }

    protected function _buildTags($tagString){
        $newTags = array_map('trim', explode(',', $tagString));
        $newTags = array_filter($newTags);
        $newTags = array_unique($newTags);

        $out = [];
        $tags = $this->Tags->find()
            ->where(['Tags.title IN' => $newTags])
            ->all();

        foreach ($tags->extract('title') as $existing){
            $index = array_search($existing, $newTags);
            if($index !== false){
                unset($newTags[$index]);
            }
        }
        foreach($tags as $tag){
            $out[] = $tag;
        }
        foreach($newTags as $tag){
            $out[] = $this->Tags->newEntity(['title'=>$tag]);
        }
        return $out;
    }

Auto-populating the Tag String
// src/Model/Table/ArticlesTable.php

    public function initialize(array $config): void
    {
        $this->addBehavior('Timestamp');
        $this->belongsToMany('Tags', [
            'joinTable' => 'articles_tags',
            'dependent' => true
        ]);
    }

[CakePHP4] Content Management Tutorial

– Make sure to get PHP version.
$ php -v
PHP 7.4.3 (cli) (built: Mar 2 2022 15:36:52) ( NTS )

$ php composer.phar create-project –prefer-dist cakephp/app:4.* cms
$ bin/cake server -H 0.0.0.0

# create database cake_cms;
# \c cake_cms;

CREATE TABLE users(
	id SERIAL PRIMARY KEY,
	email VARCHAR(255) NOT NULL,
	password VARCHAR(255) NOT NULL,
	created TIMESTAMP,
	modified TIMESTAMP
);

CREATE TABLE articles(
	id SERIAL PRIMARY KEY,
	user_id INT NOT NULL,
	title VARCHAR(255) NOT NULL,
	slug VARCHAR(191) NOT NULL,
	body TEXT,
	published BOOLEAN DEFAULT FALSE,
	created TIMESTAMP,
	modified TIMESTAMP,
	UNIQUE (slug),
	FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE tags(
	id SERIAL PRIMARY KEY,
	title VARCHAR(191),
	created TIMESTAMP,
	modified TIMESTAMP,
	UNIQUE (title)
);

CREATE TABLE articles_tags(
	article_id INT NOT NULL,
	tag_id INT NOT NULL,
	PRIMARY KEY (article_id, tag_id),
	FOREIGN KEY (tag_id) REFERENCES tags(id),
	FOREIGN KEY (article_id) REFERENCES articles(id)
);

INSERT INTO users (email, password, created, modified) VALUES 
('cakephp@example.com', 'secret', NOW(), NOW());

INSERT INTO articles(user_id, title, slug, body, published, created, modified)
VALUES
(1, 'first post', 'first-post', 'This is the first post.', TRUE, NOW(), NOW());

cake_cms=# select * from users;
cake_cms=# select * from articles;

Database Configuration
L app_local.php

'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Postgres',
            'persistent' => false,
            'host' => 'localhost',
            //'port' => 'non_standard_port_number',
            'username' => 'hoge',
            'password' => 'hoge',
            'database' => 'cake_cms',
            // 'encoding' => 'utf8mb4',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
            'url' => env('DATABASE_URL', null),
        ],

Creating our first model
src/Model/Table/ArticlesTable.php

namespace App\Model\Table;

use Cake\ORM\Table;

class ArticlesTable extends Table {

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

src/Model/Entity/Article.php

namespace App\Model\Entity;

use Cake\ORM\Entity;

class Article extends Entity {

	protected $_accessible = [
		'*' => true,
		'id' => false,
		'slug' => false
	];
}

Creating the Articles Controller
src/Controller/ArticlesController.php

namespace App\Controller;

class ArticlesController extends AppController {

	public function index() {
		$this->loadComponent('Paginator');
		$articles = $this->Paginator->paginate($this->Articles->find());
		$this->set(compact('articles'));
	}
}

Create the Article List Template
templates/Articles/index.php

<h1>Articles</h1>
<table>
	<tr>
		<th>Title</th>
		<th>Created</th>
	</tr>

	<?php foreach ($articles as $article): ?>
	<tr>
		<td>
			<?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
		</td>
		<td>
			<?= $article->created->format(DATE_RFC850) ?>
		</td>
	</tr>
	<?php endforeach; ?>
</table>

Create the View Action
src/Controller/ArticlesController.php

	public function view($slug = null){

		$article = $this->Articles->findBySlug($slug)->firstOrFail();
		$this->set(compact('article'));
	}

Create the view Template
templates/Articles/view.php

<h1><?= h($article->title) ?></h1>
<p><?= h($article->body) ?></p>
<p><small>Created: <?= $article->created->format(DATE_RFC850) ?></small></p>
<p><?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?></p>

Adding Articles

class ArticlesController extends AppController {

	public function initialize(): void {
		parent::initialize();

		$this->loadComponent('Paginator');
		$this->loadComponent('Flash');
	}

	public function index() {
		$articles = $this->Paginator->paginate($this->Articles->find());
		$this->set(compact('articles'));
	}

	public function view($slug = null){

		$article = $this->Articles->findBySlug($slug)->firstOrFail();
		$this->set(compact('article'));
	}

	public function add(){

		$article = $this->Articles->newEmptyEntity();
		if($this->request->is('post')){
			$article = $this->Articles->patchEntity($article, $this->request->getData());

			$article->user_id = 1;

			if ($this->Articles->save($article)){
				$this->Flash->success(__('Your article has been saved.'));
				return $this->redirect(['action' => 'index']);
			}
			$this->Flash->error(__('Unable to add your article.'));
		}
		$this->set('article', $article);
	}
}

Create Add Template
templates/Articles/add.php

<h1>Add Article</h1>
<?php
	echo $this->Form->create($article);
	echo $this->Form->control('user_id', ['type'=> 'hidden', 'value' => 1]);
	echo $this->Form->control('title');
	echo $this->Form->control('body', ['rows' => '3']);
	echo $this->Form->button(__('Save Article'));
	echo $this->Form->end();
?>

templates/Articles/index.php

<?= $this->Html->link('Add Article', ['action' => 'add']) ?>

Adding Simple Slug Generation
src/Model/Table/ArticlesTable.php

namespace App\Model\Table;

use Cake\ORM\Table;
use Cake\Utility\Text;
use Cake\Event\EventInterface;

public function beforeSave(EventInterface $event, $entity, $options){
	if($entity->isNew() && !$entity->slug){
		$sluggedTitle = Text::slug($entity->title);
		$entity->slug = substr($sluggedTitle, 0, 191);
	}
}

Add Edit Action

	public function edit($slug) {
		$article = $this->Articles
			->findBySlug($slug)
			->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.'));
		}
		$this->set('article', $article);
	}

Create Edit Template

<h1>Edit Article</h1>
<?php
	echo $this->Form->create($article);
	echo $this->Form->control('user_id', ['type'=> 'hidden']);
	echo $this->Form->control('title');
	echo $this->Form->control('body', ['rows' => '3']);
	echo $this->Form->button(__('Save Article'));
	echo $this->Form->end();
?>

templates/Articles/index.php

<h1>Articles</h1>
<?= $this->Html->link('Add Article', ['action' => 'add']) ?>
<table>
	<tr>
		<th>Title</th>
		<th>Created</th>
		<th>Action</th>
	</tr>

	<?php foreach ($articles as $article): ?>
	<tr>
		<td>
			<?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
		</td>
		<td>
			<?= $article->created->format(DATE_RFC850) ?>
		</td>
		<td>
			<?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?>
		</td>
	</tr>
	<?php endforeach; ?>
</table>

cakephp4.xに慣れよう

この記事ではCakePHP4.*のチュートリアルを行います。
1. composerのcurl
2. cakephpのインストール
3. build in serverを起動
4. configディレクトリの.envファイルの編集
5. envファイルの読み込み(コメントアウト解除)
6. データベースの設定
7. migrationファイルの作成
8. 管理画面ファイルをbakeで作成
9. バリデーションの作成
10. 管理画面のルーターの設定
11. 一般ユーザ用のファイル作成
12. Viewファイル作成
13. ユーザ管理機能作成(認証機能)
14. ユーザバリデーション機能追加
15. ユーザー認証機能の実装(authenticationプラグイン)
16. 管理画面レイアウトの作成

$ curl -sS https://getcomposer.org/installer | php
$ php composer.phar create-project –prefer-dist cakephp/app:4.* blog
$ cd blog
$ bin/cake server -H 0.0.0.0

4. configディレクトリの.envファイルの編集
/config/.env

export APP_NAME="MyBlog"
export DEBUG="true"
export APP_ENCODING="UTF-8"
export APP_DEFAULT_LOCALE="ja_JP"
export APP_DEFAULT_TIMEZONE="Asia/Tokyo"
export SECURITY_SALT="3mPhHebbrC9cTH7Kjg9MU5d_bXuBXcUUyGRbgJHe"

5. envファイルの読み込み(コメントアウト解除)
/config/.bootstrap.php

if (!env('APP_NAME') && file_exists(CONFIG . '.env')) {
    $dotenv = new \josegonzalez\Dotenv\Loader([CONFIG . '.env']);
    $dotenv->parse()
        ->putenv()
        ->toEnv()
        ->toServer();
}

6. データベースの設定
/config/app_local.php

use Cake\Database\Driver\Postgres; 
// 省略
    'Datasources' => [
        'default' => [
            'driver' => Postgres::class,
            'host' => 'localhost',
            /*
             * CakePHP will use the default DB port based on the driver selected
             * MySQL on MAMP uses port 8889, MAMP users will want to uncomment
             * the following line and set the port accordingly
             */
            //'port' => 'non_standard_port_number',

            'username' => 'hoge',
            'password' => 'fuga',

            'database' => 'test',
            /*
             * If not using the default 'public' schema with the PostgreSQL driver
             * set it here.
             */
            //'schema' => 'myapp',

            /*
             * You can use a DSN string to set the entire configuration
             */
            'url' => env('DATABASE_URL', null),
        ],

7. migrationファイルの作成
$ bin/cake bake migration CreatePosts

/config/Migrations/20230104050535_CreatePosts.php

    public function change(): void
    {
        $table = $this->table('posts');
        $table->addColumn('title', 'string', [
            'limit' => 150,
            'null' => false,
        ])
        ->addColumn('description', 'text', [
            'limit' => 255,
        ])
        ->addColumn('body', 'text')
        ->addColumn('published', 'boolean', [
            'default' => false,
        ])
        ->addColumn('created', 'datetime')
        ->addColumn('modified', 'datetime')
        ->create();
    }

$ bin/cake migrations migrate

8. 管理画面ファイルをbakeで作成
model, controller, viewを作成することができる
$ bin/cake bake model posts
$ bin/cake bake controller posts –prefix admin
$ bin/cake bake template posts –prefix admin
※ –prefix admin はadminディレクトリに作成するという意味合い

9. バリデーションの作成
src/Model/Table/PostsTable.php

    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->integer('id')
            ->allowEmptyString('id', null, 'create');

        $validator
            ->scalar('title')
            ->maxLength('title', 150, '150文字以内で入力してください')
            ->minLength('title', 5, '5文字以上で入力してください')
            ->requirePresence('title', 'create')
            ->notEmptyString('title', 'タイトルは必ず入力してください');

        $validator
            ->scalar('description')
            ->maxLength('description', 255, '150文字以上で入力してください。')
            ->allowEmptyString('description');

        $validator
            ->scalar('body')
            ->allowEmptyString('body');

        $validator
            ->boolean('published')
            ->allowEmptyString('published');

        return $validator;
    }

10. 管理画面のルーターの設定
/config/routes.php

use Cake\Routing\Router;
//..
        Router::prefix('admin', function ($routes) {
            $routes->fallbacks('DashedRoute');
            $routes->connect('/', ['controller' => 'Posts', 'action' => 'index']);
        });

11. 一般ユーザ用のファイル作成
$ bin/cake bake controller posts
src/Controller/PostsController.php

class PostsController extends AppController
{
    public $paginate = [
        'limit' => 10,
        'order' => [
            'Posts.created' => 'desc'
        ]
    ];

    public function index()
    {
        $posts = $this->paginate($this->Posts->findByPublished(1));

        $this->set(compact('posts'));
    }

    public function view($id = null)
    {
        $post = $this->Posts->get($id, [
            'conditions' => ['published' => 1],
        ]);

        $this->set('post', $post);
    }   
}

12. Viewファイル作成
templates/Posts/index.php

<div class="content">
	<?php foreach ($posts as $post): ?>
		<p>投稿日:<time><?= h($post->created->i18nFormat('YYYY/MM/dd HH:mm:ss')) ?></time></p>
		<h3 style="margin-bottom:0"><?= h($post->title) ?></h3>
		<?= $this->Text->autoParagrahp(h($post->description)); ?>
		<br>
		<?= $this->Html->link('記事を読む', ['action'=> 'view', $post->id], ['class'=> 'button']) ?>
		<hr>
	<?php endforeach; ?>
    <div class="paginator">
        <ul class="pagination">
            <?= $this->Paginator->first('<< 最初') ?>
            <?= $this->Paginator->prev('< 前へ') ?>
            <?= $this->Paginator->numbers() ?>
            <?= $this->Paginator->next('次へ >') ?>
            <?= $this->Paginator->last('最後 >>') ?>
        </ul>
    </div>
</div>

templates/Posts/view.php

<div class="posts view content">
	<?= h($post->created->il8nformat('YYYY/MM/dd HH:mm:ss')) ?>
    <h2><?= h($post->title) ?></h2>
    <?= $this->Text->autoParagraph(h($post->body)); ?>
    <hr>
    <?= $this->Html->link('一覧へ戻る', ['action'=>'index'], ['class'=>'button']) ?>
</div>

/config/routes.php

        // $builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
        $builder->connect('/', ['controller' => 'Posts', 'action' => 'index']);

13. ユーザ管理機能作成(認証機能)
$ bin/cake bake migration CreateUsers
/config/Migrations/20230104084657_CreateUsers.php

    public function change(): void
    {
        $table = $this->table('users');
        $table->addColumn('username', 'string', [
                'default' => null,
                'limit' => 50,
                'null' => false,
            ])
            ->addColumn('password', 'string', [
                'default' => null,
                'limit' => 255,
                'null' => false,
            ])
            ->addColumn('created', 'datetime')
            ->addColumn('modified', 'datetime')
            ->create();
    }

$ bin/cake migrations migrate
$ bin/cake bake model users
$ bin/cake bake controller users –prefix admin
$ bin/cake bake template users –prefix admin

ユーザエンティティにハッシュ化を追加
src/Model/Entity/User.php

use Cake\Auth\DefaultPasswordHasher;
class User extends Entity
{
    protected $_accessible = [
        'username' => true,
        'password' => true,
        'created' => true,
        'modified' => true,
    ];

    protected $_hidden = [
        'password',
    ];

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

14. ユーザバリデーション機能追加
src/Model/Table/UsersTable.php

    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->integer('id')
            ->allowEmptyString('id', null, 'create');

        $validator
            ->scalar('username')
            ->maxLength('username', 50)
            ->requirePresence('username', 'create')
            ->notEmptyString('username', 'ユーザ名は必ず入力してください');

        $validator
            ->scalar('password')
            ->maxLength('password', 255)
            ->requirePresence('password', 'create')
            ->notEmptyString('password', 'パスワードは必ず入力してください');

        return $validator;
    }

15. ユーザー認証機能の実装(authenticationプラグイン)
$ sudo cp composer.phar blog/
$ cd blog
$ php composer.phar require cakephp/authentication:^2.0
src/Application.php

use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Psr\Http\Message\ServerRequestInterface;

    public function getAuthenticationService(ServerRequestInterface $request):AuthenticationServiceInterface {

        $authenticationService = new AuthenticationService([
            'unauthenticatedRedirect' => '/admin/users/login',
            'queryParam' => 'redirect',
        ]);

        $authenticationService->loadIdentifier('Authentication.Password',[
            'fields' => [
                'username' => 'username',
                'password' => 'password',
            ]
        ]);

        $authenticationService->loadAuthenticator('Authentication.Session');
        $authenticationService->loadAuthenticator('Authentication.Form', [
            'fields' => [
                'username' => 'username',
                'password' => 'password',
            ],
            'loginUrl' => '/admin/users/login',
        ]);

        return $authenticationService;
    }

src/Controller/Admin/AdminController.php

namespace App\Controller\Admin;

use Cake\Controller\Controller;

class AdminController extends Controller {

	public function initialize(): void {
		parent::initialize();

		$this->loadComponent('RequestHandler');
		$this->loadComponent('Flash');
		$this->loadComponent('Authentication.Authentication');
	}
}

src/Controller/Admin/PostsController.php

use App\Controller\Admin\AdminController;

class PostsController extends AdminController{
//..
}

src/Controller/Admin/UsersController.php

use App\Controller\Admin\AdminController;

    public function login(){
        $this->request->allowMethod(['get', 'post']);
        $result = $this->Authentication->getResult();

        if($result->isValid()){
            return $this->redirect('/admin');
        }
        if($this->request->is('post') && !$request->isValid()){
            $this->Flash->error('ユーザ名かパスワードが正しくありません');
        }
    }

    public function logout(){
        $result = $this->Authentication->getResult();
        if($result->isValid()){
            $this->Authentication->logout();
            return $this->redirect(['controller'=> 'Users', 'action' => 'login']);
        }
    }

16. 管理画面レイアウトの作成
src/Controller/Admin/UsersController.php

<!DOCTYPE html>
<html>
<head>
    <?= $this->Html->charset() ?>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Controle Panel</title>
    <?= $this->Html->meta('icon') ?>
    <link href="https://fonts.googleapis.com/css?family=Raleway:400,700" rel="stylesheet">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css">
    <?= $this->Html->css('milligram.min.css') ?>
    <?= $this->Html->css('cake.css') ?>
    <?= $this->fetch('meta') ?>
    <?= $this->fetch('css') ?>
    <?= $this->fetch('script') ?>
</head>
<body>
    <nav class="top-nav">
        <div class="top-nav-title">
            <a href="/admin/">Control Panel</a>
        </div>
        <div class="top-nav-links">
            <a href="/admin/users/logout">ログアウト</a>
        </div>
    </nav>
    <main class="main">
        <div class="container">
            <?= $this->Flash->render() ?>
            <?= $this->fetch('content') ?>
        </div>
    </main>
    <footer>
    </footer>
</body>

なるほど、なかなかボリュームがありますな。

[CakePHP3.10] 認証コンポーネント

UsersController.php

use Cake\Auth\DefaultPasswordHasher;
use Cake\Event\Event;

    public function initialize(){
        parent::initialize();
        $this->loadComponent('RequestHandler');
        $this->loadComponent('Flash');
        $this->loadComponent('Auth', [
                'authorize' => ['Controller'],
                'authenticate' => [
                    'Form' => [
                        'fields' => [
                            'username' => 'username',
                            'password' => 'password'
                        ]
                    ]
                ],
                'loginRedirect' => [
                    'controller' => 'Users',
                    'action' => 'login'
                ],
                'logoutRedirect' => [
                    'controller' => 'Users',
                    'action' => 'login'
                ],
                'logoutRedirect' => [
                    'controller' => 'Users',
                    'action' => 'logout',
                ],
                'authError' => 'ログインしてください。',
            ]);
    }

    function login(){
        if($this->request->isPost()){
            $user = $this->Auth->identify();
            if(!empty($user)){
                $this->Auth->setUser($user);
                return $this->redirect($this->Auth->redirectUrl());
            }
            $this->Flash->error('ユーザ名かパスワードが間違っています。');
        }
    }

    public function logout(){
        $this->request->session()->destroy();
        return $this->redirect($this->Auth->logout());
    }

    public function beforeFilter(Event $event){
        parent::beforeFilter($event);
        $this->Auth->allow(['login', 'index', 'add']);
    }

    public function isAuthorized($user = null){
        if($user['role'] === 'admin'){
            return true;
        }
        if($user['role'] === 'user'){
            return false;
        }
        return false;
    }

authorize, authenticate, loginRedirect, logoutRedirect, authError
$this->request->session()->destroy();

### login.ctpを作成

<div class="users form">
<?= $this->Flash->render('auth') ?>
<?= $this->Form->create() ?>
	<fieldset>
		<legend>アカウント名とパスワードを入力して下さい。</legend>
		<?= $this->Form->input('username') ?>
		<?= $this->Form->input('password') ?>
	<fieldset>
<?= $this->Form->button(__('Login')); ?>
<?= $this->Form->end() ?>
</div>

AuctionBaseController.phpを作成し、継承させる
なるほどー

[CakePHP3.10] auctionサイトを作ろう

$ php composer.phar create-project –prefer-dist cakephp/app:3.* auction

bin/cake bake migration CreateUsers username:string[100] password:string[100] role:string[20]
bin/cake bake migration CreateBiditems user_id:integer name:string[100] finished:boolean endtime:datetime created
bin/cake bake migration CreateBidinfo bititem_id:integer user_id:integer price:integer created
bin/cake bake migration CreateBidrequests biditem_id:integer user_id:integer price:integer created
bin/cake bake migration CreateBidmessages bidinfo_id:integer user_id:integer message:text created
bin/cake migrations migrate
bin/cake bake all users
bin/cake bake all biditems
bin/cake bake all bidinfo
bin/cake bake all bidrequests
bin/cake bake all bidmessages

[CakePHP3.10] ページネーション

– コントローラのページネーション: 一定数ごとにレコードを取り出す
– ビューテンプレートのリンク

controller

$this->loadComponent('Paginator');

PeopleController.php

	public $paginate = [
		'limit' => 5,
		'sort' => 'id',
		'direction' => 'asc',
		'contain' => ['Messages'],
	];

	public function initialize(){
		parent::initialize();
		$this->loadComponent('Paginator');
	}

	public function index(){
		$data = $this->paginate($this->People);
		$this->set('data', $data);
	}

template

<p>This is People table records.</p>
<table>
<thead><tr>
	<th>id</th><th>name</th><th>mail</th><th>age</th><th>message</th>
</tr></thead>
<?php foreach($data->toArray() as $obj): ?>
<tr>
	<td><?=h($obj->id) ?></td>
	<td><a href="<?=$this->Url->build(["controller"=>"People", "action"=>"edit"]); ?>?id=<?=$obj->id ?>"><?=h($obj->name) ?></a></td>
	<td><?=h($obj->mail) ?></td>
	<td><?=h($obj->age) ?></td>
	<td><?php foreach($obj->messages as $item): ?>
	"<?=h($item->message) ?>"<br>
	<?php endforeach; ?></td>
	<td><a href="<?=$this->Url->build(["controller"=>"People", "action"=>"delete"]); ?>?id=<?=$obj->id ?>">delete</a></td>
</tr>
<?php endforeach; ?>
</table>
<div class="paginator">
	<ul class="pagination">
		<?=$this->Paginator->first(' |<< ' . '最初へ') ?>
		<?=$this->Paginator->prev(' << ' . '前へ') ?>
		<?=$this->Paginator->next('次へ ' . ' >> ') ?>
		<?=$this->Paginator->last('最後へ' . ' >>| ') ?>
	</ul>
</div>

custom finderを利用することもできる

なるほど、とりあえずOK 次に行こう

[CakePHP3.10] ヘルパー

$this->Html->doctype(document type)
$this->Html->charset(charset)
$this->Html->css(file name)
$this->Html->style()
$this->Html->script()
$this->Html->scriptStart()
$this->Html->scriptEnd()
$this->Html->link()
$this->Html->image()
$this->Html->script()
$this->Html->tableHeaders()
$this->Html->tableCells()

<table>
<?=$this->Html->tableHeaders(["title", "name", "mail"],
	["style"=>["background:#006; color:white"]]) ?>
<?=$this->Html->tableCells([["this is sample", "taro", "taro@yamada"],["this is sample", "taro", "taro@yamada"],["this is sample", "taro", "taro@yamada"]], ['style'=>['background:#ccf']], ['style'=>['background:#ccf']]) ?>
</table>

$this->Html->nestedList()

アクションの指定、クエリパラメータの指定、拡張子の指定などもUrlBuilderで指定できる

### Textヘルパー

<?=$this->Text->autoLinkUrls('http://google.com') ?>
<?=$this->Text->autoLinkEmails('hoge@gmail.com') ?>
<?=$this->Text->autoParagraph('one\ntwo\nthree') ?>

Numberヘルパー

<p>金額は、<?=$this->Number->currency(1234567, 'JPY') ?>です。</p>
<p>2桁で表すと、<?=$this->Number->precision(1234.56789, 2) ?>です。</p>
<p>2桁で表すと、<?=$this->Number->toPercentage(0.12345, 2, ['multiply'=>true]) ?>です。</p>

OK、大分いいところまで来た気がする

[CakePHP3.10]bakeとマイグレーション

config/bootstrap.php

Plugin::load('Migrations');

$ cake bake migration Create 項目1:タイプ

$ bin/cake bake migration CreateMoview title:string content:text stars:integer created:datetime
$ bin/cake bake migration CreateMovie title:string content:text stars:integer created:datetime

Creating file /home/vagrant/dev/cake/mycakeapp/config/Migrations/20220715040907_CreateMoview.php
Wrote `/home/vagrant/dev/cake/mycakeapp/config/Migrations/20220715040907_CreateMoview.php`

$ bin/cake migrations migrate
/config/Migrations/20220715040907_CreateMoview.php

$ bin/cake bake all movies
http://192.168.56.10:8080/movies

src/Model/Entity/Movie.php

    protected $_accessible = [
        'title' => true,
        'content' => true,
        'stars' => true,
        'created' => true,
    ];

src/Model/Table/MoviesTable.php

    public function validationDefault(Validator $validator)
    {
        $validator
            ->integer('id')
            ->allowEmptyString('id', null, 'create');

        $validator
            ->scalar('title')
            ->maxLength('title', 255)
            ->requirePresence('title', 'create')
            ->notEmptyString('title');

        $validator
            ->scalar('content')
            ->requirePresence('content', 'create')
            ->notEmptyString('content');

        $validator
            ->integer('stars')
            ->requirePresence('stars', 'create')
            ->notEmptyString('stars');

        return $validator;
    }

src/Controller/MoviesController.php

    public function index()
    {
        $movies = $this->paginate($this->Movies);

        $this->set(compact('movies'));
    }

    /**
     * View method
     *
     * @param string|null $id Movie id.
     * @return \Cake\Http\Response|null
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function view($id = null)
    {
        $movie = $this->Movies->get($id, [
            'contain' => [],
        ]);

        $this->set('movie', $movie);
    }

    /**
     * Add method
     *
     * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise.
     */
    public function add()
    {
        $movie = $this->Movies->newEntity();
        if ($this->request->is('post')) {
            $movie = $this->Movies->patchEntity($movie, $this->request->getData());
            if ($this->Movies->save($movie)) {
                $this->Flash->success(__('The movie has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The movie could not be saved. Please, try again.'));
        }
        $this->set(compact('movie'));
    }

    /**
     * Edit method
     *
     * @param string|null $id Movie id.
     * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function edit($id = null)
    {
        $movie = $this->Movies->get($id, [
            'contain' => [],
        ]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $movie = $this->Movies->patchEntity($movie, $this->request->getData());
            if ($this->Movies->save($movie)) {
                $this->Flash->success(__('The movie has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The movie could not be saved. Please, try again.'));
        }
        $this->set(compact('movie'));
    }

    /**
     * Delete method
     *
     * @param string|null $id Movie id.
     * @return \Cake\Http\Response|null Redirects to index.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function delete($id = null)
    {
        $this->request->allowMethod(['post', 'delete']);
        $movie = $this->Movies->get($id);
        if ($this->Movies->delete($movie)) {
            $this->Flash->success(__('The movie has been deleted.'));
        } else {
            $this->Flash->error(__('The movie could not be deleted. Please, try again.'));
        }

        return $this->redirect(['action' => 'index']);
    }

[CakePHP3.10] viewテンプレート

<p>This is People table records.</p>
<?=$this->Form->create($entity,
	['type'=>'post',
	'url'=>['controller'=>'Messages',
	'action'=>'index']]) ?>
	<fieldset class="form">
		person id
		<?= $this->Form->error('Messages.person_id'); ?>
		<?= $this->Form->text('Messages.person_id'); ?>
		Message
		<?= $this->Form->error('Messages.message'); ?>
		<?= $this->Form->text('Messages.message'); ?>
		<?= $this->Form->submit('投稿') ?>
	</fieldset>
<?=$this->Form->end() ?>

<hr>
<table>
	<thead>
		<tr><th>ID</th><th>Message</th><th>name</th><th>created at</th>
		</tr>
	</thead>
	<?php foreach($data->toArray() as $obj): ?>
	<tr>
		<td><?=h($obj->id) ?></td>
		<td><?=h($obj->message) ?></td>
		<td><?=h($obj->person->name) ?></td>
		<td><?=h($obj->created_at) ?></td>
	</tr>
	<?php endforeach; ?>
</table>
<p>This is People table records.</p>
<?=$this->Form->create(null, ["type"=>"post", "url"=>["controller"=>"People", "action"=>"index"]]) ?>
<div>find</div>
<div><?=$this->Form->text("People.find") ?></div>
<div><?=$this->Form->submit("検索") ?></div>
<?=$this->Form->end() ?>

<table>
<thead><tr>
	<th>id</th><th>name</th><th>messages</th>
</tr></thead>
<?php foreach($data->toArray() as $obj): ?>
<tr>
	<td><?=h($obj->id) ?></td>
	<td><a href="<?=$this->Url->build(["controller"=>"People", "action"=>"edit"]); ?>?id=<?=$obj->id ?>"><?=h($obj->name) ?></a></td>
	<td><?php foreach($obj->messages as $item): ?>
	"<?=h($item->message) ?>"<br>
	<?php endforeach; ?></td>
	<td><?=h($obj->age) ?></td>
	<td><a href="<?=$this->Url->build(["controller"=>"People", "action"=>"delete"]); ?>?id=<?=$obj->id ?>">delete</a></td>
</tr>
<?php endforeach; ?>
</table>