はじめに
こんにちは!10月に入社しました、Build Webチームの岡本です!
4年ぶりくらいにPHP を用いて開発を行っています。これまでは、フロントエンドをメインにエンジニアをやってきました。
最近、個人的に学びが多いエラーに遭遇できました👏
とある機能の実装をしていたのですが、リリース準備の作業が迫ってきているというところで、ユニットテスト でエラーが発生してしまいました。
そこで本記事では、そのエラーについて調査して整理したことを、サンプルコードを用いながらアウトプットしようと思います!
前提条件
使用技術
まずは、本記事で使用している技術を紹介します。
【言語】
- PHP v8系
【フレームワーク】
- Laravel v10系
- PHPUnit v9系
【そのほか】
- Docker
ユニットテスト のフレームワーク は、PHPUnit を使用しています。
やりたいこと
今回エラーが発生した処理でやりたかったことは、「controllerから受け取った値を画面に表示する際に、その値に対応する文字列と組み合わせて表示すること 」でした。
サンプルコード
続いて、本記事で使用するコードです。
実際にエラーに遭遇した時のコードと近い状態を再現するためのサンプルコードを用意しました。
実際のコードでは、DBからデータを取得してくるなどの処理もありますが、今回のサンプルコードではそのあたりは省いて簡単なものにしています。
ただし、処理については、前出の「やりたいこと」で示した内容とほぼ同じにしています。
controller
<?php
declare ( strict_types= 1 ) ;
namespace App\Http\Controllers;
use App\Services\SampleMenuService;
use Illuminate\Http\Request;
class SampleController extends Controller
{
private SampleMenuService $ sampleMenuService ;
public function __construct ( SampleMenuService $ sampleMenuService )
{
$ this -> sampleMenuService = $ sampleMenuService ;
}
public function getSample( Request $ request )
{
$ menuIds = $ this -> sampleMenuService-> getAllMenus() ;
return view( 'sample.index' ) -> with([
'title' => 'サンプルページ' ,
'menus' => $ menuIds ,
]) ;
}
}
blade
@extends('layouts.sample')
@section('title', $title)
@php
use App\Services\SampleMenuService;
// idに対応する文字列を組み合わせる
function convertMenuString($id) {
return '【'.$id.'】'.match($id) {
SampleMenuService::OMELET_RICE => 'オムライス',
SampleMenuService::HAMBURGER_STEAK => 'ハンバーグ',
SampleMenuService::YAKITORI => '焼き鳥',
};
}
@endphp
@section('content')
< h1 > {{ $title }}</ h1 >
< div class = "items-list" >
< h2 > メニュー</ h2 >
@foreach($menus as $id)
< div class = "item-card" >
< span class = "item-name" > {{ convertMenuString($id) }}</ span >
</ div >
@endforeach
</ div >
@endsection
テストコード
<?php
declare ( strict_types= 1 ) ;
namespace Tests\Feature\Http\Controllers\SampleController;
use App\Services\SampleMenuService;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
class GetSampleTest extends TestCase
{
use WithoutMiddleware;
protected function setUp() : void
{
parent :: setUp() ;
}
@test
public function サンプルページに渡されたタイトルが「サンプルページ」であること()
{
$ response = $ this -> get( route( 'get_sample' )) ;
$ response -> assertViewHas( 'title' , 'サンプルページ' ) ;
}
@test
public function サンプルページに渡されたメニューが正しいこと() {
$ menus = [
SampleMenuService:: OMELET_RICE,
SampleMenuService:: HAMBURGER_STEAK,
SampleMenuService:: YAKITORI,
] ;
$ response = $ this -> get( route( 'get_sample' )) ;
$ response -> assertViewHas( 'menus' , $ menus ) ;
}
}
ブラウザでは以下のような表示になっています。
遭遇したエラー
エラー内容
遭遇したエラーはこちら。
Cannot redeclare 関数名()(previously declared in ...)
すでにある関数の再定義・重複を禁止するエラーです。
しかも、PHPUnit 実行時にのみ 、発生していました。
ブラウザを用いた動作確認では問題なく動いていました。
上記のサンプルコードでもテストを実行してみましょう。
PHPUnit 実行結果(エラー発生)
今回遭遇したエラーと同様のエラーが出ているのがわかります。
この場合、convertMenuString()という関数が再定義されていることがエラーメッセージから読み取れます。
※ちなみに、テストケースが1件しかない場合は、ユニットテスト は通りました。
再定義した記憶ないよ…??
しかし、同じ名前で実装されている関数は見当たりません。
エディタの検索を使っても、ない。
そもそもあれば、ブラウザを用いた動作確認でもエラーになっているかと思います。
原因
ここからは、このエラーの原因について調査したことを整理していこうと思います!
複数のテストケースを用意しており、controllerに定義した同じ関数を毎回呼び出します。
すると、毎回同じbladeを呼び出すことになり、bladeに定義した関数がそのたびに実行されることになります。
サンプルコードのPHPUnit テストコードで見てみましょう。
/**
* @test
*/
public function サンプルページに渡されたタイトルが「サンプルページ」であること()
{
$response = $this-> get(route('get_sample'));
$response-> assertViewHas('title', 'サンプルページ');
}
/**
* @test
*/
public function サンプルページに渡されたメニューが正しいこと() {
$menus = [
SampleMenuService::OMELET_RICE,
SampleMenuService::HAMBURGER_STEAK,
SampleMenuService::YAKITORI,
];
$response = $this-> get(route('get_sample'));
$response-> assertViewHas('menus', $menus);
}
2つのテストケースがあります。
それぞれでgetを使ってControllerに実装した関数getSampleを呼び出して実行しています。
Controllerで実装した関数getSampleでは、bladeにデータを渡しています。
return view('sample.index')-> with([
'title' => 'サンプルページ',
'menus' => $menuIds,
]);
この関数が実行されることにより、bladeで定義された関数も実行されます。
さらに、PHPUnit の設定ファイルであるphpunit.xmlを見てみると、以下のような記述が…!
processIsolation="false”
上記の設定により、テスト実行時にキャッシュを利用する設定になっています。
php .iniの方も確認してみると、OPcacheがenableになっていました。
OPcacheは、コンパイル したコードをキャッシュとして保存しておき、リクエストのたびにパースする処理を省く拡張モジュールです。
つまり1件目のテストケースで関数を実行したことでキャッシュがあるものの、2件目のテストケースで同じbladeが呼び出されます。すると、bladeに定義した関数が再び実行され、「関数が再定義された 」という事象が発生してしまったということです。
解決方法
いよいよ、このエラーを解決した方法です…!!
serviceを用意するなど方法は色々あると思いますが、今回は「無名関数 」に変更して解決しました。
bladeに実装していた関数の目的は以下の2つです。
見た目を整えること
bladeの中で同じ処理をしたいところが数箇所あった
色んなところから呼ばれるというよりそのblade内だけで使いたい処理であり、わかりやすい表示にするため・使い勝手を良くするために必要な処理だったため、無名関数に変更するという修正方針をとりました。
早速サンプルコードの方も、無名関数で書き替えてみます。
@php
use App\Services\SampleMenuService;
// function convertMenuString($id) {
// return '【'.$id.'】'.match($id) {
// SampleMenuService::OMELET_RICE => 'オムライス',
// SampleMenuService::HAMBURGER_STEAK => 'ハンバーグ',
// SampleMenuService::YAKITORI => '焼き鳥',
// };
// }
// 無名関数に変更
$convertMenuString = function ($id) {
return '【'.$id.'】'.match($id) {
SampleMenuService::OMELET_RICE => 'オムライス',
SampleMenuService::HAMBURGER_STEAK => 'ハンバーグ',
SampleMenuService::YAKITORI => '焼き鳥',
};
};
@endphp
呼び出しているところも修正します。
変数に代入された無名関数を呼び出すので、$マークがついた$convertMenuStringを呼び出します。
@section('content')
< h1 > {{ $title }}</ h1 >
< div class = "items-list" >
< h2 > メニュー</ h2 >
@foreach($menus as $id)
< div class = "item-card" >
< span class = "item-name" > {{ $convertMenuString($id) }}</ span >
</ div >
@endforeach
</ div >
@endsection
それでは、PHPUnit を実行してみます…!
PHPUnit 実行結果(成功)
これで無事、PHPUnit が通りました🎉
ブラウザでの挙動にも影響なしです!
おわりに
以上、PHPUnit で遭遇したエラーと調査したことのアウトプットでした!
エラーが出た当初は、リリース準備のため急ぎで対応する必要があり、「一旦無名関数で対処する」という感覚でした。
しかし、後で原因を調査して整理していると、PHPUnit の設定ファイルの情報に行き着いたり、PHP のErrorに関する情報に行き着いたり、本記事では収まり切らないくらい学びが多い課題でした。
もっと色々と書きたかったですが、テーマの方向性がとっ散らかっちゃうのでここまでとします。
最後までお読みくださり、ありがとうございました!
参考資料
PHP公式ドキュメント 無名関数
PHP公式ドキュメント OPcache
株式会社フォトラクションでは一緒に働く仲間を募集しています
www.wantedly.com