20191205のPHPに関する記事は27件です。

Laravelで画像サイズを変更して保存する方法がめちゃめちゃ簡単だった話

はじめまして!qiita初投稿です?

最近Laravelで開発することが増えてきて、その中でも便利だなーと思った「画像サイズの圧縮」について書きます。

随時間違っている部分などは訂正していくと思います。ご指摘もいただけると嬉しいです?

開発環境

PHP 7.3.11
Laravel Framework 6.5.1
Composer version 1.9.1

Laravelで画像処理をする。

今回自分は「interventionImage」というパッケージを利用して、画像サイズを小さくして保存を実装しました。

interventionImageはphpの画像処理ライブラリGD LibraryとImagickをサポートしています。

自分はGD Libraryを使用して実装しました。

GD Libraryのインストール方法は環境によっても変わってくると思うので、お使いの環境に合わせてインストールしてください。

実装までの流れ

interventionImageの公式はこちら
http://image.intervention.io/getting_started/installation#laravel

実装の流れとしては、
1.パッケージのインストール。
2.Laravelで使用できるようにセットアップ。
3.実際に画像サイズを変更してみる。

1.Laravelにintervention/imageをインストールする。

composerを使ってintervention/imageをインストール。

composer require intervention/image

2.セットアップ

InterventionImageをインストールしたら、Laravel構成ファイルconfig/app.phpを開き、providers配列とaliases配列に次の行を追加します。

providers配列に、このパッケージのサービスプロバイダーを追加。
Intervention\Image\ImageServiceProvider::class,

このパッケージのファサードをaliases配列に追加。Imageだとモデルとして定義したクラスと被りそうだったので、InterventionImageとしてaliasを保存。
'InterventionImage' => Intervention\Image\Facades\Image::class,

config/app.php
~前略~

