実際のブラウザを使ってリクエスト処理する、"Panther"

[Symfony Component Advent Calendar 2022](https://qiita.com/advent-calendar/2022/symfony-component)の19日目の記事です。

## 最初に
[Symfony](https://symfony.com)はPHPのフレームワークのひとつです。しかし、公式サイトの説明文には

> Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.
  （SymfonyはPHPコンポーネントのセットで、Webアプリケーションフレームワークで、哲学、そしてコミュニティです。それらがハーモニーを奏でながら動作しています。）

と書かれている通り、PHPコンポーネントのセットで、たくさんのコンポーネントを提供しており、それらを組み合わせてひとつのフレームワークとして動作しています。Symfonyのコンポーネントは、Symfony上だけで動作するのではなく、他のPHPフレームワークやアプリケーションでも動作している強力なものが揃っています。

今回はそれらの中から、役立ちそうなもの・お薦めしたいものを紹介していきたいと思います。

※記事内ではautoloadのインポートは省略します。


## 実際のブラウザを使ってリクエスト処理する、"Panther"

[Panther](https://github.com/symfony/panther)は、E2Eテストやスクレイピングを実際のブラウザを使って行うコンポーネントです。


### インストール

```shell
# E2Eテストで使う場合
composer require symfony/panther --dev

# スクレイピングで使う場合
composer require symfony/panther --dev
```

### Webドライバーインストール

`Panther`はWebドライバーを利用します。まずはローカルマシンにWebドライバーをインストールします。

```shell
# Mac
brew install chromedriver geckodriver

# Windows
choco install chromedriver selenium-gecko-driver

# Ubuntu
apt install chromium-chromedriver firefox-geckodriver
```

### Webドライバーの検出

次は、プロジェクトで利用するWebドライバーを検出します。`drekelmans/bdi`を使って検出します。

```shell
composer require dbrekelmans/bdi --dev
vendor/bin/bdi detect drivers
```

このコマンドを実行することで、プロジェクト内に`drivers`ディレクトリが作成され、そこにWebドライバーが保存されます。

### スクレイピング

スクレイピングする際は[HttpClient](https://qiita.com/ippey_s/private/9878526c88d54a1cac41)と似たような感覚で行えます。各要素へのアクセスは**CSSセレクタ**, **XPath**などが利用できますが、CSSセレクタが扱いやすいのではないかなと思います。

```php

use Symfony\Component\Panther\Client;

// Chromeの場合
$client = Client::createChromeClient();
// Firefoxの場合
$client = Client::createFirefoxCLient();

// Qiitaトップにアクセス
$client->request('GET', 'https://qiita.com/');
// アドベントカレンダーのリンククリック
$client->clickLink('Advent Calendar');
// 指定されたセレクタが出力されるまで待つ
$crawler = $client->waitFor('main > section.css-1avseb1 > div > div.css-er6if3 > ul > li:nth-child(4) > a > p > span:nth-child(2)');
// 選択したセレクターでテキストを取得して出力
$io->text($crawler->filter('main > section.css-1avseb1 > div > div.css-xi606m > h2')->text());
// 現在のページのスクリーンショットを撮る
$client->takeScreenshot(__DIR__ . '/screenshot.png');
```

### E2Eテスト

E2Eテストで利用する場合、`phpunit.xml`を追記します。

```xml:phpunit.xml.dist
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         backupGlobals="false"
         colors="true"
         bootstrap="tests/bootstrap.php"
         convertDeprecationsToExceptions="false"
>
...
    <!-- ここ追加 -->
    <extensions>
        <extension class="Symfony\Component\Panther\ServerExtension" />
    </extensions>
</phpunit>
```

そして、専用のテストケースクラス`PantherTestCase`を利用します。


```php:QiitaTest.php

use Symfony\Component\Panther\PantherTestCase;

class QiitaTest extends PantherTestCase
{
    public function test(): void
    {
        $client = static::createPantherClient();
        $client->request('GET', 'https://qiita.com/');

        $this->assertPageTitleContains('Qiita');
        $this->assertSelectorTextContains('#New2GlobalHeader-react-component-2d53bd05-4c4a-411f-bdbf-40fba84ebbce > div > div > a:nth-child(9)', 'Advent Calendar');
 
        // リンククリックして移動       
        $client->clickLink('Advent Calendar');
        $client->waitFor('main > section.css-1avseb1 > div > div.css-er6if3 > ul > li:nth-child(4) > a > 
        $this->assertSelectorTextContains(''main > section.css-1avseb1 > div > div.css-xi606m > h2', 'カテゴリー');
    }
}
```

`Client`の使い方はスクレイピング時と同様です。`PantherTestCase`にブラウザでの挙動やテキスト内容確認のアサーションが追加されています。

## まとめ

今回は`Panther`の紹介でした。E2Eテストには[Codeception](https://codeception.com/)などもありますが、`Panther`であれば、`PantherTestCase`が利用でき、Symfonyとの相性もよいので、Symfony内で使うのであれば`Panther`の方がよいかなと思います。