- 投稿日:2021-01-15T23:18:10+09:00
JavaScript,PHP,Javaのrandom関数をまとめてみる
今回はrandom関数を各言語ごとにまとめてみます。
なんでrandom関数なのかというとミニゲームなど作成時に使用する頻度が高く、好きな関数の一つだからです。
言語ごとに微妙に違いがあり、備忘録として記事にしておきます。ちなみにrandom関数にもアルゴリズムがあるので「擬似乱数」であって、
「真乱数」ではないところに注意。JavaScriptのMath.random()
JavaScriptのMath.random()関数は0以上1未満の値を返します。
※0は含むが1は含みません!let num = Math.floor(Math.random() * n) + m; // n = 上限値 , m = 下限値Math.random()は浮動小数点で返ってきますので、Math.floor()で小数点以下を切り捨てます。
「n」は上限値を設定し、「m」で下限値を設定します。例 ) 1〜10の乱数を生成する
let num = Math.floor(Math.random() * 10) + 1;
- Math.random()によって0以上〜1未満の乱数が生成される
- 10を乗算することによって0以上〜10未満の乱数となる
- Math.floor()によって小数点以下を切り捨て(ここで0〜9のいずれかの整数になっている)
- 1を足してあげることで1〜10のいずれかの整数が生成されます。
注意すべき点は1未満の数字を生成する点です。
PHPのrandom_int()
PHPで擬似乱数を生成するためにはrandom_int()関数を使用します。
$num = random_int(int $min, int $max);第一引数に下限値を渡し、第二引数に上限値を渡します。
min〜maxを含む乱数を整数値で返します。例 ) 1〜10の乱数を生成する
$num = random_int(1, 10);指定した数値を含むので簡単ですね。
JavaのRandom()クラス
Javaではjava.utilパッケージにあるRandom()クラスとnextInt()メソッドを使用します。
int num = new java.util.Random().nextInt(n);int型で返ってくるため変数はint型で宣言します。
nextInt()の引数が上限値となり、0〜上限値未満の値を返します。
JavaScriptと同じく指定した上限値自体は含みません!例 ) 1〜10の乱数を生成する
public class Main { public static void main(String[] args) { int num = new java.util.Random().nextInt(10) + 1; } }上限値を含まない点に注意して値を渡しましょう。
まとめ
Javascript
0以上1未満の浮動小数点を生成。
Math.floor()とセットで覚える。PHP
下限値、上限値指定した値を含む整数値を返す。Java
Random()クラスを使用する。
nextInt()メソッドの引数に上限値を渡す。
0以上「上限値」未満の整数を返す。こぼれ話
訓練校で「なぜ1未満なのか?」と先生に質問しました。
先生の答えはこうだった「なぜでしょうね〜、、、」
何か意図があって1未満なのかなと思ったが、
そんなの言語作った人にしかわからんよな。と思いました。それ以降、定義済の関数や変数、定数に疑問を抱くのはやめましたw
- 投稿日:2021-01-15T21:38:29+09:00
Laravel × Dacapo データベースマイグレーションサポートツールの使い方
ダカーポとは
https://github.com/ucan-lab/laravel-dacapo
Laravelマイグレーションのサポートライブラリです。
データベースのテーブル定義をYAMLファイル(スキーマファイル)で管理し、マイグレーションファイルの生成を行います。※初期の開発時期に利用されることを想定しています。
2021.01.11 v4.0.0 リリース!
2020.01.11 Laravel Dacapo v4.0 Released !https://t.co/qCFAAkaDtm #Laravel #PHP pic.twitter.com/VbbuFSNdAu
— ucan@Qiita新人賞 (@ucan_lab) January 11, 2021v3.0から1年の時を経て、ついにバージョンv4.0をリリース致しました!?♂️
以前の古いコードをすべて破棄して新しく書き直してます。
少し挙動が異なる部分もあるのでこの機会に改めて使い方をご紹介します。以前のダカーポの記事
ダカーポの由来
ダ・カーポ(da capo)は音楽用語で「始めに戻って繰り返す」という意味があります。
PHPのパッケージ管理ツールであるComposerも作曲家という音楽用語であり、マイグレーションファイルをすべて削除して、最初から生成し直すことからピッタリな名前だなと思いました。
ダカーポを作った理由
デフォルトのマイグレーションのツラミ
- make:migration で自動生成された日付がバラバラで見にくい
- make:migration のコマンドの実行時間が遅い
- テーブル定義 → インデックス定義 → 外部キー制約の順に定義する手間
- ゴミマイグレーションが残る
- 最終的なテーブル構成はDBを直接見ないと把握しづらい
ダカーポの歴史
- 2018.11.06 v0.0.1 4files 初回リリース
- 2019.08.22 v1.0.0 22files 1ファイルで収まらなくなり、すべて書き直し
- 2019.08.22 v2.0.0 24files schema.ymlを分割可能、全カラムタイプに対応
- 2019.11.10 v3.0.0 35files 細々とバグFIXを続ける
- 2021.01.11 v4.0.0 144files メタプロでメンテしづらく、すべて書き直し
v1.0では4ファイルだけで書いてました...1日で主機能を書き上げたのでとてもFatなソースコードになってましたね。
それに比べて最新のv4.0ではファイル数が36倍に増えましたww
今後のバージョンアップに備えてLaravelでサポートされているカラムタイプやカラム修飾子をすべてクラスで管理するようにしました。ダカーポのインストール
$ composer require --dev ucan-lab/laravel-dacapoダカーポの執筆時バージョン
- Dacapo: 4.0.1
ダカーポのサポート
- PHP 7.4 以降
- Laravel 6.x 以降
- MySQL
- PostgreSQL
- SQL Server(サポート対象外)
- SQLite(サポート対象外)
ダカーポの初期化
$ php artisan dacapo:init
dacapo:init
Artisanコマンドを使用して、ダカーポの初期化を行います。
database/schemas/default.yml
が生成されます。
また、database/migrations
ディレクトリ内のマイグレーションファイルも削除されます。--no-clear オプション
$ php artisan dacapo:init --no-clear
database/migrations
ディレクトリ内のマイグレーションファイルの削除は行いません。--laravel6, --laravel7 オプション
$ php artisan dacapo:init --laravel6 $ php artisan dacapo:init --laravel7 $ php artisan dacapo:init --laravel8 # defaultそれぞれLaravel6, Laravel7のマイグレーションファイルを生成します。
デフォルトでは、Laravel8のマイグレーションファイルを生成します。
それぞれのバージョンで微妙にテーブル定義内容が異なるので、テンプレートを分けています。
database/schemas/default.yml
database/schemas/default.yml
には、Laravelフレームワークにて予め用意されている(database/migrations)マイグレーション3ファイルのテーブルの定義しています。database/schemas/default.ymlusers: columns: id: bigIncrements name: string email: type: string unique: true email_verified_at: type: timestamp nullable: true password: string rememberToken: true timestamps: true password_resets: columns: email: type: string index: true token: string created_at: type: timestamp nullable: true failed_jobs: columns: id: true uuid: type: string unique: true connection: text queue: text payload: longText exception: longText failed_at: type: timestamp useCurrent: trueダカーポの実行
$ php artisan dacapo
database/schemas/*.yml
からテーブル構成を読み込んでdatabase/migrations/1970_01_01_*.php
のLaravelマイグレーションファイルを生成します。
また、ダカーポで生成された古いマイグレーションファイルはコマンド実行の都度削除されます。続けて、
php artisan migrate:fresh
コマンドが実行されます。
すべてのテーブルの削除、及びマイグレーションの実行が行われます。--seed オプション
$ php artisan dacapo --seed
php artisan migrate:fresh
に続けて
php artisan db:seed
コマンドが実行されます。--no-migrate オプション
$ php artisan dacapo --no-migrateスキーマファイルからマイグレーションファイルを実行したあと、
php artisan migrate:fresh
コマンドを実行しまいオプションです。--refresh オプション
$ php artisan dacapo --refreshロールバック処理が動作するか、動作確認のためのオプションです。
スキーマの定義フォーマット
テーブル名: columns: カラム名: カラムタイプ カラム名: type: カラムタイプ args: カラムタイプの引数(任意) indexes: - columns: [カラム名] type: [インデックス修飾子] name: インデックス名(任意) foreign_keys: - columns: [カラム名] references: [カラム名] on: テーブル名スキーマの定義
カラムの定義
database/schemas/default.ymlusers: columns: # カラムタイプのみ指定する場合 votes: integer # カラムタイプに引数を指定する場合 amount: type: double args: 8, 2上記のYAMLを定義して
php artisan dacapo
を実行すると、下記のマイグレーションファイルが出力されます。database/migrations/1970_01_01_000001_create_users_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('users', function (Blueprint $table) { $table->integer('votes'); $table->double('amount', 8, 2); $table->string('email')->nullable(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } }利用可能なカラムタイプ
*.columns.*.type
に指定できるカラムタイプです。
Laravelマイグレーションで利用可能なカラムタイプはすべて対応してます。bigIncrements, bigInteger, binary, boolean, char, dateTime, dateTimeTz, date, decimal, double, enum, float, foreignId, geometryCollection, geometry, id, increments, integer, ipAddress, jsonb, json, lineString, longText, macAddress, mediumIncrements, mediumInteger, mediumText, morphs, multiLineString, multiPoint, multiPolygon, nullableMorphs, nullableTimestamps, nullableUuidMorphs, point, polygon, rememberToken, set, smallIncrements, smallInteger, softDeletes, softDeletesTz, string, text, timestamps, timestampsTz, timestamp, timestampTz, time, timeTz, tinyIncrements, tinyInteger, unsignedBigInteger, unsignedDecimal, unsignedInteger, unsignedMediumInteger, unsignedSmallInteger, unsignedTinyInteger, uuidMorphs, uuid, year
カラム修飾子
上記リストのカラムタイプに加え、データベーステーブルにカラムを追加するときに使用できるカラム「修飾子」もあります。
users: columns: email: type: string nullable: true上記のYAMLを定義して
php artisan dacapo
を実行すると、下記のマイグレーションファイルが出力されます。database/migrations/1970_01_01_000001_create_users_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('users', function (Blueprint $table) { $table->string('email')->nullable(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } }利用可能なカラム修飾子
alwaysModifier, autoIncrementModifier, charsetModifier, collationModifier, commentModifier, defaultModifier, defaultRawModifier, fromModifier, generatedAsModifier, indexModifier, nullableModifier, storedAsModifier, uniqueModifier, unsignedModifier, useCurrentModifier, useCurrentOnUpdateModifier, virtualAsModifier
利用不可なカラム修飾子
下記のカラム修飾子はダカーポではサポート対象外となっております。
after, change, first, renameColumn, dropColumn, dropMorphs, dropRememberToken, dropSoftDeletes, dropSoftDeletesTz, dropTimestamps, dropTimestampsTz, constrained, onUpdate, onDelete
インデックス修飾子
database/schemas/default.ymlusers: columns: id: bigIncrements name: string email: string indexes: - columns: [name, email] type: index name: users_name_index
- 1970_01_01_000001_create_users_table.php
- 1970_01_01_000002_create_users_index.php
- インデックスの定義ファイルは
000002
のプレフィックスが付きます。database/migrations/1970_01_01_000001_create_users_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('users', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); $table->string('email'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } }database/migrations/1970_01_01_000002_create_users_index.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersIndex extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('users', function (Blueprint $table) { $table->index(['name', 'email'], 'users_name_index'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->dropIndex('users_name_index'); }); } }利用可能なインデックス修飾子
primary, unique, index, spatialIndex
外部キー制約
database/schemas/default.ymlusers: columns: id: bigIncrements name: string email: string posts: columns: id: bigIncrements user_id: unsignedBigInteger content: string foreign_keys: - columns: [user_id] references: [id] on: users
- 1970_01_01_000001_create_users_table.php
- 1970_01_01_000001_create_posts_table.php
- 1970_01_01_000003_constraint_posts_foreign_key.php
- 外部キーの定義ファイルは
000003
のプレフィックスが付きます。database/migrations/1970_01_01_000001_create_users_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('users', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); $table->string('email'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } }database/migrations/1970_01_01_000001_create_posts_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('user_id'); $table->string('content'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('posts'); } }database/migrations/1970_01_01_000003_constraint_posts_foreign_key.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class ConstraintPostsForeignKey extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('posts', function (Blueprint $table) { $table->foreign(['user_id'])->references(['id'])->on('users'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('posts', function (Blueprint $table) { $table->dropForeign(['user_id']); }); } }その他
通常のマイグレーションファイルと併用可能
$ php artisan make:migration create_flights_table
php artisan dacapo
でdatabase/migrations
ディレクトリ内のマイグレーションファイルが削除されますが、
ダカーポで生成されたファイルのみ削除されます。(1970_01_01
のプレフィックスが付いたファイルのみ)YAMLで表現できないようなテーブル定義は通常のマイグレーションファイルを作成してください。
複数のスキーマファイルを利用可能
database/schemas/*.yml
ファイルを読み込むので、YAMLを分けて定義できます。
database/schemas/user.yml
database/schemas/staff.yml
ユーザー関連のテーブル、スタッフ関連のテーブル等、グループ分けをしてテーブルを定義していくこともできます。
今後の予定
ざっくり今後のダカーポについて検討していることを書きます。
あくまで予定で、全機能実装できるかは未定です...。v4.0
年末年始に一気に書き殴ったので、しばらくはリファクタリングに勤しみます。
新機能 v4.1
dacapo --mixin オプション
SQLiteは外部キー制約を書く場合は、テーブル定義と同じタイミングで実行する必要があるみたいです。
懸念点としては、制約を貼る順番によってはエラーになりそうなので生成順序を気にしないといけなさそう...?
難しそうなのでこのオプションの実装悩んでます。。dacapo --squash オプション
Laravel8に搭載されたマイグレーションスカッシングのイメージです。
ただ、mysqldump
コマンド入れてる必要があったり、.sql
ファイルで出力されちゃったりと不満があります。
--squash
オプションは1ファイルのマイグレーションファイルにまとめるオプションです。
これは需要ありそうかなと思ってるんですけどどうでしょうか??新機能 v4.2
テンプレート生成するコマンド
$ php artisan dacapo:generate:model $ php artisan dacapo:generate:factory $ php artisan dacapo:generate:seeder $ php artisan dacapo:generate:validationスキーマYAMLから各種テンプレートを生成する機能です。
v3系には合ったのですが、バージョンアップの兼ね合いで一旦コマンドを削除しました。v4.0系でも実装しようと思ったのですが、
Laravel8でモデルファクトリ書き方が丸っと変わったのでちょっと断念しました。新機能 v4.3
データベースからスキーマYAMLを逆生成するコマンド
$ php artisan dacapo:db:importこの機能が欲しくてv4.0の書き直しをしたので、なんとしてでもこの機能は実装する!
新機能 v4.4
データベースとスキーマYAMLの差分からマイグレーションを生成するコマンド
$ php artisan dacapo:db:diff
import
コマンドが実装できたら現行のテーブル構成とスキーマYAMLの差分ができたらいいなと思ってます。
これは実装難しそうなので悩んでます。
- 投稿日:2021-01-15T21:24:23+09:00
Laravel ウェブサイト サイバーセキュリティ#2 -SQLインジェクション-
概要
Laravelを用いたウェブサイトのサイバーセキュリティについて考察。
サイバーセキュリティに関して全く考えずにコーディングした後、「あれ、セキュリティのこと全く考えてないな」と気づいて調べてみたところ、結果的にlaravelフレームワークでほとんど対策できていた。第二回は、SQLインジェクションとディレクトリトラバーサル。
参考: https://www.ipa.go.jp/security/vuln/websecurity.html環境
- Windows 10
- PHP 8.0.0
- laravel 8.16.1
SQLインジェクション
概要
データベースを操作するSQL文を操作し、データベースを不正利用する攻撃。
【流れ】
1. 攻撃者が、入力フォームにSQL文の一部を入力し送信する。
2. サイト内で、入力情報をもとにSQL文を処理する際、意図しないデータベース操作が行われてしまう。脅威
- 非公開データの流出
- データの改ざん、消去
- 不正ログイン
Laravelにおける対策
まずは、クエリビルダを使うことを考える。
クエリビルダを使うことによって、PDO(*1)パラメータによるバインディング(*2)が自動的に使用されることになるため、追加の対策は不要。
*1: PHP Data Objectsの略。データベースの違いを吸収してくれる。
*2: 命令の一部を変数にすること。クエリビルダを使わず、生のSQL文を使用する場合は、SQL文の条件となるユーザからの入力値に対して、エスケープ処理を施すことが対策となる。ここでは、
' => ''
\ => \\
の変換を行えばよい。
こちらは、laravel特有ではなく、一般的なSQLインジェクション対策。ディレクトリトラバーサル
概要
設計者が意図しないファイルにアクセスされる攻撃。
脅威
- ファイルの不正閲覧、ダウンロード
Laravelにおける対策
まずは、外部パラメータをファイルパスとして指定しないようにする。
それが避けられない場合、固定ディレクトリ+ファイル名の構成にする。ファイル名の抽出は、
basename
関数を用いる。さらに保険として、ログイン中のユーザIDなどで、適切なアクセス制限をかけると尚良い。
まとめ
XSSやCSRFと同じく、外部からの入力を直接埋め込まないことが基本的な考え方になる。
今回は、Laravel特有のクエリビルダやPHPのbaseline関数を使う方法を紹介した。
- 投稿日:2021-01-15T21:06:38+09:00
【Laravel】php artisan コマンド
Laravelで使う php artisanコマンド
artisan アルチザンと読むらしい。アルチザンとは、 職人。技工。とかの意味らしい。
対話モード
$ php artisan tinkerこれで対話モードができる。
データベースの中身などを調べるときに使える。例えば,,,
User::all()このように行えば、Userテーブルのデータを確認することができる。
マイグレーション
$ php artisan migrateこれで、マイグレーションの実行ができる.
マイグレーションのやり直し
$ php artisan migrate:freshこれで、一旦全てのテーブルを削除しマイグレーションをやりなおすことができる。
seedデータの投入
$ php artisan db:seedseedファイルで作成したデータをDBに保存する
- 投稿日:2021-01-15T20:59:50+09:00
laravel をブラウザで表示しようとしてcould not be opened in append mode: failed to open stream: Permission deniedとなった時の解決法
larval をローカル環境であれこれ編集しようとして、
途中経過をブラウザで表示しようとしていたのですが、could not be opened in append mode: failed to open stream: Permission denied
とエラーが起きていつまでも解決しませんでした。
上記のエラーメッセージの手前に該当箇所のファイルの階層が示されており
そこの編集権限がない?といったメッセージ内容。解決できそうな参考記事をあれこれあたってみても、
sudo chmod 777 storage -R
と書かれていて、バカ正直にそのまま打ち込むと、
-R なんてファイルは存在しません。(No such file or directory) と出てくる。。。
そりゃそうか。誰かこの先詰まった時の参考になれば良いと思い書き残しておきます。
・エラーの意味
Permission denied : 権限が拒否されました。
つまり何かしら権限を書き換える必要がありそう。・どこの権限がない?
laravel プロジェクトが入っているXAMMP内の、~/laravelプロジェクト名/strage/logs/laravel.log で編集権限がないと言われました。
larval 開発の最初って多分このエラーにぶち当たることが多いのか。。?・やったこと
logsディレクトリ以下の権限を書き換えられるように以下のコードを
自分のディレクトリ名に合わせて実行(エラーで表示されたディレクトリ名を最後のファイル名除いてコピペ)$ sudo chmod 777 /Applications/XAMPP/xamppfiles/htdocs/laravelプロジェクト名/strage/logs
→ 実行これで権限の書き換えができるのでエラーが起きているファイルをリロード。
表示されました!めちゃくちゃスッキリした。。
- 投稿日:2021-01-15T18:57:48+09:00
複数ファイルを1回でrsyncする
--files-fromオプション
複数のファイルを1回のrsyncコマンドで送信する
準備
/path/to/images.txtにファイル名を入力(改行で区切る)
images.txtfile1.png file2.png file3.png実装
※送信元のディレクトリにimages.txtに記入したファイルがあることを確認
$images = '/path/to/images.txt' $src_path = '/path/to/src/'; $tar_path = '/path/to/tar/'; $rsync_host = 'ユーザ名@送信先IPアドレス'; $command = "rsync -av --files-from=".$images." ".$src_path." ".$rsync_host.":".$tar_path; exec ( $command, $output, $ret ); if($ret == 0) { return true; } else { return false; }
- 投稿日:2021-01-15T18:14:00+09:00
【vscodeで xdebug3.0 × PHP × Docker】
【ディレクトリ構造】
.docker-practice ├── html │ └─ arraypractice.php // デバックするファイル ├── php │ ├── Dockerfile │ └── php.ini └── docker-compose.yml【最終的なコードと設定】
docker-compose.yml
version: '2' services: php: build: ./php ports: - '80:80' volumes: - ./html:/var/www/html - ./php/php.ini:/usr/local/etc/php/php.ini同じ階層のphpディレクトリ内のDockerfile
FROM php:7.4-apache # php.iniをdockerコンテナ内にコピって設置している COPY php.ini /usr/local/etc/php/ # xdebugのinstallと設定に必要なzend_extensionを生成している RUN pecl install xdebug \ && docker-php-ext-enable xdebug # /usr/local/etc/php/conf.d/docker-php-ext-xdebug.iniに # zend_extension = ~ 出力されるphp.iniにxdebug3.0の設定を追加
[xdebug] xdebug.mode=debug xdebug.start_with_request = yes // host.docker.internalはdockerのhostマシンのIPを解決してくれる。 xdebug.client_host=host.docker.internal xdebug.client_port=9003vscodeのlaunch.json
{ "version": "0.2.0", "configurations": [ { "name": "Listen for XDebug", "type": "php", "request": "launch", "port": 9003, // xdebug.client_portに合わせる "pathMappings": { // dockerコンテナ側ルートディレクトリ : vscode側ルートディレクトリ "/var/www/html": "${workspaceRoot}/html" } } ] }あとは./html直下のファイルにブレークポイントをつけて実行&ブラウザで読み込みすればbreakpointで止まってくれて、変数なんかもVARIABLESに表示される。
(21行目ではちゃんとksort後の$arrayが表示されている。)
【こういったコードになった経緯】
1,最初はdockerコンテナに入って、
pecl install xdebug
でxdebugをinstallしようとしたらphpのvarsionが7.2以上にするようにエラーが出たのでDockerfileのイメージを7.0→7.4にした。2,とりあえずphpinfo()にxdebugの項目がでるようにしたかったが、
docker-php-ext-enable xdebug
でzend_extensin = ~
を作らないと表示されなかったので追加した。
また、buildしなおすたびにxdebugをinstallし直すのは面倒なのでDockerfileのRUNコマンドで実行できるようにした。3,php.iniについては、
xdebug.start_with_request = yes
がないとbreakpointで止まってくれなかった。4,vscodeのlaunch.jsonは、pathMappingsは最初意味がわからず、
"./html" : "{workspaceRoot}" みたいにしてたが、dockerコンテナ側のルートディレクトリ : vscode側のワークスペースだとわかって変更。
- 投稿日:2021-01-15T18:12:39+09:00
laravel/uiを利用した際に発生したエラーの解決
現在、Laravel(ver6.20.9)を使って、個人でWebサービスを制作している者です。
Laravel6.xからBootstrapやVueなどがデフォルトでは含まれなくなったそうなので、それらをLaravelで使えるようにしてくれる「laravel/ui」をダウンロードすることにしました。
しかし、その過程で出たエラーに詰まったのでメモしていきたいと思います。
(といっても非常に簡単なことでした。笑)laravel/uiのダウンロード
laravel/uiのダウンロードについては、以下を参考にしました。
laravel/uiの公式ドキュメント
Laravel/UIのインストール(Qiita記事)自分なりに手順をまとめると、
ターミナル①cdコマンドで、laravel/uiを利用したいディレクトリまで移動 ②composer require laravel/ui "1.x" --dev (composer必須) ③php artisan ui bootstrap (Bootstrapを使う場合) php artisan ui vue (Vueを使う場合) php artisan ui react (Reactを使う場合) ④npm install ⑤npm run dev②の"1.x"部分については、以下の表を参考にご自身のLaravelのバージョンに合わせて変えてください。
laravel/uiのバージョン Laravelのバージョン 1.x 5.8、6.x 2.x 7.x 3.x 8.x ④と⑤については、Node.jsのパッケージを管理するNPM(Node Package Manager)を利用しています。
④npm installにより、用意されている便利な機能(パッケージ)をインストールすることができ、⑤でwebpack.mix.jsというファイル内に書いてある指示を実行できるようです。
ここでエラーが…
先ほどの手順④を実行した時に
ターミナル(中略) found 1 high severity vulnerability run `npm audit fix` to fix them, or `npm audit` for detailsん? 何か出てきた。
日本語訳してみると
「1つの重要度の高い脆弱性が見つかりました。
npm audit fix
でこれらを修正してください。また、npm audit
で詳細を確認できます」とのことです。
そこで、指示通り「npm audit fix」を実行すると、
ターミナル% npm audit fix + axios@0.21.1 added 1 package from 3 contributors and updated 1 package in 5.855s 51 packages are looking for funding run `npm fund` for details fixed 1 of 1 vulnerability in 1096 scanned packages無事解決できたようです。
「あ〜よかった」と思ったのも束の間、手順⑤の「npm run dev」を実行したところ…
ターミナルInvalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - configuration.context: The provided value "/Users/ユーザー名/Laravel_CRUD/Atsumare!" contains exclamation mark (!) which is not allowed because it's reserved for loader syntax. -> The base directory (absolute path!) for resolving the `entry` option. If `output.pathinfo` is set, the included pathinfo is shortened to this directory. - configuration.module.rules[8].exclude should be one of these: RegExp | string | function | [(recursive)] | object { and?, exclude?, include?, not?, or?, test? } | [RegExp | string | function | [(recursive)] | object { and?, exclude?, include?, not?, or?, test? }] -> One or multiple rule conditions Details: * configuration.module.rules[7].exclude[0]: The provided value "/Users/ユーザー名/Laravel_CRUD/Atsumare!/resources/sass/app.scss" contains exclamation mark (!) which is not allowed because it's reserved for loader syntax. * configuration.module.rules[7].exclude[0]: The provided value "/Users/ユーザー名/Laravel_CRUD/Atsumare!/resources/sass/app.scss" contains exclamation mark (!) which is not allowed because it's reserved for loader syntax. * configuration.module.rules[8].exclude[0]: The provided value "/Users/ユーザー名/Laravel_CRUD/Atsumare!/resources/sass/app.scss" contains exclamation mark (!) which is not allowed because it's reserved for loader syntax. * configuration.module.rules[8].exclude[0]: The provided value "/Users/ユーザー名/Laravel_CRUD/Atsumare!/resources/sass/app.scss" contains exclamation mark (!) which is not allowed because it's reserved for loader syntax. - configuration.output.path: The provided value "/Users/ユーザー名/Laravel_CRUD/Atsumare!/public" contains exclamation mark (!) which is not allowed because it's reserved for loader syntax. -> The output directory as **absolute path** (required). npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! @ development: `cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --config=node_modules/laravel-mix/setup/webpack.config.js` npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the @ development script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /Users/ユーザー名/.npm/_logs/2021-01-xxTxx_xx_xx_135Z-debug.log npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! @ dev: `npm run development` npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the @ dev script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /Users/ユーザー名/.npm/_logs/2021-01-xxTxx_xx_xx_168Z-debug.logなんか赤い字でいっぱい出てきた!
ここで僕は少し詰まってしまいましたが、ある記事を見つけました。
Shawn Dhaveさんの回答を引用します。
Had the same error. The problem seems to be in the path of your folder. It must not contain an exclamation mark(!). Change folder 'HelloWorld!' to just 'HelloWorld' without the exclamation mark.
Hope this solves the problem.日本語訳すると
「自分も同じエラーを経験したよ。
フォルダーのパスに問題があるように見えるね。フォルダーのパスに感嘆符(!)を含めたらいけないんだ。だから、フォルダー名を『HelloWorld!』じゃなくて、感嘆符のない『HelloWorld』だけに変えてみて。これが解決策になりますように」になります。
ん?
フォルダー名に「!」をつけたらだめ?pwdコマンドで自分のフォルダ名(パス)を確認してみます。
ターミナル/Users/ユーザー名/Laravel_CRUD/Atsumare!つけてんじゃん!!
僕は「Atsumare!(あつまれ!)」という名前で募集掲示板型のサービスを作ろうとしており、それに合わせてフォルダー名も「Atsumare!」にしていたのですが…
どうやらそれがエラーの原因になっていたようです。というわけで、「「Atsumare!」を「Atsumare」に修正しました。
そして⑤の「npm run dev」を実行すると…
ターミナル> @ dev /Users/ユーザー名/Laravel_CRUD/Atsumare > npm run development > @ development /Users/ユーザー名/Laravel_CRUD/Atsumare > cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --config=node_modules/laravel-mix/setup/webpack.config.js DONE Compiled successfully in 7151ms 16:36:11 Asset Size Chunks Chunk Names /css/app.css 178 KiB /js/app [emitted] /js/app /js/app.js 1.08 MiB /js/app [emitted] /js/app無事実行できました!!
教訓
「フォルダー名(ディレクトリ名)に「感嘆符(!)」をつけるのはやめましょう」
という教訓でした?
皆様もお気をつけて!
- 投稿日:2021-01-15T17:10:15+09:00
【PHP】Laravel 8.X ソースページリスト
ヘルパ関数
アクセス制限
Gate
Laravel 8.xAuthorize
Laravel 8.xMiddlewareの割り当て(authによる制限を前提とする)
Laravel 8.x変数
- Bladeビューに渡す
Laravel 8.xauth関連
- ログインユーザー取得
Laravel 8.x
- コントローラー冒頭に
use Illuminate\Support\Facades\Auth;
を記述すること
- 名前空間:ファイルの居場所を示す
- use宣言:中で使うクラスを宣言する
フォームメソッド・バリデーション・エラーメッセージ
-フォームメソッド
Laravel 8.x
- バリデーション・エラーメッセージ
Laravel 8.x
- 投稿日:2021-01-15T17:10:15+09:00
【PHP】 Laravel 8.X ソースページリスト
ヘルパ関数
アクセス制限
Gate
Laravel 8.xAuthorize
Laravel 8.xMiddlewareの割り当て(authによる制限を前提とする)
Laravel 8.x変数
- Bladeビューに渡す
Laravel 8.xauth関連
- ログインユーザー取得
Laravel 8.x
- コントローラー冒頭に
use Illuminate\Support\Facades\Auth;
を記述すること
- 名前空間:ファイルの居場所を示す
- use宣言:中で使うクラスを宣言する
条件分岐(if、elseif、else、endif)
Migrateionカラム修飾子
フォームメソッド・Store&Update処理・バリデーション・エラーメッセージ
- フォームメソッド
Laravel 8.x- Store&Update処理
Laravel 8.x- バリデーション・エラーメッセージ
Laravel 8.xパラメーターを用いたルーティング
リレーション
デバッグ
- dump
Laravel 8.xページネーション
- 投稿日:2021-01-15T16:58:33+09:00
Laravel8→6系に変えたら色々苦戦した
はじめに
私は現在、PHP/Laravelでポートフォリオ作成をしている学習中の者です。
もし間違っている箇所があればお知らせいただけたら幸いです。ポートフォリオを作成中に、Laravelのバージョン8系では記述の仕方が変わっていたり、(新しいため)情報も少なく、長い目でみて時間ロスが多く発生すると思い、6系に変えてみようと思いました。
開発初期段階だったので、軽い気持ちで変えてみたら色々エラーが出たので記事にしました。
こちらの記事よりDockerの環境構築をさせていただきました。Laravelのバージョンを指定して環境構築するやり方も載っています。
最強のLaravel開発環境をDockerを使って構築する【新編集版】 - QiitaLaravel8に関してはこちらの記事が参考になりました。
Laravel8がリリースされました!新機能の翻訳 | WEBコンサルティング・WEB制作のフリーランス uiuifreeLaravelのバージョンについてはこちらの記事が参考になりました。
Laravelのバージョンのおすすめは?2020年から学び始めるなら - ネビ活 | ネットビジネス生活
Laravel6系に変更後
バージョン変更に関しては割愛します。
プロジェクト内のデータが8系の時のままだと以下のようなエラーが出ます。シーディング実行エラー
ターミナル$ php artisan migrate:fresh --seed //注意してお使いください。 //このコマンドはデータベースの中身を全部クリアにしてからシーディングデータを入れ直すものです。このコマンドでデータベースにデータを入れようとしたところ以下のエラーが発生
エラー内容Illuminate\Contracts\Container\BindingResolutionException : Target class [PostTableSeeder] does not exist.解決法
8系と6系とでは記述が変わっていた為、
seederファイル
を修正database/seeds/PostTableSeeder.php<?php //namespace Database\Seeders;←削除 use Illuminate\Database\Seeder; //use Illuminate\Support\Facades\DB;←削除 class PostsTableSeeder extends Seeder { //~~Modelファイルエラー
$ php artisan serveでlocalhostに繋ぎ確認してみると以下のエラーに
Trait 'Illuminate\Database\Eloquent\Factories\HasFactory' not found解決法
Laravel8ではHttp/Modelディレクトリの配下に入ってましたが、Laravel6ではHttpディレクトリの配下に直接入れるのがデフォルトのようです。
Httpディレクトリ配下に移し空になったModelディレクトリを削除。
Modelファイルを一部書き換えます。app/Http/Post.php<?php //namespace App\Models; //↓以下に修正 namespace App; use Illuminate\Database\Eloquent\Factories\HasFactory;//削除 use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory;//削除Controllerエラー
Controllerファイルも書き換えます。
app/Http/Controllers/PostController.php<?php namespace App\Http\Controllers; //use App\Models\Post; //↓以下に修正 use App\Post; use Illuminate\Http\Request; class PostController extends Controller {Routingエラー
Routingファイルも書き換えます
routes/web.php<?php use Illuminate\Support\Facades\Route; //use App\Http\Controllers\TestController; //削除 //Route::get('/',[PostController::class, 'index']); //↓以下のように修正 Route::get('/', 'PostController@index');
ひとまずこれにて無事に6系での開発が進めらる状態になりました。
特に初学者の方はLaravelで何か作り始める前にバージョンはよく考えて決めましょう(笑)
- 投稿日:2021-01-15T16:29:59+09:00
composerの沼【大幅バージョンアップ編】
既存のアプリケーションのバージョンアップを行うにあたって、composerの扱いにかなり悩んだ為記録しました。
状況
PHPやフレームワークのバージョンをかなり一気に上げなければならない
もし少しずつバージョンアップでも良いなら、下記で良いはずで、もしエラーが出たら個別に対処すれば良いのだがcomposer require <パッケージ名>:<バージョン>一気にバージョンを上げる場合、収集つかないくらいのエラーが表示される
依存関係や、パッケージがgitからダウンロードできないやら、色々、、個別に対処するのは不可能と判断。解決策
オプションをつける
下記のオプションをつけることにした
--update-with-dependencies 依存関係にあるパッケージをアップデートする --prefer-dist githubからではなくdistからダウンロードするただ、これでもまだ依存関係に関するエラーが表示される
(依存関係にあるものはオプションで解決できるはずなのではないの、、?)1つのコマンドに引数としてパッケージを渡す
だったら一度のrequireコマンドにエラーの元となる依存関係のあるパッケージを一気に渡せば依存パッケージの更新(追加)を解決しながらエラーなく進むかも、とやってみることにした。
composer require --update-with-dependencies --prefer-dist <パッケージ名>:<バージョン>これを実行して
Problem 1 - <パッケージ名>:<バージョン>[3.0.0, ..., 3.x-dev] require <依存パッケージ名>:<バージョン> ^3.0.0 -> found <依存パッケージ名>:<バージョン>[dev-master, 3.0.0a1, ..., 3.x-dev (alias of dev-master)] but the package is fixed to 2.3.4 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command. Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.こういうのが出たら、<依存パッケージ名>:<バージョン>を実行コマンドに追記していく。
composer require --update-with-dependencies --prefer-dist <パッケージ名>:<バージョン> <依存パッケージ名>:<バージョン>これをひたすら繰り返す事で最終的にエラーなく処理が終了するところまで辿り着き、composer.jsonも.lockも想定のバージョンになり、アップデートしたものと依存関係にないパッケージについてはバージョン据え置きという、想定どおりの動作ができた。
requireとrequire-devを分けたい時は
ひとつのコマンドでrequireとrequire-devのセクションを分けながらパッケージの追加はできない為、一気にパッケージ追加を行う中でどうしても開発用と分けてコマンド実行できない時は、一旦requireセクションにパッケージが追加された後に下記を実行すれば良い。
composer require --no-update --dev <パッケージ名>:<バージョン> <依存パッケージ名>:<バージョン>一旦まとめ
という方法でできたけどcomposer歴が短すぎてこの方法がこういう場合の正攻法なのかわからない。
もっと良い方法がある、やこの方法に落とし穴がある等もしあれば、ご指摘頂けると助かります。さらに気になったので
上記までで最低限の依存パッケージのバージョンアップデートはできたが、本当に最低限なのが気になった。
具体的には、新バージョンと諸々同じ環境で新規にフレームワークによるプロジェクトを作成した際のデフォルトのcomposer.jsonに記載されているパッケージのバージョンは、確かにフレームワーク自体のバージョンは上記までの作業で一致しているが、パッケージ名からは完全に関係しているとしか思えない(でもフレームワークをrequireした時には依存関係には出てこなかった)パッケージのバージョンが一致していない。
(PHP自体のバージョンとか、フレームワークのマイグレーションやデバッグ用のパッケージのバージョンはかなり古いまま、フレームワークのバージョンだけ新しくなっている状態)依存関係にないのが不思議なくらいだが、そうなってないなら仕方ないので自分でなんとかすることにした。
具体的には
①同じバージョンの新規プロジェクトを作成
②できあがったcomposer.jsonに記載のバージョンを元にrequireコマンドを作成
③コマンドを実行してエラーになったら上記「オプションをつける」「1つのコマンドに引数としてパッケージを渡す」を参考に対応(まぁこちらもエラー出ますよね)これやるのとやらないのとでは、やった方が良さそうだけど、これも掘り下げれば良い解決方法があるのかもしれない。
おまけ1
最初はrequireコマンドじゃなくてremoveコマンドでやろうと思っていました。依存関係とバージョンアップが多すぎて収集つかない為。
バージョンアップするものと関係ありそうなパッケージをremoveでjsonから削除し、その後update --dry-runでlockファイルから削除されるパッケージ名を確認、パッケージ名指定で
updateを行う事で関係ないパッケージのアップデートは行われずにremoveで削除したパッケージ関連のものがlockファイルから消える為、一度古いバージョン関連のものを全部消して、改めて新しいバージョンのものを追加する、的な。composer remove --no-update <パッケージ名> <パッケージ名> <パッケージ名>下記のような問題点がありこの方法はやめました
・removeするパッケージを自分で決めるしかない、名前を元にしてだいたいでやるか、packagist等で依存関係を調べるか、、後者は終わりが見えない
・--no-updateオプションを付ける事でremove時にupdateは走らない(はず)と思って実行したけど走った(これはmacOSの自動変換が走った事でオプションが有効にならなかった疑惑あり)
・updateコマンドの引数(パッケージ数)が大変なことになった採用した方の方法でrequireに付けた引数もかなり多かったけど、updateの引数よりはマシ、、
おまけ2
悩み始めた初期の頃、そもそもcomposerのバージョンが開発時と違うという点も気になりcomposerも開発時のバージョン(1.)に戻して色々試してみたけど、処理がすごく遅いですね!
ただ古いバージョンのパッケージのインストールが途中でエラーにならなかったり(gitからではなくdistが優先だったりするのかな?オプションつけなくても)等のメリットもありましたが、最新バージョン(2.)と互換性もありdistからダウンロードするようオプションつけさえすれば良いので、管理するパッケージがかなり古いバージョンの環境だとしても、composer(1.*)を使う必要は全くありませんでしたが、一応コマンドを。composer self-update --1 //1つ古いバージョンへ composer self-update --2 //新しいバージョンへ
- 投稿日:2021-01-15T16:05:34+09:00
【PHP】Formメソッド
- 投稿日:2021-01-15T13:30:14+09:00
【ボドゲ】モザイクを実装してみた【ビットボード】
前置き
PHPを用いてモザイクをビットボードで実装した際の知見を共有する記事です。
- ビットボードとは? -> 調べてください。
- モザイクとは? -> ボードゲームです。[概要]
当初はC++で実装したのですが、PHPのFFIで使おうとしたところSegfaultが発生してしまい、力不足で解消できなかったため泣く泣くPHPで再実装しました。
こんな人向けの記事
- ビットボードに興味がある。
- PHPでビットボードを実装したい。
- intで表現できる64bitより多い桁が必要。
- モザイクを実装したいがロジックが思い付かない。
完成品
https://github.com/izayoi256/mosaicgame-php
- 記事内のコードとは異なる部分があります。
- GPL 3.0ライセンスです。
ビット演算
リバーシなら
8*8=64
マス、つまり64bitのintで済みますが、モザイクは7段のピラミッド状で7²+6²+5²+...1²=140
マス、つまり140bitが必要です。なお記事内では簡略化のため、3段で
3²+2²+1²=14
マスのモザイクを実装することとします。まずは
BitSet
というインターフェースで抽象化することにします。インターフェース
AND/OR/XOR/左シフト/右シフト/popcount/set/clear/flip(反転)/文字列への変換といった必須の機能を宣言します。
その他、配列やイテレータへの変換等の補助的な機能は必要に応じてどうぞ。
interface BitSet extends ArrayAccess, Countable, IteratorAggregate { public function __toString(); public function toString(): string; public function size(): int; public function getIterator(); public function toArray(); public function count(); public function equalsTo(self $other): bool; public function set(int ...$offsets): self; public function setAll(): self; public function clear(int ...$offsets): self; public function clearAll(): self; public function and(self $other): self; public function or(self $other): self; public function xor(self $other): self; public function shift(int $amount): self; public function unshift(int $amount): self; public function flip(): self; }実装
僕はイミュータブルなクラスとして実装しました。
ただ、7段だと1ゲームで80000回以上インスタンス化することになるため、パフォーマンスが犠牲になっています。
ミュータブルかイミュータブルかはお好みでどうぞ。この記事ではイミュータブルとして話を進めます。
配列で実装する
開発開始当初は既存のライブラリを参考に、配列を用いて
ArrayBitSet
というクラスを実装しました。具体的な実装方法は参考元を確認してください。https://github.com/adagiolabs/bitset/blob/master/src/Adapter/ArrayBitSet.php
GMPで実装する
先述の配列による実装は素のPHPで実装可能ですが、パフォーマンスが良くないです。
PHPで多倍長整数を扱う方法を探したところ、GMP拡張にて実現できることが分かりました。
ビット演算に関する処理をすべてGMPに任せた
GMPBitSet
というクラスを実装して計測したところ、ArrayBitSet
と比較して57%ものパフォーマンス改善が達成できました。パフォーマンス改善のヒント
AND演算を例に上げます。
final class GMPBitSet implements BitSet { public function and(BitSet $other): BitSet { return new self( $this->size, gmp_and($this->gmp, ($other instanceof self) ? $other->gmp : gmp_init($other->toString(), 2)), ); } }
BitSet::and(BitSet $other)
の$other
はBitSet
を実装していますが、GMPBitSet
のインスタンスとは限りません。しかし常に
$other->toString()
を呼び出すと、$other
がGMPBitSet
だった場合に余計なコストがかかります。もし
$other
がGMPBitSet
だった場合はそのままgmp_and()
に渡すようにすれば、コストが節約できます。盤面
実装したBitSetを用いて、今度は盤面を表現します。
下図のように最上段の1マスを0としたインデックスで表現します。
盤面もインターフェース化して抽象化することにします。
インターフェース
盤面同士もビット演算できるように、ビット演算に関する機能を宣言します。
ある条件を満たす2*2マスの上方のマスを抽出する処理を勝手に
プロモーション
と名付けました。例えば、石が1つ置かれた上方のマスは
promoteOne()
、石が3つ以上置かれた上方のマスはpromoteMajority()
といった具合ですね。interface Board extends Countable { public function __toString(); public function toString(): string; public function getIterator(); public function toArray(); public function count(): int; public function size(): int; public function and(self $other): self; public function or(self $other): self; public function xor(self $other): self; public function equalsTo(self $other): bool; public function flip(): self; public function promoteZero(): self; public function promoteOne(): self; public function promoteTwo(): self; public function promoteThree(): self; public function promoteFour(): self; public function promoteHalfOrMore(): self; public function promoteMajority(): self; // public function mirrorHorizontal(): self; // public function flipVertical(): self; // public function flipDiagonal(): self; // public function rotate90(): self; // public function rotate180(): self; // public function rotate270(): self; }最下部のコメントアウトしてあるものは、盤面の変形(回転/反転)に関する機能です。
機械学習等で用いる際に盤面を正規化したければ、併せて実装するといいでしょう。
実装
まずは
BitSet
インターフェースで盤面を扱うBitSetBoard
という抽象クラスを実装します。盤面の処理のほとんどは
BitSetBoard
で扱うこととして、BitSet
を実装したオブジェクトを生成する処理を具象クラスとなるArrayBitSetBoard
やGMPBitSetBoard
に持たせることで、具象クラスの派生が容易になります。(以下、かなり長くなります)
abstract class BitSetBoard implements Board { public const MAX_SIZE = 7; /** @var int */ private $size; /** @var BitSet */ private $bitSet; protected function __construct(int $size, BitSet $bitSet) { assert(0 < $size && $size <= self::MAX_SIZE, sprintf('Board size must be between 1 and %d.', self::MAX_SIZE)); $this->size = $size; $this->bitSet = self::boardMask($size)->and($bitSet); } abstract protected static function stringToBitSet(int $size, string $string): BitSet; public static function fromString(int $size, string $cells): Board { return new static($size, static::stringToBitSet(self::bitSetSize($size), $cells)); } public static function emptyBoard(int $size): Board { static $boards = []; return $boards[$size] ?? ($boards[$size] = new static($size, self::zeroBitSet($size))); } public static function groundBoard(int $size): Board { static $boards = []; return $boards[$size] ?? ($boards[$size] = self::emptyBoard($size)->or(new static($size, self::layerMask($size, $size)))); } public static function neutralBoard(int $size): Board { static $boards = []; if (!isset($boards[$size])) { if ($size % 2 === 1) { $bitSet = self::oneBitSet($size); for ($i = 1; $i < $size; $i++) { $bitSet = $bitSet->shift($i ** 2); } $bitSet = $bitSet->shift(intdiv($i ** 2, 2)); $boards[$size] = new static($size, $bitSet); } else { $boards[$size] = self::emptyBoard($size); } } return $boards[$size]; } public static function filledBoard(int $size): Board { static $boards = []; return $boards[$size] ?? ($boards[$size] = new static($size, self::boardMask($size))); } public function __toString() { return $this->toString(); } public function toString(): string { return $this->bitSet->toString(); } public function size(): int { return $this->size; } public function count(): int { return count($this->bitSet); } public function flip(): Board { return new static($this->size, $this->bitSet->flip()); } public function and(Board $other): Board { return new static($this->size, $this->bitSet->and(self::boardToBitSet($other))); } public function or(Board $other): Board { return new static($this->size, $this->bitSet->or(self::boardToBitSet($other))); } public function xor(Board $other): Board { return new static($this->size, $this->bitSet->xor(self::boardToBitSet($other))); } public function equalsTo(Board $other): bool { return ($other instanceof self) ? $this->bitSet->equalsTo($other->bitSet) : $this->toString() === $other->toString(); } public function promoteZero(): Board { return $this->promote(self::PROMOTE_ZERO); } public function promoteOne(): Board { return $this->promote(self::PROMOTE_ONE); } public function promoteTwo(): Board { return $this->promote(self::PROMOTE_TWO); } public function promoteThree(): Board { return $this->promote(self::PROMOTE_THREE); } public function promoteFour(): Board { return $this->promote(self::PROMOTE_FOUR); } public function promoteHalfOrMore(): Board { return $this->promote(self::PROMOTE_HALF_OR_MORE); } public function promoteMajority(): Board { return $this->promote(self::PROMOTE_MAJORITY); } public function getIterator() { return $this->bitSet->getIterator(); } public function toArray() { return $this->bitSet->toArray(); } private function promote(int $type): self { $promotedBitSet = self::zeroBitSet($this->size); for ($srcLayerSize = $this->size; $srcLayerSize > 1; $srcLayerSize--) { $dstLayerSize = $srcLayerSize - 1; $srcLayerMask = self::layerMask($this->size, $srcLayerSize); $srcLayer = $this->bitSet->and($srcLayerMask); $promotedLayer = self::zeroBitSet($this->size); if ($type & (self::PROMOTE_ZERO | self::PROMOTE_ONE)) { $srcLayer = $srcLayer->flip()->and($srcLayerMask); } if ($type & (self::PROMOTE_ZERO | self::PROMOTE_FOUR)) { $p = $srcLayer->and($srcLayer->unshift(1)); $p = $p->and($p->unshift($srcLayerSize)); $promotedLayer = $promotedLayer->or($p); } if ($type & (self::PROMOTE_ONE | self::PROMOTE_THREE)) { $p1 = $srcLayer->and($srcLayer->unshift(1)); $p1 = $p1->xor($p1->unshift($srcLayerSize)); $p2 = $srcLayer->xor($srcLayer->unshift(1)); $p2 = $p2->xor($p2->unshift($srcLayerSize)); $promotedLayer = $promotedLayer->or($p1->and($p2)); } if ($type & self::PROMOTE_TWO) { $p1 = $srcLayer->xor($srcLayer->unshift(1)); $p1 = $p1->and($p1->unshift($srcLayerSize)); $p2 = $srcLayer->xor($srcLayer->unshift($srcLayerSize)); $p2 = $p2->and($p2->unshift(1)); $promotedLayer = $promotedLayer->or($p1)->or($p2); } if ($type & self::PROMOTE_MAJORITY) { $p1 = $srcLayer->and($srcLayer->unshift(1)); $p1 = $p1->or($p1->unshift($srcLayerSize)); $p2 = $srcLayer->and($srcLayer->unshift($srcLayerSize)); $p2 = $p2->or($p2->unshift(1)); $promotedLayer = $promotedLayer->or($p1->and($p2)); } if ($type & self::PROMOTE_HALF_OR_MORE) { $p1 = $srcLayer->or($srcLayer->unshift(1)); $p1 = $p1->and($p1->unshift($srcLayerSize)); $p2 = $srcLayer->or($srcLayer->unshift($srcLayerSize)); $p2 = $p2->and($p2->unshift(1)); $promotedLayer = $promotedLayer->or($p1)->or($p2); } for ($i = 0; $i < $dstLayerSize; $i++) { static $rowMasks = []; if (!isset($rowMasks[$dstLayerSize][$i])) { $rowMask = self::zeroBitSet($this->size) ->set(...range(0, $dstLayerSize - 1)) ->shift(self::layerShift($srcLayerSize)) ->shift(($srcLayerSize) * $i); $rowMasks[$dstLayerSize][$i] = $rowMask; } $rowMask = $rowMasks[$dstLayerSize][$i]; $promotedRow = $promotedLayer->and($rowMask); $promotedRow = $promotedRow->unshift($dstLayerSize * $dstLayerSize + $i); $promotedBitSet = $promotedBitSet->or($promotedRow); } } return new static($this->size, $promotedBitSet); } private static function zeroBitSet(int $size): BitSet { static $bitSets = []; return $bitSets[$size] ?? ($bitSets[$size] = static::stringToBitSet(self::bitSetSize($size), '0')); } private static function oneBitSet(int $size): BitSet { static $bitSets = []; return $bitSets[$size] ?? ($bitSets[$size] = static::stringToBitSet(self::bitSetSize($size), '1')); } private static function boardToBitSet(Board $board): BitSet { return ($board instanceof self) ? $board->bitSet : static::stringToBitSet($board->size(), $board->toString()); } private static function layerMask(int $size, int $layerSize): BitSet { static $layerMasks = []; if (!isset($layerMasks[$size][$layerSize])) { $layerMask = self::zeroBitSet($size); $j = $layerSize ** 2; for ($i = 0; $i < $j; $i++) { $layerMask = $layerMask->set($i); } $layerMask = $layerMask->shift(self::layerShift($layerSize)); $layerMasks[$size][$layerSize] = $layerMask; } return $layerMasks[$size][$layerSize]; } private static function layerShift(int $layerSize): int { static $layerShifts = []; if (!isset($layerShifts[$layerSize])) { $layerShift = 0; for ($i = 0; $i < $layerSize; $i++) { $layerShift += $i ** 2; } $layerShifts[$layerSize] = $layerShift; } return $layerShifts[$layerSize]; } private static function boardMask(int $size): BitSet { static $boardMasks = []; if (!isset($boardMasks[$size])) { $boardMask = self::zeroBitSet($size); for ($i = 1; $i <= $size; $i++) { $boardMask = $boardMask->or(self::layerMask($size, $i)); } $boardMasks[$size] = $boardMask; } return $boardMasks[$size]; } private static function bitSetSize(int $size): int { static $bitSetSizes = []; if (!isset($bitSetSizes[$size])) { $bitSetSize = 0; for ($i = 1; $i <= $size; $i++) { $bitSetSize += $i ** 2; } $bitSetSizes[$size] = $bitSetSize; } return $bitSetSizes[$size]; } }プロモーション処理の説明
大部分の機能は実装やメソッド名を読めば理解できるかと思います。(説明放棄)
プロモーション処理は複雑なため、下図の盤面を
promoteMajority
で処理するケースを説明します。
private function promote(int $type): self { $promotedBitSet = self::zeroBitSet($this->size);
$promotedBitSet
はプロモーション結果を保持するBitSetです。最初は空白の状態です。
for ($srcLayerSize = $this->size; $srcLayerSize > 1; $srcLayerSize--) { $dstLayerSize = $srcLayerSize - 1; $srcLayerMask = self::layerMask($this->size, $srcLayerSize); $srcLayer = $this->bitSet->and($srcLayerMask); $promotedLayer = self::zeroBitSet($this->size);プロモーション処理は段ごとに行ないます。
$srcLayerSize
はプロモーション元の段のサイズ、$dstLayerSize
はプロモーション先の段のサイズです。
$promotedLayer
は、段ごとのプロモーション処理を保持するBitSetです。下段->中段
ここでは
$srcLayerSize
は下段のサイズ=3、$dstLayerSize
は中段のサイズ=2とします。
$srcLayerMask
は下段のマスクです。これと盤面をAND演算したものが$srcLayer
です。
if ($type & self::PROMOTE_MAJORITY) { $p1 = $srcLayer->and($srcLayer->unshift(1)); $p1 = $p1->or($p1->unshift($srcLayerSize)); $p2 = $srcLayer->and($srcLayer->unshift($srcLayerSize)); $p2 = $p2->or($p2->unshift(1)); $promotedLayer = $promotedLayer->or($p1->and($p2)); }
promoteMajority
の場合、上記の箇所を通ります。
for ($i = 0; $i < $dstLayerSize; $i++) {最後に、行ごとにプロモーション処理を行ないます。
static $rowMasks = []; if (!isset($rowMasks[$dstLayerSize][$i])) { $rowMask = self::zeroBitSet($this->size) ->set(...range(0, $dstLayerSize - 1)) ->shift(self::layerShift($srcLayerSize)) ->shift(($srcLayerSize) * $i); $rowMasks[$dstLayerSize][$i] = $rowMask; } $rowMask = $rowMasks[$dstLayerSize][$i];
$rowMasks
は、$promotedLayer
でプロモーション先の行ごとに利用するマスクです。
$promotedRow = $promotedLayer->and($rowMask); $promotedRow = $promotedRow->unshift($dstLayerSize * $dstLayerSize + $i); $promotedBitSet = $promotedBitSet->or($promotedRow);
中段->上段
ここでは
$srcLayerSize
は下段のサイズ=2、$dstLayerSize
は中段のサイズ=1とします。コード自体は
下段->中段
と同じなので、画像のみ貼っていきます。
パフォーマンス改善のヒント
各所で
static
キーワードを使って計算結果やインスタンスをキャッシュすることで、重複する処理を少しでも減らすようにしています。ゲーム
書きたい内容のほとんどは書いてしまったので、実際にゲームを進行させる処理はソースコードを見てください。
https://github.com/izayoi256/mosaicgame-php
重要なポイントだけ以下で抑えます。
盤面の扱い
必須
実際の盤面を表現するには3つのBoardが必要になります。
- 先手番のBoard
$firstBoard
- 後手番のBoard
$secondBoard
- 中立(最初から置かれているマス)のBoard
$neutralBoard
補助
補助的な用途で以下のBoardがあると便利です。
- 最下段のマスク用Board
$groundBoard
コマが置かれているマスのBoard
protected function occupiedBoard(): Board { return $this->firstBoard->or($this->secondBoard)->or($this->neutralBoard); }コマが置かれていないマスのBoard
protected function vacantBoard(): Board { return $this->occupiedBoard()->flip(); }(そのマスにコマが置かれているかどうかに関わらず) コマを置けるマスのBoard
protected function scaffoldedBoard(): Board { return $this->groundBoard()->or($this->occupiedBoard()->promoteFour()); }コマを置けて、かつ空いているマスのBoard
public function legalBoard(): Board { return $this->vacantBoard()->and($this->scaffoldedBoard()); }コマを置く際の処理
概要
手番プレイヤーのBoardをOR演算で更新する。
比較用にコマが置かれているBoardを取得する。
ゲームの終了条件を満たしていたら処理を終了する。
先手番と後手番のそれぞれのプレイヤーのBoardについて、4.と5.の処理を行なう。
プレイヤーのBoardを
promoteMajority()
したBoardと、legalBoard()
でAND演算をして、自動で置かれるマスを計算する。プレイヤーの手持ちコマ数が、自動で置かれるマス数以下だった場合、自動で置かれるマスすべてにコマを置く。そうでなければ、手持ちコマ数の分だけ置く。
現時点のコマが置かれているBoardを取得する。比較用のBoardと比較して、同じ値なら処理を終了する。
比較用のコマが置かれているBoardを、現時点のコマが置かれているBoardで上書きする。
ソースコード
private function handleMove(Move $move, int $movesMade): void { /** @var Board[] $boards */ $boards = [ &$this->firstBoard, &$this->secondBoard, ]; $playerBoard =& $boards[$movesMade % 2]; $playerBoard = $playerBoard->or($move->toBoard($this->size)); $occupiedBoard = $this->occupiedBoard(); while (true) { if ($this->isOver()) { break; } foreach ($boards as &$chainingBoard) { $chain = $this->vacantBoard()->and($this->scaffoldedBoard())->and($chainingBoard->promoteMajority()); $vacancy = $this->piecesPerPlayer - $chainingBoard->count(); if ($chain->count() <= $vacancy) { $chainingBoard = $chainingBoard->or($chain); } else { $chainMoves = static::createMovesFromBoard($chain); for ($i = 0; $i < $vacancy; $i++) { /** @var Move $chainMove */ $chainMove = array_shift($chainMoves); $chainingBoard = $chainingBoard->or($chainMove->toBoard($this->size)); } } } $newOccupiedBoard = $this->occupiedBoard(); if ($occupiedBoard->equalsTo($newOccupiedBoard)) { break; } $occupiedBoard = $newOccupiedBoard; } }ベンチマーク
環境:
- Docker php:7.3-alpine
- opcacheなし<?php require_once __DIR__ . '/vendor/autoload.php'; switch ($argv[1]) { case 'gmp': $createGame = function () { return \MosaicGame\Game\GMPBitSetOneToOneGame::create(7); }; break; case 'array': $createGame = function () { return \MosaicGame\Game\ArrayBitSetOneToOneGame::create(7); }; break; default: throw new InvalidArgumentException(); } $start = microtime(true); for ($i = 0; $i < 100; $i++) { $game = $createGame(); while (!$game->isOver()) { $legalMoves = $game->legalMoves(); $game->makeMove($legalMoves[array_rand($legalMoves)]); } } $end = microtime(true); echo $end - $start, ' seconds', PHP_EOL;/app # php index.php gmp 5.7191338539124 seconds /app # php index.php array 14.644366025925 seconds7段で1ゲーム回すのに0.05秒なら、まぁ悪くはないんじゃないでしょうか。
最後に
いかがでしたか? ここまで書いておいてなンですが、自分で読み返しても分かりづらい記事だなと思います。
ちなみに、本当は2対2のチーム戦で対局する
モザイク・クオ
も実装したかったのですが、あちらは以下のルールが存在します。あがりの時、コマを置く場所が複数あった場合、どこに最後のコマを置いてあがりにするかは、そのコマの持ち主が決められます。
これをうまく実装できる方法が見つからなかったため頓挫しました。
- 投稿日:2021-01-15T13:30:14+09:00
【ボドゲ】モザイクをPHPで実装してみた【ビットボード】
前置き
PHPを用いてモザイクをビットボードで実装した際の知見を共有する記事です。
- ビットボードとは? -> 調べてください。
- モザイクとは? -> ボードゲームです。[概要]
当初はC++で実装したのですが、PHPのFFIで使おうとしたところSegfaultが発生してしまい、力不足で解消できなかったため泣く泣くPHPで再実装しました。
こんな人向けの記事
- ビットボードに興味がある。
- PHPでビットボードを実装したい。
- intで表現できる64bitより多い桁が必要。
- モザイクを実装したいがロジックが思い付かない。
完成品
https://github.com/izayoi256/mosaicgame-php
- 記事内のコードとは異なる部分があります。
- GPL 3.0ライセンスです。
ビット演算
リバーシなら
8*8=64
マス、つまり64bitのintで済みますが、モザイクは7段のピラミッド状で7²+6²+5²+...1²=140
マス、つまり140bitが必要です。なお記事内では簡略化のため、3段で
3²+2²+1²=14
マスのモザイクを実装することとします。まずは
BitSet
というインターフェースで抽象化することにします。インターフェース
AND/OR/XOR/左シフト/右シフト/popcount/set/clear/flip(反転)/文字列への変換といった必須の機能を宣言します。
その他、配列やイテレータへの変換等の補助的な機能は必要に応じてどうぞ。
interface BitSet extends ArrayAccess, Countable, IteratorAggregate { public function __toString(); public function toString(): string; public function size(): int; public function getIterator(); public function toArray(); public function count(); public function equalsTo(self $other): bool; public function set(int ...$offsets): self; public function setAll(): self; public function clear(int ...$offsets): self; public function clearAll(): self; public function and(self $other): self; public function or(self $other): self; public function xor(self $other): self; public function shift(int $amount): self; public function unshift(int $amount): self; public function flip(): self; }実装
僕はイミュータブルなクラスとして実装しました。
ただ、7段だと1ゲームで80000回以上インスタンス化することになるため、パフォーマンスが犠牲になっています。
ミュータブルかイミュータブルかはお好みでどうぞ。この記事ではイミュータブルとして話を進めます。
配列で実装する
開発開始当初は既存のライブラリを参考に、配列を用いて
ArrayBitSet
というクラスを実装しました。具体的な実装方法は参考元を確認してください。https://github.com/adagiolabs/bitset/blob/master/src/Adapter/ArrayBitSet.php
GMPで実装する
先述の配列による実装は素のPHPで実装可能ですが、パフォーマンスが良くないです。
PHPで多倍長整数を扱う方法を探したところ、GMP拡張にて実現できることが分かりました。
ビット演算に関する処理をすべてGMPに任せた
GMPBitSet
というクラスを実装して計測したところ、ArrayBitSet
と比較して57%ものパフォーマンス改善が達成できました。パフォーマンス改善のヒント
AND演算を例に上げます。
final class GMPBitSet implements BitSet { public function and(BitSet $other): BitSet { return new self( $this->size, gmp_and($this->gmp, ($other instanceof self) ? $other->gmp : gmp_init($other->toString(), 2)), ); } }
BitSet::and(BitSet $other)
の$other
はBitSet
を実装していますが、GMPBitSet
のインスタンスとは限りません。しかし常に
$other->toString()
を呼び出すと、$other
がGMPBitSet
だった場合に余計なコストがかかります。もし
$other
がGMPBitSet
だった場合はそのままgmp_and()
に渡すようにすれば、コストが節約できます。盤面
実装したBitSetを用いて、今度は盤面を表現します。
下図のように最上段の1マスを0としたインデックスで表現します。
盤面もインターフェース化して抽象化することにします。
インターフェース
盤面同士もビット演算できるように、ビット演算に関する機能を宣言します。
ある条件を満たす2*2マスの上方のマスを抽出する処理を勝手に
プロモーション
と名付けました。例えば、石が1つ置かれた上方のマスは
promoteOne()
、石が3つ以上置かれた上方のマスはpromoteMajority()
といった具合ですね。interface Board extends Countable { public function __toString(); public function toString(): string; public function getIterator(); public function toArray(); public function count(): int; public function size(): int; public function and(self $other): self; public function or(self $other): self; public function xor(self $other): self; public function equalsTo(self $other): bool; public function flip(): self; public function promoteZero(): self; public function promoteOne(): self; public function promoteTwo(): self; public function promoteThree(): self; public function promoteFour(): self; public function promoteHalfOrMore(): self; public function promoteMajority(): self; // public function mirrorHorizontal(): self; // public function flipVertical(): self; // public function flipDiagonal(): self; // public function rotate90(): self; // public function rotate180(): self; // public function rotate270(): self; }最下部のコメントアウトしてあるものは、盤面の変形(回転/反転)に関する機能です。
機械学習等で用いる際に盤面を正規化したければ、併せて実装するといいでしょう。
実装
まずは
BitSet
インターフェースで盤面を扱うBitSetBoard
という抽象クラスを実装します。盤面の処理のほとんどは
BitSetBoard
で扱うこととして、BitSet
を実装したオブジェクトを生成する処理を具象クラスとなるArrayBitSetBoard
やGMPBitSetBoard
に持たせることで、具象クラスの派生が容易になります。(以下、かなり長くなります)
abstract class BitSetBoard implements Board { public const MAX_SIZE = 7; /** @var int */ private $size; /** @var BitSet */ private $bitSet; protected function __construct(int $size, BitSet $bitSet) { assert(0 < $size && $size <= self::MAX_SIZE, sprintf('Board size must be between 1 and %d.', self::MAX_SIZE)); $this->size = $size; $this->bitSet = self::boardMask($size)->and($bitSet); } abstract protected static function stringToBitSet(int $size, string $string): BitSet; public static function fromString(int $size, string $cells): Board { return new static($size, static::stringToBitSet(self::bitSetSize($size), $cells)); } public static function emptyBoard(int $size): Board { static $boards = []; return $boards[$size] ?? ($boards[$size] = new static($size, self::zeroBitSet($size))); } public static function groundBoard(int $size): Board { static $boards = []; return $boards[$size] ?? ($boards[$size] = self::emptyBoard($size)->or(new static($size, self::layerMask($size, $size)))); } public static function neutralBoard(int $size): Board { static $boards = []; if (!isset($boards[$size])) { if ($size % 2 === 1) { $bitSet = self::oneBitSet($size); for ($i = 1; $i < $size; $i++) { $bitSet = $bitSet->shift($i ** 2); } $bitSet = $bitSet->shift(intdiv($i ** 2, 2)); $boards[$size] = new static($size, $bitSet); } else { $boards[$size] = self::emptyBoard($size); } } return $boards[$size]; } public static function filledBoard(int $size): Board { static $boards = []; return $boards[$size] ?? ($boards[$size] = new static($size, self::boardMask($size))); } public function __toString() { return $this->toString(); } public function toString(): string { return $this->bitSet->toString(); } public function size(): int { return $this->size; } public function count(): int { return count($this->bitSet); } public function flip(): Board { return new static($this->size, $this->bitSet->flip()); } public function and(Board $other): Board { return new static($this->size, $this->bitSet->and(self::boardToBitSet($other))); } public function or(Board $other): Board { return new static($this->size, $this->bitSet->or(self::boardToBitSet($other))); } public function xor(Board $other): Board { return new static($this->size, $this->bitSet->xor(self::boardToBitSet($other))); } public function equalsTo(Board $other): bool { return ($other instanceof self) ? $this->bitSet->equalsTo($other->bitSet) : $this->toString() === $other->toString(); } public function promoteZero(): Board { return $this->promote(self::PROMOTE_ZERO); } public function promoteOne(): Board { return $this->promote(self::PROMOTE_ONE); } public function promoteTwo(): Board { return $this->promote(self::PROMOTE_TWO); } public function promoteThree(): Board { return $this->promote(self::PROMOTE_THREE); } public function promoteFour(): Board { return $this->promote(self::PROMOTE_FOUR); } public function promoteHalfOrMore(): Board { return $this->promote(self::PROMOTE_HALF_OR_MORE); } public function promoteMajority(): Board { return $this->promote(self::PROMOTE_MAJORITY); } public function getIterator() { return $this->bitSet->getIterator(); } public function toArray() { return $this->bitSet->toArray(); } private function promote(int $type): self { $promotedBitSet = self::zeroBitSet($this->size); for ($srcLayerSize = $this->size; $srcLayerSize > 1; $srcLayerSize--) { $dstLayerSize = $srcLayerSize - 1; $srcLayerMask = self::layerMask($this->size, $srcLayerSize); $srcLayer = $this->bitSet->and($srcLayerMask); $promotedLayer = self::zeroBitSet($this->size); if ($type & (self::PROMOTE_ZERO | self::PROMOTE_ONE)) { $srcLayer = $srcLayer->flip()->and($srcLayerMask); } if ($type & (self::PROMOTE_ZERO | self::PROMOTE_FOUR)) { $p = $srcLayer->and($srcLayer->unshift(1)); $p = $p->and($p->unshift($srcLayerSize)); $promotedLayer = $promotedLayer->or($p); } if ($type & (self::PROMOTE_ONE | self::PROMOTE_THREE)) { $p1 = $srcLayer->and($srcLayer->unshift(1)); $p1 = $p1->xor($p1->unshift($srcLayerSize)); $p2 = $srcLayer->xor($srcLayer->unshift(1)); $p2 = $p2->xor($p2->unshift($srcLayerSize)); $promotedLayer = $promotedLayer->or($p1->and($p2)); } if ($type & self::PROMOTE_TWO) { $p1 = $srcLayer->xor($srcLayer->unshift(1)); $p1 = $p1->and($p1->unshift($srcLayerSize)); $p2 = $srcLayer->xor($srcLayer->unshift($srcLayerSize)); $p2 = $p2->and($p2->unshift(1)); $promotedLayer = $promotedLayer->or($p1)->or($p2); } if ($type & self::PROMOTE_MAJORITY) { $p1 = $srcLayer->and($srcLayer->unshift(1)); $p1 = $p1->or($p1->unshift($srcLayerSize)); $p2 = $srcLayer->and($srcLayer->unshift($srcLayerSize)); $p2 = $p2->or($p2->unshift(1)); $promotedLayer = $promotedLayer->or($p1->and($p2)); } if ($type & self::PROMOTE_HALF_OR_MORE) { $p1 = $srcLayer->or($srcLayer->unshift(1)); $p1 = $p1->and($p1->unshift($srcLayerSize)); $p2 = $srcLayer->or($srcLayer->unshift($srcLayerSize)); $p2 = $p2->and($p2->unshift(1)); $promotedLayer = $promotedLayer->or($p1)->or($p2); } for ($i = 0; $i < $dstLayerSize; $i++) { static $rowMasks = []; if (!isset($rowMasks[$dstLayerSize][$i])) { $rowMask = self::zeroBitSet($this->size) ->set(...range(0, $dstLayerSize - 1)) ->shift(self::layerShift($srcLayerSize)) ->shift(($srcLayerSize) * $i); $rowMasks[$dstLayerSize][$i] = $rowMask; } $rowMask = $rowMasks[$dstLayerSize][$i]; $promotedRow = $promotedLayer->and($rowMask); $promotedRow = $promotedRow->unshift($dstLayerSize * $dstLayerSize + $i); $promotedBitSet = $promotedBitSet->or($promotedRow); } } return new static($this->size, $promotedBitSet); } private static function zeroBitSet(int $size): BitSet { static $bitSets = []; return $bitSets[$size] ?? ($bitSets[$size] = static::stringToBitSet(self::bitSetSize($size), '0')); } private static function oneBitSet(int $size): BitSet { static $bitSets = []; return $bitSets[$size] ?? ($bitSets[$size] = static::stringToBitSet(self::bitSetSize($size), '1')); } private static function boardToBitSet(Board $board): BitSet { return ($board instanceof self) ? $board->bitSet : static::stringToBitSet($board->size(), $board->toString()); } private static function layerMask(int $size, int $layerSize): BitSet { static $layerMasks = []; if (!isset($layerMasks[$size][$layerSize])) { $layerMask = self::zeroBitSet($size); $j = $layerSize ** 2; for ($i = 0; $i < $j; $i++) { $layerMask = $layerMask->set($i); } $layerMask = $layerMask->shift(self::layerShift($layerSize)); $layerMasks[$size][$layerSize] = $layerMask; } return $layerMasks[$size][$layerSize]; } private static function layerShift(int $layerSize): int { static $layerShifts = []; if (!isset($layerShifts[$layerSize])) { $layerShift = 0; for ($i = 0; $i < $layerSize; $i++) { $layerShift += $i ** 2; } $layerShifts[$layerSize] = $layerShift; } return $layerShifts[$layerSize]; } private static function boardMask(int $size): BitSet { static $boardMasks = []; if (!isset($boardMasks[$size])) { $boardMask = self::zeroBitSet($size); for ($i = 1; $i <= $size; $i++) { $boardMask = $boardMask->or(self::layerMask($size, $i)); } $boardMasks[$size] = $boardMask; } return $boardMasks[$size]; } private static function bitSetSize(int $size): int { static $bitSetSizes = []; if (!isset($bitSetSizes[$size])) { $bitSetSize = 0; for ($i = 1; $i <= $size; $i++) { $bitSetSize += $i ** 2; } $bitSetSizes[$size] = $bitSetSize; } return $bitSetSizes[$size]; } }プロモーション処理の説明
大部分の機能は実装やメソッド名を読めば理解できるかと思います。(説明放棄)
プロモーション処理は複雑なため、下図の盤面を
promoteMajority
で処理するケースを説明します。
private function promote(int $type): self { $promotedBitSet = self::zeroBitSet($this->size);
$promotedBitSet
はプロモーション結果を保持するBitSetです。最初は空白の状態です。
for ($srcLayerSize = $this->size; $srcLayerSize > 1; $srcLayerSize--) { $dstLayerSize = $srcLayerSize - 1; $srcLayerMask = self::layerMask($this->size, $srcLayerSize); $srcLayer = $this->bitSet->and($srcLayerMask); $promotedLayer = self::zeroBitSet($this->size);プロモーション処理は段ごとに行ないます。
$srcLayerSize
はプロモーション元の段のサイズ、$dstLayerSize
はプロモーション先の段のサイズです。
$promotedLayer
は、段ごとのプロモーション処理を保持するBitSetです。下段->中段
ここでは
$srcLayerSize
は下段のサイズ=3、$dstLayerSize
は中段のサイズ=2とします。
$srcLayerMask
は下段のマスクです。これと盤面をAND演算したものが$srcLayer
です。
if ($type & self::PROMOTE_MAJORITY) { $p1 = $srcLayer->and($srcLayer->unshift(1)); $p1 = $p1->or($p1->unshift($srcLayerSize)); $p2 = $srcLayer->and($srcLayer->unshift($srcLayerSize)); $p2 = $p2->or($p2->unshift(1)); $promotedLayer = $promotedLayer->or($p1->and($p2)); }
promoteMajority
の場合、上記の箇所を通ります。
for ($i = 0; $i < $dstLayerSize; $i++) {最後に、行ごとにプロモーション処理を行ないます。
static $rowMasks = []; if (!isset($rowMasks[$dstLayerSize][$i])) { $rowMask = self::zeroBitSet($this->size) ->set(...range(0, $dstLayerSize - 1)) ->shift(self::layerShift($srcLayerSize)) ->shift(($srcLayerSize) * $i); $rowMasks[$dstLayerSize][$i] = $rowMask; } $rowMask = $rowMasks[$dstLayerSize][$i];
$rowMasks
は、$promotedLayer
でプロモーション先の行ごとに利用するマスクです。
$promotedRow = $promotedLayer->and($rowMask); $promotedRow = $promotedRow->unshift($dstLayerSize * $dstLayerSize + $i); $promotedBitSet = $promotedBitSet->or($promotedRow);
中段->上段
ここでは
$srcLayerSize
は下段のサイズ=2、$dstLayerSize
は中段のサイズ=1とします。コード自体は
下段->中段
と同じなので、画像のみ貼っていきます。
パフォーマンス改善のヒント
各所で
static
キーワードを使って計算結果やインスタンスをキャッシュすることで、重複する処理を少しでも減らすようにしています。ゲーム
書きたい内容のほとんどは書いてしまったので、実際にゲームを進行させる処理はソースコードを見てください。
https://github.com/izayoi256/mosaicgame-php
重要なポイントだけ以下で抑えます。
盤面の扱い
必須
実際の盤面を表現するには3つのBoardが必要になります。
- 先手番のBoard
$firstBoard
- 後手番のBoard
$secondBoard
- 中立(最初から置かれているマス)のBoard
$neutralBoard
補助
補助的な用途で以下のBoardがあると便利です。
- 最下段のマスク用Board
$groundBoard
コマが置かれているマスのBoard
protected function occupiedBoard(): Board { return $this->firstBoard->or($this->secondBoard)->or($this->neutralBoard); }コマが置かれていないマスのBoard
protected function vacantBoard(): Board { return $this->occupiedBoard()->flip(); }(そのマスにコマが置かれているかどうかに関わらず) コマを置けるマスのBoard
protected function scaffoldedBoard(): Board { return $this->groundBoard()->or($this->occupiedBoard()->promoteFour()); }コマを置けて、かつ空いているマスのBoard
public function legalBoard(): Board { return $this->vacantBoard()->and($this->scaffoldedBoard()); }コマを置く際の処理
概要
手番プレイヤーのBoardをOR演算で更新する。
比較用にコマが置かれているBoardを取得する。
ゲームの終了条件を満たしていたら処理を終了する。
先手番と後手番のそれぞれのプレイヤーのBoardについて、4.と5.の処理を行なう。
プレイヤーのBoardを
promoteMajority()
したBoardと、legalBoard()
でAND演算をして、自動で置かれるマスを計算する。プレイヤーの手持ちコマ数が、自動で置かれるマス数以下だった場合、自動で置かれるマスすべてにコマを置く。そうでなければ、手持ちコマ数の分だけ置く。
現時点のコマが置かれているBoardを取得する。比較用のBoardと比較して、同じ値なら処理を終了する。
比較用のコマが置かれているBoardを、現時点のコマが置かれているBoardで上書きする。
ソースコード
private function handleMove(Move $move, int $movesMade): void { /** @var Board[] $boards */ $boards = [ &$this->firstBoard, &$this->secondBoard, ]; $playerBoard =& $boards[$movesMade % 2]; $playerBoard = $playerBoard->or($move->toBoard($this->size)); $occupiedBoard = $this->occupiedBoard(); while (true) { if ($this->isOver()) { break; } foreach ($boards as &$chainingBoard) { $chain = $this->vacantBoard()->and($this->scaffoldedBoard())->and($chainingBoard->promoteMajority()); $vacancy = $this->piecesPerPlayer - $chainingBoard->count(); if ($chain->count() <= $vacancy) { $chainingBoard = $chainingBoard->or($chain); } else { $chainMoves = static::createMovesFromBoard($chain); for ($i = 0; $i < $vacancy; $i++) { /** @var Move $chainMove */ $chainMove = array_shift($chainMoves); $chainingBoard = $chainingBoard->or($chainMove->toBoard($this->size)); } } } $newOccupiedBoard = $this->occupiedBoard(); if ($occupiedBoard->equalsTo($newOccupiedBoard)) { break; } $occupiedBoard = $newOccupiedBoard; } }ベンチマーク
環境:
- Docker php:7.3-alpine
- opcacheなし<?php require_once __DIR__ . '/vendor/autoload.php'; switch ($argv[1]) { case 'gmp': $createGame = function () { return \MosaicGame\Game\GMPBitSetOneToOneGame::create(7); }; break; case 'array': $createGame = function () { return \MosaicGame\Game\ArrayBitSetOneToOneGame::create(7); }; break; default: throw new InvalidArgumentException(); } $start = microtime(true); for ($i = 0; $i < 100; $i++) { $game = $createGame(); while (!$game->isOver()) { $legalMoves = $game->legalMoves(); $game->makeMove($legalMoves[array_rand($legalMoves)]); } } $end = microtime(true); echo $end - $start, ' seconds', PHP_EOL;/app # php index.php gmp 5.7191338539124 seconds /app # php index.php array 14.644366025925 seconds7段で1ゲーム回すのに0.05秒なら、まぁ悪くはないんじゃないでしょうか。
最後に
いかがでしたか? ここまで書いておいてなンですが、自分で読み返しても分かりづらい記事だなと思います。
ちなみに、本当は2対2のチーム戦で対局する
モザイク・クオ
も実装したかったのですが、あちらは以下のルールが存在します。あがりの時、コマを置く場所が複数あった場合、どこに最後のコマを置いてあがりにするかは、そのコマの持ち主が決められます。
これをうまく実装できる方法が見つからなかったため頓挫しました。
- 投稿日:2021-01-15T12:00:36+09:00
【PHP】多対多
参考ページ
構成と概要
- CategoryとQuestionテーブルのリレーション
- Code
- belongsToMany('リレーション先Modelのパス')
- unsignedBigInteger
- $table->foreign('カラム名')->references('id')->on('テーブル名')->onDelete('cascade');
Questionsテーブル
- model
class Question extends Model { use HasFactory; protected $fillable = [ 'body', 'choice_1', 'choice_2', 'choice_3', 'choice_4', 'answer_body', 'answer_choice', 'status_num', 'user_id', ]; public function workbooks() { return $this->belongsToMany('App\Workbook'); } public function categories() { return $this->belongsToMany('App\Category'); } public function users() { return $this->belongsTO('App\User'); } }
- Migration File
class CreateQuestionsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('questions', function (Blueprint $table) { $table->id(); $table->string('body',500); $table->string('choice_1',100); $table->string('choice_2',100); $table->string('choice_3',100); $table->string('choice_4',100); $table->string('answer_body'); $table->integer('answer_choice'); $table->integer('status_num'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('questions'); } }Categoriesテーブル
- model
class Category extends Model { use HasFactory; protected $fillable = [ 'body', ]; public function questions() { return $this->belongsToMany('App\Question'); } }
- Migration File
class CreateCategoriesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->string('body',30); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('categories'); } }中間テーブル
- Model
class Category_question extends Model { use HasFactory; protected $fillable = [ 'workbook_id', 'question_id', ]; }
- Migration File
public function up() { Schema::create('category_questions', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('category_id'); $table->unsignedBigInteger('question_id'); $table->timestamps(); $table->foreign('category_id') ->references('id') ->on('categories') ->onDelete('cascade'); $table->foreign('question_id') ->references('id') ->on('questions') ->onDelete('cascade'); }); }
- 投稿日:2021-01-15T12:00:32+09:00
Wordpress 本文の抜粋にタグやショートコードが出力されないように出力
忘れてしまわないように自分用にメモです
functions.phpに以下を記述//本文の抜粋にタグやショートコードが出力されないように + 続き文字を「…」に指定 function custom_excerpt($excerpt_length){ $my_excerpt = get_the_content(); $my_excerpt = strip_tags($my_excerpt); $my_excerpt = strip_shortcodes($my_excerpt); $my_excerpt = mb_substr($my_excerpt, 0, $excerpt_length) . '…'; echo $my_excerpt; }出力させたいphpファイルには以下を記述
<?php custom_excerpt(100); ?>記入例
archive.php<?php if ( have_posts() ) : ?> <ul class="archive-post-area"> <?php while ( have_posts() ) : the_post(); ?> <li id="post-<?php the_ID(); ?>" > <a href="<?php the_permalink(); ?>"> <dl> <div class="title-area"> <?php $cat = get_the_category(); $cat = $cat[0];?> <dt class="cat <?php echo $cat->category_nicename; ?>"><?php echo get_cat_name($cat->term_id); ?></dt> <dt class="date"><?php echo get_the_date(); ?></dt> </div> <dt class="title"><?php echo wp_trim_words( get_the_title(), 40, '…' ); ?></dt> <dd class="text"><?php custom_excerpt(100); ?></dd> </dl> </a> </li> <?php endwhile;?> </ul> <?php else : ?> <div class="error"> <p>お探しの記事は見つかりませんでした。</p> </div> <?php endif; ?>以下のサイトを参考にさせていただきました。
https://www.tsukimi.net/wordpress_excerpt.html
- 投稿日:2021-01-15T11:07:28+09:00
laravel6 ジョブを使ってみる
目的
- laravel6のジョブを使って非同期処理を体験する
環境
- ハードウェア環境
項目 情報 OS macOS Catalina(10.15.5) ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports) プロセッサ 2 GHz クアッドコアIntel Core i5 メモリ 32 GB 3733 MHz LPDDR4 グラフィックス Intel Iris Plus Graphics 1536 MB
- ソフトウェア環境
項目 情報 備考 PHP バージョン 7.4.11 Homebrewを用いてこちらの方法で導入→Mac HomebrewでPHPをインストールする Laravel バージョン 6.X commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う MySQLバージョン 8.0.21 for osx10.15 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする 情報
- jobの実行を体験することがメインなので非同期で実行される処理はログ出力のみとする。
- 筆者はMacに直接構築したLaravel環境をもちいて本記事の検証を行った。
- ログの出力は
アプリ名ディレクトリ/storage/logs/laravel.log
に出力されるものとする。- 実行するコマンドは特筆しない限り前のコマンドと同じディレクトリで実行するものとする。
条件
- 下記または
$ laravel new
コマンドを実行してlaravel6のアプリが作成されていること。- 前述のlaravelアプリのローカルサーバを起動しブラウザからページを確認する事ができること。
- 前述のlaravelアプリで
$ php artisan migrate
が実行できること。概要
- ジョブの作成と記載
- ルーティング情報の記載
- 確認
詳細
ジョブの作成と記載
アプリ名ディレクトリで下記コマンドを実行してジョブクラスのファイルを作成する。
$ php artisan make:job OutputLogJob下記コマンドを実行して作成されたジョブクラスのファイルを開く。
$ vi app/Jobs/OutputLogJob.php下記のように処理を追記する。(ジョブとして動作させたい処理はジョブクラスのhandle()メソッド内部に記載する.)
アプリ名ディレクトリ/app/Jobs/OutputLogJob.php<?php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; // 下記を追記 use Illuminate\Support\Facades\Log; class OutputLogJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; /** * Create a new job instance. * * @return void */ public function __construct() { // } /** * Execute the job. * * @return void */ public function handle() { // 下記を追記 Log::info('これはジョブのテストです。'); } }ルーティング情報の記載
下記コマンドを実行してルーティングファイルを開く。
$ vi routes/web.php下記のルーティング情報を追記する。
アプリ名ディレクトリ/routes/web.php<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ // 下記を追記 use App\Jobs\OutputLogJob; Route::get('/', function () { return view('welcome'); }); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home'); // 下記を追記 Route::get('/output_log_job', function(){ $job = new OutputLogJob; dispatch($job); return 'ジョブの実行完了'; }); // 上記までを追記確認
下記コマンドを実行してローカルサーバを起動する。
$ php artisan serve下記にアクセスする。
下記のように表示される。
下記コマンドを実行してログファイルを開く。
$ vi storage/logs/laravel.log下記の一文がログに出力されていることを確認する。
アプリ名ディレクトリ/storage/logs/laravel.log[YYYY-MM-DD HH:MM:SS] local.INFO: これはジョブのテストです。
- 投稿日:2021-01-15T11:00:56+09:00
【Laravel】モデル(Model)とは何か?命名規則やマイグレーションとの関連性について
Laravelでよく出てくるモデル(Model)や、マイグレーション、Eloquentの関係性などがわからなかったので調べてみた。
・参考:Laravel公式 Eloquent (Model)
目次
Modelとは?
Databaseのデータを操作する機能のこと。
DBのテーブルに対応するモデルがあり、コントローラでそのモデルを操作するとDBに書き込むデータを指定したりできる。
参考:steemit.comコントローラはユーザーリクエストに基づいてDBデータが必要な場合にモデルに指令を出す。
↓
コントローラからの指令に基づいてDBからデータを抽出 or 保存する。
↓
コントローラは受け取ったデータをビューに渡す。
↓
ユーザーにはビューが描画される。
ModelとDBテーブルの関連付け
DBの各テーブル毎に対応するモデルを作成する。
テーブル名とモデル名の命名規則を守れば、laravelが自動で対応してくれる。▼命名規則
DBのテーブル名は複数形とし、Model名はその単数形とする。・DBのテーブル名: 複数形のスネークケース
・対応するModel名: 冒頭大文字のキャメルケース<例1>
・DBのテーブル名: articles
・対応するModel名: Article<例2>
・DBのテーブル名: maker_codes
・対応するModel名: MakerCode(補足)テーブル名の主な命名規則
DBのテーブル名には命名規則がある。
- 複数形
- スネークケース
- 省略表記しない
テーブル名(例) 判定 articles ○ article × Article × maker_codes ○ MakerCodes × maker_code × mk_code ×
マイグレーションとは?
DBに関連する機能でマイグレーションがある。
マイグレーション(migration)は移行という意味で、ここではDBの構造をLaravelに移行し連動させている。
DB操作ならModelだけあればいいのでは?と思ったので、マイグレーションの役割とモデルとの違いについて。
マイグレーションとモデルの違い
どちらもDBを操作するが、操作対象が異なる。
マイグレーションは、列(カラム)の追加や削除、型の指定を行う。
モデルはDBからデータの取り出し処理を記述する。(Eloquentでクエリを投げる)
▼マイグレーション
- カラムの作成・削除
- 型の指定
- バージョン管理(過去のテーブルの構造がわかる)
- 保存場所: app > Models
- ファイル名はタイムスタンプ & テーブル名
migrationファイルの記述例public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); //idカラム $table->string('name'); //nameカラム $table->string('email')->unique(); //emailカラム $table->timestamps(); //タイムスタンプカラム }); }マイグレーションファイルに追加したいカラムを記述し、マイグレーションを実行すれば、連携するモデルとDBのテーブルにカラムが追加される。
▼モデル
- 取得するデータを指定
- 取得するデータのフォーマットを指定
- 保存場所: app > database > migrations
- ファイル名がテーブルと対応
modelの記述例class User extends Model { $user = User::find(1); // id番号1のユーザー情報を取得 $user->email; // users.email の値を取得 }
Eloquentとは?
LaravelのModelやMigrationなど、DBについて調べていると必ずEloquent(エロクアント)という用語が出てくる。
Eloquentというのは、LaravelでDBを操作するコマンドのこと。
EloquentにはDB操作するためのメソッドが用意されており、それらをモデルやコントローラに記述することでDB操作が実行できる。
▼使い方・
名前空間::Eloquentメソッド名
Eloquentメソッドの例use App\Models\User; //対象のテーブルのモデルのuse宣言 $user = User::all()->toArray();usersテーブルからすべてのデータを抽出し配列化したものを、変数userに格納。
主なEloquentのメソッド
実例はusersテーブル(Userモデル)に対して操作する場合。
use App\Models\User;
メソッド 内容 実例 all() すべてのデータを抽出 $names = User::all() find(int) 指定したid番号のレコードを抽出 $user1 = User::find([10, 20, 30]); get() 結果を取得する $data = User::orderBy('created_at')->get(); where('フィールド名', 条件) 指定したフィールド名のカラムから条件に一致するものを抽出 $data = User::where('id',1)->get() where('フィールド名', '不等号' ,条件) 指定フィールドで条件を満たすものを抽出 $data = User::where('id','>=', 10)->get() toArray() 配列に変換する $arr = User::all()->toArray() findOrFail 条件に一致しない場合、例外処理をする User::findOrFial(1) count() 数を数える $count = User::::where('active', 1)->count(); max('フィールド名') 最大値を取得する $maxNum = User::max('id'); sum('フィールド名') 合計値を取得する $totalPrice = User::sum('price'); 基本的に一般に使えるメソッドは、Eloquentでも使える。
Eloquentでも使えるメソッド一覧
・Laravel公式 Eloquentコレクション
・Laravel公式 Eloquent実例
クエリビルダとは?
LaravelでDBを操作するコードは、Eloquent以外にクエリビルダという方法もある。
▼クエリビルダの使い方
use Illuminate\Support\Facades\DB; $変数名 = DB::table('フィールド名')->メソッド->get();クエリビルダを使うには、DBファサードのtableメソッドを使う。
これはEloquentのuse 名前空間
と同じ。実際、Eloquentはクエリビルダで使えるすべてのメソッドを使用できる。
Eloquentモデルはクエリビルダですから、クエリビルダで使用できる全メソッドを確認しておくべきでしょう。Eloquentクエリでどんなメソッドも使用できます。