'providers' => [

  ~中略~

  // Add Intervention Image.
  Intervention\Image\ImageServiceProvider::class,
],
'aliases' => [

  ~中略~

  'Hash' => Illuminate\Support\Facades\Hash::class,
  // Add Intervention Image.
  'InterventionImage' => Intervention\Image\Facades\Image::class,

~後略~

3.実際に画像サイズを変更してみる。

今回は画像のアスペクト比をそのままで保存しつつ、横幅を1080pxにしました。

config/PostsController.php
//エイリアスに追加したファサードを呼び出す。
use \InterventionImage;

~中略~

//formからの画像リクエストはimagefileで受け付けしたのでimagefileを設定しています。formに合わせて変更してください。
$file = $request->file('imagefile');
$name = $file->getClientOriginalName();
//アスペクト比を維持、画像サイズを横幅1080pxにして保存する。
InterventionImage::make($file)->resize(1080, null, function ($constraint) {$constraint->aspectRatio();})->save(public_path('/images/' . $filename ) );;

ちなみに、横幅と高さを指定する場合はもう少しシンプルにできます。

InterventionImage::make($file)->resize(1080, 700)->save(public_path('/images/' . $filename ) );;

resizeで画像の大きさを指定するだけだったので、めっちゃ簡単だなーと思いめっちゃ感動しました。

まとめ

少し面倒だと思ってた画像のresizeが、かなりシンプルな書き方で実装できて感動しました?

とはいえqiitaの初投稿もできたので、これきっかけでqiitaへの投稿を増やして行けたらいいなーと思いました!

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

今後のShizuoka.phpについて

これは、静岡 Advent Calendar 2019の5日目の記事となります。

静岡でPHPの勉強会なかったので、今年からShizuoka.phpをはじめました。
JavaScript, Python, Go, Haskell, Rubyの勉強会は開催されているのにPHPがない・・・。
そんな危機感をもってから5年ぐらい経って、やっと今年#1を開催できました?

定期的に続けて行きたい

3 ~ 4ヶ月に一度開催できればいいのかなと。
頻度そんなに多くはできませんが、定期的に開催していこうと思っています。
だから来てね!

ロゴできたよ!

refs: https://twitter.com/planpot/status/1201411166162931712?s=20

@planpot先生にロゴを作ってもらいました?
爆速で作っててマジで感動しました!!!!

静岡で勉強会を主催する事について

僕は今東京に住んでいます。

みんな勘違いされますが、静岡には住んでいません。脱藩しました。
僕は東京に住んでいます。大事なのでもう一度言います、東京に住んでいます。
(まあ毎週のように静岡に行くのでアレですが・・・w)

そんな僕が地元で勉強会を主催するのは2つの理由があります。

つながりの場を提供したい

静岡JavaScript勉強会 #1に参加して、地元静岡のエンジニアと知り合いになれました。
あれから8年も立つにに未だに交流がある人がたくさんいます。
やっぱそういうつながりがあると、嬉しいし、楽しいし、技術的に学べることもたくさんあります。
あとは勝手に勉強会作られたりとかwwww

同じようにShizuoka.phpで出来たらなと思っています。

地元に帰るという選択肢を持てる

勉強会に参加してみると、意外とIT企業があるんだということに驚かされました。
8年ほど前僕が求人情報で探したら、ぜんぜん見つからなかったのに・・・。

勉強会に参加することで、地元のIT企業を知れるというメリットもあり、地元に帰りたいエンジニアの手助けになればと。

地元が好き

静岡のエンジニアの雰囲気というか、やはり同じ地元の人と話すの楽しいし、地元の人達とPHPの話するのが楽しいです。
静岡最高です!!!!!

これからの取組みについて

当日LT枠を用意します。
勉強会でよくある「あーやっぱLT枠申し込めばよかったなあー」っていう人いますよね?
そんな人の為に宣伝枠(当日LT枠)を用意しようと考えています。

ちょっとした発表でいいので少しでもみんなに喋ってもらい、発表の敷居を少しでも下げたいという気持ちや、当日LTしたくなっちゃう気持ち。
そんな人のために枠を設けようと思っています。

スタッフの皆さんありがとうございます。

僕が勝手にスタッフと言っている @tomofさん, @toniokatanuki先輩には本当に感謝しきれないぐらい感謝しています。
僕はイベント運営が苦手な人間なので、かなり支えてもらっています。ありがとうございます。最高です。

いずれは主催者を変えたい

本来は東京に住んでいる僕が主催者をやるべきではないと考えています。
今は僕が主催者としてがんばりますが、そのうち誰かに引き継げたらと思っています。

次回開催予告

次回#2は2/8開催予定です。
場所は三島になります。
細かいことが決まり次第connpass立てます。

最後に

静岡は東京からも近いし、気候も良いし、自然も多いし、富士山は静岡のものだし、本当に良いところです。
そんな静岡でもっとITが盛り上がったらいいのにと常日頃から思っています。
すこしでも力になれればと思い、これからも継続てShizuoka.phpを開催していければと思います。

最後の最後に

ちょっとでもShizuoka.phpが気になった方は気軽に参加してください!

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

WordPress 学習メモ 第1回

急遽WordPressを触らないといけない事になりキャッチアップのフローを、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!

学習を始める前に

今回の目的は環境構築を行う事です。

既に存在する本番環境と同じ物をローカル環境に構築するのが、最初のスタートでした。
そもそもどうやって環境構築しようかなと思ってた際に、参考にさせて頂いたサイトです。
これがめちゃくちゃ簡単に環境を構築できたので驚きました。

超簡単にローカル環境が構築できるLocalbyFlywheelの使い方

本番サイトと同環境を構築する方法!

さて、環境構築が完了し、本番と状態にする際の手順を紹介します。
たった3ステップでサイトの引っ越しが完了するプラグイン「All-in-One WP Migration」が簡単すぎ!

1:作成したローカル環境にプラグインである、All-in-One WP Migrationをダウンロードします。

Screen Shot 2019-12-05 at 19.48.54.png

2:本番のAll-in-One WP Migrationエクスポートしファイルをダウンロードする。
Screen Shot 2019-12-05 at 20.01.36.png

3:ローカル環境でさっきと逆の手順でAll-in-One WP Migrationのインポートファイルを先ほどダウンロードしたファイルを選択します。

環境構築完了!

これで開発をスタートできます。
次からは、実際に開発を行う際の手順を説明させて頂きます!

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

PHPで高速・省メモリ・確実に日本語CSVを扱う方法

結論

  • composer require fw3/streamsとしてfw3/streamsを使えば、日本語CSV取り扱いにおける労苦から解放されるよ。
  • ラッパーメソッドもいいけど、\fgetcsv関数、\fputcsv関数、\SplFileObjectを使ってもシンプルに実現できるかもしれないよ。しかもより高速に動作する。
  • 取り急ぎ"PHPで高速・省メモリ・確実に日本語CSVを扱いたい"場合は使い方へどうぞ。

必要なもの

PHP 7.2.0以降のPHP + ext-mbstring + fw3/streams

または

PHP 5.3.3以降のPHP + ext-mbstring + みんなのPHP 現場で役立つ最新ノウハウ!のChapter 7.3 サポートコード

お断り

  • この記事においてShift_JISとはWindows-31Jを指します。
  • この記事において入力元・出力先のCSVファイルは文字セット:Shift_JIS、改行コード:CRLFとして扱います。
  • この記事において入力先・出力元のデータは文字セット:UTF-8として扱います。

この記事はどういうお話なの?

様々なCSV入出力方法の課題を挙げ、それらの課題に対して一括で対処できるライブラリの紹介と使い方についてお話します。

既知の日本語CSV読み書き時の課題

既存の日本語CSV読み書きには次の課題がありました。

\fgetcsv関数、\fputcsv関数、\SplFileObject::fgetcsv\SplFileObject::fputcsvだと良くわからないタイミングで文字化けが発生する

PHP標準関数、標準クラスとして用意されている\fgetcsv関数、\fputcsv関数、\SplFileObject::fgetcsv\SplFileObject::fputcsv
ASCIIのみで構成されたCSVを扱うには必要十分でしたが、日本語での文字セットを持つCSVファイルを扱う場合、文字化けが発生したり意図しない箇所で解析が終わるなど、問題のある場合がありました。

具体的には\fgetcsv関数、\fputcsv関数、\SplFileObject::fgetcsv\SplFileObject::fputcsvはOS設定であるロカールの影響を受け、ロカールの文字セットと読み書きするCSVの文字セットが一致していない場合、文字化けが発生します。

ロカールの設定自体はPHPでも\setlocaleで実行時に任意に変更することが出来るのですが、同一リクエスト中の処理全体に影響を与えることと、期待するロカールがインストールされている保証がないため、大変扱いにくい状況にありました。

<?php
// 入力
$locale = \setlocale(LC_ALL, '0');

\setlocale(LC_ALL, 'Japanese_Japan.932'); // もしも'Japanese_Japan.932'がインストールされていれば。期待薄。なければ正常に動作しない。 

$fp = \fopen($path_to_csv, 'rb');
for (;($row = \fgetcsv($fp, 1024)) !== FALSE;) {
    \mb_convert_variables('UTF-8', 'SJIS-win', $row);
    $rows[] = $row;
}
\fclose($fp);

\setlocale(LC_ALL, $locale);

// または
$locale = \setlocale(LC_ALL, '0');

\setlocale(LC_ALL, 'Japanese_Japan.932'); // もしも'Japanese_Japan.932'がインストールされていれば。期待薄。なければ正常に動作しない。 

$fileObject = new \SplFileObject($path_to_csv, 'rb')
$fileObject->setFlags(\SplFileObject::READ_CSV);
foreacho ($fileObject as $row) {
    \mb_convert_variables('UTF-8', 'SJIS-win', $row);
    $rows[] = $row;
}

\setlocale(LC_ALL, $locale);
<?php
// 出力
$fp = \fopen($path_to_csv, 'wb');
foreacho ($rows as $row) {
    \mb_convert_variables('SJIS-win', 'UTF-8', $row);
    \fputcsv($fp, $row);
}
\fclose($fp);

// または
$fileObject = new \SplFileObject($path_to_csv, 'wb')
foreacho ($rows as $row) {
    \mb_convert_variables('SJIS-win', 'UTF-8', $row);
    $fileObject->fputcsv($row);
}

\file_get_contents関数、\file_put_contents関数で実装するとプロセスあたりのメモリが足りなくなる

ロカールに頼らず文字セットを変更しようとする場合、\mb_convert_encoding関数を用いることが定石です。

この場合、出力時は逐次変換を行う選択も取れるのですが、入力時はどこまでが文字セット変換の対象文字列かがわからないため、一度すべてを変数に乗せる必要がありました。

そのため、対象となるファイルが巨大な場合、メモリーリミットに到達しfatalエラーを誘発し、UXが最悪となる、いわゆる白画面が表示されることとなりました。

<?php
// 入力
$csv_string = \mb_convert_encoding(\file_get_contents($path_to_csv), 'UTF-8', 'SJIS-win');

$locale = \setlocale(LC_ALL, '0');
\setlocale(LC_ALL, 'ja_JP.utf8'); // もしも'ja_JP.utf8'がインストールされていれば。なければ正常に動作しない。

$fp = \fopen('php://memory/maxmemory:100', 'wb+');
\fwrite($fp, $csv_string);
\rewind(0);
for (;($row = \fgetcsv($fp, 1024)) !== FALSE;) {
    $rows[] = $row;
}
\fclose($fp);

\setlocale(LC_ALL, $locale);

一時ファイルを使用するとシステム全体の実行効率が悪化する

\fopen関数 + \fread関数で読み込めたところまでで文字セット変換し、そのままファイルに出力する」という手段が用いられることもあります。

ただしこれは大量のファイルI/Oが発生するため、実行速度が低下したり、システム全体の負荷として他のリクエストや処理にまで悪影響を与えます。
また、一時ファイルの出力先や一時ファイルそのものなど、関心ごとの外まで管理しなければならないため、使い勝手がよくありませんでした。

<?php
// 入力
$locale = \setlocale(LC_ALL, '0');

\setlocale(LC_ALL, 'Japanese_Japan.932'); // もしも'Japanese_Japan.932'がインストールされていれば。期待薄。なければ正常に動作しない。 

$fp         = \fopen($path_to_csv, 'rb');
$temp_fp    = \fopen('php://temp', 'wb+');
for (;($row = \fgets($fp, 1024)) !== FALSE;) {
    \fwrite($temp_fp, \mb_convert_encoding('UTF-8', 'SJIS-win', $row));
}
\rewind(0);
for (;($row = \fgetcsv($temp_fp, 1024)) !== FALSE;) {
    $rows[] = $row;
}
\fclose($temp_fp);
\fclose($fp);

\setlocale(LC_ALL, $locale);

\popen関数で実装すると物理メモリが足りなくなっても気づけない

PHPで600MBほどのSJISのCSVを読み込んでUTF-8として処理する場合のサンプルコード - Qiitaにサンプルコードがありますが、\popen関数を用いて現在のプロセスとは別のプロセスでファイル全体に対して一括で文字セット変換を行う手段もあります。

ですが、これは意図して設定していたはずのmemory_limitを解除して動作させてしまうため、誰も意図していないサイズのメモリを確保される恐れが高いです。
また、\popen関数で開いたプロセスを正しく閉じなかった場合に意図しない状況が発生する恐れが高いです。

iconvストリームフィルタを使用すると文字セット変換に失敗した行が消える

iconvストリームフィルタを使うことで透過的に文字セット変換を行うことができます。

ですが、iconvストリームフィルタでは変換できない文字を含んだ行は0バイトとして扱われ削除されます。
出力されたCSV上では正しく1行が消えただけにしか見えません。
そのため、運用中に不具合があったことに気づくことが難しいです。

また、変換できない文字が存在した場合、強制的にNoticeが表示され、これを阻止する事は出来ません。
そのため、開発環境モードのLaravelなど、Noticeをトラップして例外としてスローする環境では必ずそこで処理が停止するため、作業の手が止まることになります。

<?php
// 入力
$fp = \fopen(\sprintf('php://filter/read=convert.iconv.Windows-31J%2FUTF-8/resource=%s', $path_to_csv), 'rb');
for (;($row = \fgetcsv($fp, 1024)) !== FALSE;) {
    $rows[] = $row;
}
\fclose($fp);
<?php
// 出力
$fp = \fopen(\sprintf('php://filter/write=convert.iconv.UTF-8%2FWindows-31J/resource=%s', $path_to_csv), 'rb');
foreacho ($rows as $row) {
    \mb_convert_variables('SJIS-win', 'UTF-8', $row);
    \fputcsv($fp, $row);
}
\fclose($fp);

既知の日本語CSV読み書き時の課題のまとめ

以上が既知の日本語CSV読み書き時の課題となります。
これらを問題単位で切り分けると次になります。

  • 正しいロカールがインストールされていないとCSVとして正しく読み込めない
  • 実行時に過大なメモリを消費する
  • 一時ファイルで回避した場合、システム全体のリソースを圧迫する

課題解決:このライブラリを使うことで得られる効果や副次的な効果

このライブラリを使うことで得られる効果

次の通り、既知の課題を全て解決できます。

  • ロカールのインストール状況問わず、正しくCSVとして読み込める
  • 行単位でメモリ展開、文字セット変換を行うため省メモリ
  • \fopen関数や\SplFileObject使用箇所などの既存実装に組み込みやすい
  • PHP nativeに近い箇所で動作するため、PHP標準関数の支援を受けやすい
  • Tempファイルなどの一時出力先が不要

副次的な効果

また、ライブラリ実装者の知見が反映されているため、次の副次的な効果を得られます。

  • エンコード自動判定時に正確な判定が期待できる
  • 変換できない文字があった場合のふるまいを決められる
  • 意図した行末改行文字で出力を行える
    • \fputcsv関数はシステムのOSに依存して出力時の改行コードが変わります
  • Specクラス経由で設定を注入することもできるため、直感的かつ確実に、経験が薄くても高速に設定を構築できます

使い方

単純に使うだけならば、次の3パターンのみで問題ありません。

デリミタやエンクロージャ、エスケープを変更したい場合は\fgetcsv関数、\fputcsv関数の引数や、\SplFileObject::setCsvControlによる設定を使用してください。

読み込み

<?php

use fw3\streams\filters\ConvertEncodingFilter;
use fw3\streams\filters\utilitys\StreamFilterSpec;
use fw3\streams\filters\utilitys\specs\StreamFilterConvertEncodingSpec;

// CSVファイルのパスを設定して下さい
$path_to_csv    = '';

// フィルタ登録
StreamFilterSpec::registerConvertEncodingFilter();

// デリミタ、エンクロージャ、エスケープの設定
$delimiter  = ','; // TSVにしたい場合にはここにタブを設定する
$enclosure  = '"';
$escape     = "\\";

// フィルタの設定
$spec   = StreamFilterSpec::resource($path_to_csv)->read([
    StreamFilterConvertEncodingSpec::toUtf8()->fromSjisWin(),
]);

// \fopenでの実装
// Shift_JIS => UTF-8としてCSV読み込みを行う
$rows   = [];

$fp     = \fopen($spec->build(), 'rb');

for (;($row = \fgetcsv($fp, 1024, $delimiter, $enclosure, $escape)) !== FALSE;) {
    $rows[] = $row;
}
\fclose($fp);

// \SplFileObjectでの実装
// Shift_JIS => UTF-8としてCSV読み込みを行う
$rows   = [];

$fileObject = \SplFileObject($spec->build(), 'rb');
$fileObject->setFlags(\SplFileObject::READ_CSV);
$fileObject->setCsvControl($delimiter, $enclosure, $escape);

foreach ($fileObject as $row) {
    $rows[] = $row;
}

書き込み

<?php

use fw3\streams\filters\ConvertEncodingFilter;
use fw3\streams\filters\ConvertLinefeedFilter;
use fw3\streams\filters\utilitys\StreamFilterSpec;
use fw3\streams\filters\utilitys\specs\StreamFilterConvertEncodingSpec;
use fw3\streams\filters\utilitys\specs\StreamFilterConvertLinefeedSpec;

// CSVファイルのパスを設定して下さい
$path_to_csv    = '';

// フィルタ登録
StreamFilterSpec::registerConvertEncodingFilter();
StreamFilterSpec::registerConvertLinefeedFilter();

// デリミタ、エンクロージャ、エスケープの設定
$delimiter  = ','; // TSVにしたい場合にはここにタブを設定する
$enclosure  = '"';
$escape     = "\\";

// フィルタの設定
$spec   = StreamFilterSpec::resource($path_to_csv)->write([
    StreamFilterConvertEncodingSpec::toSjisWin()->fromUtf8(),
    StreamFilterConvertLinefeedSpec::toCrLf()->fromAll(),
]);

// \fopenでの実装
// UTF-8 => Shift_JISとしてCSV書き込みを行う
$fp     = \fopen($spec->build(), 'wb');

foreach ($rows as $row) {
    \fputcsv($fp, $row);
}
\fclose($fp);

// \SplFileObjectでの実装
// UTF-8 => Shift_JISとしてCSV書き込みを行う
$fileObject = \SplFileObject($spec->build(), 'wb');
$fileObject->setCsvControl($delimiter, $enclosure, $escape);

foreach ($rows as $row) {
    $fileObject->fputcsv($row);
}

直接ダウンロードさせる場合

<?php

use fw3\streams\filters\ConvertEncodingFilter;
use fw3\streams\filters\ConvertLinefeedFilter;
use fw3\streams\filters\utilitys\StreamFilterSpec;
use fw3\streams\filters\utilitys\specs\StreamFilterConvertEncodingSpec;
use fw3\streams\filters\utilitys\specs\StreamFilterConvertLinefeedSpec;

// CSVファイルのパスを設定して下さい
$path_to_csv    = '';

// フィルタ登録
StreamFilterSpec::registerConvertEncodingFilter();
StreamFilterSpec::registerConvertLinefeedFilter();

// デリミタ、エンクロージャ、エスケープの設定
$delimiter  = ','; // TSVにしたい場合にはここにタブを設定する
$enclosure  = '"';
$escape     = "\\";

// フィルタの設定
$spec   = StreamFilterSpec::resourceOutput()->write([
    StreamFilterConvertEncodingSpec::toSjisWin()->fromUtf8(),
    StreamFilterConvertLinefeedSpec::toCrLf()->fromAll(),
]);

// DL用のヘッダ:簡易版
\header('Content-Type: application/octet-stream');
\header('Content-Disposition: attachment; filename=sample.csv');

// \fopenでの実装
// UTF-8 => Shift_JISとしてCSV書き込みを行う
$fp     = \fopen($spec->build(), 'wb');

foreach ($rows as $row) {
    \fputcsv($fp, $row);
}
\fclose($fp);

使用上の注意点

[必須] 日本語CSVの読み込みを行う場合はConvertEncodingFilter::startChangeLocale()を実行してください

適切なロカールが設定できていないと、\fgetcsv関数、\SplFileObject::fgetcsvによるCSV読み込み時に意図しない動作となります。

[必須] フィルタの使用を終えた場合はConvertEncodingFilter::endChangeLocale()を実行してください

ConvertEncodingFilter::endChangeLocale()を実行することで、ConvertEncodingFilter::startChangeLocale()を実行する前の状態に戻すことができます。

ロカールの設定は同一プロセス中(HTTPアクセスの場合は一般的に一リクエスト中)維持されます。
その為、ロカールの設定を元に戻さないと、意図しない箇所で意図しないロカールでの処理が実行されるリスクがあります。

[必須] フィルタを使う前にStreamFilterSpec::registerConvertEncodingFilter()StreamFilterSpec::registerConvertLinefeedFilter()を使用しフィルタの登録を行ってください

フィルタが登録されていない場合、当然のことながらフィルタを使用する事はできません。
StreamFilterSpec::registerConvertEncodingFilter()StreamFilterSpec::registerConvertLinefeedFilter()を使用してストリームフィルタを設定してください。

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

PHPにてエラーが起き、画面が真っ白になった時の対処(エラー表示の仕方)

PHPを使ってカレンダー機能を実装したいので

枠組みを作り、あとは中だけ。

requireにて外部ファイルと組み合わせてみると、さっきまで表示されていた枠組みまでもが消えてしまった。

きっと中のコードがエラーを起こしたことが原因だなと思ったので

まずはエラーが起きてしまっている場所を特定する必要があるので「PHP エラー 表示」で検索。

PHPにてエラーの場所を特定するには

<?php ini_set( 'display_errors', 1 );?>

コードの一番上に書けばphp.iniでエラー表示設定することなくエラー場所を表示できるらしい。

ちなみに、出てきたエラー表示は

Parse error: syntax error, unexpected 'mktime' (T_STRING), expecting ')' in /Applications/MAMP/htdocs/calender_index.php on line 13

と表示されたので13行目をチェック。

なんと、「,」が抜けていたことが原因でした...

またスペルミスか〜〜。

修正し、無事カレンダーは表示!
スペルミスは怖いものです。

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

【Wordpress】投稿ページ(single.php)において、複数ページ(複数テンプレート)に出し分ける方法

実現したかったこと

ひとつの投稿において、情報によってページを切り替えられるようにしたい

投稿が一つのお店のページになっていて、さらにタブでメニューやクーポンをぶら下げるような作りに。
要は食べログやぐるなびのイメージです。
各ページの出しわけは、URLにパラメーターを追加することで判定します。

投稿(美味しい餃子 渋谷店)のトップURL
https://hogehoge.com/cat/美味しい餃子渋谷店/

投稿(美味しい餃子 渋谷店)のメニューURL
https://hogehoge.com/cat/美味しい餃子渋谷店/?info=menu

投稿(美味しい餃子 渋谷店)のアクセスURL
https://hogehoge.com/cat/美味しい餃子渋谷店/?info=access

やること

1. テンプレの用意

トップ用のテンプレとは別に、menu用、access用のテンプレを用意します。
single.php
single-menu.php
single-access.php

2. function.phpにフィルターフックを追加

single_templateに対してフィルターフックを追加します。
以下、ソースの例です。

function.php
function shop_template_switch($template) {
    $new_template = $template;
    if (isset($_GET['info'])) {
        $new_template = 'single-' . esc_html($_GET['info']) . '.php';
        if (is_array($template)) {
            $new_template = array(
                $new_template,
                isset($template[1]) ? $template[1] : 'single.php'
                );
        } else {
            $new_template = preg_replace('/[^\/]+\.php$/i', $new_template, $template);
            if (!file_exists($new_template)) {
                $new_template = $template;
            }
        }
    }
    return $new_template;
}
add_filter('single_template', 'shop_template_switch');

3. 完成

URLに各パラメーターを追加し、表示されれば完成です。
https://hogehoge.com/cat/美味しい餃子渋谷店/
https://hogehoge.com/cat/美味しい餃子渋谷店/?info=menu
https://hogehoge.com/cat/美味しい餃子渋谷店/?info=access

パラメーター名でテンプレを作れば、さらにページを増やしていけます。

例:メッセージページを以下のURLで作成する場合
https://hogehoge.com/cat/美味しい餃子渋谷店/?info=message
→ single-message.php

特定のカスタム投稿タイプの場合

複数のカスタム投稿タイプがある場合など、特定のカスタム投稿タイプだけに反映させたいこともあると思います。
function.phpの記述を以下のようにすれば、特定のカスタム投稿タイプだけで実装することができます。

例:カスタム投稿タイプ「shop」にだけ適用したい場合

function.php
function shop_template_switch($template) {
    $new_template = $template;
    $post = get_queried_object();
    if (is_singular( 'shop', $post)) {
        if (isset($_GET['info'])) {
            $new_template = 'single-' . esc_html($_GET['info']) . '.php';
            if (is_array($template)) {
                $new_template = array(
                    $new_template,
                    isset($template[1]) ? $template[1] : 'single-shop.php'
                    );
            } else {
                $new_template = preg_replace('/[^\/]+\.php$/i', $new_template, $template);
                if (!file_exists($new_template)) {
                    $new_template = $template;
                }
            }
        }
    }
    return $new_template;
}
add_filter('single_template', 'shop_template_switch');

参考サイト

https://dogmap.jp/2011/02/01/single-template-switch/

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

wordpress 公開サイト移行のちょっとした手順

はじめに

案件でwprdpress構築を任せていただき、クライアントオッケーがでて、いよいよ公開!のタイミングになりました。
スムーズにクライアント希望時間に公開できるようにいろいろと下準備をしましたので、メモ代わりに書きます。
(実際には、まだ公開はしていないのでミスが発覚したら、随時書き換えます。)

経緯

とあるサイトにて、40ページ近くあるコンテンツを最初はhtmlで組んでほしいとの依頼で制作をしたものの、やはり自分たちで更新をしたいからCMS(wordpress)で作り直してほしいとのことで再度制作。

環境

新規でドメイン・サーバー(xさん)を契約し、htmlファイルを置いていました。
サブドメインにwordpressをインストールし、basic認証をかけながら構築を進めました。
なので、実質wordpressでの新規のサイト構築といったかんじです。
cssやjsは手を加えなくて済んだんで、助かりました〜。

WP構成

サブフォルダ

wordpressのインストール先をテスト・公開ともに、フォルダを一つかましています。
home/public_html/__cms
こうすることで、__cmsと同階層にindex.phpを置かない限り、テーマは表示されない。(ディレクトリアクセスされたら、エラーは出ますが少しの間なのでokとしておきます。)
なので、公開サイトに本来の静的ファイルを公開しつつ、裏ではテーマ移行を進めておくことができる。
worpressを本番サイトにインストールして、テスト環境とは別にデータベースも作成しておく

/assets/

また、__cms/と同階層にassets/をおき、その中にcss・js・img等をぶち込んでおく。必要なのはimgのみですが
こうすることで、静的ページなんかは長ったらしい画像パスを書かなくても、/assets/path/to/imgで読み込むことができる。
※元々の静的ファイルも画像パスは、全てルートパス読み込みしています。

データベース

エクスポート

phpMyAdminを使用して、テストサイトのデータベースを複製(エクスポート)する。

  • エクスポート方法は、全てのテーブルをインポートする場合は「詳細」(必要に応じて除外選択できる)を選択する。
  • 生成オプションについては、DROP TABLE / PROCEDURE / FUNCTION ...を選択しておくといいらしい。(ドキュメント参照)

DROP TABLE / VIEW / PROCEDURE / FUNCTION / EVENT / TRIGGER コマンドを追加する」オプションを選択します。復元するデータベース上でテーブルを作成する前に、同名のテーブルが存在すれば DROP コマンドでこれを削除します。

  • あとは触らなくても、デフォルトで基本おkのはず。今回は特に触っていません。

そしたら、sqlファイルが出力されます!

インポート

今度は、新しく作成したDBに先ほどエクスポートしたDBを反映(インポート)させる。
インポートファイルを読み込んで実行する!
これでDBの複製ができる。

注意

今回の案件では一箇所かなとは思いますが、DB内にドメインが記述されている箇所がある。
そこをテストドメイン→本番ドメインに書き換えないとエラーが起こってサイトが見られない。ここは注意してねと言われただけなので、詳しく理解できていない、後日調べなければ!

今回変更したのは以下

  • wp_options option_name: site_url
  • wp_options option_name: home

テーマ

テーマの以降をする際は、duplicationを使うことが弊社では多い。しかし、今回は新規構築とのことで、プラグインを使用せずに、テスト環境から該当ファイルをインポートした。
テーマ移行に必要となるものは以下

  • themes / theme / 該当のテーマファイル一式
  • uploads (メディアアップの画像等が保管されている)
  • plugins インストールされていたプラグイン。これがインポートもエクスポートも時間かかる。我慢。

確認

ひとまず、上記が一通り完了したら、ちゃんと反映されているか確認する。緊張する。
https://domaindesu/__cms/wp-admin/で管理画面にアクセス。
(本当は、セキュリティのために管理画面URL変えていますが!)
投稿データ等間違えがなければひとまずおっけ

公開

サブフォルダにwordpressをインストールすると、同階層にindex.phpを置かないと、サイトアクセスできなくなるので注意。
テストサイトに置いているindex.phpを公開サイトに映して完了(のはず。。。現状、実作業がまだなのでわからない、、、)

あと、テーマのパーマリンクディレクトリと既存の静的URL(ディレクトリ)が同一の場合は、静的を消しておかないと、503エラーが出る。怖い。
index.php以降のタイミングの前に消しちゃいましょう!

スムーズに行くはずといっても、瞬間的にサイトが見れなくなるタイミングはあるので、メンテナンス画面も一緒に準備して、.htacessでリダイレクトするとよし!
公開終了後には、コメントアウトするなりしないと、忘れたら怒られる。

最後に

本番公開うまくいくといいな。

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

[jQuery + php] 簡易ソートアプリ作成してみた(デザイン込)

簡易的なアイテムソートできるアプリを実装してみましたので
記録しておきます。

成果物

ファーストビュー
スクリーンショット 2019-12-05 20.13.07.png

ソートビュー
スクリーンショット 2019-12-05 20.13.20.png

コア機能

○フィルターソート機能(jQuery)
|- カテゴリーボタンを押したらカテゴリーのアイテムのみ表示

○一部php化(v5.6)
|- 簡易アプリなのでデータベースは使用なし
|- データベースを利用してもできる形にしているつもりです(問題あったらこちらの記事下にリプください)

データダウンロード

こちらにパッケージ化してありますので、
下記コマンドでクローンし「/php_app/filter_search_php/」のフォルダを
Xamppまたはphpを動かせる場所に入れてください。

$ git clone https://d-mori-570415@bitbucket.org/d-mori-570415/webdesigntemplate.git

jQueryでフィルターソート実装/phpアプリ化

もともと作成していたWebデザインをベースに実装しています。
データはphp上で記述し、データベースは使っていません。
本来はデータベースを作成し、そちらからデータを取るようにしてください。

※コアな部分しか明記しませんので、ダウンロードして見てください。

php
<?php
    /* db設計(Laravelの場合、id、timestampは自動で生成されるので省略)
        * title
        * comment
        * category
        * image
    */
    $listItems = [
        [ 'title'=>'food1', 'comment'=>'これはfood1の画像です', 'category'=>'food', 'image'=>'ph1.jpg' ],
        [ 'title'=>'food2', 'comment'=>'これはfood2の画像です', 'category'=>'food', 'image'=>'ph2.jpg' ],
        [ 'title'=>'view1', 'comment'=>'これはview1の画像です', 'category'=>'view', 'image'=>'ph3.jpg' ],
        [ 'title'=>'view2', 'comment'=>'これはview2の画像です', 'category'=>'view', 'image'=>'ph4.jpg' ],
        [ 'title'=>'sweets1', 'comment'=>'これはsweets1の画像です', 'category'=>'sweets', 'image'=>'ph5.jpg' ],
        [ 'title'=>'sweets2', 'comment'=>'これはsweets2の画像です', 'category'=>'sweets', 'image'=>'ph6.jpg' ],
    ];

    // アイテムの合計数を取得
    $allListItemsCount = count($listItems);
?>

<!DOCTYPE HTML>
<html lang="ja">
<head>
<!--省略-->
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="./js/jquery.min.js"></script>
<!--省略-->
</head>
<body>
<div class="container">
        <div class="header">
            <ul>
                <li><a href="">Work</a></li>
                <li><a href="">About</a></li>
                <li><a href="">Contact</a></li>
            </ul>
            <div class="logo"><a href="">DesignWorks<span>®️</span></a></div>
        </div>

        <div class="navi">
            <h1>Work: <?= $allListItemsCount; ?></h1>
            <ul class="filter-button">
                <li><a href="" class="allItem">ALL</a></li>
                <li><a href="javascript:void(0)" class="food">FOOD</a></li>
                <li><a href="javascript:void(0)" class="view">VIEW</a></li>
                <li><a href="javascript:void(0)" class="sweets">SWEETS</a></li>
            </ul>
        </div>

        <div class="contents">
            <div class="items filter-list">
                <?php foreach ($listItems as $listItem): ?>
                <div class="item <?= $listItem['category']; ?>">
                    <img src="./img/<?= $listItem['image']; ?>">
                    <a href="">
                        <div class="mask">
                            <div class="caption">
                                <div class="caption-title"><?= $listItem['title']; ?></div>
                                <div class="caption-content"><?= $listItem['comment']; ?></div>
                            </div>
                        </div>
                    </a>
                </div>
                <?php endforeach; ?>
            </div>  
        </div>

        <div class="footer">
            <p>Copyright© 2019 Sample Example All rights reserved.</p>
        </div>
    </div>
<script>
          $(function(){
            // filter
            let setFilter = $('.filter-button'),
                filterBtn = setFilter.find('a'),
                setList = $('.filter-list'),
                filterList = setList.find('.item'),
                listWidth = filterList.outerWidth();

            filterBtn.on('click', function(){
                if(!($(this).hasClass('active'))){
                    let filterClass = $(this).attr('class');
                    filterList.each(function(){
                        if($(this).hasClass(filterClass)){
                            $(this).css({ display: 'block' })
                                .stop().animate({ width: listWidth }, 100);
                            $(this).find('*').stop().animate({ opacity: '1' }, 100);
                        } else {
                            $(this).find('*').stop().animate({ opacity: '0' }, 100);
                            $(this).stop().animate({ width: '0' }, 100, function(){
                                $(this).css({ display: 'none' });
                            });
                        }
                    });
                    filterBtn.removeClass('active');
                    $(this).addClass('active');
                }
            });
          });
</script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuで自分のサイトを公開する方法  〜DBとの接続、GitHubからdeployまで〜

自分で作ったサイトを簡単に公開できる、Herokuの導入方法について紹介します。

Herokuとは??

Heroku(ヘロク)は大まかに説明すると、PHPやPythonなどのプログラミング言語を用いて開発したWebアプリ、WebサービスなどをWeb上で簡単に公開できるというサービスです。今回はPHPで作ったサイトを公開することをメインに説明していきます。

事前準備

まずは、進めていくに当たってこの準備が必要になります。
まだの方のために隣にリンク貼っときます!

heroku CLI(旧Heroku toolbelt)のインストール

こちらからheroku CLIをインストールできます。
Heroku CLIとは、Herokuをコマンドで操作するためのツールになります!

https://devcenter.heroku.com/articles/heroku-cli

今回はターミナルからインストールしていきます。
以下のコードを入力してインストールします。

$brew tap heroku/brew && brew install heroku

インストールが無事終了したら、

$heroku login

を入力するとherokuのサイトにとんでログインできます。

HerokuからCreate a new appを作成

ログインしたら、SourcetreeでローカルのフォルダからGitHubへpushした自分のWebサイトを用意しておきましょう!
SourcetreeはローカルのファイルをGitHubへpushするために必要になるのですがここではその説明は省きます。

https://dashboard.heroku.com/apps
このURLをクリックすると、このような画面になると思います。
スクリーンショット 2019-12-05 18.16.22.png

ここのCreate new appをクリック。

スクリーンショット 2019-12-05 18.30.23.png

するとこのような画面になると思います。App nameに入力した内容はアクセスする際のURLの一部分になります。
例えば、App nameにhogeと入力すると、URLはhttps://hoge.herokuapp.comとなります。

regionはそのまま「United States」でオッケーです。

GitHubからdeploy

Herokuでアプリを作成したら、「Deploy」をクリックして、Deployment methodからGitHubを選択。

スクリーンショット 2019-12-05 18.46.25.png
ここで、選択する際に「Connect to GitHub」というボタンが表示されるので、それをクリックして
Herokuと自分のGitHubを紐付けましょう。
次に、Connect to GitHubのところに検索欄があるので、そこに自分のGitHubの公開させたいリポジトリを検索して「connect」ボタンをクリックします。

うまくいったら、画面下に行くとこのような画面があるはずです。
スクリーンショット 2019-12-05 18.54.28.png

「Automatic deploys」は自動でdeployする場合、
「Manual deploy」は手動でdeployする場合にそれぞれ用います。

どちらの方法でdeployするか選択したら、「Enable Automatic Deploys」または「Deploy Branch」をクリック。

これでGitHubからのdeployは完了です!!
ちゃんと公開されてるか確認したい場合は先ほど作成したURLをブラウザで入力すると確認できます。
ここまでが公開までの手順です!

Heroku Add-onsからClearDB MySQLをインストール

続いて、HerokuにDBを接続していきます。Resourcesをクリック。
スクリーンショット 2019-12-05 19.14.23.png
Find more add-onsボタンをクリック

すると別のサイトにとぶので、そこから「ClearDB MySQL」を見つけてクリック。
スクリーンショット 2019-12-05 19.16.17.png

クリックして「ClearDB MySQL」のページに入ったら「Install ClearDB MySQL」ボタンをクリックします。
注意 : ここでクレカ登録をする必要があります。

スクリーンショット 2019-12-06 9.06.45.png
クリックするとこんな画面にいくかと思います。
「Add-on plan」はDBの容量になります。Free(無料)だと5MBになります。もっとDBの容量を増やしたいよ、という方は月間でそれぞれの容量に応じた金額を支払う必要があります。
「App to provision to」にはHerokuの「create a new app」であらかじめ作成しておいたアプリの名前を入力します。名前を入力するとこんな感じで「Provision add-on」のボタンが押せるようになるかと思います。
スクリーンショット 2019-12-06 8.30.22.png
「Provision add-on」をクリック。

無事インストールが終わったら、Resourcesのところに「ClearDB MySQL」が追加されてることを確認します。

スクリーンショット 2019-12-05 19.19.50.png

Sequel Proのインストール

DBの操作するために、Sequel Proをダウンロードします。
こちらからダウンロードできます。
https://www.sequelpro.com/

インストールしてアプリを開くと、こんな画面が表示されると思います。
スクリーンショット 2019-12-05 19.30.19.png
ここに入力する内容は先ほど追加した「ClearDB MySQL」で取得されるデータを入力します。
一度Herokuの自分のアプリ編集画面に戻って、
Settingsの「Config Vars」から確認できると思います。

CLEARDB_DATABASE_URLの値から取得できます。読み方はこんな感じです。
mysql://(--username--):(--password--)@(--hostname--)/(--dbname--)?reconnect=true

ここに該当する内容をさっきの画面に入力します。

Sequel Proのデータベースと、ポートには何も入力しなくて大丈夫です。
一度「接続をテスト」ボタンを押して接続を確認してみてください。

入力後、接続できたらオッケーです。サイトで使用するテーブルを作成しておいてください。

これでSequel Proとの接続は完了です!

WebサイトをDBとつなげるためのファイルを作成する

WebサイトとDBをつなげるためのファイルを作成しましょう。
作成したらこのように入力しましょう

<?php

$host = getenv('ホストネームの値'); //MySQLがインストールされてるコンピュータ
$dbname = getenv('DBネームの値'); //使用するDB
$charset = "utf8"; //文字コード
$user = getenv('ユーザーネームの値'); //MySQLにログインするユーザー名
$password = getenv('パスワード'); //ユーザーのパスワード

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, //SQLでエラーが表示された場合、画面にエラーが出力される
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, //DBから取得したデータを連想配列の形式で取得する
    PDO::ATTR_EMULATE_PREPARES   => false, //SQLインジェクション対策
];

