はじめに
こんにちは!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実行時にのみ、発生していました。
ブラウザを用いた動作確認では問題なく動いていました。
上記のサンプルコードでもテストを実行してみましょう。

この場合、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の設定ファイルの情報に行き着いたり、PHPのErrorに関する情報に行き着いたり、本記事では収まり切らないくらい学びが多い課題でした。
もっと色々と書きたかったですが、テーマの方向性がとっ散らかっちゃうのでここまでとします。
最後までお読みくださり、ありがとうございました!