20210115のPHPに関する記事は19件です。

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;
  1. Math.random()によって0以上〜1未満の乱数が生成される
  2. 10を乗算することによって0以上〜10未満の乱数となる
  3. Math.floor()によって小数点以下を切り捨て(ここで0〜9のいずれかの整数になっている)
  4. 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel × Dacapo データベースマイグレーションサポートツールの使い方

laravel-dacapo.png

ダカーポとは

https://github.com/ucan-lab/laravel-dacapo

Laravelマイグレーションのサポートライブラリです。
データベースのテーブル定義をYAMLファイル(スキーマファイル)で管理し、マイグレーションファイルの生成を行います。

※初期の開発時期に利用されることを想定しています。

2021.01.11 v4.0.0 リリース!

v3.0から1年の時を経て、ついにバージョンv4.0をリリース致しました!?‍♂️

以前の古いコードをすべて破棄して新しく書き直してます。
少し挙動が異なる部分もあるのでこの機会に改めて使い方をご紹介します。

以前のダカーポの記事

ダカーポの由来

ダ・カーポ(da capo)は音楽用語で「始めに戻って繰り返す」という意味があります。

PHPのパッケージ管理ツールであるComposerも作曲家という音楽用語であり、マイグレーションファイルをすべて削除して、最初から生成し直すことからピッタリな名前だなと思いました。

ダカーポを作った理由

  • @mpyw さんのLTに感化された
  • SymfonyのようにYAMLでテーブル定義するツールが欲しかった
  • ファイル名、クラス名、upメソッド、downメソッドを書くのが面倒だった

デフォルトのマイグレーションのツラミ

  • 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.yml
users:
  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.yml
users:
  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.yml
users:
  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.yml
users:
  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 dacapodatabase/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の差分ができたらいいなと思ってます。
これは実装難しそうなので悩んでます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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関数を使う方法を紹介した。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】php artisan コマンド

Laravelで使う php artisanコマンド
artisan アルチザンと読むらしい。

アルチザンとは、 職人。技工。とかの意味らしい。

対話モード

$ php artisan tinker

これで対話モードができる。
データベースの中身などを調べるときに使える。

例えば,,,

User::all()

このように行えば、Userテーブルのデータを確認することができる。

マイグレーション

$ php artisan migrate

これで、マイグレーションの実行ができる.

マイグレーションのやり直し

$ php artisan migrate:fresh

これで、一旦全てのテーブルを削除しマイグレーションをやりなおすことができる。

seedデータの投入

$ php artisan db:seed

seedファイルで作成したデータをDBに保存する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
→ 実行

これで権限の書き換えができるのでエラーが起きているファイルをリロード。
表示されました!

参考にさせていただいた記事:
http://www.thirtyfive.info/entry/2018/06/07/laravel%E3%81%AE%E8%B5%B7%E5%8B%95%E8%A8%AD%E5%AE%9A%E3%81%A8%E3%82%A8%E3%83%A9%E3%83%BC#%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%AD%E3%82%B0%E3%81%A7failed-to-open-stream-Permission-denied-in-

めちゃくちゃスッキリした。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数ファイルを1回でrsyncする

--files-fromオプション

複数のファイルを1回のrsyncコマンドで送信する

準備

/path/to/images.txtにファイル名を入力(改行で区切る)

images.txt
file1.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;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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=9003

vscodeの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が表示されている。)
800A6CDE-D918-45F0-A97C-D189623072C2.jpeg

【こういったコードになった経緯】

1,最初はdockerコンテナに入って、pecl install xdebug でxdebugをinstallしようとしたらphpのvarsionが7.2以上にするようにエラーが出たのでDockerfileのイメージを7.0→7.4にした。

2,とりあえずphpinfo()にxdebugの項目がでるようにしたかったが、
docker-php-ext-enable xdebugzend_extensin = ~ を作らないと表示されなかったので追加した。
また、buildしなおすたびにxdebugをinstallし直すのは面倒なのでDockerfileのRUNコマンドで実行できるようにした。

3,php.iniについては、 xdebug.start_with_request = yes がないとbreakpointで止まってくれなかった。