//DBへの接続設定
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";
try {
    //DBへ接続
    $dbh = new PDO($dsn, $user, $password, $options);
} catch (\PDOException $e) {
    throw new \PDOException($e->getMessage(), (int) $e->getCode());
}

上記5行が先ほどSequel Proに入力した内容です。入力した内容をダイレクトに打ち込んでもうまく行くのですが、GitHubなどで公開されるため、そのまま入力するのはまずいです。

そのため、PHPの定義済み変数の環境変数を使用します。
getenv('')''の部分に値を代入しています。

では、この環境変数はどこで定義するのかについて説明します。

Herokuのページに戻ってSettingsの「Config Vars」のところを開きます。
するとこのような欄があるかと思います。
スクリーンショット 2019-12-05 19.57.26.png
ここの「KEY」に定義する環境変数、
「VALUE」に実際の値を入力します。

「KEY」で入力した値を先ほどのファイルに入力することで「KEY」に対応した「VALUE」が読み込まれるという仕組みになってます!

入力が終わったらDBと接続したいPHPファイルにrequire_once('DBを接続するファイル名.php');
を入力してください。入力例はこんな感じです。ここでは、WebサイトをDBとつなげるためのファイル名をdbconnect.php
としています。

<?php

require_once('dbconnect.php');

//DBと紐付ける値
$A = $_POST['a'];
$B = $_POST['b'];
$C = $_POST['c'];

//DBのテーブルに上記の変数を代入する設定
$stmt = $dbh->prepare('INSERT INTO テーブル名 (カラムA, カラムB, カラムC) VALUES (?, ?, ?)');

//(?, ?, ?)に入る値の定義
$stmt->execute([$A, $B, $C]);
?>

これでWebサイトとDBをつなげることができるかと思います。
これでDBとの接続も完了です!!

以上がHerokuで自分のサイトを公開する方法です。

わからないこと、不明な点があればコメントをお願いします。

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

【Laravel 6.x】マイグレーションでMySQLデータベースのテーブルを作成・編集する

Laravelではデータベースのテーブルを作成・編集する方法としてマイグレーションという仕組みが用意されている。

前提

  • Laravel 6.6.1
  • PHP7.3
  • MySQL 5.6

マイグレーションの流れ

マイグレーションの流れは大まかに言えば、次の3ステップ。

  1. マイグレーションファイルを作成する
  2. マイグレーションファイルを編集する
  3. マイグレーションを実行する

1.マイグレーションファイルを作成する

マイグレーションファイルを作成するには、php artisan make:migration コマンドを使う。テーブルを新規作成する場合とテーブルを編集する場合でオプションが異なる。

1-1.テーブルを新規作成する場合

コマンド
$ php artisan make:migration 名前 --create=テーブル名

このコマンドを実行すると タイムスタンプ_名前.php のファイル名で、database/migrations にファイルが作成される。

例えば、名前を create_users_table 、テーブル名が users とすると、コマンドは以下のようになる。

コマンド例
$ php artisan make:migration create_users_table --create=users

上記をコマンドを実行すると、database/migrations2019_12_05_000000_create_users_table.php が作成される。

1-2.テーブルを編集する場合

コマンド
$ php artisan make:migration 名前 --table=テーブル名

このコマンドを実行すると タイムスタンプ_名前.php のファイル名で、database/migrations にファイルが作成される。

例えば、名前を add_api_token_column 、テーブル名が users とすると、コマンドは以下のようになる。

コマンド例
$ php artisan make:migration add_api_token_column --table=users

上記をコマンドを実行すると、database/migrations2019_12_05_083543_add_api_token_column.php のような名前のファイルが作成される。

2.マイグレーションファイルを編集する

作成したマイグレーションファイルと開くと、以下のようになっている。up メソッドと down が出来ている。

作成したファイル例:2019_12_05_083543_add_api_token_column.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddApiTokenColumn extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            //
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            //
        });
    }
}

