[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>

ubuntu20.04にredisを入れる

$ sudo apt update
$ sudo apt install redis-server
$ sudo vi /etc/redis/redis.conf

supervised systemd

$ sudo systemctl restart redis.service
$ sudo systemctl status redis
● redis-server.service – Advanced key-value store
Loaded: loaded (/lib/systemd/system/redis-server.service; enabled; vendor >
Active: active (running) since Thu 2022-07-14 09:02:47 UTC; 11s ago
Docs: http://redis.io/documentation,
man:redis-server(1)
Process: 273821 ExecStart=/usr/bin/redis-server /etc/redis/redis.conf (code>
Main PID: 273833 (redis-server)
Tasks: 4 (limit: 4677)
Memory: 2.0M
CGroup: /system.slice/redis-server.service
└─273833 /usr/bin/redis-server 127.0.0.1:6379

$ sudo apt install php-redis
$ redis-cli
127.0.0.1:6379> ping

set

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$redis->set('key', 'PHP');

$redis->close();

get

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$value = $redis->get('key');
print($value);

$redis->close();

ほうほう、php-redisも何となく理解した

セッションをCookieでもつか、サーバ側のファイルで持つか

### Cookieで持つ場合
■メリット
サーバにデータをため込まないので、サーバサイドの負荷を気にしないで良い
アプリケーションサーバを分散させても一貫性を持たせられる
■デメリット
ユーザ側にデータがあるため、改ざんリスクが高まる(Railsは暗号化によって対策してますが)
Cookieの仕様上、4KBの容量制限がある
Cookieを乗っ取られた場合等でもサーバサイドからセッションを破棄する手段がない
ユーザがcookieを有効にしていない場合、ログイン情報などがうまく保存されない

### サーバ側でファイルで持つ場合
CakePHP、Laravelのでファオルトで、CookieにセッションIDだけ持たせて、サーバサイドに実データをファイルとして置く方法
■メリット
サーバサイドにデータがあるので改ざんを回避できる
データ容量制限がない
サーバサイドでセッションの破棄ができる
■デメリット
アプリケーションサーバを分散させた場合、一貫性が確保できない
1セッションにつき1ファイル生成されるので、大量にセッションファイルが出来てファイルシステムに負担が掛かる

### KVS(NoSQL)
CookieにセッションIDだけ持たせて、Memcached、RedisなどサーバサイドのKVSに実データを置く方法。
■メリット
– サーバサイドにデータがあるので改ざんを回避できる
– データ容量制限がない
– サーバサイドでセッションの破棄ができる
– アプリケーションサーバを分散させても一貫性を持たせられる
– 大量の同時I/Oに強く、アクセスが増大しても大丈夫!
■デメリット
セッション専用にひとつデータベースが追加されるのでコストが増
https://qiita.com/SuguruOoki/items/6ca36ad1d366df6c98af

コストに問題なければredis(NoSQL)が良さそうではある

セッション情報をデータベースで管理するサンプル

create database test;
use test;
create table TBL_SESSION (
SESSION_ID varchar(50) NOT NULL,
SESSION_DATA text,
CRA¥EATE_DATE int(10),
PRIMARY KEY (SESSION_ID));

class MySessionHandler implements SessionHandlerInterface {

	function close(){
		return true;
	}

	function destroy($session_id) {
		$db = get_db();
		if($stmt = $db->prepare("DELETE FROM TBL_SESSION WHERE SESSION_ID = ?")){
			$stmt->bind_param("s", $session_id);
			$stmt->execute();
			$stmt->close();
			$stmt = null;
		}
		$db->close();
		$db = null;
		return true;
	}

	function open($save_path, $name) {
		return true;
	}

	function read($session_id) {
		$session_data = "";
		$db = get_db();
		if($stmt = $db->prepare("SELECT SESSION_DATA FROM TBL_SESSION WHERE SESSION_ID = ?")){
			$stmt->bind_param("s", $session_id);
			$stmt->bind_result($session_data);
			$stmt->execute();
			$stmt->fetch();
			$stmt->close();
			$stmt = null;
		}
		$db->close();
		$db = null;

		if(is_null($session_data)){
			$session_data = "";
		}
		return $session_data;
	}

	function write($session_id, $sesion_data){
		$affect_rows = 0;
		$create_date = time();
		$db = get_db();
		if($stmt = $db->prepare("INSERT INTO TBL_SESSION (SESSION_ID, SESSION_DATA, CREATE_DATE) VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE SESSION_DATA = ?, CREATE_DATE = ?")){
			$stmt->bind_param("ssisi", $session_id, $session_data, $create_date, $session_data, $create_date);
			$stmt->execute();
			$affected_rows = $stmt->affected_rows;
			$stmt->close();
			$stmt = null;
		}
		$db->close();
		$db = null;
		return $affected_rows ? true : false;
	}
}

function get_db(){
	return new mysqli('localhost:3306', 'user', 'password', 'db_name');
}

session_set_save_handler(new MySessionHandler(), true);

session_start();

$_SESSION['data'] = 0;

Railsは全てのセッション情報をcookieに保存

ログインにおけるセッション管理

<body>
	<form action="login.php" method="post">
		ユーザID: <input type="text" name="userid"/><br>
		パスワード: <input type="password" name="password"/>
		<input type="submit" name="ログイン"/>
	</form>
</body>
session_start();

$userId = $_POST['userid'];

if(!isset($_SESSION[$userId])){
	$_SESSION[$userId] = $userId;
	echo "ログインしました。";
} else {
	echo "ログイン済みです。";
}

なるほど

### セッション情報の保管場所

<?php

phpinfo();

session.save_path /var/lib/php/sessions
セッション情報の場所

cat sess_vikafie7r8km1288sdkn1j5sat
username|s:4:”sato”;count|i:3;hello|s:5:”hello”;

php.iniで指定した場所にセッションファイルが保存される
冗長化の場合は、別に保存する