4,vscodeのlaunch.jsonは、pathMappingsは最初意味がわからず、
"./html" : "{workspaceRoot}" みたいにしてたが、dockerコンテナ側のルートディレクトリ : vscode側のワークスペースだとわかって変更。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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!

image.png

つけてんじゃん!!

僕は「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

無事実行できました!!

教訓

「フォルダー名(ディレクトリ名)に「感嘆符(!)」をつけるのはやめましょう」

という教訓でした?

皆様もお気をつけて!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】Laravel 8.X ソースページリスト

ヘルパ関数

Laravel 8.x

アクセス制限

変数

auth関連

  • ログインユーザー取得
    Laravel 8.x
    • コントローラー冒頭にuse Illuminate\Support\Facades\Auth;を記述すること
      • 名前空間:ファイルの居場所を示す
      • use宣言:中で使うクラスを宣言する

フォームメソッド・バリデーション・エラーメッセージ

-フォームメソッド
Laravel 8.x
- バリデーション・エラーメッセージ
Laravel 8.x

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】 Laravel 8.X ソースページリスト

ヘルパ関数

Laravel 8.x

アクセス制限

変数

auth関連

  • ログインユーザー取得
    Laravel 8.x
    • コントローラー冒頭にuse Illuminate\Support\Facades\Auth;を記述すること
      • 名前空間:ファイルの居場所を示す
      • use宣言:中で使うクラスを宣言する

条件分岐(if、elseif、else、endif)

Laravel 8.x

Migrateionカラム修飾子

Laravel 8.x

フォームメソッド・Store&Update処理・バリデーション・エラーメッセージ

パラメーターを用いたルーティング

Laravel 8.x

リレーション

Laravel 8.x

デバッグ

ページネーション

Laravel 8.x

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel8→6系に変えたら色々苦戦した

はじめに

私は現在、PHP/Laravelでポートフォリオ作成をしている学習中の者です。
もし間違っている箇所があればお知らせいただけたら幸いです。

ポートフォリオを作成中に、Laravelのバージョン8系では記述の仕方が変わっていたり、(新しいため)情報も少なく、長い目でみて時間ロスが多く発生すると思い、6系に変えてみようと思いました。

開発初期段階だったので、軽い気持ちで変えてみたら色々エラーが出たので記事にしました。


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で何か作り始める前にバージョンはよく考えて決めましょう(笑)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 //新しいバージョンへ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】Formメソッド

ソース

Laravel 8.x

要点

記述方法

<form action="/example" method="POST">
    @method('PUT')
    @csrf
</form>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ボドゲ】モザイクを実装してみた【ビットボード】

前置き

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拡張にて実現できることが分かりました。

PHP: GMP - Manual

ビット演算に関する処理をすべて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)$otherBitSetを実装していますが、GMPBitSetのインスタンスとは限りません。

しかし常に$other->toString()を呼び出すと、$otherGMPBitSetだった場合に余計なコストがかかります。

もし$otherGMPBitSetだった場合はそのままgmp_and()に渡すようにすれば、コストが節約できます。

盤面

実装したBitSetを用いて、今度は盤面を表現します。

下図のように最上段の1マスを0としたインデックスで表現します。

mosaic.png

盤面もインターフェース化して抽象化することにします。

インターフェース

盤面同士もビット演算できるように、ビット演算に関する機能を宣言します。

ある条件を満たす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を実装したオブジェクトを生成する処理を具象クラスとなるArrayBitSetBoardGMPBitSetBoardに持たせることで、具象クラスの派生が容易になります。

(以下、かなり長くなります)

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で処理するケースを説明します。

promotion01.png


    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です。

promotion02.png


            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の場合、上記の箇所を通ります。

promotion03.png

 

promotion04.png

 

promotion05.png

 

promotion06.png

 

promotion07.png


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

$i = 0;
promotion08.png

 

promotion09.png

 

promotion10.png

$i = 1;
promotion11.png

 

promotion12.png

 

promotion13.png

中段->上段

ここでは$srcLayerSizeは下段のサイズ=2、$dstLayerSizeは中段のサイズ=1とします。

コード自体は下段->中段と同じなので、画像のみ貼っていきます。

promotion22.png

 

promotion23.png

 

promotion24.png

 

promotion25.png

 

promotion26.png

 