up メソッドには追加したい作業を記述し、down メソッドには up メソッドの内容を元に戻す作業を記述する。それぞれ Schema::table() が作成されている。テーブルを作成する、すなわち、make:migration のオプションを --create にすると `Schema::create()' が作成されている。

例にupメソッドとdownメソッドを追加
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddApiTokenColumn extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            // api_token カラムを password カラムの次に追加。
            // ユニークキーを追加し、
            // NULL を許可で、デフォルトをNULLにする。
            $table->string('api_token', 80)->after('password')
                        ->unique()
                        ->nullable()
                        ->default(null);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            // api_token カラムを削除
            $table->dropColumn('api_token');
        });
    }
}

3.マイグレーションを実行する

以下の php artisan migrate コマンドを実行することで、マイグレーションファイルの内容がデータベースに反映される。同時にデータベースの migrations テーブルにファイル名と実行順序が保存される。

コマンド
$ php artisan migration

以下のように MigratingMigrated が表示されたらOK。

コマンド実行後の例
Migrating: 2019_12_05_083543_add_api_token_column
Migrated:  2019_12_05_083543_add_api_token_column (0.01 seconds)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel view composerでロジックを一箇所にまとめよう!

この記事について

あらすじ

自社サービス開発の中で共通でuser情報を使い回している箇所があり、その部分が煩雑な書き方になっていたので、処理をどこか一つにまとめたいと思っていました。

やりたい事

  • 共通でuser情報を使い回しているを共通化する。
  • ヘッダーのbladeファイル以外でサイドバーなどの箇所でもuser情報を使いたい。

やってみた事

  • Laravel view composerを使って、処理を共通化する。

view composerとは?

ビューコンポーザはビューがレンダーされる時に呼び出される、コールバックかクラスメソッドのことです。

ビューがレンダーされるたびに結合したい情報があるなら、ビューコンポーザがロジックを一箇所にまとめるのに役立ちます。

処理イメージ図

スクリーンショット 2019-12-05 17.40.11.png

どんな時に使うのか?

  • サイドバーなどすべてのページで共通するデータを表示したい時。

  • ビューロジックはできればコントローラーに書きたくないし、テンプレートもできるだけロジックを入れず、綺麗なhtmlのままでありたい。

  • コントローラーの肥大化を防ぎたい。

実装方法

実装手順

  • ①サービスプロバイダーの用意
  • ②app/config.phpに登録
  • ③Composerクラスの作成
  • ④SeriviceProviderへの設定
  • ⑤bladeファイルの修正

①サービスプロバイダーの用意

$ php artisan make:provider ViewComposerServiceProvider を実行して、サービスプロバイダーを生成する。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use View;

/**
 * Class ViewComposerServiceProvider
 * @package App\Providers
 */
class ViewComposerServiceProvider extends ServiceProvider
{
    /**
     *
     */
    public function boot()
    {
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

②app/config.phpに登録

「config/app.php」へ下記のようにSeriviceProviderを登録する

<?php

'providers' => [
        App\Providers\ViewComposerServiceProvider::class,
    ],

③Composerクラスの作成

「app/Http/ViewComposers」という形でディレクトリを作成し、ViewComposersディレクトリの中に「LayoutComposer.php」を作成します。

<?php

namespace App\Http\ViewComposers\User\Worker;

use Auth;
use Illuminate\View\View;

/**
 * Class LayoutComposer
 * @package App\Http\ViewComposers\User\Worker
 */
class LayoutComposer
{
    /**
     * @param View $view
     */
    public function compose(View $view)
    {
        $view->with([
            'loginUser' => Auth::user(),
        ]);
    }

}

④SeriviceProviderへの設定

bootメソッドにViewComposerが値を渡すviewを、View::composersメソッドを使って指定します。

<?php

namespace App\Providers;

use App\Http\ViewComposers\User\Agent as Agent;
use App\Http\ViewComposers\User\FriendRecommendComposer;
use App\Http\ViewComposers\User\Worker as Worker;
use Illuminate\Support\ServiceProvider;
use View;

/**
 * Class ViewComposerServiceProvider
 * @package App\Providers
 */
class ViewComposerServiceProvider extends ServiceProvider
{
    /**
     *
     */
    public function boot()
    {
        View::composers([
            Agent\LayoutComposer::class => 'agent.*',
            Worker\LayoutComposer::class => 'worker.*',
            FriendRecommendComposer::class => 'user.friend_recommend.block',
        ]);
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

⑤bladeファイルの修正

$loginUserの変数を該当箇所に書く

<?php
                <div class="user-block">
                    <div class="img-wrap">
                        <img src="{{ $loginUser->userProfile->image->url }}" alt="">
                    </div>
                    <div class="user-name">{{ $loginUser->userProfile->display_name }}</div>
                    <div class="user-type">{{ \App\Enums\UserRole::toJapanese($loginUser->userProfile->role) }}</div>
                </div>

スクリーンショット 2019-12-05 18.01.45.png

まとめ

  • 共通で使うような変数やロジックがあればview composerにまとめよう!

参考記事

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

PHPのバージョンを7.4にアップデートする

課題

古くなったPHPのバージョンをアップデートする

現状のバージョンを確認する

下記のコマンドでバージョン確認。

ターミナル
$ php -v
PHP 7.1.30 (cli)

*ちなみにですが今回はHomebrewを使ってアップデートしようと思います。
Homebrewのインストールがまだという方はこちらをソッと置いておきます
(https://brew.sh/index_ja.html)

PHP7系を検索する

次にHomebrewコマンドを使って現在インストールができるPHPのバージョンを検索します

ターミナル
$ brew search php@7
==> Formulae
php@7.2                    php@7.3                    php@7.4

最新のPHP 7.4 をインストールしてみる

どのバージョンをインストールするか決めたら
PHP 7.4を Homebrew経由でインストールします。

ターミナル
$ brew install php@7.4

PATHを通そう

インストールは無事にされたと思いますが、まだ終わりではありません。
PATHを通す必要があるので下記コマンドにて設定を上書きします。

ターミナル
$ echo 'export PATH="/usr/local/opt/php@7.4/bin:$PATH"' >> ~/.bash_profile
$ echo 'export PATH="/usr/local/opt/php@7.4/sbin:$PATH"' >> ~/.bash_profile

PHPとの冒険の再出発へ

上書きまで できたら下記コマンドでPHPを再スタートさせます。

ターミナル
$ brew services start php

ここで一旦ターミナルを閉じます。
大事なことなのでもう一度言います。

ここで一旦ターミナルを閉じます。

ターミナルをもう一度起動させたらバージョンを確認してみます。

ターミナル
$ php -v
PHP 7.4.0 (cli)

これで完了です!

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

symfony/mailer でTwigを使ってテキストメールをbase64で送る。

Symfony Advent Calendar 2019 15日目の記事です。

はじめに

メールを業務で利用する場合は、定型文を用意して、例えば、お客様の名前とか日付とかを、送信する相手毎に置換をしてから送信をすることが多いです。

メールの本文を、str_replaceで置換をしてもよいのですが、テンプレートエンジンのtwigを使ってみたくなります。

Twig: HTML & CSSを試す

Twigを使ってメールを送る方法について見ていきます。

Mailerドキュメントに、Twig: HTML & CSSの項目がありますので、そのまま実行してみます。

MailerController.php
class MailerController extends AbstractController
{
    /**
     * @Route("/mailer", name="mailer")
     */
    public function index(MailerInterface $mailer)
    {

        $email = (new TemplatedEmail())
            ->from('fabien@example.com')
            ->to(new Address('ryan@example.com'))
            ->subject('Thanks for signing up!')

            // path of the Twig template to render
            ->htmlTemplate('emails/signup.html.twig')

            // pass variables (name => value) to the template
            ->context([
                'expiration_date' => new \DateTime('+7 days'),
                'username' => 'foo',
            ])
        ;

        $mailer->send($email);

        return $this->render('mailer/index.html.twig', [
            'controller_name' => 'MailerController',
        ]);
    }
}

このソースは、https://github.com/idani/symfony_mailer_tutorial3 で保管しています。
今回のコミットは以下で見れます。
https://github.com/idani/symfony_mailer_tutorial3/commit/39d7caa797f0d1e663ba517553d8b23b8b319410

http://localhost:8000/mailer を開くと、以下の画面が表示されました。メール送信ができたようです。一番下のデバッグ用のツールバーのメールアイコンにも、メールが1通送信されたことが表示されていますね。

image.png

http://127.0.0.1:8025/ で動作しているMailDevを確認すると、1通メールが届いていました。
デフォルトで表示されるのは、TwigでレンダリングされたHTMLメールになります。

image.png

MailDevで設定を切り替えて、テキストメールにしてみます。
その場合は、HTMLからテキストを抽出したテキストメールが表示されます。

誤解されないように説明すると、MailDevはメールソースに記載されたテキストメールの部分を表示しています。テキストのメールは、symfony/mailerによって、配信前にHtmlからテキストを抽出して、メールソースに組み込まれています。

image.png

メールソースは以下です。

Content-Typetext/plain; charset=utf-8と指定されたテキストと、text/html; charset=utf-8と指定されたHTMLがquoted-printableでエンコードされて送られています。

メールソース
From: fabien@example.com
To: ryan@example.com
Subject: Thanks for signing up!
Message-ID: <a7169f3be53c5470e3765e9fc649f5b1@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 15:53:55 +0900
Content-Type: multipart/alternative;
 boundary="_=_symfony_1575528835_8df53d757797744c9475726f052de156_=_"

--_=_symfony_1575528835_8df53d757797744c9475726f052de156_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Welcome !


    You signed up as foo the following email:

ryan@e=
xample.com


    Click here to activate your account
    (this link=
 is valid until December 12th)


--_=_symfony_1575528835_8df53d757797744c9475726f052de156_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<h1>Welcome !</h1>

<p>
    You signed up as foo the following email:=

</p>
<p><code>ryan@example.com</code></p>

<p>
    <a href=3D"#"=
>Click here to activate your account</a>
    (this link is valid until De=
cember 12th)
</p>

--_=_symfony_1575528835_8df53d757797744c9475726f052de156_=_--

Text Contentを試す。

Mailerドキュメントを読み進めると、Text Contentの部分で、メールに入っているテキスト部分をカスタマイズできるようです。

さっそく、サンプルソースに->textTemplate('emails/signup.txt.twig')を追加します。

MailerController.php
        $email = (new TemplatedEmail())
            ->from('fabien@example.com')
            ->to(new Address('ryan@example.com'))
            ->subject('Thanks for signing up!')

            // path of the Twig template to render
            ->htmlTemplate('emails/signup.html.twig')

            // pass variables (name => value) to the template
            ->context([
                'expiration_date' => new \DateTime('+7 days'),
                'username' => 'foo',
            ])
            ->textTemplate('emails/signup.txt.twig')
        ;

        $mailer->send($email);
signup.txt.twig
Welcome !

    You signed up as foo the following email:

    {{ email.to[0].address }}


    Click here to activate your account

    http://www.example.com/xxxxxxxxxxx
    (this link is valid until {{ expiration_date|date('F jS') }})

https://github.com/idani/symfony_mailer_tutorial3/commit/2931651279278b57ad8c3f2bfe045ac03d59f141

実際に送信をされたメールを見ると、修正通りのテキストメールが送られていました。

image.png

メールソース
From: fabien@example.com
To: ryan@example.com
Subject: Thanks for signing up!
Message-ID: <f6cc0e2166ff05ab5816d2bb26255080@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 16:27:04 +0900
Content-Type: multipart/alternative;
 boundary="_=_symfony_1575530824_57df7e1ab6fbb47d7ffce4184ba1587c_=_"

--_=_symfony_1575530824_57df7e1ab6fbb47d7ffce4184ba1587c_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Welcome !

    You signed up as foo the following email:

    ryan@=
example.com


    Click here to activate your account

    http:/=
/www.example.com/xxxxxxxxxxx
    (this link is valid until December 12th)
--_=_symfony_1575530824_57df7e1ab6fbb47d7ffce4184ba1587c_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<h1>Welcome !</h1>

<p>
    You signed up as foo the following email:=

</p>
<p><code>ryan@example.com</code></p>

<p>
    <a href=3D"#"=
>Click here to activate your account</a>
    (this link is valid until De=
cember 12th)
</p>

--_=_symfony_1575530824_57df7e1ab6fbb47d7ffce4184ba1587c_=_--

テキストメールだけ送る

これまではHTMLメールが主役で、テキストメールがおまけ的な感じでした。
Twigを使って、テキストメールだけ送るにはどうしたらよいでしょうか?

さきほどのサンプルコードから、htmlTemplateを外してみましょう。

        $email = (new TemplatedEmail())
            ->from('fabien@example.com')
            ->to(new Address('ryan@example.com'))
            ->subject('Thanks for signing up!')

            // path of the Twig template to render
            //->htmlTemplate('emails/signup.html.twig') <=コメントアウト

            // pass variables (name => value) to the template
            ->context([
                'expiration_date' => new \DateTime('+7 days'),
                'username' => 'foo',
            ])
            ->textTemplate('emails/signup.txt.twig')
        ;

        $mailer->send($email);

https://github.com/idani/symfony_mailer_tutorial3/commit/8313dcc169ee6cf4f4ab0a0356ea15997aa0bada

MailDevでは、意図した通り、テキストメールが見れました。
画像では変化がないので、メールソースを見ていただくと、テキストメールになっていることがわかります。

image.png

メールソース
From: fabien@example.com
To: ryan@example.com
Subject: Thanks for signing up!
Message-ID: <3d16b774412841ddbdef19077a4e8fc2@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 16:33:07 +0900
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Welcome !

    You signed up as foo the following email:

    ryan@=
example.com


    Click here to activate your account

    http:/=
/www.example.com/xxxxxxxxxxx
    (this link is valid until December 12th)

日本語テンプレートで送信をしてみる

問題ないと思いますが、テンプレートを日本語にしてみます。

signup.txt.twig
Welcome !
ようこそ 

    You signed up as {{ username }} the following email:
    次のメールアドレスで{{ username }}としてサインアップしました

    {{ email.to[0].address }}


    Click here to activate your account
    アカウントを有効にするにはここをクリックしてください

    http://www.example.com/xxxxxxxxxxx
    (this link is valid until {{ expiration_date|date('F jS') }})
    このリンクは{{ expiration_date|date('F jS') }}まで有効です

https://github.com/idani/symfony_mailer_tutorial3/commit/14a7ee816dbb4acb8e263060e97e8230d785cc99

送信すると、以下のように問題なく日本語も表示されました。

image.png

メールソース
From: fabien@example.com
To: ryan@example.com
Subject: Thanks for signing up!
Message-ID: <0072478296801e97a3472e1ce06a9f38@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 16:41:55 +0900
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Welcome !
=E3=82=88=E3=81=86=E3=81=93=E3=81=9D =EF=BC=81

    You sig=
ned up as foo the following email:
    =E6=AC=A1=E3=81=AE=E3=83=A1=
=E3=83=BC=E3=83=AB=E3=82=A2=E3=83=89=E3=83=AC=E3=82=B9=E3=81=A7foo=E3=81=
=A8=E3=81=97=E3=81=A6=E3=82=B5=E3=82=A4=E3=83=B3=E3=82=A2=E3=83=83=E3=83=
=97=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82

    ryan@example.com=



    Click here to activate your account
    =E3=82=A2=E3=82=
=AB=E3=82=A6=E3=83=B3=E3=83=88=E3=82=92=E6=9C=89=E5=8A=B9=E3=81=AB=E3=81=
=99=E3=82=8B=E3=81=AB=E3=81=AF=E3=81=93=E3=81=93=E3=82=92=E3=82=AF=E3=83=
=AA=E3=83=83=E3=82=AF=E3=81=97=E3=81=A6=E3=81=8F=E3=81=A0=E3=81=95=E3=81=
=84

    http://www.example.com/xxxxxxxxxxx
    (this link is valid u=
ntil December 12th)
    =EF=BC=88=E3=81=93=E3=81=AE=E3=83=AA=E3=83=B3=
=E3=82=AF=E3=81=AF=E3=80=81December 12th=E3=81=BE=E3=81=A7=E6=9C=89=
=E5=8A=B9=E3=81=A7=E3=81=99=E3=80=82

Base64で送信する

ここまでできても合格なのですが、可能であれば、quoted-printableではなく、base64で送信をしたいです。

どうしたらよいのでしょうか?

TemplatedEmail.phpを見てみると、Email.phpの派生クラスであることがわかります。

Email.phpの中身は、symfony/mailerでbase64やiso-2022-jpを試してみるで見ていきましたが、quoted-printable固定となってしまいます。

そうすると、Twigでテキストをレンダリングして、それをメール本文として指定するのが良さそうです。

Creating and Using TemplatesRendering a Template in Servicesに、サービス内でのレンダリング方法が記載されています。

こちらを参考にして、サンプルコードを変更します。
Base64のメールの作成方法は、symfony/mailerでbase64やiso-2022-jpを試してみるから流用しますので、ガラッと変わります。

MailerController.php
        mb_language("uni");
        mb_internal_encoding("UTF-8");

        $subject = mb_encode_mimeheader('Thanks for signing up! 登録してくれてありがとうございます!!');
        $subject = str_replace("\r\n", '', $subject);

        $headers = (new Headers())
            ->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
            ->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
            ->addTextHeader('Subject', $subject)
        ;

        $body = $twig->render('emails/signup.txt.twig', [
            'expiration_date' => new \DateTime('+7 days'),
            'username' => 'foo',
            'email' => 'you@example.com',
        ]);
        $textContent = new TextPart($body, 'utf-8', 'plain', 'base64');
        $email = new Message($headers, $textContent);

        $mailer->send($email);

テンプレートの方も、メールアドレスなどの情報も、自動で設定されていたようです。
このためメールアドレスも個別に指定するように変更します。

signup.txt.twig
Welcome !
ようこそ 

    You signed up as {{ username }} the following email:
    次のメールアドレスで{{ username }}としてサインアップしました

    {{ email }}


    Click here to activate your account
    アカウントを有効にするにはここをクリックしてください

    http://www.example.com/xxxxxxxxxxx
    (this link is valid until {{ expiration_date|date('F jS') }})
    このリンクは{{ expiration_date|date('F jS') }}まで有効です

https://github.com/idani/symfony_mailer_tutorial3/commit/06fd543a301c8bb61f0a362c3fb66e0636d65fd5

想定通り、Twigを使ってテキストメールをBase64で送信することができました。

image.png

メールソース
From: =?UTF-8?B?6YCB5L+h6ICF5ZCN?= <hello@example.com>
To: =?UTF-8?B?5Y+X5L+h6ICF5ZCN?= <you@example.com>
Subject: Thanks for signing up!
 =?UTF-8?B?55m76Yyy44GX44Gm44GP44KM44Gm44GC44KK44GM?=
 =?UTF-8?B?44Go44GG44GU44GW44GE44G+44GZ77yB77yB?=
Message-ID: <a98efb8654df5bc230c364e0d1e476f4@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 17:15:07 +0900
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64

V2VsY29tZSAhCuOCiOOBhuOBk+OBnSDvvIEKCiAgICBZb3Ugc2lnbmVkIHVwIGFzIGZvbyB0aGUg
Zm9sbG93aW5nIGVtYWlsOgogICAg5qyh44Gu44Oh44O844Or44Ki44OJ44Os44K544GnZm9v44Go
44GX44Gm44K144Kk44Oz44Ki44OD44OX44GX44G+44GX44Gf44CCCgogICAgeW91QGV4YW1wbGUu
Y29tCgoKICAgIENsaWNrIGhlcmUgdG8gYWN0aXZhdGUgeW91ciBhY2NvdW50CiAgICDjgqLjgqvj
gqbjg7Pjg4jjgpLmnInlirnjgavjgZnjgovjgavjga/jgZPjgZPjgpLjgq/jg6rjg4Pjgq/jgZfj
gabjgY/jgaDjgZXjgYQKCiAgICBodHRwOi8vd3d3LmV4YW1wbGUuY29tL3h4eHh4eHh4eHh4CiAg
ICAodGhpcyBsaW5rIGlzIHZhbGlkIHVudGlsIERlY2VtYmVyIDEydGgpCiAgICDvvIjjgZPjga7j
g6rjg7Pjgq/jga/jgIFEZWNlbWJlciAxMnRo44G+44Gn5pyJ5Yq544Gn44GZ44CCCg==

TwigのテンプレートをDBに保存する

メールの本文をテンプレートという形で保存をして、レンダリングすることもできました。

ところが、メールは本文以外にサブジェクトというデータも必要です。
サブジェクトはDBに保存し、メールの本文のテンプレートはファイルシステムというのは微妙ですよね。

TwigのテンプレートもDBに保存できるとよいですね。

調べてみると、Twigのドキュメントで、Using a Database to store Templatesがありました。

ただ、こちらは純粋にTwigのテンプレートの読み出し先をDBにするということみたいです。

サイトとしてHTMLを表示するためでしたらよいのですが、メールとしては使いづらいですね。

それよりは、その次に記載のTwigのテンプレートを変数として受け取って処理をするための、Loading a Template from a Stringが良さそうです。

サンプルでテンプレートを読み込む部分を書き換えます。

MailerController.php
   public function index(MailerInterface $mailer, Environment $twig)
    {

        mb_language("uni");
        mb_internal_encoding("UTF-8");

        $subject = mb_encode_mimeheader('Thanks for signing up! 登録してくれてありがとうございます!!');
        $subject = str_replace("\r\n", '', $subject);

        $headers = (new Headers())
            ->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
            ->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
            ->addTextHeader('Subject', $subject)
        ;

        $twigEmailTemplate =<<<EOL
Welcome !
ようこそ 

    これはTwigテンプレートを変数から読み込んでレンダリングしたメールです

    You signed up as {{ username }} the following email:
~~~中略~~~
    このリンクは{{ expiration_date|date('F jS') }}まで有効です

EOL;

        $template = $twig->createTemplate($twigEmailTemplate);
        $body = $template->render([
            'expiration_date' => new \DateTime('+7 days'),
            'username' => 'foo',
            'email' => 'you@example.com',
        ]);
        $textContent = new TextPart($body, 'utf-8', 'plain', 'base64');
        $email = new Message($headers, $textContent);

        $mailer->send($email);

        return $this->render('mailer/index.html.twig', [
            'controller_name' => 'MailerController',
        ]);
    }

https://github.com/idani/symfony_mailer_tutorial3/commit/c97d5ac4a4e02c6c74111aa3b249f7abb2a0fc5c

image.png

まとめ

Twigを使って簡単にメールが送れることがわかりました。
Base64にするには、面倒なので、良さそうな修正案を考える必要てプルリクした方がよいですね。

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

MAMP PHP7.3.1とVSCodeでXdebugを使う

よくわかっていないので、間違っているところ・余計なところ・足りないところがあれば教えて下さい。

MAMP 5.3
PHP 7.3.1
Xdebug v2.8.1

homebrewを使います。

Xdebugをインストールする

/Applications/MAMP/bin/php/php7.3.1/lib/php/extensions/no-debug-non-zts-20180731/の中身を見てもxdebug.soがなかったので、インストールしました。

以下のリンク先に手順の案内があります。
Xdebug: Support — Tailored Installation Instructions

詳しく説明していきます。

↓ まず、MAMPのStart Serversを押した後、Open WebStart pageを押します。
MAMPの画面イメージ

PHPINFOを開きます。
open web start pageを押した後の画面イメージ

PHPINFOのHTMLソースをすべてコピーします。

先程のリンク先にソースを貼る場所があるので、そこに貼ります。
そうすると、ダウンロード手順が案内されます。

まず、xdebug-2.8.1.tgzをダウンロードします。

ターミナルを開き、ダウンロードしたファイルがあるディレクトリにcdコマンドで移動します。
そこで以下のコマンドを実行してファイルを解凍します。

ターミナル
$ tar -xvzf xdebug-2.8.1.tgz

↓ 解凍したファイルに移動して、phpizeを実行します。

ターミナル
$ cd xdebug-2.8.1

$ phpize

# こんな表示が出ます
Configuring for:
...
Zend Module Api No:      20180731
Zend Extension Api No:   320180731

もしもここでエラーが出た場合はautoconfをインストールします。

ターミナル
$ brew install autoconf

成功したら以下を実行します。

ターミナル
$ ./configure

$ make

xdebug.soをMAMPの中にコピーします。

ターミナル
$ cp modules/xdebug.so /Applications/MAMP/bin/php/php7.3.1/lib/php/extensions/no-debug-non-zts-20180731

php.iniの設定を変更する

/Applications/MAMP/bin/php/php7.3.1/conf/php.iniの設定を変更します。
一番下に以下の記述を足します。
zend_extension="~"の行がコメントアウトされている場合は先頭の;を削除します。

/Applications/MAMP/bin/php/php7.3.1/conf/php.ini
[xdebug]
zend_extension="/Applications/MAMP/bin/php/php7.3.1/lib/php/extensions/no-debug-non-zts-20180731/xdebug.so"
xdebug.remote_enable=1
xdebug.remote_autostart=1

MAMPのStop ServersStart Serversを押して再起動をしてください。

ターミナルでMAMPのPHPを実行するようにする

大変参考にさせていただきました。
MAMPのPHPをターミナルから実行する方法 - Qiita

パスを通します。
.bashrcに以下を記述します。bashではなくzshなどの場合は、がんばってください。

.bashrc
export PATH=/Applications/MAMP/bin/php/php7.3.1/bin:$PATH 
ターミナル
# パスの確認
$ which php
/Applications/MAMP/bin/php/php7.3.1/bin/php

確認する

ターミナル
$ php -m

(省略)
[Zend Modules]
Xdebug

正しく動いていれば、オレンジ色のエラーがブラウザに表示されたり、var_dump()が見やすくなったりします。

Visual Studio Codeに拡張機能をインストールする

拡張機能 PHP Debug をインストールします。

VSCodeの左側にある虫のマークをクリックし、デバッグ画面を開きます。
デバッグ画面の上部にある歯車のマークをクリックし、PHPを選択します。
デバッグ画面イメージ
自動でlaunch.jsonが作成されます。

デバッグする

MAMPを起動します。

VSCodeでブレークポイントを打ちます。
↓ 左の丸印です。
ブレークポイントを打った画面イメージ

Listen for XDebugを選択し、三角の再生マークを押します。
X Debugを実行した画面イメージ

ブラウザを開き、ページを更新すると、VSCodeに自動で戻ってきます。
変数の中身とか見れます。
ブレークポイントを見ている画面イメージ
エラーとかも見れます。
例外が発生した場合の画面イメージ

以上です。

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

ログイン必須なLaravelシステムでファイルの保存と取得を行う

ログイン必須なLaravelシステムでファイルの保存と取得を行う

ログイン必須なLaravelシステムでファイルの保存と取得の方法を軽く調べてみました。

要約

だいたいドキュメントに書いてある。。。
- Storageを使う
- ファイルの操作はLaravelのドキュメントの通り。
- ファイルの保存場所のシンボリックリンクをpublicに作らない
- コントローラでファイルを取得して返す

やりたいこと

  • Laravelでファイルの保存と取得
  • ファイルはログイン済みのユーザのみが見れる
    • 非ログインユーザは見れない

方法

authのミドルウェアを指定したコントローラを経由してファイルを返すようにする。

ファイルの保存

use Illuminate\Support\Facades\Storage;
Route::group(['middleware' => 'auth'], function() {
  Route::get('/', function() {
    $content = "hello world\n";
    Storage::put('test.txt', $content);
    return "saved!";
  });
});

これで$contentの内容がstorage/app/text.txtに保存される。

ファイルの取得

use Illuminate\Support\Facades\Storage;
Route::group(['middleware' => 'auth'], function() {
  Route::get('/test.txt', function() {
    $content = Storage::get('test.txt');
    return $content;
  });
});

最後に

簡単にできた。あとは、コントローラでファイルに対しての認可を行ったりすれば、もっと複雑なことができそうです。

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

Moodle 3.8 マニュアル - Debian ベースのディストリビューションに Moodle をインストールする

Installing Moodle on Debian based distributions
Contents

1 Debian パッケージとして Moodle をインストールする
2 moodle を .tgz(.tar.gz) あるいは .zip ファイルからインストールする
2.1 ステップ 1:必要なパッケージをインストールする
2.2 ステップ 2:MySQL データベースをセットアップする
2.3 ステップ 3:moodle をダウンロードする
2.4 ステップ 4:ファイルを解凍する
2.5 ステップ 5:データディレクトリを作成し Moodle ディレクトリの権限を設定する
2.6 ステップ 6:Apache が Moodle を web サイトとして使用するように変更する
2.7 ステップ 7:最後にインストールする
2.8 ステップ 8:Moodle をセットアップする
2.9 ステップ 9:cron をインストールする

1 Debian パッケージとして Moodle をインストールする

Moodle は Debian パッケージとして含まれていました。Moodle をどんな種類のインストーラーによっても、インストールすることは推奨されません。結局は、手動で(あるいは git を使用して)インストールすることが、コードがオリジナルから変更されていないために、もっとよいということになります。

2 moodle を .tgz(.tar.gz) あるいは .zip ファイルからインストールする

2.1 ステップ 1:必要なパッケージをインストールする

After installing your Debian distro, install these packages (if you've not already done so). See Installing Apache, MySQL and PHP or refer to the respective user manuals. Using apt-get, aptitude or synaptic you can install these very easily.

  • Web Server (Apache highly recommended)
  • Database Server (MySQL or PostgreSQL recommended)
  • PHP, PHP-MySQL mod (or mod for your database)
  • Various PHP modules necessary for Moodle

LAMP in Debian

Setting up a LAMP in Debian is very easy. Once you get used to Debian administration including installation and configuration are much simpler compared to other linux distros. The following describes how to install apache, php and mysql on the Debian distribution.

For installation of the necessary packages the easiest option to use apt-get through the command line interface. Debian has easy access to a Root Terminal

Use the following command to install apache2, php5 and mysql (Note: Debian is now shipping with MariaDB as default database server - I have included instructions on how to install MySQL community edition instead.) MySQL Server Package install:

wget https://dev.mysql.com/get/mysql-apt-config_0.8.6-1_all.deb
mysql-apt-config_0.8.6-1_all.deb
apt-get install gdebi-core
gdebi mysql-apt-config_0.8.6-1_all.deb

Update Repositories:

apt-get update

Install LAMP:

apt-get install apache2 php7.1 mysql-server php7.1-mysql libapache2-mod-php7.1 php7.1-gd php7.1-curl php7.1-xmlrpc php7.1-intl php7.1-zip php7.1-mbstring 

The mentioned packages are installed along with the dependencies depending on what was already installed on your Debian system.

Now you may fire up a browser and type localhost to check whether the apache2 default page is shown,

If you are familiar with apache settings, you can edit the apache configuration files using the text editor gedit or nano by typing:

gedit /etc/apache2/apache2.conf

or

nano /etc/apache2/apache2.conf

However, for a basic install, you should not need to mess with this file.

Now we must make a few changes in the php7.1 configuration file. Open it using

gedit /etc/php/7.1/apache2/php.ini

or

nano /etc/php/7.1/apache2/php.ini

add the entries

extension=mysql.so 
extension=gd.so

Sometimes these entries are provided as example lines being commented out . You can remove the commenting to activate the entries. Then make the following changes (adjust to your preference) This will allocated more memory and allow files to be uploaded up to 80MB. This should be enough for most multi-media files. Hard drive space is cheap and the default is only 2MB. It is recommended that you change the settings to the following values:

memory_limit = 40M
post_max_size = 80M
upload_max_filesize = 80M

To test the php installation, you can create a text file named phpinfo.php with the contents <?phpinfo()?> and save it at /var/www. Restart apache with the command below. Now access this file through the browser localhost/phpinfo to check the installation of php.

You can restart apache 2 by

/etc/init.d/apache2 restart

or

service apache2 restart

2.2 ステップ 2:MySQL データベースをセットアップする

Set a secure root password for the database (you might have already done this in the initial install process)

mysqladmin -u root password "mySecurePassword"

Note that on a secure production server, you will want to create a different user than root to access the database.

Now log in

mysql -u root -p

Enter your password

Create the Moodle database

mysql> CREATE DATABASE moodle CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

(this should set the database up correctly to work with Moodle)

Then exit the database

mysql> exit;

You can restart mysql by

/etc/init.d/mysql restart

2.3 ステップ 3:moodle をダウンロードする

Download moodle from http://download.moodle.org/?lang=en.

2.4 ステップ 4:ファイルを解凍する

  • For zip file, use unzip . For tgz(tar.gz), use tar -zxvf . You can also use any of the GUI front-ends such as file-roller or ark or just let Debian unzip it with Archive Manager. You will get a folder moodle (or moodle-2.8.1 or something similar). It will probably be located in your Downloads folder unless you told the Archive Manager or the Download utility to save it somewhere else. We will presume it is in your Download folder.

From terminal type

cd /home/YourUSERNAME/Downloads
mv moodle /var/www

You can also copy instead of moving it in case you want a backup of the code on your machine.

cp -R moodle /var/www

You will probably need to do this as root (no problem if you are using the Root Terminal!)

2.5 ステップ 5:データディレクトリを作成し Moodle ディレクトリの権限を設定する

Switch to the correct directory

cd /var/www

Create a directory for user and course files

mkdir moodledata

Set ownership and permissions so that Apache can access the files

chown -R www-data:www-data moodle
chown -R www-data:www-data moodledata
chmod -R 755 moodle
chmod -R 755 moodledata

On a production server, you may want to tighten up privileges.

2.6 ステップ 6:Apache が Moodle を web サイトとして使用するように変更する

Note that the server comes with Apache running and looking at the /var/www directory. But there is nothing in that folder, so one just gets a redirect. Edit as follows to have it point at Moodle instead:

gedit /etc/apache2/sites-available/default or
nano /etc/apache2/sites-available/default

On about line 4, change DocumentRoot "/var/www/html" to
DocumentRoot "/var/www/moodle"

On about line 10, change <Directory "/var/www/html/"> to
<Directory "/var/www/moodle/">

Around line 17, comment out the line for the default page:

# RedirectMatch ^/$ /apache2-default/

You can change other values like ServerAdmin if appropriate. For all changes, you should restart Apache for the new settings to take effect.

/etc/init.d/apache2 restart

2.7 ステップ 7:最後にインストールする

2.8 ステップ 8:Moodle をセットアップする

If you are only going to test Moodle on your internal network, just enter the local IP address as the web address. You can find the local IP address under DHCP by typing

ifconfig eth0

If you have a web address that points to your server, use that instead.

From a browser on another machine, enter

http:// ----- your web address -----

  • For the database user, enter root
  • For Password, enter the password for the database that you created earlier

Continue through the dialogs until installed.

2.9 ステップ 9:cron をインストールする

Moodle needs a periodic call to do maintenance work like sending out emails, cleaning up the database, updating feeds, etc. To run the cron every 10 minutes, do the following

crontab -u www-data -e

Add this line and save.

*/10 * * * * /usr/bin/php /var/www/moodle/admin/cli/cron.php  >/dev/null

Use CNTL-X to save

カテゴリ:管理者 インストール

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

【 Laravel 6 】ユーザーログイン機能の実装

【 Laravel 6 】ユーザーログイン機能の実装

2019年11月から、Laravel歴数ヶ月の初心者が投稿型ナレッジベースのコミュニティサイトを作るというチャレンジ中。作りたいアプリケーション→機能を因数分解→ググる→先人の轍をたどる(写経する)→ぬかるみにはまる→エラー解消の神を探す→解決を繰り返す日々( ·ㅂ·)و 。備忘録として、Qiitaに投稿しています。

ユーザーログイン機能を作成

下記URLに倣って、ユーザーログイン機能を実装。
Laravel 6「make:Auth」が無くなった 〜Laravel6でのLogin機能の実装方法〜MyMemo
https://qiita.com/daisu_yamazaki/items/a914a16ca1640334d7a5

ターミナル
$ php artisan serve

下記の通り、画面右上に、「LOGIN」と「REGISTER」が表示されていればOK。
〈ブラウザデカクニン〉
スクリーンショット 2019-09-10 13.26.27.png

以上です(ง ´͈౪`͈)ว

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

