【CakePHP】Authorization Plugin

$ php composer.phar require “cakephp/authorization:^2.0”

bootstrap() method in src/Application.php:
src/Application.php

        $this->addPlugin('Authorization');

Enabling the Authorization Plugin

use Authorization\AuthorizationService;
use Authorization\AuthorizationServiceInterface;
use Authorization\AuthorizationServiceProviderInterface;
use Authorization\Middleware\AuthorizationMiddleware;
use Authorization\Policy\OrmResolver;
            ->add(new AuthorizationMiddleware($this));
    public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface {
        $resolver = new OrmResolver();

        return new AuthorizationService($resolver);
    }

lets add the AuthorizationComponent to AppController. In src/Controller/AppController.php add the following to the initialize() method.

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

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

Lastly we’ll mark the add, login, and logout actions as not requiring authorization by adding the following to src/Controller/UsersController.php

        $this->Authorization->skipAuthorization();

Creating our First Policy
$ bin/cake bake policy –type entity Article

src/Policy/ArticlePolicy.php

namespace App\Policy;

use App\Model\Entity\Article;
use Authorization\IdentityInterface;

class ArticlePolicy {

	public function canAdd(IdentityInterface $user, Article $article){
		return true;
	}

	public function canEdit(IdentityInterface $user, Article $article){
		return $this->isAuthor($user, $article);
	}

	public function canDelete(IdentityInterface $user, Article $article){
		return $this->isAuthor($user, $article);
	}

	public function isAuthor(IdentityInterface $user, Article $article){
		return $article->user_id === $user->getIdentifier();
	}
}

