--> -->

skimemo


Laravel のバックアップ差分(No.8)


  • 追加された行はこの色です。
  • 削除された行はこの色です。
* 今日のLaravel [#ta529ca4]
これは、2018アドベントカレンダー記事で、Laravel(5.4)を使った開発の中で気づいた事を毎日一つメモしていくものです。
#contents
----
** 2018/12/5 resourceのroute名の変更方法 [#k2619b38]
開発を進めていくと、routeの記述がどうしても長大になりがちな中で、resource定義は一行で複数の挙動が定義できて、しかもroute名も自動で定義してくれてとても助かります。~
しかし、例えば以下のように別々のディレクトリ内で同じ定義名を使いたい場合があります。
#code(php){{
Route::resource('user/regist', 'UserHogeController');
 Route::resource('product/regist', 'ProductHogeController');
}}
この場合、route名はどちらも「regist.index」等となってしまい、別々に識別することができなくなってしまいます。~
~
この問題の解決方法として、[[マニュアル:https://readouble.com/laravel/5.4/ja/controllers.html#restful-naming-resource-routes]]では以下のように再定義できると書かれています。
#code(php){{
Route::resource('photo', 'PhotoController', ['names' => [
    'create' => 'photo.build'
]]);
}}
しかし、アクションがindex,create,store,show,edit,update,destroyと7つもあるのに、いちいち書いていたらroute定義が無駄に長くなってしまいます。
そこで、resource()のソースを確認すると、&inlinecode{Illuminate\Routing\ResourceRegistrar::getResourceRouteName()};で以下のように作成していました。
#code(php){{
if (isset($options['names'])) {
	if (is_string($options['names'])) {
		$name = $options['names'];
	} elseif (isset($options['names'][$method])) {
		return $options['names'][$method];
	}
}
(後略)
}}
stringも許可しています。なので、以下の通り指定すると7つのアクション全てを書き換えてくれます。
#code(php){{
Route::resource('user/regist', 'UserHogeController',['names'=>'user-regist']);
 Route::resource('product/regist', 'ProductHogeController',['names'=>'product-regist']);
}}
便利(^^)~
~
----
** 2018/12/6 apiでセッションを使おうとするとformと両立できない [#o0b28b82]
LaravelではAPIアクセスはapi.phpで定義できますが、ステートレスなアクセスを前提としているため、セッションを使うことができません。((LaravelでセッションIDで認証状態をチェックするステートフルなAPIを使う&br;https://qiita.com/pinekta/items/d10c8374b1a3003cd952))~
参考ページの通りに対応すればセッションが有効になりますが、以下の手順で操作した場合にCSRFエラーになります。~

+ フォームの中でボタンを押すとajaxを使用してAPIアクセス
+ フォームからpostでsubmit

#ref(csrf.png)~
~
想像ですが、APIアクセス時にセッション保持しているCSRFトークンが書き換わってしまい、フォームに埋め込まれたトークンと一致しなくなってしまうと考えられます。~
~
私の場合はやむなく無認証にしましたが、どうしても認証したい場合は、アクセストークン方式にする必要があるのではないかと思います。
~
----
** 2018/12/7 php5とphp7での配列を使ったメソッド呼び出しの挙動の違い [#bfdade1e]
以下のコードにおいて、php5とphp7で挙動の違いがありました。
#code(php){{
<?php

class test {

	public function __construct() {
		echo "test!\n";
		$method[0] = "test2";
		$this->$method[0]();
	}

	private function test2() {
		echo "test2\n";
	}

}

new test();
}}
- php5(5.6.30)の場合
 >php test.php
 test!
 test2
- php7(7.1.24)の場合
 >php test.php
 test!
 PHP Notice:  Array to string conversion in F:\test.php on line 8
 PHP Stack trace:
 PHP   1. {main}() F:\test.php:0
 PHP   2. test->__construct() F:\test.php:17
         :
 PHP Fatal error:  Uncaught Error: Function name must be a string in F:\test.php:8
どうやら変数が配列の場合、php7だと「$this->$method」で一旦区切って解釈するようです。~
~
解決方法は以下の通り。
 -	$this->$method[0]();
 +	$this->{$method[0]}();
これでphp5でも7でも動作するコードになりました。
~
----
** 2018/12/8 WYSWYGエディタには要注意 [#j53c17f7]
WYSWYGはWhat You See What You Getの略で、直訳すると見たままの物が得られる、という意味です。~
そんなの当たり前じゃん、と言っているそこの若者! [[これが誕生した頃は画期的:https://ja.wikipedia.org/wiki/WYSIWYG#%E6%84%8F%E5%91%B3]]だったのですよ(Appleが広めた)。~
~
まあそんなことはいいとして、フリーで使えるWYSWYGエディタは各種あります。((商用でも利用できる、イケてるWYSIWYGエディタ7選 2017年版&br;https://engineer.blog.lancers.jp/2017/12/wysiwyg_editor_best_7/))~
私が試したのは[[Trumbowyg:https://alex-d.github.io/Trumbowyg/]]。簡単にこんなTEXTAREAが得られます。~
#ref(trumbowyg1.png)~
~
使い方はこんな感じ。~
#code(php){{{
{{Form::textarea('description')}}
 {{Html::style(asset('trumbowyg/dist/ui/trumbowyg.min.css'))}}
 {{Html::script(asset('trumbowyg/dist/trumbowyg.min.js'))}}
 {{Html::script(asset('trumbowyg/dist/langs/ja.min.js'))}}
 <script language="JavaScript">
	$('textarea').trumbowyg({
		lang: 'ja',
	});
 </script>
}}}
&size(10){(スクショはプラグインを幾つか入れてます)};~
~
このエディタ、画面上に得るべき物を表示しているということは、HTMLタグをレンダリングしているということになります。このTrumbowygは生HTMLも書けるので、試しにJavascriptタグを埋めてみます。~
#ref(trumbowyg2.png)~
~
そして生HTMLモードを解除してみると・・・~
#ref(trumbowyg3.png)~
~
うーん、ダメだこりゃ。これではユーザーが自由にこのサーバー上でJavascriptを動かせちゃうということです。~
生HTMLは書けないモードにして使う必要がありそうです。~
Trumbowygはボタンのカスタマイズができますので、例えばこんな感じです。~
#code(php){{
<script language="JavaScript">
	$('textarea').trumbowyg({
		lang: 'ja',
		btns: [
			['undo', 'redo'], // Only supported in Blink browsers
			['formatting'],
			['strong', 'em', 'del'],
			['superscript', 'subscript'],
			['link'],
			['insertImage'],
			['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'],
			['unorderedList', 'orderedList'],
			['horizontalRule'],
			['removeformat'],
			['fullscreen'],
		]
	});
</script>
}}
~
ただし、これでもブラウザのJavascriptを無効にして、<script>タグ書いて、再度有効にすればやはり実行できます。うーん悩ましい・・・。~
~
追記: ぬれぎぬでした・・・→[[#q4ca3ee1]]
----
** 2018/12/9 Laravelのファイルストレージ(S3)を使う [#b40b240a]
一番ひっかかっのはS3の設定です(^^;)。当初S3の設定が良く分からなかったのですが、こちら((超簡単!LaravelでS3を利用する手順&br;https://qiita.com/tiwu_official/items/ecb115a92ebfebf6a92f))に非常に詳しく書かれています。感謝!~
S3のバケットを作るだけではダメで、S3とは独立した設定でユーザーを作るのが必要でした。AWSの基本?(^^;)~
但し、上記の方法はユーザーの認証情報とバケットを結びつけていないため、このユーザーのS3には全てアクセスできてしまうと思われます。実際には当該バケットに限定した権限設定が必要でしょう。~
~
まずはコードの確認でlocalに保存してみます。~
- View
#code(PHP){{{
{{Form::open(['files' => true])}}
	{{Form::file('file')}}
	{{Form::submit()}}
{{Form::close()}}
}}}
いや、まあこれだけじゃないでしょうけど(笑)、Form::open()に&inlinecode{'files' => true};を付けるのを忘れずに。~
- Controller
#code(php){{
$file = $request->file('file');
$filename = $file->getClientOriginalName();
$path= Storage::disk('local')->putFile('hoge', $file);
}}
hogeはサブディレクトリ名です。特に必要がなければ'/'。~

すると、&inlinecode{storage/app/hoge};の下に、ユニークなファイル名でファイルが保存されます。(Oc4GsSGFIe3NkjnUUttt4mnaRtpKklQhv5ZArdMq.txt みたいな感じ)~
従って、元のファイル名は&inlinecode{getClientOriginalName()};を使って別途保存しておく必要があります。~
~
次に、S3に保存してみます。~
これは簡単で、コードを以下のように変えるだけです。(もちろんS3の設定、&inlinecode{.env};の設定、&inlinecode{league/flysystem-aws-s3-v3};の導入は済んでいる前提です)
#code(php){{
$path= Storage::disk('s3')->putFile('hoge', $file);
}}
~
しかし、ここで問題が起きました(ネタ、来た(笑))~
以下のようなエラーが発生しました。~
#ref(curlsslerr.png)~
~
要約すると、ローカルの証明書が無いからSSLアクセスができない、ということです。調べるとWindowsのCURLはデフォルトで証明書が無いためSSLアクセスがエラーになるということが分かりました(S3関係ないやん・・・(笑))。~
検索するとCURLにオプションを付ける対策などが出てくるのですが、LaravelFWをいじることになるので却下。~
こちら((cURL error 60: SSL certificate in Laravel 5.4&br;https://stackoverflow.com/questions/42094842/curl-error-60-ssl-certificate-in-laravel-5-4))のページにある方法で解決しました。
~
+ [[http://curl.haxx.se/ca/cacert.pem]]から証明書をダウンロード。
+ 適当なディレクトリに保存。
+ php.iniで&inlinecode{curl.cainfo = "C:\php5.6.37\cacert.pem"};のように指定
+ Apache再起動

実行後、S3のコンソールでファイルが確認できれば成功です。~
ちなみに、localからS3に切り替える場合、いちいちコードを修正しなければいけないのでしょうか? もちろんそんなことはありません。&inlinecode{Storage::disk()};のソースを見ると、
#code(PHP){{
public function disk($name = null)
{
   $name = $name ?: $this->getDefaultDriver();
   return $this->disks[$name] = $this->get($name);
}
      :
public function getDefaultDriver()
{
   return $this->app['config']['filesystems.default'];
}
}}
のようになっています。つまり&inlinecode{Storage::disk()};と引数無しにしておけば、.envを変更するだけで切り替えられるようになります。(もちろん切り替え時のデータ移行は必要ですけど)
~
----
** 2018/12/10 phpの5と7.1と7.2、MySQLの5.5と5.6 [#h6d99c11]
私は複数箇所に開発環境があるのですが、片方は本番と同じphp5.6+MySQL5.7です。もう一方はなるべく新しい環境でも動作することを担保するため、php7+MySQL8にしてみました。そこで分かったことを2つ(3つ?)。~
+ php7の環境でcomposer updateしてはいけない~
updateするとLaravelのライブラリが色々上がります。その環境ではもちろん動くのですが、php5の環境で動かそうとした時に問題が発生します。php7で新たに追加された機能(戻り値の型の宣言など((PHP 5.6.x から PHP 7.0.x への移行&br;http://php.net/manual/ja/migration70.new-features.php#migration70.new-features.return-type-declarations)))でエラーになるのです。~
従って、composer updateは必ずphp5.6の環境で行う必要があります。~
もし誤ってphp7で上げてしまってもphp5.6環境でupdateすれば戻ります。~
~
+ count(null)がエラーになる~
php7.1では問題ありませんが、php7.2ではcount(null)がNOTICEを吐くようになりました((PHP7.2のcountにハマった話&br;https://qiita.com/masaki-ogawa/items/1671d110b2286ececd09))。LaravelではExceptionが発生します。~
厳密に型を考えてコードを書いている人(なんてphpで居るの?)は問題無いと思いますが、私はそこかしこでやってしまっているので、php7.2は断念しました。~
~
+ MySQL8ではstrictのモードが違う
MySQLはバージョンが変わるとデフォルトの設定があれこれ変わることで有名(?)です。~
MySQL8とLaravelの環境では、こんなエラーが出ました。~
#ref(strict.png)~
簡単に回避するには、database.phpの設定でstrictをfalseにしてやると出なくなります。((Laravel5 MySQL8.0 NO_AUTO_CREATE_USERのSQL_MODEエラー対策&br;https://qiita.com/ucan-lab/items/2a482a9537dcc5daeb97))~
でも本当はMySQLの環境を合わせた方が良いでしょうね。((Default laravel's strict mode isn't compatible with MySQL 8.0 (NO_AUTO_CREATE_USERS)&br;https://github.com/laravel/framework/issues/23970))
~
----
** 2018/12/11 WYSWYGエディタの使い方 [#q4ca3ee1]
12/8にWYSWYGエディタに生HTMLが書けてしまう危険性について書きました。~
その後色々調べてみると、どうやら使い方に問題があることが分かりました。エディタのせいにしてしまってごめんなさいm(_ _)m~
~
前回は&inlinecode{{{Form::textarea()}}};を使っていたのですが、これだとJavascriptを無効にしてもテキスト入力ができてしまい、脆弱性が生まれます。~
Javascriptが無効の時は入力も無効になるよう、以下のようにすればOKです。
#code(php){{{
<div id="description">{!!$contents['description']!!}</div>
                    :
<script language="JavaScript">
    $('#description').trumbowyg({
        lang: 'ja',
    });
</script>
}}}
サンプルもこのようになっているんですが、一部のドキュメント((https://alex-d.github.io/Trumbowyg/documentation/#prefix))はtextareaを使っちゃってるんですよね。これ危険では・・・(^^;)~
~
ちなみにsubmitの時は、divでは値が飛ばないので一工夫必要です。
#code(php){{{
<script language="JavaScript">
	function doSubmit(){
		let desc = $('#description').html();
		// 要素追加
		let elm = document.createElement('input');
		// 属性を設定
		elm.setAttribute('type', 'hidden');
		elm.setAttribute('name', 'description');
		elm.setAttribute('value', desc);
		// 要素を追加
		let frm = document.getElementById('formEdit');
		frm.appendChild(elm);
		frm.submit();
	}
</script>
         :
{{Form::submit('更新',['onclick'=>'doSubmit();'])}}
}}}
~
----
** 2018/12/12 WindowsのMultiByteファイル名とエンコード [#p30aeebb]
今日はディレクトリ内にあるファイルを拾ってSQL文にかけるという[[コマンドライン:https://readouble.com/laravel/5.4/ja/artisan.html#generating-commands]]処理を書いていました。~
サンプル的に書くと以下のような感じです。~
#code(php){{
public function handle() {
	$list = scandir('F:\\');
	foreach($list as $filename) {
		echo $filename."\n";
		DB::table('migration')->where('filename',$filename)->count();
	}
}
}}
2カ所ある環境のうち、1カ所(環境A)では問題無く動いたのですが、もう一方(環境B)では何故かエラーになりました。どちらもWindows10Proです。~
>
SQLSTATE[HY000]: General error: 1267 Illegal mix of collations (utf8_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,COERCIBLE) for operation '=' (SQL: select count(*) as aggregate from `migration` where `name` = 20181202_instにstatus追加.txt)
<
DBのcollationはutf8_general_ciです。何故かWindowsのファイル名はutf8mb4_unicode_ciとなっているようです。ちなみにソースはUTF8で書かれています。~
~
環境Aでこのまま動いたのが謎ではあるのですが、普通に考えるとWindowsはSJIS環境ですので、以下の変換を入れてみます。~
 $filename = mb_convert_encoding($filename,mb_internal_encoding(),(PHP_OS=='WINNT'?'SJIS':'UTF-8'));
すると、エラー無く動作するようになりました((この変換処理が環境AやLinux環境で正常に動作するかは未確認です))。(DBへの照会も正しくされていました)~
~
何故同じWindows10でこのような差が出るのかは謎です。思い当たる違いと言えば、環境Aは今年のWindows10クリーンインストールで、環境BはMSDNのWindows10 1151からのアップデートということぐらいですが・・・。なぜ?(笑)