【 Laravel 6 】新規プロジェクト作成

【 Laravel 6 】新規プロジェクト作成

2019年11月から、Laravel歴数ヶ月の初心者が投稿型ナレッジベースのコミュニティサイトを作るというチャレンジ中。作りたいアプリケーション→機能を因数分解→ググる→先人の轍をたどる(写経する)→ぬかるみにはまる→エラー解消の神を探す→解決を繰り返す日々( ·ㅂ·)و 。備忘録として、Qiitaに投稿しています。

新規プロジェクト作成

MAMP htdocsディレクトリでLaravelの新規プロジェクトを作成
(˘ਊ˘)コマンドタタクヨ

ターミナル
$ Laravel new sample_board

作成したプロジェクトのディレクトリへ移動
(˘ਊ˘)コマンドタタクヨ

ターミナル
$ cd sample_board

仮想サーバーの立ち上げ

ターミナル
$ php artisan serve

Laravel development server started: http://127.0.0.1:8000と表示されればOK。
ローカルホストでLaravelのホーム画面が表示されればOK
スクリーンショット 2019-09-10 0.07.58.png

以上です(ง ´͈౪`͈)ว

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

JenkinsがPHPUnitの結果ファイルをパースしなくなったときの対処方

JenkinsでxUnit pluginを使う場合、PHPUnitでjunit形式のログを出力しますが、あるときから、以下のようなエラーが大量に出力され、解析に失敗するようになりました。

type.3.2.2: 要素'testsuite'に属性'assertions'を含めることはできません。
WARNING: At line 2161 of file:/var/lib/jenkins/workspace/hoge/reports/phpunit.xml:cvc-complex-type.3.2.2: 要素'testsuite'に属性'warnings'を含めることはできません。
WARNING: At line 2161 of file:/var/lib/jenkins/workspace/hoge/reports/phpunit.xml:cvc-pattern-valid: 値'2.395662'は、タイプ'SUREFIRE_TIME'のパターン'(([0-9]{0,3},)*[0-9]{3}|[0-9]{0,3})*(\.[0-9]{0,3})?'に対してファセットが有効ではありません。

いろいろと調べたんですが、これという解決法が見つからなかったため、xUnit pluginが文句を言わないよう、出力されたログファイルを変換することで対処しました。

$project_root/tools/convert_phpunit_log.php
<?php
$buf = file_get_contents(__DIR__ . '/../reports/phpunit.xml');
$doc = new DOMDocument();
$doc->loadXML($buf);

$doc->formatOutput = true;

$nodes = $doc->getElementsByTagName('testcase');

foreach ($nodes as $node) {
    $node->removeAttribute('class');
    $node->removeAttribute('file');
    $node->removeAttribute('line');
    $node->removeAttribute('assertions');
    $time = round($node->getAttribute('time'), 3);
    $node->setAttribute('time', $time);
}

$nodes = $doc->getElementsByTagName('testsuite');

foreach ($nodes as $node) {
    $node->removeAttribute('assertions');
    $node->removeAttribute('warnings');
    $node->removeAttribute('file');
    $time = round($node->getAttribute('time'), 3);
    $node->setAttribute('time', $time);
}

file_put_contents(__DIR__ . '/../reports/phpunit.xml', $doc->saveXML());

入出力のパスは適切なものに変更してください。

これをphpunit終了後に実行してください。

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

【 Laravel 6 】投稿の表示・修正・更新・削除

【 Laravel 6 】投稿アプリの基本的機能(一覧表示・詳細表示・修正・更新・削除)を実装する

2019年11月から、Laravel歴数ヶ月の初心者が投稿型ナレッジベースのコミュニティサイトを作るというチャレンジ中。作りたいアプリケーション→機能を因数分解→ググる→先人の轍をたどる(写経する)→ぬかるみにはまる→エラー解消の神を探す→解決を繰り返す日々( ·ㅂ·)و 。備忘録として、Qiitaに投稿しています。

一覧表示・詳細表示・修正・更新・削除

投稿型ナレッジベースということで、「投稿アプリ」を軸に色々と機能を付加していきます。
「投稿アプリ」については、こちら↓のサイトを参考に(ほぼ写経)させてもらいました アザマス!!m(__)m

【入門】Laravelチュートリアル – 掲示板を作成してみよう
https://blog.hiroyuki90.com/articles/laravel-bbs/#i-2

備忘録

私自身が素人すぎて、途中でつまずいたところを追記しておきます。

投稿一覧が表示されるViewファイル(resources/views/posts/index.blade.php)について、Laravelのバージョンアップにより非推奨となっている記述があったので、Laravel 5.8以降に推奨となった書き方に修正。

index.blade.php
{!! nl2br(e(str_limit($post->body, 200))) !!}  ← Laravel5.8から非推奨

{{ \Illuminate\Support\Str::limit($post->body, 200) }} ← こちらの書き方に変更

以上です(ง ´͈౪`͈)ว

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