promotion27.png

 

$i = 0;
promotion28.png

 

promotion29.png

 

promotion30.png

パフォーマンス改善のヒント

各所で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());
    }

コマを置く際の処理

概要

  1. 手番プレイヤーのBoardをOR演算で更新する。

  2. 比較用にコマが置かれているBoardを取得する。

  3. ゲームの終了条件を満たしていたら処理を終了する。

  4. 先手番と後手番のそれぞれのプレイヤーのBoardについて、4.と5.の処理を行なう。

  5. プレイヤーのBoardをpromoteMajority()したBoardと、legalBoard()でAND演算をして、自動で置かれるマスを計算する。

  6. プレイヤーの手持ちコマ数が、自動で置かれるマス数以下だった場合、自動で置かれるマスすべてにコマを置く。そうでなければ、手持ちコマ数の分だけ置く。

  7. 現時点のコマが置かれているBoardを取得する。比較用のBoardと比較して、同じ値なら処理を終了する。

  8. 比較用のコマが置かれている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 seconds

7段で1ゲーム回すのに0.05秒なら、まぁ悪くはないんじゃないでしょうか。

最後に

いかがでしたか? ここまで書いておいてなンですが、自分で読み返しても分かりづらい記事だなと思います。


ちなみに、本当は2対2のチーム戦で対局するモザイク・クオも実装したかったのですが、あちらは以下のルールが存在します。

あがりの時、コマを置く場所が複数あった場合、どこに最後のコマを置いてあがりにするかは、そのコマの持ち主が決められます。

これをうまく実装できる方法が見つからなかったため頓挫しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ボドゲ】モザイクを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拡張にて実現できることが分かりました。

PHP: GMP - Manual

ビット演算に関する処理をすべて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)$otherBitSetを実装していますが、GMPBitSetのインスタンスとは限りません。

しかし常に$other->toString()を呼び出すと、$otherGMPBitSetだった場合に余計なコストがかかります。

もし$otherGMPBitSetだった場合はそのままgmp_and()に渡すようにすれば、コストが節約できます。

盤面

実装したBitSetを用いて、今度は盤面を表現します。

下図のように最上段の1マスを0としたインデックスで表現します。

mosaic.png

盤面もインターフェース化して抽象化することにします。

インターフェース

盤面同士もビット演算できるように、ビット演算に関する機能を宣言します。

ある条件を満たす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を実装したオブジェクトを生成する処理を具象クラスとなるArrayBitSetBoardGMPBitSetBoardに持たせることで、具象クラスの派生が容易になります。

(以下、かなり長くなります)

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で処理するケースを説明します。

promotion01.png


    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です。

promotion02.png


            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の場合、上記の箇所を通ります。

promotion03.png

 

promotion04.png

 

promotion05.png

 

promotion06.png

 

promotion07.png


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

$i = 0;
promotion08.png

 

promotion09.png

 

promotion10.png

$i = 1;
promotion11.png

 

promotion12.png

 

promotion13.png

中段->上段

ここでは$srcLayerSizeは下段のサイズ=2、$dstLayerSizeは中段のサイズ=1とします。

コード自体は下段->中段と同じなので、画像のみ貼っていきます。

promotion22.png

 

promotion23.png

 

promotion24.png

 

promotion25.png

 

promotion26.png

 

promotion27.png

 

$i = 0;
promotion28.png

 

promotion29.png

 

promotion30.png

パフォーマンス改善のヒント

各所で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());
    }

コマを置く際の処理

概要

  1. 手番プレイヤーのBoardをOR演算で更新する。

  2. 比較用にコマが置かれているBoardを取得する。

  3. ゲームの終了条件を満たしていたら処理を終了する。

  4. 先手番と後手番のそれぞれのプレイヤーのBoardについて、4.と5.の処理を行なう。

  5. プレイヤーのBoardをpromoteMajority()したBoardと、legalBoard()でAND演算をして、自動で置かれるマスを計算する。

  6. プレイヤーの手持ちコマ数が、自動で置かれるマス数以下だった場合、自動で置かれるマスすべてにコマを置く。そうでなければ、手持ちコマ数の分だけ置く。

  7. 現時点のコマが置かれているBoardを取得する。比較用のBoardと比較して、同じ値なら処理を終了する。

  8. 比較用のコマが置かれている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 seconds

