- 投稿日:2021-12-24T23:56:29+09:00
php-mysql-engineをCSVデータの集計に使ってみる
この記事は ラクス Advent Calendar 2021 24日目の記事です。 php-mysql-engineとは php-mysql-engineは、Psalmでも知られるVimeo社が公開したライブラリで、PDOを使ったMySQLの操作をデータベースサーバに接続することなくエミュレートしてくれるというものです。 これを使えばDBを含めた自動テストを手軽に実行できるということで、Vimeo社はもちろん、日本だとpixivの一部サービスでも利用しているそうです。 PHPerKaigi 2021のLTでも紹介されていましたね。 ところで 話は変わりますが、ログなどの集計にSQLを使いたいことが稀によくありませんか?私はあります。 数ヶ月に一回しか使わないawkよりも、普段よく使うSQLでデータ集計したいところです。 ということで、php-mysql-engineを使ってCSVデータを集計できるツール、csv-queryを作成しました! 記事公開時点 (2021/12/24) ではまだ正常系が最低限動くというレベルですが、一応上記のようなログ集計にも使えるかと思います。 csv-query インストール 上記リポジトリをcloneし、composer install --no-devします。 bin/ディレクトリの中にcsv-queryコマンドがあります。 使い方 csv-queryは標準入力のデータをstdinという名前のテーブルに格納します。コマンドの第一引数にSQLを指定します。 デフォルトでは一行目をヘッダ行とみなし、その内容をカラム名に利用します。 $ cat data.csv id,name 1,John 2,Alice 3,Dave 4,Bob $ cat data.csv | csv-query 'SELECT * FROM stdin WHERE id < 4 ORDER BY name' id,name 2,Alice 3,Dave 1,John オプションによってヘッダ行を無効にしたり、タブ区切りのようなファイルを読み込むことも可能です。詳細はREADMEに譲ります。 システム要件 PHP>=8.0, PDO_mysqlが必要です 所感 php-mysql-engineについて 実際のPDOとは細かい差異がある 先述のpixivさんのブログでもあるように、普通のPDOを使っていれば問題ない場面でもphp-mysql-engineの制限(またはバグ?)によって動かない場面がありました。 CREATE TABLE文にCOLLATEかCHARACTER SETが必要 DSNのmysql:hostname=localhost;dbname=csv_query;の末尾セミコロンを取るとエラーになる まだサポートされていないメソッドもある PDOStatement::getColumnMeta()を利用しようとしたところ、未サポートのようでPDO object is uninitializedというFatal Errorが発生しました。 CLIツールの作成 READMEを先に書くとやりやすい 個人開発ということで、設計書を書いたりはしていませんが、READMEにある程度仕様をまとめたおかげで確認しながら開発を進められました。 まとめ 通常テスト用ツールとして用いられるphp-mysql-engineの少し変わった使い方を紹介しました。 SQLの文法は基本的にMySQL準拠なので、普段MySQLで開発している方には特に使いやすいのではないでしょうか。 ちなみに弊社が業務で使っているのはPostgreSQLなので、私自身文法など若干馴染めていません。 (誰かphp-pgsql-engine作ってくれないかな……)
- 投稿日:2021-12-24T23:56:29+09:00
php-mysql-engineをCSVファイルのデータ集計に使ってみる
この記事は ラクスAdvent Calendar 2021 24日目の記事です。 php-mysql-engineとは php-mysql-engineは、Psalmでも知られるVimeo社が公開したライブラリで、PDOを使ったMySQLの操作をデータベースサーバに接続することなくエミュレートしてくれるというものです。 これを使えばDBを含めた自動テストを手軽に実行できるということで、Vimeo社はもちろん、日本だとpixivの一部サービスでも利用しているそうです。 PHPerKaigi 2021のLTでも紹介されていましたね。 ところで 話は変わりますが、ログなどの集計にSQLを使いたいことが稀によくありませんか?私はあります。 数ヶ月に一回しか使わないawkよりも、普段よく使うSQLでデータ集計したいところです。 ということで、php-mysql-engineを使ってCSVデータを集計できるツール、csv-queryを作成しました! 記事公開時点 (2021/12/24) ではまだ正常系が最低限動くというレベルですが、一応上記のようなログ集計にも使えるかと思います。 csv-query インストール 上記リポジトリをcloneし、composer install --no-devします。 bin/ディレクトリの中にcsv-queryコマンドがあります。 使い方 csv-queryは標準入力のデータをstdinという名前のテーブルに格納します。コマンドの第一引数にSQLを指定します。 デフォルトでは一行目をヘッダ行とみなし、その内容をカラム名に利用します。 $ cat data.csv id,name 1,John 2,Alice 3,Dave 4,Bob $ cat data.csv | csv-query 'SELECT * FROM stdin WHERE id < 4 ORDER BY name' id,name 2,Alice 3,Dave 1,John オプションによってヘッダ行を無効にしたり、タブ区切りのようなファイルを読み込むことも可能です。詳細はREADMEに譲ります。 システム要件 PHP>=8.0, PDO_mysqlが必要です 所感 php-mysql-engineについて 実際のPDOとは細かい差異がある 先述のpixivさんのブログでもあるように、普通のPDOを使っていれば問題ない場面でもphp-mysql-engineの制限(またはバグ?)によって動かない場面がありました。 CREATE TABLE文にCOLLATEかCHARACTER SETが必要 DSNのmysql:hostname=localhost;dbname=csv_query;の末尾セミコロンを取るとエラーになる まだサポートされていないメソッドもある PDOStatement::getColumnMeta()を利用しようとしたところ、未サポートのようでPDO object is uninitializedというFatal Errorが発生しました。 CLIツールの作成 READMEを先に書くとやりやすい 個人開発ということで、設計書を書いたりはしていませんが、READMEにある程度仕様をまとめたおかげで確認しながら開発を進められました。 まとめ 通常テスト用ツールとして用いられるphp-mysql-engineの少し変わった使い方を紹介しました。 SQLの文法は基本的にMySQL準拠なので、普段MySQLで開発している方には特に使いやすいのではないでしょうか。 ちなみに弊社が業務で使っているのはPostgreSQLなので、私自身文法など若干馴染めていません。 (誰かphp-pgsql-engine作ってくれないかな……)
- 投稿日:2021-12-24T23:40:27+09:00
PHP プロパティに関しまして。
PHPのClassとfunctionをつかうとき、 プロパティを下記のように生成すると思うが、 test.php <?php class Myclass{ public $q; } Class内で生成しなくても、プロパティの生成はできることをおそわりました。 test.php $test1 = new Myclass(); $test1->$y = 1; var_dump($test1); 結果は object(test1)#1 (2) { ["q"]=> NULL ["y"]=> int(1) } PHPの講義では、丁寧にプロパティを一々生成してくれますが、 一般的ではないようですが、プロパティをClass内で生成しないコードもあると思うので、 参考までという感じで記事を書いておきます。 test2.php <?php class Myclass2{ public function __construct($a){ $this->pdo = $a } Class内でもプロパティの宣言なしで$pdoを生成することもできます。 ※個人の勉強のメモのための記事でございます。 (udemyの先生に質問して、もらった返答をもとに作成しました。)
- 投稿日:2021-12-24T22:44:47+09:00
declare(strict_types=1); でPHPのstrictモードの設定をする
業務でPHPファイルに declare(strict_types=1); を導入するにあたり調べたことをまとめます。 declare(strict_types=1); の意味を解く declare あるコードブロックに対して、何らかの設定をするための構文。 declare(ディレクティブ)のように書いて使う。 現在のところ使用できるディレクティブは ticks, encoding, strict_typesの3つ。 strict_types=1 PHPの暗黙の型変換を禁止して、型指定に厳密にチェックする設定のこと。 PHPのデフォルトでは自動変換モード(詳しくは後述)になっているが、 declare(strict_types=1); をファイル先頭に書くことでそのファイルはstrictモードに設定することができる。 スカラー 型宣言 には二つの方式があります。デフォルトの自動変換 (coercive) モードと、 厳密に判断する strict モードです。 strict モードを有効にするには、declare 文を strict_types 宣言と一緒に使います。 strict モードでは、型宣言に正確に対応する値のみを受け入れ、 そうでない場合、TypeError がスローされます。 このルールに関する唯一の例外は、int の値が float 型の宣言に渡せることだけです。 coerciveモードとstrictモードの違い PHPのデフォルトでは、与えられた値の型を期待される型に自動的に変換する。(coercive モード)。 たとえば関数の引数に int が与えられたが、 引数には文字列が期待されていた場合、int を文字列型に変換した値を取得する。(戻り値も同様に、自動で型変換される。) strictモードを有効にすると、与えられた型が期待する型と異なる場合TypeErrorになる。 ↓ PHPのプレイグラウンドのようなサイトで書いたサンプルコードです。 何かコードを編集して、青い eval(); というボタンを押すと実行できます。 int と float の例 // declare(strict_types=1); が無い場合 <?php function int(int $a){ return $a; } function float(float $b) { return $b; } print_r([int(2.5), float(2.5)]); // 結果 Array ( [0] => 2 [1] => 2.5 ) // declare(strict_types=1); がある場合 <?php declare(strict_types=1); function int(int $a): int { return $a; } function float(float $b): float { return $b; } print_r([int(2.5), float(2.5)]); // 結果 Fatal error: Uncaught TypeError: int(): Argument #1 ($a) must be of type int, float given, called in /in/42G2d on line 10 and defined in /in/42G2d:6 注意点 適用したいPHPファイルごとの先頭にセットする必要があり、php.iniに記載したりしてまとめて適用することは出来ない。 <?php の直後に書かなければいけない。(ファイルの途中に記述不可) declare(strict_types=1); の影響範囲はそのファイルで完結している。 関数を定義するファイルと関数を呼び出すファイルが異なる場合は、定義するファイルがstrictモードであるかは動作に影響がなく、関数を実行するファイルがstrictモードかどうかで動作が決まる。 ファイルをrequireする際も、requireするファイル・requireされるファイルのstrictモードが相互に影響することはない。 PHPの型宣言でユニオン型(int | float)を使えるのはPHP8から。もしPHP8以前のバージョンであれば、intでもfloatでも良い、もしくはnullも含めたいといった場合にstrictモードが足枷になることも。 参考記事
- 投稿日:2021-12-24T22:21:24+09:00
PHP+SQL 課題
掲示板課題 機能要件 ①ユーザー登録機能 仮登録と本登録、メール認証のながれ。 ・仮登録時に登録ユーザーのメールアドレスにアドレス送信 ②ログイン機能 ・ユーザーID(もしくはメールアドレス)とパスワード入力によるログイン ③記事の投稿機能 ・ユーザーが記事の文章を投稿出来る ・記事のタイトル、投稿日時、投稿者を表示する ④コメント機能 ・記事に対してのコメント ・コメント日時、コメント投稿者を表示する
- 投稿日:2021-12-24T15:36:53+09:00
macOSに最新のPHPとcomposer installをインストールする方法
macに homebrewをインストールする /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" phpの最新バージョンをインストールする brew install php 既にhomebrewがインストールされておりhomebrewのversionが古く下記のエラーが発生した場合は、 homebrew自体のバージョンアップをして下さい。 brew update-reset php インストール時のエラー Error: homebrew-core is a shallow clone. homebrew-cask is a shallow clone. To `brew update`, first run: git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask fetch --unshallow This restriction has been made on GitHub's request because updating shallow clones is an extremely expensive operation due to the tree layout and traffic of Homebrew/homebrew-core and Homebrew/homebrew-cask. We don't do this for you automatically to avoid repeatedly performing an expensive unshallow operation in CI systems (which should instead be fixed to not use shallow clones). Sorry for the inconvenience! Traceback (most recent call last): 11: from /usr/local/Homebrew/Library/Homebrew/brew.rb:31:in `<main>' 10: from /usr/local/Homebrew/Library/Homebrew/brew.rb:31:in `require_relative' 9: from /usr/local/Homebrew/Library/Homebrew/global.rb:79:in `<top (required)>' 8: from /usr/local/Homebrew/Library/Homebrew/vendor/portable-ruby/2.6.3_2/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require' 7: from /usr/local/Homebrew/Library/Homebrew/vendor/portable-ruby/2.6.3_2/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require' 6: from /usr/local/Homebrew/Library/Homebrew/os.rb:7:in `<top (required)>' 5: from /usr/local/Homebrew/Library/Homebrew/os.rb:43:in `<module:OS>' 4: from /usr/local/Homebrew/Library/Homebrew/os/mac.rb:65:in `prerelease?' 3: from /usr/local/Homebrew/Library/Homebrew/os/mac.rb:28:in `version' 2: from /usr/local/Homebrew/Library/Homebrew/os/mac/version.rb:32:in `from_symbol' 1: from /usr/local/Homebrew/Library/Homebrew/os/mac/version.rb:32:in `fetch' /usr/local/Homebrew/Library/Homebrew/os/mac/version.rb:32:in `block in from_symbol': unknown or unsupported macOS version: :dunno (MacOSVersionError) brew link してインストールしたPHPを使える様にする brew link php brew link php で下記のエラーが発生した場合 Linking /usr/local/Cellar/php/8.1.1... Error: Could not symlink sbin/php-fpm /usr/local/sbin is not writable. (参考) brew link php71: Could not symlink sbin/php-fpm https://stackoverflow.com/questions/46778133/brew-link-php71-could-not-symlink-sbin-php-fpm I solved this problem by first creating the directory sbin: sudo mkdir /usr/local/sbin then if you are like me using macOS highSierra 10+ you need to run: sudo chown -R $(whoami) $(brew --prefix)/sbin after this brew link php 動作確認 php -v composerをinstallをする Download Composer https://getcomposer.org/download/ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php -r "if (hash_file('sha384', 'composer-setup.php') === '906a84df04cea2aa72f40b5f787e49f22d4c2f19492ac310e8cba5b96ac8b64115ac402c8cd292b8a03482574915d1a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" php composer-setup.php php -r "unlink('composer-setup.php');" sudo mv composer.phar /usr/local/bin/composer 動作確認 composer -v
- 投稿日:2021-12-24T14:55:44+09:00
【python】dict変数に苦労した(1)
2021年8月からAWSのLAMBDAとdjangoでpythonの仕事をやり始めたんですが、これまでPHPばかりに偏ってたものでpythonに慣れるのに結構苦労した事の中で、dict変数があったのでその辺の事を書いてみます。 1.PHPだと連想配列は簡単に作れたが、dictは宣言が無いとエラーになって勝手に作ってくれない PHPでは、自分の必要な時に $sample1["hoge"] = "aaabbbccc"; のように書けば、配列変数\$sample1が作成され、$sample1["hoge"]という連想配列が作成され、aaabbbcccを代入してくれる。 pythonのdict変数でこれをやると、NameErrorとなってしまう。 C:\project\python_test>python3 sample1.py Traceback (most recent call last): File "sample1.py", line 2, in <module> sample1["hoge"] = "aaabbbccc" NameError: name 'sample1' is not defined 通常の変数は宣言が必要ないのに、dictやlist変数になると、途端に話が変わってくる事に最初はとまどった。 以下のように書くと正常に実行できる。 sample1 = {"hoge":""} sample1["hoge"] = "aaabbbccc" ここで、覚えたての頃は、sample1 = {"hoge":""}と書くのかsample1 = dict(hoge="")と書くのがいいのか?といった表記で悩んだが、どっちでもいいというのがわかった。 2.連想配列のキーを変数で渡したい時 次に、連想配列のキーを変数で渡したい時 $keystr = "hoge"; $samples1[$keystr] = "aaabbbccc"; PHPでは定数 or 変数で連想配列のキーを渡したい時にさほど違いはないが、dictも同様に keystr = "hoge" sample1 = {keystr:""} sample1[keystr] = "aaabbbccc" で、同じ操作を実現可能です。1点気を付けることがあるとすれば、keystrを他のobjectから取り出した値のような場合に、必ず文字列になってるとは限らないので、str()関数で文字列化しておくと安全です。 3.連想配列に複数のキーを変数で渡して代入したい時 最初に一番悩んだ部分がここだった。 というのは、jsonに変換した際に、 {"hoge1":"a","hoge2":"a"} となってほしかった。 PHPでこういう事をしたかった。 $sample1 = array(); $keylist = array("hoge1","hoge2"); foreach($keylist as $keystr) { $sample1[$keystr] = "a"; } これと同じことをpythonのdictでやろうとするとPHPと同様に keylist = ["hoge1","hoge2"] value = "test" sample1 = {} for keystr in keylist: sample1[keystr] = value 先にsample1 = {}とsample1がdict変数であることを定義しておけば問題ない。 4.既に中身の入ったdict変数に要素を追加する時 $sample1 = array("hoge1"=>"test"); $keystr = "hoge2"; $value = "test2"; $sample1[$keystr] = $value; この時もほぼPHPと同様な方法で代入できる。 keystr = "hoge2" value = "test2" sample1 = {"hoge1":"test"} sample1[keystr] = value または sample1.update({keystr:value}) 1回目はここまで。
- 投稿日:2021-12-24T13:17:15+09:00
todolist_php
環境 以下の環境で作成します。 mac php8 laravel8 vscode(なんでも良い) php artisan serve(laravelのbuilt in サーバ) mysql(ローカルにインストールする) mysqlのインストール homebrewを使えばできる https://prog-8.com/docs/mysql-env *上記ドキュメントは、mysql5.7を使ってますが、平塚は8です。ここはどっち使ってもいいです。 インスールできてるか確認 以下のコマンドでmysqlのバージョンが取得できればOK mysql --version 一応ログインできるか確認 brew services start mysql mysql -uroot *上の例では、rootのパスワードをセットしに行ってますが、そこは余力があれば対応しましょう。 *基本的に自分のmacの勉強用なら、rootのパスワードはあってもなくてもどっちでもいいと思います。 大した情報を管理することもないと思うので。ただし自己責任で。 いよいよ todo listの作成 https://qiita.com/s_harada/items/39cfbd64a6df4f2ccf5d をベースにしてますが、ちょっと変えてます。 データベースの作成 これから作るtodoリストのデータベースを作成しておきましょう mysql> create database todo_db; laravelインストール brew install composer composer create-project "laravel/laravel=6.*" プロジェクト名 プロジェクト作成 既にcomposerが入ってる人は以下 laravel new todo_list model,view,controllerを作成 todoのデータを格納するモデルを作成していきます。 laravelは model(データを扱うところ。データベースと一番近いものと覚えておけば最初はいいです。) view(実際に画面に表示する部分) controller(modelからとってきたデータをviewに渡す部分) で構成されており、これらをそれぞれ作らないといけないです。 まずは、データベースにtableを作らないと意味ないので、migrationを作成します。 *最近のwebフレームワークは、言語問わず、データベースにtableを作る際はmigrationを使います。 php artisan make:migration create_todos_table --create=todos 必要なカラム todoですること 締め切り(いつまでに完了するか) タスクのstatus 上記をmigrationで入れると <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateTodosTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('todos', function (Blueprint $table) { $table->increments('id'); $table->string('task'); // 追加 $table->date('task_limited_at')->nullable()->default(null); // 追加 $table->tinyInteger('task_status'); // 追加 $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('todos'); } } こんな感じになります。 では、早速migrationを実行しましょう。 *実行の前に.envにいかがあることを確認しましょう。 *ローカルなので、DBの設定は雑ですw DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=todo_db DB_USERNAME=root DB_PASSWORD= ➜ todo_list git:(master) ✗ php artisan migrate Migrating: 2021_12_20_073530_create_todos_table Migrated: 2021_12_20_073530_create_todos_table (14.48ms) 上記のようになれば成功です。 あることを確認 mysqlのコンソールであることを確認します。 mysql> use todo_db Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed mysql> show tables; +------------------------+ | Tables_in_todo_db | +------------------------+ | failed_jobs | | migrations | | password_resets | | personal_access_tokens | | todos | | users | +------------------------+ 6 rows in set (0.00 sec) mysql> desc todos; +-----------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-----------------+--------------+------+-----+---------+----------------+ | id | int unsigned | NO | PRI | NULL | auto_increment | | task | varchar(255) | NO | | NULL | | | task_limited_at | date | YES | | NULL | | | task_status | tinyint | NO | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +-----------------+--------------+------+-----+---------+----------------+ 6 rows in set (0.01 sec) 実際のmodel,view,controllerの作成 では、ここからmodel,view,controllerを作っていきます。 model作成 php artisan make:model Todo コントローラ作成 php artisan make:controller TodolistFormController routesの作成 これは画面表示のURLになります <?php use Illuminate\Support\Facades\Route; /* |-------------------------------------------------------------------------- | 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! | */ // Route::get('/', function () { // return view('welcome'); // }); Route::get('/', 'App\Http\Controllers\TodolistFormController@index'); status statusはconfigの下に定義します。 laravelはdefaultは、こういう定義系はconfigに置きます。 *使い方は様々なので、自分たちにあった使い方をすればいいかなと思います <?php return [ 'task_status' => [ '未着手', '対応中', '保留', '完了', '途中終了', ], ]; view artisanコマンドはないので、自力で作成します todo_list.blade.php <h1>ToDo List</h1> <div> <h2>タスクの一覧</h2> <a href="/create-page">タスクを追加</a> <table border="1"> <tr> <th>タスクの内容</th> <th>タスクの期限</th> <th>タスクのstatus</th> <th colspan="2">操作</th> </tr> @foreach($todos as $todo) <tr> <td>{{$todo->task}}</td> <td>{{$todo->task_limited_at}}</td> <td>{{Config::get('status.task_status')[$todo->task_status]}}</td> <td><a href="/edit-page/{{$todo->id}}">編集</a></td> <td><a href="/delete-page/{{$todo->id}}">削除</a></td> </tr> @endforeach </table> </div> ここまでで 一回動かしてみましょう。 php artisan serve を叩いて起動するか確認。 builtinサーバが起動したら、上のコマンドを叩いた際に出てきたURLを叩いてみて、表示されるか確認してみる 投稿ページを作成 コントローラとviewをそれぞれ以下で用意します。 以下メソッドを追加してください // タスク作成画面を表示 public function createPage() { return view('todo_create'); } // タスクを登録 public function create(Request $request) { $todo = new Todo(); $todo->task = $request->task; $todo->task_limited_at = $request->task_limited_at; $todo->task_status = STATUS_BEGIN; $todo->save(); ret todo_create.blade.php <h1>ToDo List</h1> <div> <h2>タスクを追加</h2> <form method="POST" action="/create"> @csrf <p> タスク:<input type="text" name="task"> </p> <p> タスクの期限:<input type="text" name="task_limited_at"> </p> <input type="submit" name="create" value="追加"> </form> <a href="/">戻る</a> </div> 編集と削除 続いて、編集と削除をやってきます。 こっちがcontroller // タスク編集画面を表示 public function editPage($id) { $todo = Todo::find($id); return view('todo_edit', [ "todo" => $todo ]); } // タスクを更新 public function edit(Request $request) { Todo::find($request->id)->update([ 'task' => $request->task, 'task_limited_at' => $request->task_limited_at, ]); return redirect('/'); } こっちがview <h1>ToDo List</h1> <div> <h2>タスクの修正</h2> <form method="POST" action="/edit"> @csrf <input type="hidden" name="id" value="{{$todo->id}}"> <p> タスク:<input type="text" name="task" value="{{$todo->task}}"> </p> <p> タスクの期限:<input type="text" name="task_limited_at" value="{{$todo->task_limited_at}}"> </p> <input type="submit" name="edit" value="修正"> </form> <a href="/">戻る</a> </div> 削除 routeはこちら Route::get('/delete-page/{id}', 'App\Http\Controllers\TodolistFormController@deletePage'); Route::post('/delete/{id}', 'App\Http\Controllers\TodolistFormController@delete'); controller // タスク削除画面を表示 public function deletePage($id) { $todo = Todo::find($id); return view('todo_delete', [ "todo" => $todo ]); } // タスクを削除 public function delete(Request $id) { Todo::find($id)->delete(); return redirect('/'); } view <h1>ToDo List</h1> <div> <h2>タスクを削除</h2> <form method="POST" action="/delete/{{$todo->id}}"> @csrf <p> タスクの名前:{{$todo->task}} </p> <p> タスクの期限:{{$todo->task_limited_at}} </p> <input type="submit" name="delete" value="削除"> </form> <a href="/">戻る</a> </div> これでうまくいくはずです。 その他 余力がある人は、ステータスの変更や、ステータスでの絞り込み機能追加などをやってみると理解が深まると思います。
- 投稿日:2021-12-24T09:49:07+09:00
AdminPageEditable プラグインを作りました。
こんにちは! 私は普段CMSを使ってコーポレートサイトを構築しているWEB系のエンジニアです。 なかでもbaserCMSを使って構築することが多いでのすが、今回専用プラグインを作ったのでご紹介です。 baserCMS4.5.4で当該プラグインが動作することを確認しました。 ※4系の最近のバージョンだと動きそうですが3系とかはまったく動作しないかと 前提 CMS、PHPフレームワーク、MVCモデルがわかる イベント処理がわかると尚良いかも baserCMSとは AdminPageEditable プラグイン 概要 baserCMSでは管理画面で自由に固定ページを追加、編集することができます。 コンテンツ編集エリアにテキストや画像などのファイルを貼り付けたりできる高機能なwysiwygエディタを備えているのですが時々一部どうしてもcssやjs、phpを使用して動的表示したい場合があると思いますがそういうかゆいところに手が届く機能が固定ページ編集画面に「コード」欄としてデフォルトで存在します。 しかしながらこの機能、管理画面にログインするユーザグループによって一部機能に制限が設けられています。 ※通常baserCMSではユーザグループごとにアクセス権限を設定します。 こちらのようにデフォルトでシステム管理グループとサイト運営グループが存在するのですが運営グループのユーザの場合、この「コード」欄が表示されず、使用できません。 運営ユーザのグループを変更してシステム管理グループに所属させれば、「コード」欄の入力が可能になるのですが他の機能も利用できるようになるため、セキュリティ的に問題になる場合があります。 通常パーミッション設定によってページごとにアクセスを制限可能ですが今回は固定ページ編集画面の一部になるのでこちらが使用できません。 ビューファイル /lib/Baser/View/Pages/admin/form.php の 47行目付近の「BcUtil::isAdminUser()」システム管理者ユーザ(グループ)だったら該当部分を表示するみたいな条件分岐が記述されています。 <?php if (BcUtil::isAdminUser()): ?> <div class="section"> <table cellpadding="0" cellspacing="0" class="form-table"> <tr> <th class="col-head"><?php echo $this->BcForm->label('Page.page_template', __d('baser', '固定ページテンプレート')) ?></th> <td class="col-input"> <?php echo $this->BcForm->input('Page.page_template', ['type' => 'select', 'options' => $pageTemplateList]) ?> <div class="helptext"> <?php echo __d('baser', 'テーマフォルダ内の、Pages/templates テンプレートを配置する事で、ここでテンプレートを選択できます。') ?> </div> <?php echo $this->BcForm->error('Page.page_template') ?> </td> </tr> <tr> <th class="col-head"><?php echo $this->BcForm->label('Page.code', __d('baser', 'コード')) ?></th> <td class="col-input"> <?php echo $this->BcForm->input('Page.code', [ 'type' => 'textarea', 'cols' => 36, 'rows' => 5, 'style' => 'font-size:14px;font-family:Verdana,Arial,sans-serif;' ]); ?> <?php echo $this->Html->image('admin/icn_help.png', ['class' => 'btn help', 'alt' => __d('baser', 'ヘルプ')]) ?> <div class="helptext"> <?php echo __d('baser', '固定ページの本文には、ソースコードに切り替えてPHPやJavascriptのコードを埋め込む事ができますが、ユーザーが間違って削除してしまわないようにこちらに入力しておく事もできます。<br />入力したコードは、自動的にコンテンツ本体の上部に差し込みます。') ?> </div> <?php echo $this->BcForm->error('Page.code') ?> </td> </tr> <?php echo $this->BcForm->dispatchAfterForm() ?> </table> </div> <?php else: ?> <?php echo $this->BcForm->input('Page.code', ['type' => 'hidden']) ?> <?php endif ?> そこで今回固定ページの「コード」欄を運営ユーザで利用できるようにする専用プラグインを作成しました。 仕様 当該プラグインを導入することで管理画面ログインユーザが固定ページ追加、編集画面を表示する際にアクセス権限設定をシステム管理者グループに差し替える処理を差し込むことで「コード」欄を表示させることを可能にします。 具体的にはプラグインのイベント処理を利用します。 AdminPageEditable/Event/AdminPageEditableControllerEventListener.php public function pagesBeforeRender(CakeEvent $event) { $Controller = $event->subject(); /** * 管理画面チェック */ if (!BcUtil::isAdminSystem() || !Hash::get($Controller->request->params, 'controller') == 'pages' || !in_array(Hash::get($Controller->request->params, 'action'), ['admin_add', 'admin_edit'])) { return true; } /** * 管理画面 対象ユーザチェック */ //対象ユーザグループ $target_user_groups = Configure::read('AdminPageEditable.target_user_group'); $user = $Controller->BcAuth->user(); if (!$user || !in_array((int)Hash::get($user, 'user_group_id'), $target_user_groups)) { return true; } //一時的にユーザのグループ変更 Configure::write('BcApp.adminGroupId', (int)Hash::get($user, 'user_group_id')); } 設定ファイル AdminPageEditable/Config/setting.php で設定されている該当グループに対してシステム管理グループに設定を差し替える処理を実装。 ユーザグループが増減した場合は、この設定ファイルを変更することで対応可能です。 /** * [Config] AdminPageEditable */ /** * システムナビ */ $config['AdminPageEditable'] = [ //対象固定ページを管理者権限で編集可能にする 'target_user_group' => [ 2, 3, //サイト運営、CMS運営: UserGroup.id ], ]; このままではフォーム保存時にModelのバリデーションで弾かれてしまうので AdminPageEditable/Event/AdminPageEditableModelEventListener.php にも処理を実装しています。 /** * pageBeforeValidate * * @param CakeEvent $event */ public function pageBeforeValidate(CakeEvent $event) { $params = Router::getParams(); /** * 管理画面チェック */ if (!BcUtil::isAdminSystem() || !Hash::get($params, 'controller') == 'pages' || !in_array(Hash::get($params, 'action'), ['admin_add', 'admin_edit'])) { return true; } /** * 管理画面 対象ユーザチェック */ //対象ユーザグループ $target_user_groups = Configure::read('AdminPageEditable.target_user_group'); $user = BcUtil::loginUser('admin'); if (!$user || !in_array((int)Hash::get($user, 'user_group_id'), $target_user_groups)) { return true; } //一時的にユーザのグループ変更 Configure::write('BcApp.adminGroupId', (int)Hash::get($user, 'user_group_id')); Configure::write('BcApp.allowedPhpOtherThanAdmins', true); return true; } 使用方法 1 こちらのソースをクローンするかダウンロード。 2 ソースを {プロジェクト}/app/Plugin/AdminPageEditable に配置。 3 ソースを配置すると管理画面のプラグイン管理で下記のように一覧に表示されるのでインストール。 導入作業は以上です。 運営ユーザでログインした場合、プラグイン導入前は のように「コード」欄が表示されていなかったところ下記のように表示されるようになったと思います。 管理側表示 公開側表示 最後に 運営ユーザだけど固定ページで特殊なことをやりたいといった特定のシチュエーションに限定されるかもしれませんが機会があればご利用ください。 baserCMSは非常に拡張性の高い国産CMSです。 機会があれば自作プラグイン開発に挑戦してみるというのも良いかもしれません♪
- 投稿日:2021-12-24T09:49:07+09:00
【baserCMS】AdminPageEditable プラグインを作りました。
こんにちは! 私は普段CMSを使ってコーポレートサイトを構築しているWEB系のエンジニアです。 なかでもbaserCMSを使って構築することが多いでのすが、今回専用プラグインを作ったのでご紹介です。 baserCMS4.5.4で当該プラグインが動作することを確認しました。 ※4系の最近のバージョンだと動きそうですが3系とかはまったく動作しないかと 前提 CMS、PHPフレームワーク、MVCモデルがわかる イベント処理がわかると尚良いかも baserCMSとは AdminPageEditable プラグイン 概要 baserCMSでは管理画面で自由に固定ページを追加、編集することができます。 コンテンツ編集エリアにテキストや画像などのファイルを貼り付けたりできる高機能なwysiwygエディタを備えているのですが時々一部どうしてもcssやjs、phpを使用して動的表示したい場合があると思いますがそういうかゆいところに手が届く機能が固定ページ編集画面に「コード」欄としてデフォルトで存在します。 しかしながらこの機能、管理画面にログインするユーザグループによって一部機能に制限が設けられています。 ※通常baserCMSではユーザグループごとにアクセス権限を設定します。 こちらのようにデフォルトでシステム管理グループとサイト運営グループが存在するのですが運営グループのユーザの場合、この「コード」欄が表示されず、使用できません。 運営ユーザのグループを変更してシステム管理グループに所属させれば、「コード」欄の入力が可能になるのですが他の機能も利用できるようになるため、セキュリティ的に問題になる場合があります。 通常パーミッション設定によってページごとにアクセスを制限可能ですが今回は固定ページ編集画面の一部になるのでこちらが使用できません。 ビューファイル /lib/Baser/View/Pages/admin/form.php の 47行目付近の「BcUtil::isAdminUser()」システム管理者ユーザ(グループ)だったら該当部分を表示するみたいな条件分岐が記述されています。 <?php if (BcUtil::isAdminUser()): ?> <div class="section"> <table cellpadding="0" cellspacing="0" class="form-table"> <tr> <th class="col-head"><?php echo $this->BcForm->label('Page.page_template', __d('baser', '固定ページテンプレート')) ?></th> <td class="col-input"> <?php echo $this->BcForm->input('Page.page_template', ['type' => 'select', 'options' => $pageTemplateList]) ?> <div class="helptext"> <?php echo __d('baser', 'テーマフォルダ内の、Pages/templates テンプレートを配置する事で、ここでテンプレートを選択できます。') ?> </div> <?php echo $this->BcForm->error('Page.page_template') ?> </td> </tr> <tr> <th class="col-head"><?php echo $this->BcForm->label('Page.code', __d('baser', 'コード')) ?></th> <td class="col-input"> <?php echo $this->BcForm->input('Page.code', [ 'type' => 'textarea', 'cols' => 36, 'rows' => 5, 'style' => 'font-size:14px;font-family:Verdana,Arial,sans-serif;' ]); ?> <?php echo $this->Html->image('admin/icn_help.png', ['class' => 'btn help', 'alt' => __d('baser', 'ヘルプ')]) ?> <div class="helptext"> <?php echo __d('baser', '固定ページの本文には、ソースコードに切り替えてPHPやJavascriptのコードを埋め込む事ができますが、ユーザーが間違って削除してしまわないようにこちらに入力しておく事もできます。<br />入力したコードは、自動的にコンテンツ本体の上部に差し込みます。') ?> </div> <?php echo $this->BcForm->error('Page.code') ?> </td> </tr> <?php echo $this->BcForm->dispatchAfterForm() ?> </table> </div> <?php else: ?> <?php echo $this->BcForm->input('Page.code', ['type' => 'hidden']) ?> <?php endif ?> そこで今回固定ページの「コード」欄を運営ユーザで利用できるようにする専用プラグインを作成しました。 仕様 当該プラグインを導入することで管理画面ログインユーザが固定ページ追加、編集画面を表示する際にアクセス権限設定をシステム管理者グループに差し替える処理を差し込むことで「コード」欄を表示させることを可能にします。 具体的にはプラグインのイベント処理を利用します。 AdminPageEditable/Event/AdminPageEditableControllerEventListener.php public function pagesBeforeRender(CakeEvent $event) { $Controller = $event->subject(); /** * 管理画面チェック */ if (!BcUtil::isAdminSystem() || !Hash::get($Controller->request->params, 'controller') == 'pages' || !in_array(Hash::get($Controller->request->params, 'action'), ['admin_add', 'admin_edit'])) { return true; } /** * 管理画面 対象ユーザチェック */ //対象ユーザグループ $target_user_groups = Configure::read('AdminPageEditable.target_user_group'); $user = $Controller->BcAuth->user(); if (!$user || !in_array((int)Hash::get($user, 'user_group_id'), $target_user_groups)) { return true; } //一時的にユーザのグループ変更 Configure::write('BcApp.adminGroupId', (int)Hash::get($user, 'user_group_id')); } 設定ファイル AdminPageEditable/Config/setting.php で設定されている該当グループに対してシステム管理グループに設定を差し替える処理を実装。 ユーザグループが増減した場合は、この設定ファイルを変更することで対応可能です。 /** * [Config] AdminPageEditable */ /** * システムナビ */ $config['AdminPageEditable'] = [ //対象固定ページを管理者権限で編集可能にする 'target_user_group' => [ 2, 3, //サイト運営、CMS運営: UserGroup.id ], ]; このままではフォーム保存時にModelのバリデーションで弾かれてしまうので AdminPageEditable/Event/AdminPageEditableModelEventListener.php にも処理を実装しています。 /** * pageBeforeValidate * * @param CakeEvent $event */ public function pageBeforeValidate(CakeEvent $event) { $params = Router::getParams(); /** * 管理画面チェック */ if (!BcUtil::isAdminSystem() || !Hash::get($params, 'controller') == 'pages' || !in_array(Hash::get($params, 'action'), ['admin_add', 'admin_edit'])) { return true; } /** * 管理画面 対象ユーザチェック */ //対象ユーザグループ $target_user_groups = Configure::read('AdminPageEditable.target_user_group'); $user = BcUtil::loginUser('admin'); if (!$user || !in_array((int)Hash::get($user, 'user_group_id'), $target_user_groups)) { return true; } //一時的にユーザのグループ変更 Configure::write('BcApp.adminGroupId', (int)Hash::get($user, 'user_group_id')); Configure::write('BcApp.allowedPhpOtherThanAdmins', true); return true; } 使用方法 1 こちらのソースをクローンするかダウンロード。 2 ソースを {プロジェクト}/app/Plugin/AdminPageEditable に配置。 3 ソースを配置すると管理画面のプラグイン管理で下記のように一覧に表示されるのでインストール。 導入作業は以上です。 運営ユーザでログインした場合、プラグイン導入前は のように「コード」欄が表示されていなかったところ下記のように表示されるようになったと思います。 管理側表示 公開側表示 最後に 運営ユーザだけど固定ページで特殊なことをやりたいといった特定のシチュエーションに限定されるかもしれませんが機会があればご利用ください。 baserCMSは非常に拡張性の高い国産CMSです。 機会があれば自作プラグイン開発に挑戦してみるというのも良いかもしれません♪
- 投稿日:2021-12-24T06:45:10+09:00
PHP ファイルパスやURLからファイル名や拡張子等を取得する
目的 ファイルパスやURLから最後のファイル名や拡張子などを取得する方法をまとめる 詳細 下記のサービスを用いて今回紹介するコードの検証を行った。 https://paiza.io/ja 方法 /で区切られた最後のフィールドを取得する。 https://qiita.com/miriwoのmiriwoの部分を取得したい場合、下記の様に記載する。 <?php $path = 'https://qiita.com/miriwo'; $endFeald = substr($path, strrpos($path, '/') + 1); echo $endFeald; // 「miriwo」 と出力される ファイルパスからファイルの拡張子を取得する。 /etc/nginx/conf.d/default.confのdefault.confファイルの拡張子部分confのみを取得したい場合、下記の様に記載する。 <?php $filePath = '/etc/nginx/conf.d/default.conf'; $extensions = substr($filePath, strrpos($filePath, '.') + 1); echo $extensions; // 「conf」と出力される 簡単な解説 使用している文字列を操作する関数を分解してどんなことをしているのか詳しく見てみる。 strrpos() strrpos 下記の様に呼び出す。 strrpos(評価対象の文字列, 区切り文字列, どこから数え始めるか): 数値か文字列を返す。 下記の様に記載した場合、「$pathの文字列の最後の'/'は$pathに格納されている文字列の1番目から何文字目(何バイト目)である」を出力している事となる。 echo strrpos($path, '/',); substr() substr 割とよく使うPHPではおなじみの文字列の中からその一部を抜き出す関数である。 下記の様に呼び出す。 substr(評価対象の文字列, 文字列を抜き出す開始位置, 文字列を抜き出す開始位置からの長さ(バイト数)): 抜き出した文字列を返す。 ちなみに「文字列を抜き出す開始位置からの長さ(バイト数)」を指定しなかった場合「文字列を抜き出す開始位置」から文字列の最後までを返す。 下記の様に記載した場合、「$strの文字列の10文字目(10バイト目)から文字列の最後までを切り出して返す」処理を行っている事となる。 substr($str, 10); 合わせると substr($path, strrpos() + 1)なので substr($path, strrpos()にて返される最後の区切り文字までの文字数 + 1)となり substr($path, 最後の区切り文字から+1文字した数値)となるため、strrpos()`で指定した区切り文字の後ろの文字列を取得する事ができる。
- 投稿日:2021-12-24T00:11:09+09:00
DockerでLaravel×Elasticsearch環境を作る
前提条件 環境 ・ローカル 検証端末:MacOS Monterey (12.1) docker:Docker version 20.10.11, build dea9396 docker-compose:v2.2.1 ・Docker環境 PHP:8.0.13 MySQL:8.0.27 Elasticsearch:7.12.0 読むべき人 Elasticsearch導入したことない人。 家で検証してみたい人。 Elasticという響きがかっこいいなーと思ってる人。 コンテナの構成 ・nginx リクエストを最初に受けるサーバー ・php Laravelが動作するコンテナ。 ・mysql メインのデータストレージとして使用するDB。 ・Elasticsearch 全文検索エンジン。Scout経由で使用する想定。 ・node 画面実装にはないと困るので。 Laravelのモジュールをクローンする git clone https://github.com/laravel/laravel.git 今回作成する環境のディレクトリ構成 elasticsearch_test/ ←ディレクトリ名は任意のものに変更 ├── CHANGELOG.md ├── README.md ├── app ├── artisan ├── bootstrap ├── composer.json ├── composer.lock ├── config ├── database ├── docker ←[追加] 設定ファイルを追加 ├── docker-compose.yml ←[追加] 設定ファイルを追加 ├── package.json ├── phpunit.xml ├── public ├── resources ├── routes ├── server.php ├── src ├── storage ├── tests ├── vendor └── webpack.mix.js 任意のプロジェクト名に変更 今回は「elasticsearch_test」とする。 mv laravel elasticsearch_test Dockerの設定ファイルを追加していく nginxの設定ファイル nginxコンテナ。 Laravelのrootディレクトリはpublic配下なので設定しておく。 listenするポートはコンテナの内側なのでwebサーバーのデフォルトの80でおk。 docker/nginx/default.conf server { listen 80; index index.php index.html; root /var/www/elasticsearch_test/public; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass php:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } } phpコンテナのDockerfile phpコンテナはimageではなくDockerfileで設定を指定する。 理由はコンテナ起動時にRUNで叩きたいコマンドがあるからだと思うけど、今回はその辺から拝借してきた記述をそのままにしてある。 docker/php/Dockerfile FROM php:8.0-fpm COPY php.ini /usr/local/etc/php/ RUN apt-get update \ && apt-get install -y zlib1g-dev mariadb-client vim libzip-dev \ && docker-php-ext-install zip pdo_mysql #Composer install RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" RUN php composer-setup.php RUN php -r "unlink('composer-setup.php');" RUN mv composer.phar /usr/local/bin/composer ENV COMPOSER_ALLOW_SUPERUSER 1 ENV COMPOSER_HOME /composer ENV PATH $PATH:/composer/vendor/bin WORKDIR /var/www COPY . /var/www/ RUN composer global require "laravel/installer" php.ini docker/php/php.ini [Date] date.timezone = "Asia/Tokyo" [mbstring] mbstring.internal_encoding = "UTF-8" mbstring.language = "Japanese" docker-compose.yml コンテナ達を1セットに扱うためのdocker-compose。 それの設定ファイルを定義しておきます。 version: '3' services: php: container_name: php_container build: ./docker/php volumes: - ./:/var/www/elasticsearch_test nginx: container_name: nginx_container image: nginx ports: - 81:80 volumes: - ./:/var/www/elasticsearch_test - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf depends_on: - php db: container_name: mysql_container image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: database MYSQL_PASSWORD: root TZ: 'Asia/Tokyo' command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: - ./docker/db/data:/var/lib/mysql - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf - ./docker/db/sql:/docker-entrypoint-initdb.d ports: - 3306:3306 es: container_name: elasticsearch_container image: elasticsearch:7.12.0 ports: - "9200:9200" environment: - discovery.type=single-node - cluster.name=docker-cluster - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ulimits: memlock: soft: -1 hard: -1 node: image: node:12.13-alpine tty: true volumes: - ./src:/var/www working_dir: /var/www コンテナ起動 起動! docker-compose up -d コンテナで実行 vendor周りを作成。 composerの意味がわからずvendor配下を他プロジェクトからコピーしてきていた日が僕にもありました。 依存ライブラリはちゃんとcomposerでインストールしよう! composer install envファイルを作成 cp .env.example .env keyを生成 これあんまり意味分かってない。謎の儀式。 php artisan key:generate 疎通確認 phpコンテナ ブラウザでアクセス localhost:81 mysqlコンテナ コンテナにログイン docker exec -it mysql_container /bin/bash Mysqlにログイン mysql -uroot -proot Databaseを作成する create database elasticsearch_test_db; Mysql8系はデフォルトの接続方法が異なるため変更する ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root'; .envファイルのDBホスト情報をdocker-composeで定義したサービス名に変更 DB_CONNECTION=mysql DB_HOST=db // ←ここ DB_PORT=3306 DB_DATABASE=elasticsearch_test_db DB_USERNAME=root DB_PASSWORD=root Elasticsearchコンテナ とりあえずコンテナに入る docker exec -it elasticsearch_container /bin/bash Elasticserachコンテナの中からなのでhostは「localhost」 [root@8970f3395096 elasticsearch]# curl -X GET localhost:9200 { "name" : "8970f3395096", "cluster_name" : "docker-cluster", "cluster_uuid" : "JKMTJ9JYRDuTfq0o4phKIQ", "version" : { "number" : "7.12.0", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "78722783c38caa25a70982b5b042074cde5d3b3a", "build_date" : "2021-03-18T06:17:15.410153305Z", "build_snapshot" : false, "lucene_version" : "8.8.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" } .envにElasticsearchの設定を追加。 識別はdocker-compose.ymlのserviceに書いたservice名 SCOUT_DRIVER=elasticsearch ELASTICSEARCH_HOST=es:9200 サンプルとなるDBを用意する migration migrationファイルを作成 php artisan make:migration create_scout_test_records_table --create=scout_test_records database/migrations/2021_12_17_031813_create_scout_test_records_table.php のサンプル <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateScoutTestRecordsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('scout_test_records', function (Blueprint $table) { $table->increments('id'); $table->string('name', 255)->comment('名前'); $table->string('hash_code', 255)->comment('ハッシュコード'); $table->text('text')->nullable()->comment('テキスト'); $table->double('latitude')->nullable()->comment('緯度'); $table->double('longitude')->nullable()->comment('経度'); $table->integer('type')->comment('タイプ'); $table->tinyInteger('status')->comment('ステータス'); $table->timestamps(); $table->softDeletes(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('scout_test_records'); } } Model modelを作成 php artisan make:model ScoutTestRecord Models/ScoutTestRecord.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class ScoutTestRecord extends Model { use HasFactory; use SoftDeletes; public $table = 'scout_test_records'; public $dates = ['deleted_at']; public $fillable = [ 'id', 'name', 'hash_code', 'text', 'latitude', 'longitude', 'type', 'status', ]; protected $casts = [ 'id' => 'integer', 'name' => 'string', 'hash_code' => 'string', 'text' => 'string', 'latitude' => 'double', 'longitude' => 'double', 'type' => 'integer', 'status' => 'integer', ]; public static $rules = [ 'name' => 'required|string', 'hash_code' => 'required|string', 'text' => 'string', 'type' => 'integer', 'status' => 'integer', ]; } Seeder seederも作成しておく php artisan make:seeder ScoutTestRecordsTableSeeder <?php namespace Database\Seeders; use Illuminate\Database\Seeder; use App\Models\ScoutTestRecord; use Faker\Factory as Faker; class ScoutTestRecordsTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { ScoutTestRecord::factory()->count(20)->create(); } } DatabaseSeeder.php <?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Database\Seeders\ScoutTestRecordsTableSeeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run() { $this->call([ ScoutTestRecordsTableSeeder::class, ]); } } Factory config/app.php factoryの設定をするため一部変更する 'fallback_locale' => 'en', factoryを作成 php artisan make:factory ScoutTestRecordFactory --model=ScoutTestRecord database/factories/ScoutTestRecordFactory.php <?php namespace Database\Factories; use App\Models\ScoutTestRecord; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; class ScoutTestRecordFactory extends Factory { protected $model = ScoutTestRecord::class; /** * Define the model's default state. * * @return array */ public function definition() { return [ 'name' => $this->faker->name, 'hash_code' => $this->faker->password(), 'text' => $this->faker->realText($maxNbChars = 50, $indexSize = 2), 'latitude' => $this->faker->latitude(-90, 90), 'longitude' => $this->faker->longitude(0, 180), 'type' => $this->faker->randomNumber($nbDigits = 1), 'status' => $this->faker->randomNumber($nbDigits = 1), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), 'deleted_at' => null, ]; } } DBにデータを登録する php artisan migrate --seed Elasticsearchにデータを連携する 各モジュールの役割は下記の記事で解説してます。 Scoutの設定 まずはScoutをインストールする。 このあとインストールするエンジンがscoutの8.x系に依存しているため、バージョンを指定する。 バージョン指定のセマンティックバージョンの概念はこれがわかりやすかったです。 composer require laravel/scout:^8.0 設定ファイルをバージョン管理配下に配置する php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider" 連携したいデータソースのModelにモデルオブザーバを登録する <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Laravel\Scout\Searchable; // 追加 class ScoutTestRecord extends Model { use HasFactory; use SoftDeletes; use Searchable; // 追加 public $table = 'scout_test_records'; ・ ・ ・ エンジンを設定 Elasticsearchのクライアントモジュールをインストール composer require elasticsearch/elasticsearch エンジンは先人達がけっこういっぱい用意してくれてるけど、今回はtamayoさんのやつをチョイス。 composer require tamayo/laravel-scout-elastic エンジンの処理の挙動はカスタマイズしたい場合があるので、app配下にエンジンを継承したファイルを用意する。 mkdir app/Scout/ touch app/Scout/ElasticsearchEngine.php app/Scout/ElasticsearchEngine.php 若干挙動がおかしいところがあったので継承して修正。 <?php namespace App\Scout; use Laravel\Scout\Builder; use Elasticsearch\Client; use Tamayo\LaravelScoutElastic\Engines\ElasticsearchEngine as ScoutElasticsearchEngine; class ElasticsearchEngine extends ScoutElasticsearchEngine { protected $elastic; protected $index; private $query; public function __construct(Client $elastic, $index) { $this->elastic = $elastic; $this->index = $index; } protected function performSearch(Builder $builder, array $options = []) { $params = [ 'index' => $builder->model->searchableAs(), 'type' => get_class($builder->model), 'body' => [ 'query' => [ 'bool' => [ 'must' => [['query_string' => ['query' => "*{$builder->query}*"]]] ] ] ] ]; if ($sort = $this->sort($builder)) { $params['body']['sort'] = $sort; } if (isset($options['from'])) { $params['body']['from'] = $options['from']; } if (isset($options['size'])) { $params['body']['size'] = $options['size']; } if (isset($options['z']) && count($options['numericFilters'])) { $params['body']['query']['bool']['must'] = array_merge( $params['body']['query']['bool']['must'], $options['numericFilters'] ); } if ($builder->callback) { return call_user_func( $builder->callback, $this->elastic, $builder->query, $params ); } return $this->elastic->search($params); } /** * Perform the given search on the engine. * @inheritDoc * @see ScoutElasticsearchEngine::paginate * @param Builder $builder * @param int $perPage (limit) * @param int $page (offset) * @return mixed */ public function paginate(Builder $builder, $perPage, $page) { $result = $this->performSearch($builder, [ 'numericFilters' => $this->filters($builder), 'from' => (($page * $perPage) - $perPage), 'size' => $perPage, ]); $result['nbPages'] = $result['hits']['total']['value'] / $perPage; return $result; } /** * Map the given results to instances of the given model. * * @inheritDoc * @see ScoutElasticsearchEngine::map * @param \Laravel\Scout\Builder $builder * @param mixed $results * @param \Illuminate\Database\Eloquent\Model $model * @return Collection */ public function map(Builder $builder, $results, $model) { if ($results['hits']['total']['value'] === 0) { return $model->newCollection(); } $keys = collect($results['hits']['hits'])->pluck('_id')->values()->all(); $modelIdPositions = array_flip($keys); return $model->getScoutModelsByIds( $builder, $keys )->filter(function ($model) use ($keys) { return in_array($model->getScoutKey(), $keys); })->sortBy(function ($model) use ($modelIdPositions) { return $modelIdPositions[$model->getScoutKey()]; })->values(); } /** * Get the total count from a raw result returned by the engine. * * @inheritDoc * @see ScoutElasticsearchEngine::getTotalCount * @param mixed $results * @return int */ public function getTotalCount($results) { return $results['hits']['total']['value']; } } Providerを読み込む config/app.php 'providers' => [ /* * Laravel Framework Service Providers... */ Illuminate\Auth\AuthServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class, ・ ・ ・ // Elasticsearch Tamayo\LaravelScoutElastic\LaravelScoutElasticProvider::class, // ←これを追加 ], 軽く動作確認 ここまで書いたらphpのコンテナにログインしてScoutが提供しているデータの取り込みスクリプトを実行する。 php artisan scout:import "App\Models\ScoutTestRecord" Imported [App\Models\ScoutTestRecord] models up to ID: 20 All [App\Models\ScoutTestRecord] records have been imported. // こうなれば成功 次はElasticsearchに検索リクエストを投げてみる。 コンテナ間の通信はdocker-composeのおかげでservice名で識別できる。 curl -X GET es:9200/scout_test_records/_search {"took":9,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":20,"relation":"eq"},"max_score":1.0,"hits":[{"_index":"scout_test_records","_type":"App\\Models\\ScoutTestRecord","_id":"1","_score":1.0,"_source":{"id":1,"name":"山田 涼平","has... // 結果が返却されればOK! まとめ MySQL8.0とかPHP8を使ったせいで実プロジェクトで完コピして作業するには難しいかも。 でもおおまかな流れは全て一緒で、変わるとしたらcompsoerを使って依存ライブラリを導入した時のバージョンが変わるくらいかなー。 あと使ったライブラリがあきらか個人名みたいな名前だけどMITライセンス、みたいな感じ、 これってどれくらい信用度あるんだろ。 フレームワーク本体とかよりは信頼度低い気がする。 そのまま使ったら普通にエラーになったし。 クエリの使い方とかちょいちょい上げていきます。 記載漏れとかあったら是非ご連絡ください!
- 投稿日:2021-12-24T00:03:51+09:00
【PHP】buttonタグのsubmitした値がPOSTされなかった話
クリックしたボタンによって処理を分ける formでpostする際において複数のボタンを設置している場合、クリックしたボタンの値を受け取って処理を分岐できる。 例えばメールフォームなどで、確認画面から入力画面に戻るか、送信するかという場合、以下のような処理で実現できる。 <form action="post.php" method="post"> <button type="submit" name="action" value="back">戻る</button> <button type="submit" name="action" value="send">送信</button> </form> $action = (string)filter_input(INPUT_POST, 'action'); if ($action === 'back') { // 入力画面に戻る処理 // リダイレクトとか } // 送信処理 なんてことない、よくある実装だ。 buttonの値が送信されない 上記のようなフォームを実装して、実際に動かしてみたところどちらのボタンをクリックしても、送信処理だけが実行されてていた。$actionのチェックがすり抜けているようだ。 $_POSTをダンプすると、actionの値そのものが送信されていない。 原因 しばらく原因がわからず、さまざまな検証をしたが、JavaScriptでこのようなコードを記述していた。 $(function($) { $('form').submit(function() { $('.submit', this).prop('disabled', true); }); }); disabled…? このコードを削除すると、buttonタグの値が送信された。 これなに? いわゆる多重送信を防ぐために、サブミット後にボタンを無効化する処理。 さいきょうの二重サブミット対策 - Qiita disabledの要素の値が送信されないことは承知だが、submit後に変更してもダメなのか? と、この記事を書いている途中に以下の記事を見つけた。 Chromeの場合、submitボタン押下時にボタンをdisabledにすると、フォームデータが送信されない。 - Qiita なるほど…、Chrome特有の問題なのか? しかし、改善後の内容と現在の記述は(私はjQueryだが)同じものと思われる。 結論(未解決) 記述を変更したが、二重送信防止のためのdisabed処理を実装している場合、Chromeでは値が送信されなかった。 二重送信については、これにおいては直接の問題にはならないので、該当コードを削除して対応した。 ググって出てきた参考リンク(忘備録) Google Chromeでsubmitボタンの多重送信防止対策に対する対策【JavaScript】 JQueryでsubmitボタン連打禁止 送信ボタンの二重クリック防止を目的とするボタン無効化の推奨される対応方法について