Laravel 6.x 投稿型ナレッジベースのコミュニティサイトを作る

Laravel歴数ヶ月の初心者が投稿型ナレッジベースのコミュニティサイトを作る

ある趣味を盛り上げるためのコミュニティサイトを作ることを発想。Qiitaや、CrieitといったWeb開発系のものに着想を得て、投稿型のナレッジベースを主軸としたWebアプリケーションを作ります。

だいぶ初心者なので、他の方のブログやQiita投稿をガシガシ写経が基本です。詰まった部分について、備忘録としてQiitaに挙げていく感じでいきます。

機能一覧

入れたい機能は、こちら。順不同で、触れそうな機能から作っていくので、「作る⇄崩し」の繰り返しがありそう。機能実装の都度、Qiita投稿を下記の機能一覧にリンク貼っつけていく。

  • Laravelプロジェクト新規作成(←機能では無いですが(·ε·))
  • ログイン機能
  • ソーシャルログイン機能
  • マイページ
    • 投稿一覧
    • ストック機能
  • ユーザープロフィール
    • 表示
    • 修正・更新・削除
  • 投稿の表示・修正・更新・削除
    • マークダウンエディタ
    • 画像のアップ
  • ハッシュタグ機能
  • Like機能
  • ランディングページ
    • 投稿一覧の表示
    • カテゴリー別の投稿一覧(Like順or新着順)
    • 広告
    • 広告(アフェリエイト)
  • データベース周り

環境

  • Laravel 6.2
  • php 7.2
  • Bootstrap(とりあえず、動作のみを作成するので”ガワ”は基本ノータッチ・・・の予定)

背景

機能やサービス設計の諸々について考えあぐねていたら、気づけばどんどん時間ばかりが経過してしまっていた・・・(´ε`;)ハァ。アイディア思考ばかりで、コードからもどんどん離れていることに気づき、その焦りとで、結局どっちつかずの日々。
技術力が全く無いのに「考えても始まらんだろう」と思い、「手を動かす」。そして、開発の試行錯誤しながら、サービスをまとめていくということだけを決意。2019年11月中旬に制作開始しました。年越しのお籠り時期を経て、2020年1月中頃までに一通りの機能を回せるようになるといいなぁ・・・と画策中(´-_ゝ-`)

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

お手軽Hack!

メリークリスマス!
ユアマイスターアドベントカレンダーの24日目を担当させていただきます、インターンの京極と申します。
https://qiita.com/advent-calendar/2019/yourmystar

今回は、Hack言語を触ってみた記事を書いてみました。ご興味ある方は年末年始のお休み中にHackしていただければ幸いです。

Hackとは

HackとはHHVMという実行環境上で動くプログラミング言語で、PHPとの互換性を持ち、Facebookによって開発が行われています。

公式サイト
https://hacklang.org/
Github
https://github.com/facebook/hhvm/tree/master/hphp/hack

Java言語がJVMという実行環境で動くように、Hack言語は、HHVMという実行環境で動くということです。

公式サイトを見るとジェネリクスをサポートしていたり、ラムダがファーストクラス関数として使えるなど試してみたい気持ちがクリスマスを迎えるにあたり出てきますのでちょっと触ってみたいと思います。

Hack on Docker

マシンの直接インストールするのは避けて、Docker HubからHackの動く環境を作リます。

Dockerのは行っている環境で以下のコマンドを叩くと、そこはすでに白銀の世界ではなく、HHVMの手に入った世界の中です。

docker run -it hhvm/hhvm bash

# hhvm --version
HipHop VM 4.34.0 (rel)
Compiler: 1575318969_858014129
Repo schema: e5c15841820ab38c7ec75ebe5ec9201013caa5bf

適当なディレクトリにhello.hackを置いて、実行させてみます。

<<__EntryPoint>>
function main(): noreturn {
  echo "Hello World!";
  exit(0);
}
docker run -it -v $PWD:/app hhvm/hhvm hhvm /app/hello.hack

Hello World!

おまけにフィボナッチ数を計算するプログラムも書いてみました。普段からPHPを書く方にとっては違和感が少ないと思います。

function fib(int $n): int {
  if ($n <= 1)
    return $n;
  return fib($n - 1) + fib($n - 2);
}

<<__EntryPoint>>
function main(): noreturn {
  echo fib(40);
  exit(0);
}

Webサーバ

調べているとHHVMはWebサーバの機能が組み込まれているということなので起動させてみます。

docker run -it -v $PWD:/var/www/public -p 12345:80 hhvm/hhvm-proxygen

こうするとDocker内の/var/www/publicディレクトリに先程の、hello.hackが存在することになりますので、ブラウザからアクセスしてみます。

http://localhost:12345/hello.hack

スクリーンショット 2019-12-05 14.17.51.png

もう少しHackを書いてみる

ここまで環境構築が主だったので、もう少しプログラムを書いてみたいと思います。様々な機能がHackにはありますが、目についた面白い機能を紹介して終わりにしたいと思います。

以下の書き方をすると、 \$inc は \$a という引数を受け取り、 \$b を引数として受け取る関数を返す関数になります。 \$inc3 は \$a が3になったバージョンの関数なので、 \$inc3 に対して10を引数に渡すと、13が返ってくることが確認できます。

このようなカリー化された関数をさらっと書けるというのもHack言語の魅力だと感じました。

<<__EntryPoint>>
function main(): noreturn {
  $inc = $a ==> $b ==> $a + $b;
  $inc3 = $inc(3);
  echo $inc3(10);
  exit(0);
}

まとめ

ちょっと触っただけでも様々な機能がHack言語にあることが分かりましたので、年末年始中にもう少しHackに触れたいと思います。メリーハッククリスマス!

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

symfony/mailerでbase64やiso-2022-jpを試してみる

Symfony Advent Calendar 2019 8日目の記事です。
昨日は@brtriverさんSymfonyの歴史を振り返ってみるでした!

はじめに

こちらの記事は、symfony/mailerで試行錯誤したことの続きになります。

前回は、配信処理までの設定で四苦八苦しましたが、今回は送信するメールの文字コードやエンコードの仕方について試行錯誤していきたいと思います。

メールは、インターネット時代のコミュニケーション手段として、

  • utf-8、iso-2022-jpなどの文字コード
  • 7bit, 8bit, base64, quoted-printableなどのエンコーディング
  • 単純にテキストのみ、テキストとHTMLの併用など

様々な設定ができるようになっています。

そしてSPAM配信の増加による、迷惑メール対策技術の向上によって、
メールを迷惑メールとして判定させないために、いろいろと気をつける部分となります。

新規のシステムならよいのですが、
システムのリニューアルによって、メールが届かなくなるということはよくあります。

これは送信されるメールの文面が、以前のシステムと異なっていることも関係します。

例えば、夏休みに明けに、気になるあの子が茶髪ギャルになっていたら、違和感覚えますよね。
迷惑メール判定も、そんな感じです。

夏休み前 夏休み明け

ここまでの変化ではないにしても、ちょっと髪型変えたかな?ぐらいの変化、
メールの世界だと、ヘッダーにX-Maillerが追加されただけでも、迷惑メールとして判定されたりします。

感覚的には、文字コード、エンコーディングが違うと大きな変化だったりします。

なので、前回のシステムと同様のメールを送信ができるように、Symfony/mailerも使いこなせる様になる必要があるのです。

テスト環境の構築

Dockerで開発環境を作って、SymfonyのWebスケルトンプロジェクトを作ります。
そこまで作ったのが以下になります。

https://github.com/idani/symfony_mailer_tutorial2/tree/de7e20b8af6886a3f1c07927ed5e7a6f3be0801b

一番かんたんな送信方法

Creating & Sending Messages」のサンプルコードを実行します。

MaillerController.php(その1)
class MailerController extends AbstractController
{
    /**
     * @Route("/mailer", name="mailer")
     */
    public function index(MailerInterface $mailer)
    {
        $email = (new Email())
            ->from('hello@example.com')
            ->to('you@example.com')
            //->cc('cc@example.com')
            //->bcc('bcc@example.com')
            //->replyTo('fabien@example.com')
            //->priority(Email::PRIORITY_HIGH)
            ->subject('Time for Symfony Mailer!')
            ->text('Sending emails is fun again!')
            ->html('<p>See Twig integration for better HTML integration!</p>');

        $mailer->send($email);

        return $this->render('mailer/index.html.twig', [
            'controller_name' => 'MailerController',
        ]);
    }
}

https://github.com/idani/symfony_mailer_tutorial2/commit/7e17f841fc116d0cacab976ec767028c0ce99d49

http://localhost:8000/mailer を開くと以下の画面が表示されてメールが送信されました。

image.png

MailDevで確認すると、受信されています。
1つ目の画像はHTMLメールを表示した場合。2つ目の画像は、テキストの画面を表示した場合です。

image.png

image.png

メールのソースは、以下になります。

メールのソース
From: hello@example.com
To: you@example.com
Subject: Time for Symfony Mailer!
Message-ID: <f9549d7eac6696b8cd513cc398e5bcc9@example.com>
MIME-Version: 1.0
Date: Wed, 04 Dec 2019 14:51:44 +0900
Content-Type: multipart/alternative;
 boundary="_=_symfony_1575438704_55dde3f773e700c11643dd0965c1db31_=_"

--_=_symfony_1575438704_55dde3f773e700c11643dd0965c1db31_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Sending emails is fun again!
--_=_symfony_1575438704_55dde3f773e700c11643dd0965c1db31_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<p>See Twig integration for better HTML integration!</p>
--_=_symfony_1575438704_55dde3f773e700c11643dd0965c1db31_=_--

日本語を試してみる

送信者名、受信者名、サブジェクト、本文に日本語を試してみましょう。

メールアドレスでの名前の指定は、Email Addressesに記載がありますように、Address.phpクラスを利用すると良さそうです。

サブジェクト、本文は、何も考えずに日本語を入れてみます。
また、HTMLは面倒なので、ここでは一旦、オミットしておきます。

以下のように修正しました。

MailerController.php(その2)
class MailerController extends AbstractController
{
    /**
     * @Route("/mailer", name="mailer")
     */
    public function index(MailerInterface $mailer)
    {
        $body = <<<EOL
https://symfony.com/why-use-a-framework

フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを

~~~中略~~~

この点で、フレームワークはブラックボックスではありません!Symfonyの場合、それはまだPHPです...開発されるアプリケーションはSymfonyユニバースに限定されず、たとえば他のPHPライブラリとネイティブに相互運用できます。
EOL;


        $email = (new Email())
            ->from(new Address('hello@example.com', '送信者名'))
            ->to(new Address('you@example.com', '受信者名'))
            //->cc('cc@example.com')
            //->bcc('bcc@example.com')
            //->replyTo('fabien@example.com')
            //->priority(Email::PRIORITY_HIGH)
            ->subject('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?')
            ->text($body)
//            ->html('<p>See Twig integration for better HTML integration!</p>')
        ;

        $mailer->send($email);

        return $this->render('mailer/index.html.twig', [
            'controller_name' => 'MailerController',
        ]);
    }
}

https://github.com/idani/symfony_mailer_tutorial2/commit/91811b3273a7010af25cb132831c4c1a107d64a6

MailDevではこのように受信ができました。
パッと見はよさそうです。

image.png

メールソースを見ると、quoted-printable でエンコーディングをされていました。

メールソース(日本語)
From: =?utf-8?Q?=E9=80=81=E4=BF=A1=E8=80=85=E5=90=8D?= <hello@example.com>
To: =?utf-8?Q?=E5=8F=97=E4=BF=A1=E8=80=85=E5=90=8D?= <you@example.com>
Subject: =?utf-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE?=
 =?utf-8?Q?=E3=82=B5=E3=83=96=E3=82=B8=E3=82=A7?=
 =?utf-8?Q?=E3=82=AF=E3=83=88=E3=81=AB=E3=81=AA?=
 =?utf-8?Q?=E3=82=8A=E3=81=BE=E3=81=99=E3=80=82?=
 =?utf-8?Q?=E9=95=B7=E3=81=8F=E3=81=AA=E3=82=8B?=
 =?utf-8?Q?=E3=81=A8=E6=96=87=E5=AD=97=E5=8C=96?=
 =?utf-8?Q?=E3=81=91=E3=81=99=E3=82=8B=E3=81=A8?=
 =?utf-8?Q?=E3=81=84=E3=81=86=E8=A9=B1=E3=82=82?=
 =?utf-8?Q?=E3=81=82=E3=82=8A=E3=81=BE=E3=81=99?=
 =?utf-8?Q?=E3=81=AE=E3=81=A7=E3=80=81=E9=95=B7?=
 =?utf-8?Q?=E3=81=8F=E6=9B=B8=E3=81=84=E3=81=A6?=
 =?utf-8?Q?=E3=81=BF=E3=81=BE=E3=81=99=E3=80=82?=
 =?utf-8?Q?=E3=81=93=E3=82=8C=E3=81=90=E3=82=89?=
 =?utf-8?Q?=E3=81=84=E9=95=B7=E3=81=84=E3=81=A8?=
 =?utf-8?Q?=E3=81=A9=E3=81=86=E3=81=8B=E3=81=AA=EF=BC=9F?=
Message-ID: <964a02541e6dc9af98f5af3f31b3bf77@example.com>
MIME-Version: 1.0
Date: Wed, 04 Dec 2019 15:22:13 +0900
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

https://symfony.com/why-use-a-framework

=E3=83=95=E3=83=AC=E3=83=BC=
=E3=83=A0=E3=83=AF=E3=83=BC=E3=82=AF=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=99=
=E3=82=8B=E5=BF=85=E8=A6=81=E3=81=8C=E3=81=82=E3=82=8B=E3=81=AE=E3=81=AF=
=E3=81=AA=E3=81=9C=E3=81=A7=E3=81=99=E3=81=8B=EF=BC=9F
=E3=83=95=E3=83=
=AC=E3=83=BC=E3=83=A0=E3=83=AF=E3=83=BC=E3=82=AF=E3=81=AF=E7=B5=B6=E5=AF=
=BE=E3=81=AB=E5=BF=85=E8=A6=81=E3=81=A8=E3=81=84=E3=81=86=E3=82=8F=E3=81=
=91=E3=81=A7=E3=81=AF=E3=81=82=E3=82=8A=E3=81=BE=E3=81=9B=E3=82=93=E3=80=
以下省略

Base64で送信する

日本語は送信ができるようですが、エンコーディングがquoted-printableなので、いろいろとデータ量が増えています。日本だとbase64が主流なのでbase64に変更できるかトライしてみます。

Emailクラスでメールデータを作成しますので、Email.phpを見ていきます。

そうすると、EmailクラスがMimeコンポーネントに所属していることがわかりました!

メールデータをカスタマイズする

The Mime Componentドキュメントに目を通すと、Emailクラスは高レベルAPI、Messageクラスは低レベルAPIと記載があります。

そして、Creating Raw Email Messagesとして、直接メッセージを作成する方法が記載されていました。

MIMEコンポーネントドキュメント内のサンプル
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
use Symfony\Component\Mime\Part\TextPart;

$headers = (new Headers())
    ->addMailboxListHeader('From', ['fabien@symfony.com'])
    ->addMailboxListHeader('To', ['foo@example.com'])
    ->addTextHeader('Subject', 'Important Notification')
;

$textContent = new TextPart('Lorem ipsum...');
$htmlContent = new TextPart('<h1>Lorem ipsum</h1> <p>...</p>', 'html');
$body = new AlternativePart($textContent, $htmlContent);

$email = new Message($headers, $body);

本文をBase64でエンコードする

サンプルを見ると、メール本文は、TextPartクラスが作成しています。

TextPartクラスのコンストラクタを見ると、ビンゴです!
エンコーディングを引数に取っていました。

/vendor/symfony/mime/Header/AbstractHeader.php(38)
public function __construct($body, ?string $charset = 'utf-8', $subtype = 'plain', string $encoding = null)
    {
        parent::__construct();

           ~~~中略~~~

        if (null === $encoding) {
            $this->encoding = $this->chooseEncoding();
        } else {
            if ('quoted-printable' !== $encoding && 'base64' !== $encoding && '8bit' !== $encoding) {
                throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable", "base64", or "8bit" ("%s" given).', $encoding));
            }
            $this->encoding = $encoding;
        }
    }

メールアドレスの名前部分をBase64でエンコードする

ヘッダーは、Headersクラスで定義していますが、エンコーディングについては、基底クラスのAbstractHeaderクラスで、quoted-printableに固定されていました。

/vendor/symfony/mime/Header/AbstractHeader.php
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Encoder\QpMimeHeaderEncoder;
/**
 * An abstract base MIME Header.
 *
 * @author Chris Corbyn
 */