7段で1ゲーム回すのに0.05秒なら、まぁ悪くはないんじゃないでしょうか。

最後に

いかがでしたか? ここまで書いておいてなンですが、自分で読み返しても分かりづらい記事だなと思います。


ちなみに、本当は2対2のチーム戦で対局するモザイク・クオも実装したかったのですが、あちらは以下のルールが存在します。

あがりの時、コマを置く場所が複数あった場合、どこに最後のコマを置いてあがりにするかは、そのコマの持ち主が決められます。

これをうまく実装できる方法が見つからなかったため頓挫しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】多対多

参考ページ

Laravel 8.x

構成と概要

  • 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');
        });
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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が実行できること。

概要

  1. ジョブの作成と記載
  2. ルーティング情報の記載
  3. 確認

詳細

  1. ジョブの作成と記載

    1. アプリ名ディレクトリで下記コマンドを実行してジョブクラスのファイルを作成する。

      $ php artisan make:job OutputLogJob
      
    2. 下記コマンドを実行して作成されたジョブクラスのファイルを開く。

      $ vi app/Jobs/OutputLogJob.php
      
    3. 下記のように処理を追記する。(ジョブとして動作させたい処理はジョブクラスの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('これはジョブのテストです。');
          }
      }
      
  2. ルーティング情報の記載

    1. 下記コマンドを実行してルーティングファイルを開く。

      $ vi routes/web.php
      
    2. 下記のルーティング情報を追記する。

      アプリ名ディレクトリ/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 'ジョブの実行完了';
      });
      // 上記までを追記
      
  3. 確認

    1. 下記コマンドを実行してローカルサーバを起動する。

      $ php artisan serve
      
    2. 下記にアクセスする。

    3. 下記のように表示される。

      127_0_0_1_8000_output_log_job.png

    4. 下記コマンドを実行してログファイルを開く。

      $ vi storage/logs/laravel.log
      
    5. 下記の一文がログに出力されていることを確認する。

      アプリ名ディレクトリ/storage/logs/laravel.log
      [YYYY-MM-DD HH:MM:SS] local.INFO: これはジョブのテストです。
      
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】モデル(Model)とは何か?命名規則やマイグレーションとの関連性について

Laravelでよく出てくるモデル(Model)や、マイグレーション、Eloquentの関係性などがわからなかったので調べてみた。

・参考:Laravel公式 Eloquent (Model)

目次

  1. Modelとは?
  2. ModelとDBテーブルの関連付け(命名規則)
  3. マイグレーションとは?
  4. マイグレーションとモデルの違い
  5. Eloquentとは?
  6. クエリビルダとは?


Modelとは?

Databaseのデータを操作する機能のこと。

DBのテーブルに対応するモデルがあり、コントローラでそのモデルを操作するとDBに書き込むデータを指定したりできる。

image.png
参考: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のテーブルにカラムが追加される。

▼ディレクトリ構造
image.png



▼モデル

  • 取得するデータを指定
  • 取得するデータのフォーマットを指定
  • 保存場所: app > database > migrations
  • ファイル名がテーブルと対応
modelの記述例
class User extends Model
    {
        $user = User::find(1); // id番号1のユーザー情報を取得
        $user->email; // users.email の値を取得
}

▼ディレクトリ構造
image.png


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でも使えるメソッド一覧

image.png

Laravel公式 Eloquentコレクション
Laravel公式 Eloquent実例


クエリビルダとは?

LaravelでDBを操作するコードは、Eloquent以外にクエリビルダという方法もある。

▼クエリビルダの使い方

use Illuminate\Support\Facades\DB;
$変数名 = DB::table('フィールド名')->メソッド->get();

クエリビルダを使うには、DBファサードのtableメソッドを使う。
これはEloquentのuse 名前空間と同じ。

実際、Eloquentはクエリビルダで使えるすべてのメソッドを使用できる

Eloquentモデルはクエリビルダですから、クエリビルダで使用できる全メソッドを確認しておくべきでしょう。Eloquentクエリでどんなメソッドも使用できます。

Laravel公式 Eloquent
Laravel公式 クエリビルダ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む