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

【Python3】LambdaでS3にファイルを作成する方法

LambdaでS3にファイルを作成する方法を簡単に解説します。

(1)boto3のput_objectでS3にファイルを作成する
(2)access_key_id, secret_access_key, region_nameをLambdaの環境変数で設定
(3)lambdaにdeployし、テストで実行
(4)S3で作られたファイルを確認

ソースコード

import boto3
import datetime
import os

def lambda_handler(event, context): 
	client = boto3.client(
	    's3',
	    aws_access_key_id=os.environ['aws_access_key_id'],
	    aws_secret_access_key=os.environ['aws_secret_access_key'],
	    region_name=os.environ['region_name']
	)
	dt = datetime.datetime.now()
	client.put_object(Body="This is lockfile", Bucket = "hpscript", Key = dt.strftime("%Y-%m-%d") + ".txt")

PythonでS3に接続

#!/usr/bin/env python3  Line 1
# -*- coding: utf-8 -*- Line 2

import boto3

client = boto3.client(
	's3',
	aws_access_key_id="",
	aws_secret_access_key="",
	region_name="ap-northeast-1"
)

print(client.list_buckets())

$ pip3 install python-dotenv

.env

AWS_ACCESS_KEY_ID=''
AWS_SECRET_ACCESS_KEY=''
REGION_NAME='ap-northeast-1'

Fileupload

import boto3
from dotenv import load_dotenv

load_dotenv()
client = boto3.client('s3')

Filename = "lock.txt"
Bucket = "hpscript"
Key = "lockfile.txt"
client.upload_file(Filename, Bucket, Key)
import boto3
from dotenv import load_dotenv
import datetime

load_dotenv()
client = boto3.client('s3')
dt = datetime.datetime.now()
client.put_object(Body="This is lockfile", Bucket = "hpscript", Key = dt.strftime("%Y-%m-%d") + ".txt")

これをlambdaで実行したい。

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

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

openapi-generatorを使いたい2

openapi.yaml

openapi: 3.0.3
info:
  description: "GET/POST IPv4 Address"
  version: "1.0.0"
  title: "openapi-rust"
tags:
  - name: "IP"
paths:
  /ip:
    get:
      responses:
        "200":
          description: "Get Global IPv4 address of the system"
          content:
            application/json:
              schema:
                type: object
                properties:
                  IPv4_address:
                    type: string
                    format: ipv4
                  checked_at:
                    type: string
                    format: date-time
        "500":
          description: "Internal Server Error"
    post:
      requestBody:
        description: "IPv4 address to register"
        content:
          application/json:
            schema:
              properties:
                IPv4_address:
                  type: string
                  format: ipv4
      responses:
        "200":
          description: "The new IPv4 address has been registered"
        "500":
          description: "Internal Server Error"

$ sudo npm install @openapitools/openapi-generator-cli -g

Makefile

generate:
  openapi-generator-cli generate \
    -i ./openapi.yaml \
    -g rust-server \
    -o .

なんか上手くいかないです

openapi-generatorを使いたい

openapi.yaml

openapi: 3.0.2
info:
  version: 0.1.0
  title: example
servers:
  - url: 'http://192.168.56.10:8080/example'
paths:
  /hello:
    get:
      description: Hello World
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Hello'
      tags:
        - example
components:
  schemas:
    Hello:
      type: object
      properties:
        message:
          type: string
          example: 'Hello World'
      x-tags:
        - example

application.yaml

inputSpec: 'openapi.yaml'
generatorName: spring
outputDir: modules/application
additionalProperties:
  configPackage: 'com.mamezou_tech.example.controller.configuration'
  modelPackage: 'com.mamezou_tech.example.controller.model'
  apiPackage: 'com.mamezou_tech.example.controller.api'
  invokerPackage: 'com.mamezou_tech.example.controller.api'
  groupId: 'com.mamezou_tech.example-service'
  dateLibrary: java8
  java8: true
  library: spring-boot
  artifactId: 'example-application'
  artifactVersion: '0.1.0'
  snapshotVersion: 'true'
  useTags: true

$ sudo docker run -it –rm -v `pwd`:/build -v example:/root/.m2 maven:3.8-eclipse-temurin-17-focal bash
$ curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.0.0/openapi-generator-cli-6.0.0.jar -o /tmp/openapi-generator-cli.jar
$ cd /build
$ java -DsupportingFiles -jar /tmp/openapi-generator-cli.jar batch application.yaml
[main] INFO o.o.codegen.cmd.GenerateBatch – Batch generation using up to 4 threads.
Includes: /build
Root: /build
[pool-1-thread-1] Generating spring (outputs to /build/modules/application)…
################################################################################
# Thanks for using OpenAPI Generator. #
# Please consider donation to help us maintain this project 🙏 #
# https://opencollective.com/openapi_generator/donate #
################################################################################
[pool-1-thread-1] Finished generating spring…
[SUCCESS] Batch generation finished 1 generators successfully.
$ cd modules/application
$ mvn install

なんだろう、spring bootが多いな

Swaggerの各フィールドの詳細

Info

info:
  title: Sample API
  description: A short description of API.
  termsOfService: http://example.com/terms/
  contact:
    name: API support
    url: htttp://www.example.com/support
    email: support@example.com
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
  version: 1.0.0

servers

servers:
  - url: https://dev.sample-server.com/v1
    description: Development server
  - url: https://stg.sample-server.com/v1
    description: Staging server
  - url: https://api.sample-server.com/v1
    description: Production server

Path

paths:
  /users:
    get:
      tags:
        - users
      summary: Get all users.
      description: Returns an array of User model
      parameters: []
      response:
        '200': # http status
          description: A Json array of User model
          content:
            application/json: # レスポンスの形式指定
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User' # 参照するモデル
                example: 
                  - id: 1
                    name: Jane Doe
                  - id: 2
                    name: Jane Doe
    post:
      tags:
        - users
      summary: Create a new User
      descrpption: Create a new User
      parameters: []
      requestBody:
        description: user to create
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
            example:
              id: 3
              name: Richard Roe
      responses:
        '201':
          description: CREATED
  /users/{userId}:
    get:
      tags:
        - users
      summary: Get user by ID.
      description: Returns a single User model
      parameters: # リクエストパラメター
        - name: userId
          in: path # パラメータをパス内に含める
          description: user id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: A single User model
          content:
            application/json:
              schema:
                type: object
                items:
                  $ref: '#/components/schemas/User'
                example:
                  id: 1
                  name: John Doe

components
L 定義したモデルは $ref: ‘#/components/schemas/User’ というように参照する
L modelエリアに表示され、構造やサンプル値が参照できる

components:
  schemas:
    User:
      type: object # 型
      required:
        - id
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
    Product:
      type: object
      required:
        - id
        - price
      properties:
        id:
          type: integer
          format: int64
          example: 1
        name:
          type: string
          example: Laptop
        price:
          type: integer
          example: 1200

security
メソッド定義部分に鍵マークが表示される

security:
  - api_key []
  - users_auth:
    - write:users
    - read:users

externalDocs

exeternalDocs:
  description: Find more info here
  url: https://example.com

tags:

tags:
  - name: users
    description: Access to users
  - name: products
    descrption: Access to Products

なるほど、実際に書いてみると全然違うな…