src/Controller/ArticlesController.php

    public function add()
    {
        $article = $this->Articles->newEmptyEntity();
        $this->Authorization->authorize($article);

	public function edit($slug) {
		$article = $this->Articles
			->findBySlug($slug)
			->contain('Tags')
			->firstOrFail();
		$this->authorization->authorize($article);

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

	    $article = $this->Articles->findBySlug($slug)->firstOrFail();
	    $this->Authorization->authorize($article);

add tag, view, index method

$this->Authorization->skipAuthorization();

なるほど、methodの中でAuthorizationを使うか使わないか記述するのね。

Pythonのpycファイルのマジックナンバー

Pythonファイル(平文ソースコード)をコンパイルするとpycファイルになる。
pythonユーザがpycファイルを作成する必要なし。モジュールとしてインポートされる場合に自動的に作成
元のモジュールが更新されると、次回インポート時にpycファイルも自動的に再作成

マジックナンバーとは、ファイルの種類を識別するため、ファイル先頭に付与する特別なバイト列。Pythonのバージョンごとにpycファイルのマジックナンバーが決まっている。

$ python3
Python 3.8.10 (default, Mar 15 2022, 12:22:08)
[GCC 9.4.0] on linux
Type “help”, “copyright”, “credits” or “license” for more information.
>>> import importlib.util
>>> importlib.util.MAGIC_NUMBER.hex()
‘550d0d0a’

sessionファイルの生成場所、中身、有効期限

sessionファイルの場所
1. php.iniのsession.save_path
2. php.iniのsys_temp_dir
3. /tmp

今回ubuntuの場所では、/var/lib/php/sessionsに格納されていた。
$ sudo cat sess_t8mk08vtmj3vh29s8rhq4tsh22
hoge|s:4:”hoge”;fuga|s:4:”fuga”;
【セッション名】|s:【セッションのID】:【値】で保存される

session_encodeという関数でエンコードしている
なるほど、基礎的な挙動は理解した。

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

composerを使ったautoload

Composerはパッケージ管理以外にオートロードという機能を合わせ持ち、require文を使用しなくてもクラスファイルを読み込むことができる

### require文の使い方
Userクラスを作成する

src/User.php

class User{

	protected $name;

	public function __construct($name){
		$this->name = $name;
	}

	public function get_user_name(){
		return $this->name;
	}
}

index.php

require('src/User.php');

$user  = new User('taro');

var_dump($user->get_user_name());

composer.json

{
	"autoload": {
		"psr-4": {
			"Test\\": "src"
		}
	}
}

php composer.phar dump-autoload
/vendor/composer/autoload_psr4.php

$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);

return array(
    'Test\\' => array($baseDir . '/src'),
);
namespace Test;

class User{

	protected $name;

	public function __construct($name){
		$this->name = $name;
	}

	public function get_user_name(){
		return $this->name;
	}
}

なるほど、requireではなく、composerでautoloadできることはわかった。

PHP PSR-4とは

PSRとはPHP Standard Recommendationsのことで、PHP-FIG(PHP Interop Group)が昨制定しているコーディング規約のこと。PRSにはいくつか種類がある。
PSR-4はAutoloader
ファイルパスからクラスをオートロードするための規約
PSR-0という削除予定の規約もある
\(\)*\

どのパスにどのクラスを配置するのか仕様を決めたいというのが根にある

psr-0とpsr-4があるってことはわかったが、autoloadの仕組みがよくわからん。require_onceとは違うのね。。。

PHPと名前空間

namespaceは名前空間と呼ばれ、項目をカプセル化するときに使用する
通常、同じファイルに同じクラスや関数名、定数めいが存在することはできないが、名前空間を使用することにより、関連するクラスやインターフェイス、関数、定数などをグループ化することが可能
名前が衝突することを防ぐことができる
namespaceは以下のように記述する

namespace 名前空間名;

ソースコードの一番先頭で宣言する必要があり、namespaceの前にHTMLを記述するとエラーになる

sample1.php

namespace name1;
function getName(){
	return "fruit";
}

namespace name2;
function getName(){
	return "ore";
}

sample2.php

//phpファイルを読み込む
require_once 'sample1.php';
 
//関数の呼出し
echo name1\getName();
echo name2\getName();

$ php sample2.php
fruitore

namespaceを宣言すると、その後に記述した関数はその名前空間の関数に属する
名前空間\関数名とすれば同一の関数を呼び出すことが可能になる

波括弧で指定したい範囲を囲むこともできる

namespace name1 {
	function getName(){
		return "fruit";
	}
}

namespace name2{
	function getName(){
		return "ore";
	}
}

波括弧で囲むと、名前空間の範囲外で命令文を記述することができなくなってしまう

namespace name1 {
	function getName(){
		return "fruit";
	}
}


namespace name2{
	function getName(){
		return "ore";
	}
}

echo "Hello World!";

### useの使い方
useとは名前空間の拡張機能で、外部のエイリアスやその一部を参照したり、クラス・関数・定数などをインポートするときに使用する

namespace asia\japan\tokyo\shibuya;

function getName(){
	return "shibuya";
}


namespace asia\japan\tokyo\minato;

function getName(){
	return "minato";
}

namespace asia\japan\tokyo\shinagawa;

function getName(){
	return "shinagawa";
}
[_code]


require_once "sample1.php";

use asia\japan\tokyo as name;

echo name\shibuya\getName();
echo name\minato\getName();

名前空間のnamespaceとuseはなんとなくわかったが、composer.jsonのpsr-4, psr-0がイマイチよくわからん。。

packageを自作してcomposer.jsonで動かそう

### ライブラリ側
$ tree
.
├── composer.json
├── src
│   └── Muuu
│   └── Uuuu.php
└── test

/src/Muuu/Uuuu.php

class Dog {

	private $name;

	public function __construct($name){
		$this->name = $name;
	} 

	public function getName(){
		return $this->name . "さん!";
	}
}

$ git init
$ git add .
$ git commit -m “first commit”
$ git remote add origin
$ git push -u origin master

### プロジェクト側
composer.json

{
    "name": "library/test",
    "description": "Composer sample project",
    "authors": [
    	{
    		"name": "hpscript",
    		"email": "hpscript@gmail.com"
    	}
    ],
    "type": "project",
    "minimum-stability": "dev",
    "require": {
    	"nuuu/muuu": "*"
    },
    "autoload": {
        "psr-4": {"App\\": "src/"}
    },
    "repositories": {
    	"nuuu/muuu": {
    		"type": "vcs",
    		"url": "https://github.com/hpscript/package.git"
    	}
    }
}
require_once "./vendor/autoload.php";
use App\Muuu\Uuuu;

$dog = new Uuuu::Dog("hoge");
$dog_name = $dog->getName();
echo $dog_name;

名前空間とclassの概念はよくわからん。。。

composer管理の考え方

・パッケージ作成
・composerで管理
・別プロジェクトで使用
・gitのプライベートリポジトリでパッケージは管理

パッケージのディレクトリがプロジェクト
基本的にはPackagist(https://packagist.org/)で管理されている
公開したくないパッケージはprivate リポジトリからの読み取りも可能
タグでバージョン管理ができる

### パッケージのディレクトリ構成

<package>/
    .git/
    .gitignore
    composer.json
    src/
        <namspace>/
            <class>.php
    tests/
        <namspace>/
            <class>Test.php

### パッケージを使う側のプロジェクト

<root>/
    composer.json
    composer.lock
    <vendor>/
        autoload.php
        <composer>/
        <any packages>/
        ...

### composer.jsonの書式
読み込む側のcomposer.json

{
    "name": "<vendor>/<project_name>",
    "description": "Composer sample project",
    "authors": [
    	{
    		"name": "<author>",
    		"email": "<mail@gmail.com>"
    	}
    ],
    "require": {
    	"nuuu/muuu": "^1.0"
    },
    "autoload": {
    	"psr-4": {"<NamespacePrefix>\\": "src/"}
    },
    "repositories": [
    	{
    		"type": "vcs",
    		"url": "ssh://user@loacalhost/var/git/repos/nuu_muu.git"
    	}

    ]
}

呼び出し

<?php

require_once "./vendor/autoload.php";
use Nuuu\Muuu\Uuuu;

Uuuu::sayHello();

なるほど、テストしてみないとわからんな。

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
        ]);
    }