abstract class AbstractHeader implements HeaderInterface
{
    private static $encoder;

~~~中略~~~
    /**
     * Get a token as an encoded word for safe insertion into headers.
     */
    protected function getTokenAsEncodedWord(string $token, int $firstLineOffset = 0): string
    {
        if (null === self::$encoder) {
 ここで固定=>     self::$encoder = new QpMimeHeaderEncoder();
        }
~~~省略~~~

ただし、エンコーディングするのは条件があるようです。

MailboxListHeader.php(106)で、$this->createPhraseで名前をエンコードしています。

/vendor/symfony/mime/Header/MailboxListHeader.php(106)
    public function getAddressStrings(): array
    {
        $strings = [];
        foreach ($this->addresses as $address) {
            $str = $address->getEncodedAddress();
            if ($name = $address->getName()) {
                $str = $this->createPhrase($this, $name, $this->getCharset(), !$strings).' <'.$str.'>';
            }
            $strings[] = $str;
        }

        return $strings;
    }

これはAbstractHeader.php(88)で定義されていて、preg_match('/^'.self::PHRASE_PATTERN.'$/D', $phraseStr)にマッチしなければ$phraseStr = $this->encodeWords($header, $string, $usedLength);でエンコードするようです。

/vendor/symfony/mime/Header/AbstractHeader.php(88)
    protected function createPhrase(HeaderInterface $header, string $string, string $charset, bool $shorten = false): string
    {
        // Treat token as exactly what was given
        $phraseStr = $string;
        // If it's not valid
        if (!preg_match('/^'.self::PHRASE_PATTERN.'$/D', $phraseStr)) {
            // .. but it is just ascii text, try escaping some characters
            // and make it a quoted-string
            if (preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $phraseStr)) {
                foreach (['\\', '"'] as $char) {
                    $phraseStr = str_replace($char, '\\'.$char, $phraseStr);
                }
                $phraseStr = '"'.$phraseStr.'"';
            } else {
                // ... otherwise it needs encoding
                // Determine space remaining on line if first line
                if ($shorten) {
                    $usedLength = \strlen($header->getName().': ');
                } else {
                    $usedLength = 0;
                }
                $phraseStr = $this->encodeWords($header, $string, $usedLength);
            }
        }
        return $phraseStr;
    }

PHRASE_PATTERNAbstractHeader.php(23)で定義されていますが、長くて意味がわかりません。

たぶん、エンコード済みならスキップする処理じゃないかな?と予想します。
そしたら、名前は、mb_encode_mimeheaderして渡せば良さそうです。

/vendor/symfony/mime/Header/AbstractHeader.php(23)
const PHRASE_PATTERN = '(?:(?:(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]+(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?)|(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?"((?:(?:[ \t]*(?:\r\n))?[ \t])?(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21\x23-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])))*(?:(?:[ \t]*(?:\r\n))?[ \t])?"(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?))+?)';

サブジェクトをbase64でエンコードする

次にサブジェクトですが、Headers.php(112)からたどっていくと、AbstractHeaderクラスを継承したUnstructuredHeaderクラスに格納されていました。

/vendor/symfony/mime/Header/Headers.php(112)
   /**
     * @return $this
     */
    public function addTextHeader(string $name, string $value): self
    {
        return $this->add(new UnstructuredHeader($name, $value));
    }

最終的には、UnstructuredHeader.php(65)でエンコードして渡しています。

/vendor/symfony/mime/Header/UnstructuredHeader.php(65)
    /**
     * Get the value of this header prepared for rendering.
     */
    public function getBodyAsString(): string
    {
        return $this->encodeWords($this, $this->value);
    }

これはAbstractHeaderで定義されていて、$this->tokenNeedsEncodingがあります。

/vendor/symfony/mime/Header/AbstractHeader.php(120)
    /**
     * Encode needed word tokens within a string of input.
     */
    protected function encodeWords(HeaderInterface $header, string $input, int $usedLength = -1): string
    {
        $value = '';
        $tokens = $this->getEncodableWordTokens($input);
        foreach ($tokens as $token) {
            // See RFC 2822, Sect 2.2 (really 2.2 ??)
            if ($this->tokenNeedsEncoding($token)) {
                // Don't encode starting WSP
                $firstChar = substr($token, 0, 1);
                switch ($firstChar) {
                    case ' ':
                    case "\t":
                        $value .= $firstChar;
                        $token = substr($token, 1);
                }
                if (-1 == $usedLength) {
                    $usedLength = \strlen($header->getName().': ') + \strlen($value);
                }
                $value .= $this->getTokenAsEncodedWord($token, $usedLength);
            } else {
                $value .= $token;
            }
        }
        return $value;
    }

tokenNeedsEncoding()を確認すると、制御文字と改行が含まれてれば、エンコードが必要という判定をしています。

ですから、こちらもエンコード済みなら、この判定でFalseになって、そのまま使ってくれそうです。

/vendor/symfony/mime/Header/AbstractHeader.php(148)
    protected function tokenNeedsEncoding(string $token): bool
    {
        return (bool) preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token);
    }

サンプルを以下のように書き換えます。

MailerController.php(その3)
class MailerController extends AbstractController
{
    /**
     * @Route("/mailer", name="mailer")
     */
    public function index(MailerInterface $mailer)
    {
        $body = <<<EOL
https://symfony.com/why-use-a-framework

フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;

        $headers = (new Headers())
            ->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
            ->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
            ->addTextHeader('Subject', mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?'))
        ;

        $textContent = new TextPart($body, 'utf-8', 'plain', 'base64');
        $email = new Message($headers, $textContent);

        $mailer->send($email);

        return $this->render('mailer/index.html.twig', [
            'controller_name' => 'MailerController',
        ]);
    }
}

https://github.com/idani/symfony_mailer_tutorial2/commit/a3085a095fb71068b5bb856f31c9d9d6ef4d2d9c

これでBase64のいい感じなメールがでるかと思いましたが、MailDevではサブジェクトが文字化けしてしまいました。。。

image.png

メールソースを確認すると、
本文は想定通りのUTF-8なのですが、
Subjectは、base64でエンコードしたデータを渡しているのですが、quoted-printableで更にエンコードがされていました。

また、From/Toはiso-2022-jpが使われています。

メールソース
From: =?ISO-2022-JP?B?GyRCQXc/LjxUTD4bKEI=?= <hello@example.com>
To: =?ISO-2022-JP?B?GyRCPHU/LjxUTD4bKEI=?= <you@example.com>
Subject: =?utf-8?Q?=3D=3FISO-2022-JP=3FB=3FGyRCRnxLXDhsJE4lNSVWJTglJyUv?=
 =?utf-8?Q?JUgkSyRKJGokXiQ5ISNEOSQvGyhC=3F=3D?=
 =?utf-8?Q?_=3D=3FISO-2022-JP=3FB=3FGyRCJEokayRISjg7ejI9JDEkOSRrJEgkJCQ?=
 =?utf-8?Q?mT0MkYiQiJGokXiQ5GyhC=3F=3D?=
 =?utf-8?Q?_=3D=3FISO-2022-JP=3FB=3FGy?=
 =?utf-8?Q?RCJE4kRyEiRDkkLz1xJCQkRiRfJF4kOSEjJDMkbCQwJGkkJEQ5GyhC=3F=3D?=
 =?utf-8?Q??= =?ISO-2022-JP?B?GyRCJCQkSCRJJCYkKyRKISkbKEI=?=
Message-ID: <d3e1f390c82ee075dbcf96b7565bccff@example.com>
MIME-Version: 1.0
Date: Wed, 04 Dec 2019 16:19:51 +0900
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64

aHR0cHM6Ly9zeW1mb255LmNvbS93aHktdXNlLWEtZnJhbWV3b3JrCgrjg5Xjg6zjg7zjg6Djg6/j
g7zjgq/jgpLkvb/nlKjjgZnjgovlv4XopoHjgYzjgYLjgovjga7jga/jgarjgZzjgafjgZnjgYvv
vJ8K44OV44Os44O844Og44Ov44O844Kv44Gv57W25a++44Gr5b+F6KaB44Go44GE44GG44KP44GR
44Gn44Gv44GC44KK44G+44Gb44KT44CC44Gd44KM44Gv44CB44GC44Gq44Gf44GM44KI44KK6Imv
44GP44CB44KI44KK6YCf44GP6ZaL55m644GZ44KL44Gu44KS5Yqp44GR44KL44Gf44KB44Gr5Yip
55So44Gn44GN44KL44OE44O844Or44Gu44CM44Gh44KH44GG44Gp44CNMeOBpOOBp+OBme+8gQoK
44OV44Os44O844Og44Ov44O844Kv44Gv44CB44OT44K444ON44K544Or44O844Or44Gr5a6M5YWo
44Gr5rqW5oug44GX44CB5qeL6YCg5YyW44GV44KM44CB44Oh44Oz44OG44OK44Oz44K544Go44Ki
44OD44OX44Kw44Os44O844OJ44Gu5Lih5pa544GM5Y+v6IO944Gq44Ki44OX44Oq44Kx44O844K3

iso-2022-jpについては、よくある話で、mb_language("uni")を宣言すれば良さそうです。

        mb_language("uni");
        mb_internal_encoding("UTF-8");

サブジェクトの方でquoted-printableになっていますので、
さきほどのAbstractHeader.php(120)のencodeWords()を更に見ていきましょう。

/vendor/symfony/mime/Header/AbstractHeader.php(120)
    /**
     * Encode needed word tokens within a string of input.
     */
    protected function encodeWords(HeaderInterface $header, string $input, int $usedLength = -1): string
    {
        $value = '';
        $tokens = $this->getEncodableWordTokens($input);
        foreach ($tokens as $token) {
            // See RFC 2822, Sect 2.2 (really 2.2 ??)
            if ($this->tokenNeedsEncoding($token)) {
                // Don't encode starting WSP
                $firstChar = substr($token, 0, 1);
                switch ($firstChar) {
                    case ' ':
                    case "\t":
                        $value .= $firstChar;
                        $token = substr($token, 1);
                }
                if (-1 == $usedLength) {
                    $usedLength = \strlen($header->getName().': ') + \strlen($value);
                }
                $value .= $this->getTokenAsEncodedWord($token, $usedLength);
            } else {
                $value .= $token;
            }
        }
        return $value;
    }

$this->getEncodableWordTokens($input)を見ていなかったので見てみます。

/vendor/symfony/mime/Header/AbstractHeader.php(158)
    protected function getEncodableWordTokens(string $string): array
    {
        $tokens = [];
        $encodedToken = '';
        // Split at all whitespace boundaries
        foreach (preg_split('~(?=[\t ])~', $string) as $token) {
            if ($this->tokenNeedsEncoding($token)) {
                $encodedToken .= $token;
            } else {
                if (\strlen($encodedToken) > 0) {
                    $tokens[] = $encodedToken;
                    $encodedToken = '';
                }
                $tokens[] = $token;
            }
        }
        if (\strlen($encodedToken)) {
            $tokens[] = $encodedToken;
        }
        return $tokens;
    }

preg_split('~(?=[\t ])~', $string)で、=?UTF-8?B?77yf?='の最後の?=<タブ or スペース>で分割しています。

それを$this->tokenNeedsEncodingでエンコード済みかチェックしているのですが。。。

サブジェクトをmb_encode_mimeheader()した結果は、以下になります。

=?UTF-8?B?5pel5pys6Kqe44Gu44K144OW44K444Kn44Kv44OI44Gr44Gq44KK44G+44GZ?=
 =?UTF-8?B?44CC6ZW344GP44Gq44KL44Go5paH5a2X5YyW44GR44GZ44KL44Go44GE44GG?=
 =?UTF-8?B?6Kmx44KC44GC44KK44G+44GZ44Gu44Gn44CB6ZW344GP5pu444GE44Gm44G/?=
 =?UTF-8?B?44G+44GZ44CC44GT44KM44GQ44KJ44GE6ZW344GE44Go44Gp44GG44GL44Gq?=
 =?UTF-8?B?77yf?='

さきほどの、preg_split('~(?=[\t ])~', $string)で分割をすると、分割したデータの最後に改行がついてしまうみたいで、その後の$this->tokenNeedsEncoding($token)でエンコード済みと判定されないようです。

それでは、mb_encode_mimeheader()の結果から改行コードを外してみます。

サンプルコードを以下のように修正しました。

MailerController.php(その4)
class MailerController extends AbstractController
{
    /**
     * @Route("/mailer", name="mailer")
     */
    public function index(MailerInterface $mailer)
    {
        $body = <<<EOL
https://symfony.com/why-use-a-framework

フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;

        mb_language("uni");
        mb_internal_encoding("UTF-8");

        $subject = mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?');
        $subject = str_replace("\r\n", '', $subject);

        $headers = (new Headers())
            ->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
            ->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
            ->addTextHeader('Subject', $subject)
        ;

        $textContent = new TextPart($body, 'utf-8', 'plain', 'base64');
        $email = new Message($headers, $textContent);

        $mailer->send($email);

        return $this->render('mailer/index.html.twig', [
            'controller_name' => 'MailerController',
        ]);
    }
}

https://github.com/idani/symfony_mailer_tutorial2/commit/71283d6216dcc3f4e1b710a0d2dd452ca63a87e9

実行してみると、subjcetの文字化けが治りましたね。

image.png

メールソースを確認しましたが、想定どおりです。

メールソース
From: =?UTF-8?B?6YCB5L+h6ICF5ZCN?= <hello@example.com>
To: =?UTF-8?B?5Y+X5L+h6ICF5ZCN?= <you@example.com>
Subject: =?UTF-8?B?5pel5pys6Kqe44Gu44K144OW44K444Kn44Kv44OI44Gr44Gq44KK44G+44GZ?=
 =?UTF-8?B?44CC6ZW344GP44Gq44KL44Go5paH5a2X5YyW44GR44GZ44KL44Go44GE44GG?=
 =?UTF-8?B?6Kmx44KC44GC44KK44G+44GZ44Gu44Gn44CB6ZW344GP5pu444GE44Gm44G/?=
 =?UTF-8?B?44G+44GZ44CC44GT44KM44GQ44KJ44GE6ZW344GE44Go44Gp44GG44GL44Gq?=
 =?UTF-8?B?77yf?=
Message-ID: <1916f37d03fc486c2babe15a606eda8f@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 11:19:48 +0900
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64

aHR0cHM6Ly9zeW1mb255LmNvbS93aHktdXNlLWEtZnJhbWV3b3JrCgrjg5Xjg6zjg7zjg6Djg6/j
g7zjgq/jgpLkvb/nlKjjgZnjgovlv4XopoHjgYzjgYLjgovjga7jga/jgarjgZzjgafjgZnjgYvv
vJ8K44OV44Os44O844Og44Ov44O844Kv44Gv57W25a++44Gr5b+F6KaB44Go44GE44GG44KP44GR
44Gn44Gv44GC44KK44G+44Gb44KT44CC44Gd44KM44Gv44CB44GC44Gq44Gf44GM44KI44KK6Imv
以下省略

base64のまとめ

ということで、サブジェクトのワークアラウンドが微妙ですが、UTF-8+base64での送信が実現できました。

パッチを送る感じになるのかな?

iso-2022-jpで送信する

それでは、文字コードがiso-2022-jpで、エンコーディングが7bitにチャレンジしてみます。

ヘッダーは、mb_encode_mimeheader()で準備ができますし、本文が7bitが通れば行けそうです。

サンプルコードを以下のように修正します。

MailerController.php(その5)
class MailerController extends AbstractController
{
    /**
     * @Route("/mailer", name="mailer")
     */
    public function index(MailerInterface $mailer)
    {
        $body = <<<EOL
https://symfony.com/why-use-a-framework

フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;

        $subject = mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?');
        $subject = str_replace("\r\n", '', $subject);

        $headers = (new Headers())
            ->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
            ->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
            ->addTextHeader('Subject', $subject)
        ;

        $textContent = new TextPart($body, 'iso-2022-jp', 'plain', '7bit');
        $email = new Message($headers, $textContent);

        $mailer->send($email);

        return $this->render('mailer/index.html.twig', [
            'controller_name' => 'MailerController',
        ]);
    }
}

https://github.com/idani/symfony_mailer_tutorial2/commit/5bc62be13f645e0f0554dabebb8ce7bd095fc6ad

以下のようなエラーがでました。
7bitのエンコーディングに対応していないようです。

image.png

しょうがないので、エンコーディングをbase64に変更してトライしてみます。

MailerController.php(その6)
class MailerController extends AbstractController
{
    /**
     * @Route("/mailer", name="mailer")
     */
    public function index(MailerInterface $mailer)
    {
        $body = <<<EOL
https://symfony.com/why-use-a-framework

フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;

        $subject = mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?');
        $subject = str_replace("\r\n", '', $subject);

        $headers = (new Headers())
            ->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
            ->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
            ->addTextHeader('Subject', $subject)
        ;

        $textContent = new TextPart($body, 'iso-2022-jp', 'plain', 'base64');
        $email = new Message($headers, $textContent);

        $mailer->send($email);

        return $this->render('mailer/index.html.twig', [
            'controller_name' => 'MailerController',
        ]);
    }
}

https://github.com/idani/symfony_mailer_tutorial2/commit/71f73ca030d516c4e9ff95521798f7b65344dbd1

本文が痛い感じになってしまいました。

image.png

こちらがメールソースになります。From/To/Subjectのヘッダーは、想定通りですし、Content-Typeも想定どおりです。

メールソース
From: =?ISO-2022-JP?B?GyRCQXc/LjxUTD4bKEI=?= <hello@example.com>
To: =?ISO-2022-JP?B?GyRCPHU/LjxUTD4bKEI=?= <you@example.com>
Subject: =?ISO-2022-JP?B?GyRCRnxLXDhsJE4lNSVWJTglJyUvJUgkSyRKJGokXiQ5ISNEOSQvGyhC?=
 =?ISO-2022-JP?B?GyRCJEokayRISjg7ejI9JDEkOSRrJEgkJCQmT0MkYiQiJGokXiQ5GyhC?=
 =?ISO-2022-JP?B?GyRCJE4kRyEiRDkkLz1xJCQkRiRfJF4kOSEjJDMkbCQwJGkkJEQ5GyhC?=
 =?ISO-2022-JP?B?GyRCJCQkSCRJJCYkKyRKISkbKEI=?=
Message-ID: <e2e57a249f7daeaff165d25eddc2a876@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 12:20:15 +0900
Content-Type: text/plain; charset=iso-2022-jp
Content-Transfer-Encoding: base64

aHR0cHM6Ly9zeW1mb255LmNvbS93aHktdXNlLWEtZnJhbWV3b3JrCgrjg5Xjg6zjg7zjg6Djg6/j
g7zjgq/jgpLkvb/nlKjjgZnjgovlv4XopoHjgYzjgYLjgovjga7jga/jgarjgZzjgafjgZnjgYvv
vJ8K44OV44Os44O844Og44Ov44O844Kv44Gv57W25a++44Gr5b+F6KaB44Go44GE44GG44KP44GR
44Gn44Gv44GC44KK44G+44Gb44KT44CC44Gd44KM44Gv44CB44GC44Gq44Gf44GM44KI44KK6Imv
44GP44CB44KI44KK6YCf44GP6ZaL55m644GZ44KL44Gu44KS5Yqp44GR44KL44Gf44KB44Gr5Yip
55So44Gn44GN44KL44OE44O844Or44Gu44CM44Gh44KH44GG44Gp44CNMeOBpOOBp+OBme+8gQoK
以下省略

またまた、ソースコードを見てみると、TextPartクラスで渡しているiso-2022-jpというのは、Content-Typeの出力に使うだけで、文字コードの変換はしていませんでした。

一応、Base64Encoder.php(26)$charsetとして文字コードまでもらっているのですが、使っていません;;

/vendor/symfony/mime/Encoder/Base64Encoder.php(26)
class Base64Encoder implements EncoderInterface
{
    public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
    {
        if (0 >= $maxLineLength || 76 < $maxLineLength) {
            $maxLineLength = 76;
        }

        $encodedString = base64_encode($string);
        $firstLine = '';
        if (0 !== $firstLineOffset) {
            $firstLine = substr($encodedString, 0, $maxLineLength - $firstLineOffset)."\r\n";
            $encodedString = substr($encodedString, $maxLineLength - $firstLineOffset);
        }

        return $firstLine.trim(chunk_split($encodedString, $maxLineLength, "\r\n"));
    }
}

そしたら本文も自前で変換する必要がわかったので、以下のようにします。

MailerController.php(その7)
class MailerController extends AbstractController
{
    /**
     * @Route("/mailer", name="mailer")
     */
    public function index(MailerInterface $mailer)
    {
        $body = <<<EOL
https://symfony.com/why-use-a-framework

フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;

        $subject = mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?');
        $subject = str_replace("\r\n", '', $subject);

        $headers = (new Headers())
            ->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
            ->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
            ->addTextHeader('Subject', $subject)
        ;

        $body = mb_convert_encoding($body, 'ISO-2022-JP-MS', 'UTF-8');
        $textContent = new TextPart($body, 'iso-2022-jp', 'plain', 'base64');
        $email = new Message($headers, $textContent);

        $mailer->send($email);

        return $this->render('mailer/index.html.twig', [
            'controller_name' => 'MailerController',
        ]);
    }
}

https://github.com/idani/symfony_mailer_tutorial2/commit/dd161129fdcf0ba36bfb93e6461b67f03eac6b59

本文も問題なさそうです。

image.png

メールソースはこんな感じ。

メールソース
From: =?ISO-2022-JP?B?GyRCQXc/LjxUTD4bKEI=?= <hello@example.com>
To: =?ISO-2022-JP?B?GyRCPHU/LjxUTD4bKEI=?= <you@example.com>
Subject: =?ISO-2022-JP?B?GyRCRnxLXDhsJE4lNSVWJTglJyUvJUgkSyRKJGokXiQ5ISNEOSQvGyhC?=
 =?ISO-2022-JP?B?GyRCJEokayRISjg7ejI9JDEkOSRrJEgkJCQmT0MkYiQiJGokXiQ5GyhC?=
 =?ISO-2022-JP?B?GyRCJE4kRyEiRDkkLz1xJCQkRiRfJF4kOSEjJDMkbCQwJGkkJEQ5GyhC?=
 =?ISO-2022-JP?B?GyRCJCQkSCRJJCYkKyRKISkbKEI=?=
Message-ID: <e9e0eefd92fd68df3ec3a8bf6133bf9b@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 12:36:00 +0900
Content-Type: text/plain; charset=iso-2022-jp
Content-Transfer-Encoding: base64

aHR0cHM6Ly9zeW1mb255LmNvbS93aHktdXNlLWEtZnJhbWV3b3JrCgobJEIlVSVsITwlYCVvITwl
LyRyO0hNUSQ5JGtJLE1XJCwkIiRrJE4kTyRKJDwkRyQ5JCshKRsoQgobJEIlVSVsITwlYCVvITwl
LyRPQGRCUCRLSSxNVyRIJCQkJiRvJDEkRyRPJCIkaiReJDskcyEjJD0kbCRPISIkIiRKJD8kLCRo
JGpOSSQvISIkaCRqQi4kLzMrSC8kOSRrJE4kcj11JDEkayQ/JGEkS014TVEkRyQtJGslRCE8JWsk
TiFWJEEkZyQmJEkhVxsoQjEbJEIkRCRHJDkhKhsoQgoKGyRCJVUlbCE8JWAlbyE8JS8kTyEiJVMl
OCVNJTklayE8JWskSzQwQTQkSz1gNXIkNyEiOT1CJDI9JDUkbCEiJWElcyVGJUolcyU5JEglIiVD
JVclMCVsITwlSSROTj5KfSQsMkRHPSRKJSIlVyVqJTEhPCU3JWclcyRyMytILyQ3JEYkJCRrJEgk
JCQmM048QkAtJHJEczYhJDkkayQ/JGEkRyQ5ISMbKEIKChskQjMrSC88VCQsSEZNUSViJTglZSE8
JWskcjpGTXhNUSQ3JEZCPiROTk4waCRLPThDZiQ5JGskMyRIJEc7fjRWJHJAYUxzJEckLSRrJD8k
以下省略

iso-2022-jpのまとめ

メール本文に7bitエンコーディングが使えなかった。
base64でエンコーディングするなら、迷惑メール判定としては、大きく変更と感じる可能性があります。

つまり迷惑メールに初回は入る可能性があります。

それならISO-2022-jpを捨てて、utf-8にしても結局迷惑メールになりますので大差ありません。

UTF-8だと、顔文字など使える文字の種類が増えるメリットがありますから、UTF-8に乗り換えがよさそうです。

個人的なまとめ

symfony/mailerを使う場合は、システム刷新として使うなら、utf-8の文字コード、base64エンコーディングを使うのが良さそうです。

新規システムなら、エンコーディングがquoted-printableになりますが、簡単に使えるのも魅力的だと思います。

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

LravelのLogLevelを環境変数で変更したい (6.0LTS)

タイトルの通り Laravel6.0LTS にて、ログレベルを環境変数で変更するための手順です。
チャンネルやその他の項目も含めてログに関するいろいろな設定については他に詳しい記事がありますのでそちらをご参照ください。
Laravel5.6 での ログ設定について

ここでは、そこまで複雑なことがしたいわけではなく、シンプルに、開発環境ではDEBUGまで、ステージ、本番環境ではINFO以上でログを出力するのを、環境変数で調整したい、というやり方です。

.env に変数を追加

適当な値でいいですが、 project_root/.env ファイルにLEVELを指定するための変数を追加します。
ここでは LOG_LEVEL としました。

LOG_LEVEL=info

config/logging.php を修正

ファイルの中身を見ると以下のようになっているかと思います。
(コメント部分と後半は省略)

    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['daily'],
            'ignore_exceptions' => false,
        ],

        'single' => [
            'driver' => 'single',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
        ],

        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'days' => 14,
        ],
