- 投稿日:2019-08-21T23:38:06+09:00
laravelで検索機能を実装する
今回は文字列を入力して部分一致検索、プルダウンメニューから選択して検索を行う機能を実装します。具体的には下記の画面でユーザ名、棋力、好きな戦法による検索を行います。
Userモデルは以下のように name(ユーザ名)、stength(棋力)、tactics(好きな戦法)というカラムを定義します。
class User extends Authenticatable { protected $fillable = [ 'name','strength','tactics', ]; }コントローラは以下のようなindexメソッドを定義します。
class SearchController extends Controller { public function index(Request $request){ $query = User::query(); //$request->input()で検索時に入力した項目を取得します。 $search1 = $request->input('strength'); $search2 = $request->input('tactics'); $search3 = $request->input('name'); // プルダウンメニューで指定なし以外を選択した場合、$query->whereで選択した棋力と一致するカラムを取得します if ($request->has('strength') && $search1 != ('指定なし')) { $query->where('strength', $search1)->get(); } // プルダウンメニューで指定なし以外を選択した場合、$query->whereで選択した好きな戦法と一致するカラムを取得します if ($request->has('tactics') && $search2 != ('指定なし')) { $query->where('tactics', $search2)->get(); } // ユーザ名入力フォームで入力した文字列を含むカラムを取得します if ($request->has('name') && $search3 != '') { $query->where('name', 'like', '%'.$search3.'%')->get(); } //ユーザを1ページにつき10件ずつ表示させます $data = $query->paginate(10); return view('users.search',[ 'data' => $data ]); } }検索ボタンを押したときのルーティングを以下のように記述します。
//検索ボタンを押すとコントローラのindexメソッドを実行します Route::get('Search','SearchController@index')->name('search');ユーザ検索ページのビューを以下のように記述します。
<div class="row"> <div class="col-sm-4"> <div class="text-center my-4"> <h3 class="brown border p-2">ユーザ検索</h3> </div> {!! Form::open(['route' => 'search', 'method' => 'get']) !!} <div class="form-group"> {!! Form::label('text', 'ユーザ名:') !!} {!! Form::text('name' ,'', ['class' => 'form-control', 'placeholder' => '指定なし'] ) !!} </div> <div class="form-group"> {!! Form::label('strength', '棋力:') !!} {!! Form::select('strength', ['指定なし' => '指定なし'] + Config::get('strength.kiryoku') ,'指定なし') !!} </div> <div class="form-group"> {!! Form::label('tactics', '好きな戦法:') !!} {!! Form::select('tactics', ['指定なし' => '指定なし'] + Config::get('tactics.senpou') , '指定なし') !!} </div> {!! Form::submit('検索', ['class' => 'btn btn-primary btn-block']) !!} {!! Form::close() !!} </div> <div class="col-sm-8"> <div class="text-center my-4"> <h3 class="brown p-2">ユーザ一覧</h3> </div> <div class="container"> <!--検索ボタンが押された時に表示されます--> @if(!empty($data)) <div class="my-2 p-0"> <div class="row border-bottom text-center"> <div class="col-sm-4"> <p>ユーザ名</p> </div> <div class="col-sm-4"> <p>棋力</p> </div> <div class="col-sm-4"> <p>好きな戦法</p> </div> </div> //検索条件に一致したユーザを表示します @foreach($data as $item) <div class="row py-2 border-bottom text-center"> <div class="col-sm-4"> <a href="">{{ $item->name }}</a> </div> <div class="col-sm-4"> {{ $item->strength }} </div> <div class="col-sm-4"> {{ $item->tactics }} </div> </div> @endforeach </div> {{ $data->appends(request()->input())->render('pagination::bootstrap-4') }} @endif </div> </div> </div>ここでは以下のように、あらかじめconfigディレクトリにstrength.phpを作成して'kiryoku'という配列を定義しています。上記のビューではconfig::get('strength.kiryoku')でその配列を取得して、プルダウンメニューを作成しています。さらに
['指定なし' => '指定なし'] + config::get('strength.kiryoku')
としてkiryoku配列に「指定なし」という要素を追加しています。config/strength.php
<?php return array( 'kiryoku' => array( '10級' => '10級', '9級' => '9級', '8級' => '8級', '7級' => '7級', '6級' => '6級', '5級' => '5級', '4級' => '4級', '3級' => '3級', '2級' => '2級', '1級' => '1級', '初段' => '初段', '二段' => '二段', '三段' => '三段', '四段' => '四段', '五段' => '五段', '六段' => '六段', ), ); ?>config::get('tactics.senpou')も同様です。
config/tactics.php<?php return array( 'senpou' => array( '角換わり' => '角換わり', '矢倉' => '矢倉', '相掛かり' => '相掛かり', '横歩取り' => '横歩取り', '向かい飛車' => '向かい飛車', '三間飛車' => '三間飛車', '四間飛車' => '四間飛車', '中飛車' => '中飛車' ), ); ?>ユーザ名のみで検索すると、以下のように正しく表示されました。
また棋力、好きな戦法で検索した場合も、以下のように正しく表示されました。
今回の検索機能のポイントは、コントローラの
$request->input
メソッドと$query->where
メソッドかなと思いました。あとプルダウンメニューの実装はconfigファイルを使用しましたが、意外と躓きました。プルダウンメニューはほかの方法でより簡単に実装できるのかもしれません。
- 投稿日:2019-08-21T23:26:39+09:00
EC2インスタンスを立ち上げて、PHPを動かすまで
はじめに
インフラ初心者のエンジニアにEC2のインスタンスを立てる手順を教えることになりました。
私も専門領域外ですが、初心者がハマりそうなポイントとPHPを入れて動かすところまで書いてみます。EC2インスタンス立ち上げる
インスタンスを立てる手順は既に分かりやすい記事が多数あるので省略。
こちらの記事がシンプルで分かりやすかったので、オススメです。AWS EC2でWebサーバーを構築してみる
https://qiita.com/Arashi/items/629aaed33401b8f2265c上記記事の手順に沿って、無事にApache動作確認できたとします。
もし導入中にハマったら、最後のセクションでハマりやすいポイントを書いたので参考にしてみてください。PHPを入れる
sudo yum -y install php実行すると以下の通り。
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd Resolving Dependencies --> Running transaction check ---> Package php.x86_64 0:5.4.16-45.amzn2.0.6 will be installed --> Finished Dependency Resolution ... Installed: php.x86_64 0:5.4.16-45.amzn2.0.6 Complete!yumの場合、デフォルトだとPHPは5.4です。
PHP7はちょっと一工夫しないといけないので、初心者がいきなり7を入れようと頑張るよりは、まず5.4を入れて動くところまで確認したほうが良いかと思います。index.phpを有効にする
vi /etc/httpd/conf/httpd.confvimは使い慣れていないと使いづらいかもしれません。
サーバでサクッと設定いじったりする際に使えると便利なので、この期に覚えてしまいましょう。... # <IfModule dir_module> DirectoryIndex index.php index.html </IfModule> # ...こう記述すると、ファイル名を指定せずアクセスした際にindex.phpが存在すれば表示し、存在しなければindex.htmlを表示します。
vimの操作が分からない方
- vimは開いたときはコマンドモードで、文章の編集ができません。
- iキーでインサートモード(文字入力ができるモード)に切り替わります。
- インサートモード中にEscでコマンドモードに戻ります。基本的に、このインサートモードとコマンドモードを切り替えて使います。
- 最初は超絶使いづらいですが、慣れるとサクサクコーディングできるようになります。
index.phpを追加し保存するには
- コマンドモードで /index[Enter]
- スラッシュの後に入力した文字で検索ができる
- iキーでインサートモードに切り替え、index.phpの文字列をタイピングした後にEscでコマンドモードに戻る
- コマンドモードで :w[Enter] :q[Enter]と入力
- :wで保存(write)、:qでファイルを閉じる(quit)
- :wq[Enter]と入力すると、保存しファイルを閉じる処理を一度に実行できる
設定を反映する
設定ファイルを変更してアクセスしても、まだ反映されません。
基本的にミドルウェアの設定を変更した場合、反映するには再起動が必要です。sudo service httpd configtest
まずはconfigでsyntax errorがないかテスト。
慣れてくるとついつい即再起動しがちですが、syntax errorが起きているとApache起動に失敗してサービスが止まるので、必ずconfigtestでエラーが起きないことを確認する癖を付けましょう。sudo service httpd graceful
gracefulで再起動します。
ちなみに再起動はgracefulじゃなくrestartでもできますが、restartはWebサーバにアクセスしているユーザーがいて子プロセスがあってもkillします。
gracefulは子プロセス終了まで待って設定を反映するので安全です。
ただしgracefulだと反映されない設定もあったりするので、その場合はrestartしましょう。index.phpを配置する
vi /var/www/html/index.phpvimが開くので
<?php echo 'hoge' ?>で保存し終了。
Apacheの設定がうまくいっていれば、Webブラウザで開いた際のページに「hoge」と書かれたページが表示されます。
ハマりやすいポイント
他にもあれば随時追加します。
EC2インスタンスにsshで入れない
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: UNPROTECTED PRIVATE KEY FILE! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Permissions 0666 for 'xxxxx.pem' are too open. It is required that your private key files are NOT accessible by others. This private key will be ignored. Load key 'xxxxx.pem': bad permissions ec2-user@xx.xx.xx.xx: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).ハマることが非常に多いです。
秘密鍵のパーミッションが緩いとエラーになります。sudo chmod 400 xxxxx.pem
でいけるはず。
Apacheの設定ファイルが見つからない/読み取り専用で編集できない
E45: 'readonly' オプションが設定されています (! を追加で上書き)ミドルウェアのconfigいじったり起動/停止等を実行する際は一般ユーザーが簡単にいじれちゃうと困るので、基本的にrootユーザーで実行します。
sudo su -
index.phpが真っ白な画面になる
ソースを表示すると、phpファイルがそのままテキストとして表示されていませんか?
その場合はApacheの... # <IfModule dir_module> DirectoryIndex index.php index.html </IfModule> # ...が同じように記述されているか確認してください。
index.phpが読み取り専用で保存できない
/var/www/html/ディレクトリのパーミッションがおかしい可能性があります。
雑ですが777にパーミッションを変えてしまいましょう。sudo chmod 777 /var/www/html/
以上です。
- 投稿日:2019-08-21T18:58:04+09:00
file_get_contents('php://stdin') で、標準入力を渡さないとどうなるか。
ハマったこと
PHPの入出力ストリームラッパーを使って、標準入力を受け取る場合。(手元のPHP7.1で動作確認)
https://www.php.net/manual/ja/wrappers.php.phpたとえばこんなスクリプトで
<?php $str = file_get_contents('php://stdin'); echo $str;標準入力を渡すと、その文字列をそのまま表示します。
$ echo 'hoge' | php test.php hogeでは、このスクリプトに標準入力を渡さなかった場合どうなるでしょう?
$ php test.php
空の文字列でも帰ってくるのかなと思ってたのですが。。。
実は、 処理が止まってプロンプトが帰ってきません。
(正確には、パイプを渡してない時は標準入力が入力待ちになってブロックされている)対処策
posix_isatty()
関数を利用します。
http://php.net/manual/ja/function.posix-isatty.phpこの関数を使って、 STDINがオープンされていて、 かつ端末に接続されているか否かを判定します。
(標準入力が渡っている場合はfalseになる)<?php $str = (posix_isatty(STDIN)) ? 'default' : file_get_contents('php://stdin'); echo $str;動作検証
$ echo 'hoge' | php test.php hoge $ php test.php default
- 投稿日:2019-08-21T17:17:38+09:00
Wordpress 開発でも静的解析したい!
静的解析しよう
Wordpress のプログラムを書いているときにサイトが止まるような実行エラーって怖いですよね。
それらを少しでも減らすために、プログラム公開前に自身の開発環境で、型のチェックや引数のチェックなど、静的解析を正しく行うことで、予め実行エラーに気づける環境を作りましょう。環境
- Wordpress プラグイン開発
- PHP7
ディレクトリ構成
. ├── RAPI_Plugin.php # plugin main ファイル ├── composer.json ├── composer.lock ├── phpstan.neon # phpstan 設定ファイル ├── require.php ├── src # 静的解析したいプログラムのディレクトリ │ ├── Controllers │ ├── RAPI.php │ ├── Models │ └── Routes ├── bin ├── tests └── vendorPHPStan
PHP 向けの静的解析ツールです。
こちらの方の説明が詳しいです。
https://qiita.com/qiita_masaharu/items/24bf34579119628eefe2WordPress extensions for PHPStan
szepeviktor/phpstan-wordpress を利用します。
https://github.com/szepeviktor/phpstan-wordpresscomposer.json{ "require-dev": { "szepeviktor/phpstan-wordpress": "^0.2.0" }, "scripts": { "post-install-cmd": "PHPStan\\WordPress\\Composer\\FixWpStubs::php73Polyfill", "post-update-cmd": "PHPStan\\WordPress\\Composer\\FixWpStubs::php73Polyfill" } }これを
composer install
設定ファイル(phpstan.neon)
phpstan.neonincludes: - vendor/szepeviktor/phpstan-wordpress/extension.neon parameters: excludes_analyse: ['bin', 'tests', 'vendor'] autoload_directories: - ./src bootstrap: null level: max fileExtensions: - php inferPrivatePropertyTypeFromConstructor: true実行準備
composer update --classmap-authoritativeこのコマンド実行する
実行
./vendor/bin/phpstan analyse --memory-limit 256M . 11/11 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% [OK] No errors良さそうですね。
- 投稿日:2019-08-21T16:44:09+09:00
Wordpress でカスタム URL を追加する
Wordpress 環境で開発をする際、新しくページ(URL)を追加したい場合があるかと思います。
ただ、ページ追加に関しては、rewrite_rules_array に URL やパラメータを追加するなどの Wordpress 独特の知識が必要となります。
そういったものを取り払い、Routing としてページ追加を実現することが記事の趣旨です。環境
・Wordpress プラグイン開発
・PHP 7.2.19
・RAPI という namespace でプラグイン開発Route Class
このクラスはこちらのサイトから引用させていただいています。
https://firegoby.jp/archives/1213これは、作りたい URL を wordpress の URL の取り扱いに変換させるためのクラスです。
Route.php<?php namespace RAPI\Routes; class Route { private $rule = null; private $query = null; private $callback = null; public function __construct($rule, $query, $callback){ $this->rule = $rule; $this->query = $query; $this->callback = $callback; add_filter('query_vars', array(&$this, 'query_vars')); add_filter('rewrite_rules_array', array(&$this, 'rewrite_rules_array')); add_action('init', array(&$this, 'init')); add_action('wp', array(&$this, 'wp')); } public function init() { global $wp_rewrite; $rules = $wp_rewrite->wp_rewrite_rules(); if (!isset($rules[$this->rule])) { $wp_rewrite->flush_rules(); } } public function rewrite_rules_array($rules) { global $wp_rewrite; $new_rules[$this->rule] = $wp_rewrite->index . '?'.$this->query.'=1'; $rules = array_merge($new_rules, $rules); return $rules; } public function query_vars($vars) { $vars[] = $this->query; return $vars; } public function wp() { if (get_query_var($this->query)) { call_user_func($this->callback); } } } ?>Router class
これは URL の登録管理のためのクラスです。
Route インスタンスを作ることで Routing を作成します
下の例であれば /rapi/api/article の URL のアクションを article_api_controller の index function にマッピングして登録しています。Router.php<?php namespace RAPI\Routes; use RAPI\Controllers\Api\ArticleController; class Router { protected $article_api_controller; public function __construct() { $this->article_api_controller = new ArticleController(); $this->addRoutes(); } public function addRoutes() { new Route('rapi/api/article', 'rapi_api_article_index', [$this->article_api_controller, 'index']); } }利用方法
App.php<?php namespace RAPI; use RAPI\Routes\Router; class App { protected $router; public function __construct() { $this->router = new Router() } }Controller class (参考)
上の例であれば、 /rapi/api/article の URL で、この index() メソッドが実行され、
JSON で ["id":1] が出力されるものとなります。
REST API 作るだけであれば WP REST API で良いですけどね。ArticleController.php<?php namespace RAPI\Controllers\Api; class ArticleController { public function index() { wp_send_json(['id' => 1]); } }
- 投稿日:2019-08-21T14:39:42+09:00
【WordPress】年別の記事の出し分けとページネーション
Advanced custom fieldを使用
releases.php<div class="releases_list"> <?php $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1; $query = new WP_Query(array( 'posts_per_page' => 4, 'post_type' => 'releases', 'orderby' => 'date', 'paged' => $paged )); ?> <?php $prev_year = null; ?> <?php if ($query -> have_posts()): ?><?php while ($query -> have_posts()) : $query -> the_post(); ?> <?php $this_year = get_the_date('Y'); ?> <?php if ($prev_year != $this_year): ?> <div class="releases_list_year"><p><?php echo $this_year; ?></p></div> <?php endif; ?> <ul class="releases_list_news"> <li class="releases_list_news_list"> <dl class="releases_list_news_dl"> <dt class="releases_list_news_ttl"><p><?php the_time('Y-n-j'); ?></p></dt> <dd class="releases_list_news_read"> <a><?php the_field('news'); ?></a> </dd> </dl> </li> </ul> <?php $prev_year = $this_year; ?> <?php endwhile; ?><?php endif; ?> </div> <div class="pagi"> <?php custom_pagination('/releases/', $paged, $query->max_num_pages); ?> </div>参考サイト:
https://stackoverflow.com/questions/3397725/wordpress-how-can-i-display-post-link-group-by-yearfunctions.phpfunction custom_pagination($uri, $current, $last, $range = 5){ $showitems = ($range * 2)+1; if(preg_match('/\\?/', $uri)){ $uri .= '&'; } else { $uri .= '?'; } if(1 !== (int)$last){ $prev = $current-1; $next = $current+1; $currentPage = false; $beforePage = false; $afterNum = 0; $path = get_template_directory_uri(); echo "<div class='page_nsv'>"; // echo "<p class=\"prev prne nav_list\"><a href=\"{$uri}paged={$prev}\"><</a></p>"; echo "<ul class=\"nav_list nav_con_list dot_left dot_right\">"; for ($i=1; $i <= $last; $i++){ if($i === 1 && $current !== 1){ echo "<p class=\"prev prne nav_list\"><a href=\"{$uri}paged={$prev}\"><</a></p>"; } if($current === $i){ echo "<li class=\"nav_con_list_li\"><a href=\"#\" onclick=\"return false;\" class=\"page-link\">{$current}</a></li>"; } else { echo "<li class=\"nav_con_list_li\"><a href=\"{$uri}paged={$i}\" class=\"page-link\">{$i}</a></li>"; } if($i == $last && $current != $last){ echo "<li class=\"nav_con_list_li\"><a class=\"page-link\" href=\"{$uri}paged={$next}\"></a></li><p class=\"next prne nav_list\"></p>"; echo "<p class=\"next prne nav_list\"><a href=\"{$uri}paged={$next}\">></a></p>"; } } echo "</ul>"; echo "<p class=\"next prne nav_list\"></p>"; echo "</div>"; } }functions.phpでページネーションを設定
- 投稿日:2019-08-21T14:05:08+09:00
POST送信後と送信前の文字数の違い
概要
ThreeLでwikiのように整形を行っている部分で
うまいこと変換されないことが問題点にあたる。結論
改行コードの違い
htmlのtextareaの改行コードがCR/LFhtmlのtextareaの改行コードがCRLFであるが
javascriptの場合は、LF。この違いがあるため \n\n 部分が認識されなかった。
経緯
ajaxのプレビューと本ページが差分が存在
文字列の長さの検証、違うことが確認できる。調べてみるとtextareaの改行コードがCRLF
jsではLF
違いの確認が出来た。解決策
$text = str_replace("\r\n", "\n", $post_textarea);どちらかにそろえてあげるだけで大丈夫です。
もしくはパーサー側で対応するのもありかと思う。ほかの問題点として文字数が違うため文字数制限をかける際に少なくなったりなどがあるなるので、知っておくといいですね」。
- 投稿日:2019-08-21T12:49:09+09:00
Laravelで閉包テーブル(Closure Table) を簡単に実現してくれるライブラリ ClosureTable
Laravelで閉包テーブル(Closure Table) を簡単に実現してくれるライブラリ ClosureTable
DBのツリー構造のテーブルにはいくつかありますが、その中に閉包テーブルというものがあります。
この記事にたどり着く人には説明不要だと思うので説明は省きます!!!!各ツリー構造についてはこちらの記事がわかりやすい気がします!
https://qiita.com/hirashunshun/items/06adf4f42f03a9f3b63d実際に閉包テーブルを自前で実現しようとすると、CRUDのクエリ投げるときに整合性とれてるのかこれ。。。って部分があり、孫だけ取得とかそのへんのクエリをゴリゴリ書くのは面倒な部分が多く、そこがデメリットな感じもします
本題
ゴリゴリクエリ書くのが面倒だなーというデメリットを解決してくれるのがこれ!
https://github.com/franzose/ClosureTableテーブル構造のことなんて何も考えず脳死で閉包テーブルが作れます!
めちゃくちゃ良いライブラリだと思います使い方(Migrationまで
ライブラリが提供しているコマンドでmigrationを作成
php artisan closuretable:make --entity=treesすると、migration・Model・Interfaceとかが作られる
create 2019_08_19_105450_create_trees_table_migration
create Tree
create TreesInterface
create TreesClosure
create TreesClosureInterface
2019_08_19_105450_create_trees_table_migrationuse Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateTreesTableMigration extends Migration { public function up() { Schema::create('trees', function (Blueprint $table) { $table->increments('id'); $table->integer('parent_id')->unsigned()->nullable(); $table->integer('position', false, true); $table->integer('real_depth', false, true); $table->softDeletes(); $table->foreign('parent_id') ->references('id') ->on('trees') ->onDelete('set null'); }); Schema::create('trees_closure', function (Blueprint $table) { $table->increments('closure_id'); $table->integer('ancestor', false, true); $table->integer('descendant', false, true); $table->integer('depth', false, true); $table->foreign('ancestor') ->references('id') ->on('trees') ->onDelete('cascade'); $table->foreign('descendant') ->references('id') ->on('trees') ->onDelete('cascade'); }); } public function down() { Schema::table('trees_closure', function (Blueprint $table) { Schema::dropIfExists('trees_closure'); }); Schema::table('trees', function (Blueprint $table) { Schema::dropIfExists('trees'); }); } }最低限の構造を作ってくれます。必要であれば、カラムとか追加可能
基本的にはtrees側に追加すればよいかとinterface抜粋
Treeinterfaceuse Franzose\ClosureTable\Contracts\EntityInterface; interface treesInterface extends EntityInterface { }継承しているInterfaceがClosureTable独自のものになっている
モデル
Tree.phpuse Franzose\ClosureTable\Models\Entity; class trees extends Entity implements treesInterface { /** * The table associated with the model. * * @var string */ protected $table = 'trees'; /** * ClosureTable model instance. * * @var treesClosure */ protected $closure = 'App\treesClosure'; }継承しているEntityがClosureTable独自のものになっている
この辺のclass名がloweerCaseなのは気持ち悪いので好みで変える…!
rotected $closure = 'App\treesClosure';実際の使い方
一番上のモデルを作る時
$rootTree = new Tree(); $rootTree->save();普通にモデル作るのと同じ
子供追加
// 親作る $rootTree = new Tree(); $rootTree->save(); // 子供作る $childTree = new Tree(); $rootTree->addChild($childTree); $childTree->save(); // 子供2作る $childTree2 = new Tree(); $rootTree->addChild($childTree2); $childTree2->save();これもいい感じにできる
削除
// 親作る $rootTree = new Tree(); $rootTree->save(); // 子供作る $childTree = new Tree(); $rootTree->addChild($childTree); $childTree->save(); // 子供2作る $childTree2 = new Tree(); $rootTree->addChild($childTree2); $childTree2->save(); // 子供達だけ削除の場合 $rootTree->deleteSubtree(); // 自分を含む子供達を削除する場合 $rootTree->deleteSubtree(true);取得
$rootTree = Tree::find(いつもの); // これで子供達とれる(孫まで取れるわけではないので再帰処理とか別メソッドを使う必要がある $children = $rootTree->getChildren();こんなかんじ
書くのが辛くなったのでまとめ
使いやすい!
各メソッドはいい感じに抽象化されてて使いやすい!公式みて!!
https://github.com/franzose/ClosureTable#ancestors使ってみて!!
- 投稿日:2019-08-21T12:47:12+09:00
IntelliJ・WebStrom・PhpStorm等のJetBrains製IDEで、文字列の直前に「language=JSON」と書くと、その文字列にJSONのシンタックスハイライトが効いて便利だった。
IntelliJやWebStrom、PhpStormなどのJetBrains製IDEで、文字列の前に
// language=JSON
というコメントをつけると、IDEが文字列をJSONとして認識してくれるため、
- JSONとしてのシンタックスハイライト
- JSON構文エラーの警告
- JSONのコード補完
- コード整形
といった、地の文でJSONを書いたときにIDEがやってくれるような恩恵を享受できるようになる。
この機能はLanguage Injectionと呼ばれるもの。コメントが書ける大抵の言語なら、JavaでPHPでもJavaScriptでもScalaでも使えるようだ。
この機能はJSONに限ったものでなく、
language=SQL
やlanguage=HTML
などのlanguage_ID
を指定することで他の言語にも対応可能。// language=<language_ID>
PHPは
@lang
が使えるPHPではPhpDocの
@lang
でもLanguage Injectionすることができる。Javaなどでは
@Language
アノテーションが使えるJavaやGroovy、Kotlinでは
org.jetbrains:annotations
をMavenなどの依存に追加することで、@Language
アノテーションを使うことができる。Scalaには対応していない様子。参考文献
所感
IntelliJやWebStrom、PhpStormなどのJetBrains製IDEで、
— suin❄️TypeScriptが好き (@suin) August 20, 2019
文字列の前に
// language=JSON
って書くと、
・JSONがシンタックスハイライトされる
・構文エラーが表示される
・補完がされる
・コード整形もできる
って知ってました?
僕は知りませんでした? pic.twitter.com/2UphpOThoM
- 投稿日:2019-08-21T12:15:05+09:00
SendGrid + CakePHPでWeb API利用時にTemplateを使う
SMTPを使うならいつものsetTemplateでテンプレートを指定できますが、Web APIを使う場合SendGridの純正ライブラリはCakePHP用に書かれたものではないためCakePHPのテンプレートを使うようになっていません。
テンプレートに値を埋めたテキスト(HTML)を取得して、addContentします。宛先や件名の設定などは省略しているので公式を参照してください。
// 実際はComponentにして使っています。以下のコードはControllerで使うことを想定しています // Viewを自分でnewするので使えるようにしておきます use Cake\View\View; $view = new View($this->request, $this->response); // $varsにはテンプレートで使う値が入ってます // ['username' => 'テスト太郎'] // のような連想配列を想定しています // viewに値を渡すとき使う$this->set()と同じなので設定できればどう書いてもいいです foreach ($vars as $key => $value) { $view->set($key, $value); } // $templateにテンプレートを指定します // Template直下から指定する必要があるので、 // いつものEmailテンプレートを使いたければ Email/text(またはhtml)/テンプレート名 にします // これで$contentにテンプレートに値を埋め込んだテキストが入ります $content = $view->render($template, false); // マルチパートにしたければplain、html両方addContentします if ($format == 'text') { $email->addContent('text/plain', $content); } else { $email->addContent('text/html', $content); } $sendgrid = new SendGrid('API-KEY'); try { $response = $sendgrid->send($email); // ログは好きにしてください $this->log($response->statusCode()); $this->log($response->headers()); $this->log($response->body()); } catch (Exception $e) { $this->log('Caught exception: '. $e->getMessage(), LOG_ERR); }SendGridの例を書きましたが、Controller中でTemplateに値を埋めた状態のテキスト取得する方法は汎用的なので知っておくと他でも使えます。
- 投稿日:2019-08-21T11:19:12+09:00
ローカルのPHPからsshトンネルを使ってサーバーへmysql接続
ちょっとしたクエリを試したいときなどローカルから直接アクセスできないDBサーバーへ接続したいときがあるかと思います。
mysqlクライアントツールなど使えば簡単にできるのですが、今回はphpの処理も絡めて行いたかったのでローカルのPHPプログラムからサーバのDBにアクセスできるようにしました。ポートフォワードさせる(sshトンネル)
ターミナルで以下のコマンドを実行させます。
ssh -N -L {任意のポート番号}:{DBエンドポイント} -i {鍵ファイル} -p {踏み台のsshポート番号} {ユーザ名}@{踏み台サーバのホスト}上記コマンドの波括弧部分を変えます。任意のポート番号は使用されていないものならなんでも良いです。このポート番号にアクセスするとポートフォワードされるようになります。
オプションですが
N...リモートコマンドを実行しない
L...指定されたようにポートフォワードさせる
のようになっています。PDO
$pdo = new PDO('mysql:host='. "127.0.0.1:{任意のポート番号}" .';dbname=' . "db_name" .';charset=utf8', "user_name", "password");PDOはいつもの通りですが、hostがサーバーではなくローカルを指すようにします。その際、sshコマンドで入力したポートを利用します。
これでテスト的にプログラムを実行したいときに便利になりました。
- 投稿日:2019-08-21T11:03:17+09:00
phpunitで作成したテストケースをもとに試験項目表を作成する-その2
やりたいこと
前回phpunitで作成したテストケースをもとに試験項目表を作成するでテストケースから試験の内容を出力するところまで作成できました。
ただ、以下のような不満があります。
- コマンドを2回実行しないと試験表が出力されない。
- テストしたクラス全部の結果が一つにレポートにまとまってしまうので個別のテストクラスごとの結果も保存しておきたい。
- ※製品のリポジトリとは別のリポジトリに管理しやすい形だと嬉しい
そこで今回はPHPUnitのテストランナーの拡張して試験あとに実行する処理を追加していこうと思います。
処理の全体像
イメージとしてはPHPUnitのテストランナーで実行されたテストクラスを監視して以下のような簡単なしくみを実装しています。
出来上がった試験結果はこんな感じでとりあえず配置しようと思います。
実装方法
テストランナー拡張用のクラスを用意する
phpunitの実行中に処理を挟むためのテストランナー拡張用クラスを用意します。
今回は以下のようなfileを配置します。配置場所はどこでもよいですが今回はtestsフォルダ配下にExtensionフォルダを設けてそちらに格納するようにしました。
BeforeTestHook
で実行中のテストクラス名を取得して、処理完了後にHTMLへの変換処理を実行しています。余談ですが
AfterLastTestHook
というイベントフックもあったのですが、HTMLへの変換のもとにしているファイル出力がAfterLastTestHookのタイミングではまだ生成されていなかったのでデストラクタでテスト完了後の処理を記述しています。TestRunnerExtension.php<?php namespace Tests\Extension; use PHPUnit\Runner\BeforeTestHook; final class TestRunnerExtension implements BeforeTestHook { /** * @var string */ private $resultFilePath = 'tests/log/logfile.xml'; /** * @var string */ private $testedFileList = 'tests/log/testedFileList.txt'; /** * @var string */ private $resultDir = 'tests/log/result/'; /** * @var array */ private $classList = []; /** * PHPUnitのログが出力後にファイル生成を行いたいので * __destructで終了処理を記述する。 */ public function __destruct() { if (!file_exists($this->resultFilePath)) { return; } if (count($this->classList) > 1) { $this->multiClassTestReport(); } else { $this->singleClassTestReport(); } } /** * 複数のクラスにまたがって試験した場合は個別のテストクラスも実行する。 * @throws \ReflectionException */ private function multiClassTestReport() { exec('xsltproc phpunit.xslt ' . $this->resultFilePath . ' > ' . $this->resultDir . 'output.html'); if (file_exists($this->testedFileList)) { unlink($this->testedFileList); } foreach ($this->classList as $className) { $reflection = new \ReflectionClass($className); exec('vendor/bin/phpunit ' . $reflection->getFileName()); } } /** * 単一のクラスの試験ではクラス名と対応したパスにファイルを生成する。 */ private function singleClassTestReport() { $filePathArr = explode('\\', $this->classList[0]); $fileName = array_pop($filePathArr); $filePath = implode('/', $filePathArr); if (!file_exists($this->resultDir . $filePath)) { mkdir($this->resultDir . $filePath, 0777, true); } exec('xsltproc phpunit.xslt ' . $this->resultFilePath . ' > ' . $this->resultDir . $filePath . '/' . $fileName . '.html'); } /** * 実行されたテスト情報からテスト対象のクラス名を取得する。 * @param string $test */ final public function executeBeforeTest(string $test): void { $test = substr($test, 0, strpos($test, '::')); if (!in_array($test, $this->classList, true)) { $this->classList[] = $test; } } }phpunit.xmlに拡張クラスを追記する。
上記で作成した拡張クラスをphpnit.xmlに以下のように追記します。
これでphpunitを実行するだけで拡張クラスに記述した処理を自動で実行してくれます。phpunit.xml<extensions> <extension class="Tests\Extension\TestRunnerExtension"/> </extensions>
- 投稿日:2019-08-21T10:02:39+09:00
PHP のエラーログを root が作成してしまってアプリから書けなくなる問題
php のエラーログ、
php.ini
で次のように設定していると・・error_log = /var/log/php/php.log log_errors = Onなにかの拍子で root で php を実行してエラーが出力されると
/var/log/php/php.log
が root 所有の 0644 とかで作成されてしまい、アプリケーションの実行ユーザーからエラーログが書けなくなってしまいます。この問題を解決するためのいくつかの案。
案:logrotate で 0666 でファイルを作成する
logrotate で次のように
create 0666 root root
を指定します。/var/log/php/*.log { missingok notifempty create 0666 root root }こうしておけばローテーション時に 0666 でログファイルができるので、root でログに書き込んでしまっても Web アプリからのエラーログが書き込めなくなったりはしません。
と思ったら次のような問題があるらしいです。
logrotateがrenameして新しいファイルを作るよりはやくPHPのloggerがファイルを作ってしまい、logrotateがfile existsで死んでしまう。どうしてPHPはファイルを開きっぱなしにできないのかしら
https://twitter.com/kazeburo/status/960717004494684161うーん・・・
案:php.ini ではエラーログを設定しない
php.ini ではエラーログのファイル名を空にして SAPI のデフォの出力先に出るようにします(CLI なら標準出力?か標準エラー?、mod_php なら Apache の error_log、php-fpm なら??)。
php.ini
error_reporting = -1 display_errors = off display_startup_errors = off error_log = log_errors = Onその上で httpd.conf とか php-fpm.conf とかの SAPI に固有の設定ファイルでエラーログの出力先を指定します。
httpd.conf
php_value error_log /var/log/php/apache.logphp-fpm.conf
php_value[error_log] = /var/log/php/apache.logもしくは
php-<sapi>.ini
のような特定の SAPI でだけ読まれる php.ini でも良いかもしれません。cron とかから CLI を実行するときはコマンドラインオプションで指定します。
cron.d/app
SHELL=/bin/bash MAILTO=root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin * * * * * apache php -d error_log=/var/log/php/batch.log -d display_errors=stderr /path/to/batch.php | logger -i -t applogrotate では nocreate を指定します。
/var/log/php/*.log { missingok notifempty nocreate }これで不意に root で
php -r xxx
とかしてしまってもログファイルは作成されません。さいごに
PHP を使い始めた最初のころから php.ini で↓みたいにするのは鉄板だと思ってたのですが、
error_log = /var/log/php/php.log log_errors = On実はそうでもなくて世間一般では SAPI 固有の設定ファイルで指定してたりする? のだろうか。いや、syslog という手もあるか。。。
追記
Web アプリケーションを root で動かすことは一般的にはありえないですが、サーバのオペレーション用のスクリプトやちょっとしたワンライナーを PHP で作って root で実行とかすると、サーバグローバルな php.ini でログを吐く設定していると問題になることがあるということですね。ログ以外でも php.ini で出力先が設定できるものは同様です(なにかあったっけ?思いつかない)。アプリケーションのキャッシュとかは PHP のコアが吐くわけではないので問題ありません(それはアプリケションが吐くものなので)。
もっとも、PHP で Web アプリケーション以外を作ることはまあまずないですけど・・実際のところシステムの運用中にこの問題が起こったことはないと思います。
ただ、たまに PHP が一番得意だという理由で PHP でなんでも書く人はいますしそれをあまり止めたりもしないです。もちろん、Web アプリケーションと同じ言語で書いてしまうと Web アプリケーションの都合で言語のバージョンアップをするときに影響範囲がわかりにくくなるので、サーバオペレーション系のスクリプトはディストリから提供されているものをそのまま使うのが良いだろうと思います(Bash とか Python とか)。Web アプリケーション用の PHP とサーバオペレーション用の PHP を別々に入れる? それはやめておいたほうが良いと思いますね、、、どっちの PHP で実行されるかわからなくて新規の人がとてつもなく混乱しそうです(勝手なイメージですが Ruby だと Web アプリケーションとサーバオペレーションをを同じ Ruby で実行してることが多そう?)。
- 投稿日:2019-08-21T09:52:31+09:00
【Laravel】artisanコマンドまとめ
よく使うartisanコマンドのメモ
https://readouble.com/laravel/5.8/ja/
routeの確認
php artisan route:list
マイグレーション
php artisan make:migration create_users_table
マイグレーションを作成
php artisan migrate
マイグレーションを実行する
php artisan migrate:rollback
マイグレーションをロールバック
Seeder
php artisan make:seeder UsersTableSeeder
seederを作成する
シーダクラスを書き上げたら、Composerのオートローダーを再生成する。
composer dump-autoload
php artisna db:seed
DatabaseSeeder
クラスに追加したSeed
を実行するpublic function run() { $this->call([ UsersTableSeeder::class, PostsTableSeeder::class, CommentsTableSeeder::class, ]); }
php artisan db:seed --class=UsersTableSeeder
特定のファイルを個別に実行する
php artisan migrate:refresh --seed
php artisan migrate:refresh
でテーブルを再構築し、seederの値を初期値として設定。
本番環境でやるとやばい。とてもやばい。モデルクラスを作成する
php artisan make:model User
マイグレーションも同時に作成できる。
php artisan make:model Flight --migration
php artisan make:model Flight -m
リクエストクラスを生成する
php artisan make:request UserRegistPost
ページネーションビューのカスタマイズ
php artisan vendor:publish --tag=laravel-pagination
resources/views/vendor
ディレクトリ以下にページネーションのビューファイルが生成される。
デフォルトではbootstrap-4.blade.php
が使用されている。
https://readouble.com/laravel/5.8/ja/pagination.html#customizing-the-pagination-view