[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で指定した場所にセッションファイルが保存される
冗長化の場合は、別に保存する

セッションとは?

– セッションとは、通信を開始してから終了するまで
– セッションとはサーバ側にデータを保存する仕組み

### セッションとクッキーの違い
セッションはサーバにデータを保存するのに対し、クッキーはブラウザ側に保存する
ブラウz側に保存されているデータはユーザ側で内容を書き換えられる
セッションに格納する場合はサーバ側にファイルが作成される
セッションIDはクッキーで保存されている

<?php

session_start();

PHPSESSIDという値でcookieに保存される

セッション情報の取得

<?php

print("セッションIDは".$_COOKIE['PHPSESSID']. ".");

### セッション情報の書き込みと削除

session_start();
print("セッションIDは".$_COOKIE['PHPSESSID']. ".");

$_SESSION['username'] = 'sato';
echo 'ユーザ名は'. $_SESSION['username']. '.';

unset($_SESSION['username']);

if(!isset($_SESSION['username'])){
	echo 'ユーザ名は削除されました';
} else {
	echo 'ユーザ名は'. $_SESSION['username']. '.';
}

セッションはブラウザを閉じるまで保存
unset($_SESSION[‘username’]); でセッションを削除
セッションIDがブラウザに保存される 
通常はランダムのセッションIDを発行する

クッキーは管理したい値をクライアント側で管理するが、セッションはサーバ側で管理する(セッションIDのみクライアント側で管理)
セッションのデフォルト値はphp.iniのsession.cookie_lifetimeに保存されている値

有効期限を上書きできる

session_start([
	'cookie_lifetime' => $300,
]);

なるほど、sessionの値がどこに管理されているかわからんが… なんとなく基礎的なことはわかった

[CakePHP3.10] バリデーション2

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

		$validator
			->scalar("name")
			->requirePresence("name", "create")
			->notEmpty("name");

		$validator
			->scalar("mail")
			->allowEmpty("mail")
			->email("mail");

		$validator
			->integer("age")
			->requirePresence("age", "create")
			->notEmpty("age");

		return $validator;
	}

### エラーメッセージを個別に表示

<p><?=$msg ?></p>
<?=$this->Form->create($entity,
	['type'=>'post',
	'url'=>['controller'=>'People',
	'action'=>'add']]) ?>
<fieldset class="form">
	NAME: <?=$this->Form->error('People.name') ?>
	<?=$this->Form->text('People.name') ?>

	MAIL: <?=$this->Form->error('People.mail') ?>
	<?=$this->Form->text('People.mail') ?>
	AGE: <?=$this->Form->error('People.age') ?>
	<?=$this->Form->text('People.age') ?>
	<?=$this->Form->submit('送信') ?>
</fieldset>
<?=$this->Form->end() ?>

### 日本語のバリデーション

	public function validationDefault(Validator $validator){
		$validator
			->integer('id', "idは整数で入力下さい。")
			->allowEmpty('id', 'create');

		$validator
			->scalar("name", "テキストを入力下さい。")
			->requirePresence("name", "create")
			->notEmpty("name", "名前は必ず記入してください。");

		$validator
			->scalar("mail", "テキストを入力下さい。")
			->allowEmpty("mail")
			->email("mail", false, "メールアドレスを記入してください。");

		$validator
			->integer("age")
			->requirePresence("age", "create")
			->notEmpty("age", "必ず値を入力下さい。")
			->greaterThan('age', -1, "ゼロ以上の値を記入ください。");

		return $validator;
	}

### バリデーションメソッド
必須項目: requirePresence, notBlank, notEmpty
空白許可: allowEmpty
ASCII: ascii
数字のみ: numeric, integer, naturalNumber, nonNegativeInteger, decimal
文字と数字のみ: alphaNumeric
真偽値のみ許可: boolean
配列のみ: isArray
半角英数字: containsNonAlphaNumeric
等式: equals, notEquals
比較式: lessThan, lessThanOrEquals, greaterThan, greaterThanOrEquals
指定した範囲: range
半角数字が含まれているか: constrainsNonAlphaNumeric
等式: equal, notEquals
比較式: lessThan, lessThanOrEquals, greaterThan, greaterThanOrEquals
文字数: minLength, maxLength, lengthBetween
日時の入力: date, time, dateTime
配列に含まれるか: inList
2つの項目が同じか: sameAs
項目が含まれるか: hasField
メールアドレス: email
クレジットカード番号: creditCard
URLチェック: url
ipアドレス: ip, ipv4, ipv6
uuid: uuid

なるほど、バリデーションは主にmodelのテーブルで行うのか。controllerではないのね。

[CakePHP3.10] バリデーション

PeopleTable.php

namespace App\Model\Table;

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

// 省略

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

		$validator
			->scalar("name")
			->requirePresence("name", "create")
			->notEmpty("mail");

		$validator
			->integer("age")
			->requirePresence("age", "create")
			->notEmpty("age")

		return $validator;
	}

integer(項目名), scalar(項目名): 値のタイプを指定
requirePresence(項目名, モード), フィールドの確認
allowEmpty(項目名, モード): 空を許可

なるほどー、少し理解が深まったが、まだまだ時間が足りんな…

[CakePHP3.10] 動的ファインダーとクエリビルダー

動的ファインダーは findBy${name} or findBy${name}Or${name} というような使い方をする

$data = $this->People->findByNameOrMail($find, $find);

クエリビルダーは検索条件を分けて書くことができる
演算子も使える

$data = $this->People->find()->where(["name"=>$find]);
$data = $this->People->find()->where(["name like"=>$find]);

// andWhere と orWhere
$arr = explode(",", $find);
$data = $this->People->find()->where(["age >=" => $arr[0]])
				->andWhere(['age >=' => $arr[1]])->order(["People.age"=>"asc"]);

			$data = $this->People->find()
				->order(["People.age" => "asc"])
				->order(["People.name" => "asc"])
				->limit(3)->page($find);

findの後をmodelのtableで定義しておいて、controllerの呼び出しメソッドで使えるのね。なるほど。