...

上の例では、default=>stackが指定されており、stackの中でchannels=>dailyが指定されています。
そしてdailyのlevel=>debugになっているため、DEBUG以上のログが出力されることになります。

そこで、この daily=>levelの部分を以下のように書き換えます。

            'level' => env('LOG_LEVEL','debug'),

あとは環境に合わせて、先程 .env に追加した LOG_LEVEL の値を変更してやれば、環境ごとに合わせたログレベルでログが出力されます。

ログレベルの一覧は PSR-3でこのように定義されていて 記載されている順番の並びで、指定したレベルより上に書かれている内容が出力されることになります。

Laravelのバージョンいくつから使えるのかまでは調査していません(4.2と5.1のあと6.0に飛んだだめ…)が、config/logging.php の中身が同じような状態であれば使える方法かと思います。


なにか他にもっと簡単な切り替え方があるような気はするんですが、 .env にはじめから設定されている値はLOG_CHANNEL だけで、別にチャンネル切り替えるつもりは無いんだけど、レベルだけ切り替えるやり方というのが見当たらなかったのでやってみました。
「本来はこうするべき」というやり方がありましたら教えていただけると嬉しいです。

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

【Laravel】routes.phpでDBから値を取得する

routes.phpからレコードの値を取ってきてそのままviewへ表示させたい、という部分があったので、メモ程度に書きます。

環境

PHP : 5.6.30
MySQL : 10.1.21
Laravel : 5.1.46

テーブル

以下サンプルテーブルです。
wordsテーブル

id message
1 Hello
2 こんにちは

コード

route.php
Route::get('/', function () {
  // cookieからidを設定する
  if ($_COOKIE['lang'] == 'en') {
    $id = 1;
  } else {
    $id = 2;
  }

  // DB::table()を使用して値を取得する
  $query = DB::table('words')
    ->select('message')
    ->where('id', $id)
    ->first();

  $message = $query->message

   return view('main',['message' => $message]);
}
main.blade.php
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        {{$message}}
    </body>
</html>

bladeの方はおまけ的な感じですが、一応念のため。

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

社内基幹システムのバージョンアップに挑んだ話

こちらは Fusic Advent Calendar 2019 - Qiita 5日目の記事です。
昨日は @NaoyaMiyagawa によるS3オブジェクトレベルのログ記録+監視方法 でした。
ログと監視は大事ですよね~。

ということで、こんにちは。早﨑です。
毎度のことですが時間の流れが速く、気づけばもう年の瀬ですね。
1年を振り返ると今年も業務システムをガッツリやってました。

早速ですが今年は弊社の基幹システムのバージョンアップをした話を書こうと思います。

弊社の基幹システムとは?

ざっくり機能を上げると

  • お取引をさせていただいてる企業・案件・見積・請求・入金データの管理機能
  • 社員一人一人の工数管理機能
  • 各種集計出力機能
  • 社内回覧機能
  • 社員管理機能
  • 各種マスタ

等々を持ったシステムです。

top.JPG
これは工数入力画面

これらCakePHP3の出た当初に作成しました。(4年くらい前)
PHPのバージョンも古く5.6
JSもjqueryで。。。

上げ始めたらキリがないです:sob:

CakePHP3が出たはじめの頃でEntityとは何ぞや?右も左も良く分からないまま実装したためカオスなコードを書いています。
テストコードもホボ無いという状況です。

一念発起してやった

その前に何から手を付けようかと考えました。
思いつきベースでやりたいことを貯めていたのですが、全部は無理。
社内から遅いと声が上がっていたので、まずは最低限のPHPとCakePHPを上げようということで社内各方面で相談しながら何をやるかを決めました。

最終的には、PHPとCakePHPのバージョンを上げる。となりました。

  • PHP5.6 → 7.3系へ
  • CakePHP3.1 → 3.8系へ

PHPはサポートも切れてますし、速度面でもPHP7には劣ります。
CakePHP4を見据えて最新化することを目指しましょうという感じですね。

ということで次にどういう手順でやるかを考えました。

順番

  • 1.開発環境をPHP7.3系で構築(本番環境も併せて検討)
  • 2.システムを設置しとりあえず動くところまで持っていく
  • 3.非推奨関数の置換
  • 4.リファクタ
  • 5.テスト
  • 6.本番リリース

やってきます

1.ローカルにPHP7.3系の環境を構築(本番環境も併せて検討)

対象のシステムはdocker環境で運用されています。
しかし私のローカルはvagrant、本番では基幹システム用のimageを作ることが必須となるのでこれを期に開発環境もdockerにしました。
(docker難しい。。。)

2.システムを設置しとりあえず動くところまで持っていく

git cloneしcomposer install!が動きません。
composerで入るプラグインが入りませんw
PHPのバージョンが新しくプラグインのバージョンを上げてくださいと言われました。
上げてプラグインをインストール。

で、ブラウザを叩きます。
画面はホワイトアウトです。

感の良い方は分かると思いますがCakePHP3.3からMiddlewareが導入されています。
ディレクトリやファイルの中身が若干変わっています。
Cake3.8系をcloneしWinMergeを使いながら3.1と比較しながら書き直していきました。

修正してはdumpを吐いてどこまで来ているかのチェックの繰り返しです。
かなり骨が折れました。

3.非推奨関数の置換

CakePHP3.8に上げたことでScrutinizerがあらぶり始めました。

02.JPG

↑のIssueは殆どがDeprecatedでした:joy:
動作としては非推奨を無視するように設定していたのでブラウザからのアクセスには問題は無いのですが
はやりここはきっちりと対応しておくべきでしょう。4も控えていますし。

ということで以下を変更しエラーを全て出すようにしました。

app.php
    'Error' => [
        // 'errorLevel' => E_ALL & ~E_USER_DEPRECATED, // ← ここを
        'errorLevel' => E_ALL, // ← こうする
        'exceptionRenderer' => ExceptionRenderer::class,
        'skipLog' => [],
        'log' => true,
        'trace' => true,
    ],

これでエラーが見れるようになります。
03.png
殆どgetter/setter(笑)

画面をポチポチしながら出てきたもの片っ端から対応します。

4.リファクタ

3をしながら思うわけです。

俺、なんでこんな残念なコード書いてたんだろう。。。

複雑な部分は一旦さておき、パッと見で修正できるレベルのものを書き換えていきます。
折角なのでtypehintも入れていきます。完全に作業の世界です。
併せてコードはcommit時にphp-cs-fixer/phpcbf/phpmdが走るようにしたので静的解析チェックをしていきます。
Scrutinizerがあったのも良かったですね。併せてチェックができて良かったです。
template(ctp)以外の全てのコードに対して修正を行えたので凄く綺麗になりました。

05.JPG
数を消すのがとても楽しくなりますね♪

5.テスト

最後にテストコードを書きたい!!
と思ってましたがこの時点では出来ませんでした。
そこまでモチベーションが続かなかったです。

画面ポチポチで確認していきます。

そんなに複雑ではないから大丈夫だろうと思ってたけど、やっぱり漏れはありますね。
本番リリース後にやはり動かないんだけど。。。という事がありました。

テストコードで100%安心できるわけでは無いですが、これにより気づける事もあります。
少しでも安心できる状況を作るのが大事ですね。

6.本番リリース

インフラ担当者と日時、手順を相談してえいっとやりました。
特に問題は無かったです。

事前に手順を相談し認識を合わせたのが良かったと思います。

まとめ

  • 協力者を得つつ、やりたいこと・やるべきこと・やらないことを整理することが大事。
    • コードのリファクタはちょっと踏み込み過ぎてどこまでやるか悩む。
  • テストは大事
    • バグは出ます。弊社基幹システムでこのシステムがバグってしまうと仕事が出来なくなります(特にバックオフィス)
    • 軽微なバグで直ぐ対応出来たもののやはりお客様のシステムだとNGですよね。
  • バージョンアップは大変
    • 動かないところが多すぎて心が折れそうになる。
    • 業務の合間や盆休みを使用して行っているため数か月掛かった。

最後に

社内からも速くなったねーとお言葉をいただけましたし、勉強にもなりました。
やってよかったです。

さあ次はテストコードを書かねば、俺達の戦いは始まったばかりだ:muscle:

それでは、明日のアドベントカレンダーは @ya_ma23 です!
お楽しみに!

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

Laravel コントローラーからビューを表示

ビューの作成

resources/viewsにindex.blade.phpファイルを作成する。
書き方に関しては下記参照
https://blog.hiroyuki90.com/articles/laravel-blade/

//変数を展開する場合
<h1>{{ $hello }}</h1>

コントローラーの作成

app/Http/Controllersにコントローラーを作成する。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\View;

class HomeController extends Controller
{
    public function index()
    {
        $hello = 'Hello World';
        return view('index', ['hello' => $hello]);
        //1つ目の引数にディレクトリとファイル名指定
        //2つ目の引数でビューに$hello変数を渡す
    }
}

viewヘルパを使用する際はuse Illuminate\Support\Facades\View;を記載しないとエラーが出てしまう。
viewヘルパの中でresources/viewsにサブディレクトリを作成する際はドット記法を使い記述する。

return view('admin.index', ['hello' => $hello]);

1つ目の引数のindexの部分がblade.phpのファイル名
2つ目の引数のhelloの部分がblade.phpファイルの中の変数を指定
$helloがビューに渡す変数

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