💧

DrupalでHeadless Chromeを実行する

2025/01/18に公開

目標

  • Headless Chromeが使えるDocke環境を構築する
  • Drupa10でHeadless Chromを利用するカスタムモジュールを作成する

対象者

  • DrupalでHeadless Chromを利用するカスタムモジュールを作りたい方
  • PHPでHeadless Chromeが使えるDocker環境を構築したい人

動作環境・前提

バージョン
Mac 14.7.1
チップ Apple M2
PHP 8.3.6
Drupal 10.x
chrome-php/chrome 1.12

早速構築していく

Docker

フォントとchromiumをインストールするため、Dockerfileに以下を追加してください。

RUN apt-get update && apt-get install -y --no-install-recommends \
  fonts-liberation chromium-driver
fonts-liberationって何?

https://packages.debian.org/ja/sid/fonts-liberation

The Liberation font family is a set of serif, sans-serif and monospaced fonts with exactly the same metrics as the non-free Microsoft fonts Times New Roman, Arial and Courier New.
(Liberationフォント・ファミリーは、セリフ体、サンセリフ体、等幅体のフォントで構成され、フリーではないマイクロソフトのフォントTimes New Roman、Arial、Courier Newとまったく同じメトリクスを持つ。(by DeepL翻訳))

これでコンテナをBuildし直し、起動したコンテナの中で以下のコマンドを実行すると、chromiumがインストールできていることを確認できます。

chromium -version

Composer

コンテナの中でchrome-phpをインストールします。

composer require chrome-php/chrome

これでエラーになる方は、以下のようにオプションを追加してみてください。

composer require --ignore-platform-req=ext-sockets --with-all-dependencies chrome-php/chrome:*

カスタムモジュールの実装

Twig(PDFのテンプレートになる)

とりあえず、テスト用の簡単なやつを作ります。
titleとbodyはサーバーサイドから渡す形にしてます。

<html>
<head>
    <style>
        body { font-family: Arial, sans-serif; }
        h1 { color: #333; }
    </style>
</head>
<body>
<h1>{{ title }}</h1>
<p>{{ body }}</p>
</body>
</html>

Controller

テスト用ということで、PDFを発行するコードをControllerにベタ書きします。
本当はServiceクラスとかを作ってControllerから切り離した方が良いと思います。

<?php

declare(strict_types=1);

namespace Drupal\sample\Controller;

use Drupal;
use Drupal\Core\Controller\ControllerBase;
use HeadlessChromium\BrowserFactory;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Throwable;

class GeneratePdfController extends ControllerBase
{
    public function index(): Response
    {
        // twigテンプレートをレンダリング
        $renderElement = [
            '#theme' => 'generate_pdf',
            '#title' => 'Generate PDF Test',
            '#body' => 'sample body.sample body.sample body.sample body.',
        ];
        $markUp = Drupal::service('renderer')->renderPlain($renderElement);

        $browserFactory = new BrowserFactory($_ENV['CHROMIUM_BIN_PATH']);
        $browserFactory->addOptions([
            'noSandbox' => true,
        ]);

        // ブラウザ起動
        $browser = $browserFactory->createBrowser();

        try {
            $page = $browser->createPage();
            $page->setHtml($markUp->_toString());

            // 一時保存先
            $tmpPath = Drupal::service('uuid')->generate() . '.pdf';
            $page->pdf()->saveToFile($tmpPath);
        } catch (Throwable $e) {
            throw new ErrorException(sprintf(
                'PDFの生成に失敗しました: %s',
                $e->getMessage(),
            ));
        } finally {
            // ブラウザを閉じる
            $browser->close();
        }

        $response = new Response(file_get_contents($tmpPath));
        $response->headers->set('Content-Type', 'application/pdf');
        $response->headers->set(
            'Content-Disposition',
            'inline',  // 生成したPDFをブラウザに表示するオプション
        );

        return $response;
    }
}

コードを分解して、簡単に解説していきます。

// twigテンプレートをレンダリング
$renderElement = [
    '#theme' => 'generate_pdf',
    '#title' => 'Generate PDF Test',
    '#body' => 'sample body.sample body.sample body.sample body.',
];
$markUp = Drupal::service('renderer')->renderPlain($renderElement);

ここでは、1つ前で作ったTwigテンプレートをレンダリングして取得しています。
renderPlain()の返り値はDrupal\Core\Render\Markupです。

$browserFactory = new BrowserFactory($_ENV['CHROMIUM_BIN_PATH']);
$browserFactory->addOptions([
    'noSandbox' => true,
]);

// ブラウザ起動
$browser = $browserFactory->createBrowser();

ここで、chromiumのブラウザを作って起動しています。
noSandboxオプションを有効にして、サンドボックスモードで実行するようにします。

他にも色々オプションがあるので、詳細は公式Githubを見てみてください。

また、$_ENV['CHROMIUM_BIN_PATH']はコンテナ内にあるchromiumのpathです。
コンテナ内でwhich chromiumして出てくるpathを直接書き込むか、.envに設定してください。

$page = $browser->createPage();
$page->setHtml($markUp->_toString());

// 一時保存先
$tmpPath = Drupal::service('uuid')->generate() . '.pdf';
$page->pdf()->saveToFile($tmpPath);

ここでいよいよPDFを生成します。

まず、ページを作成し、そこに先ほどレンダリングしたものを文字列で渡します。
次に、PDFを生成するメソッドであるpdf()を実行し、一時保存先のPathを渡しています。
これでPDFが生成されます。

$response = new Response(file_get_contents($tmpPath));
$response->headers->set('Content-Type', 'application/pdf');
$response->headers->set(
    'Content-Disposition',
    'inline',  // 生成したPDFをブラウザに表示するオプション
);

return $response;

最後に、生成したPDFをreturnします。
file_get_contents($tmpPath)で一時保存しているPDFファイルを取得し、これをresponseのheaderに含めています。

Content-Dispositionについて

以下のようなルールになっているので、お好みで変更してください。

# ブラウザ上に表示
Content-Disposition: inline

# ダウンロード
Content-Disposition: attachment

# ダウンロード(ファイル名を指定)
Content-Disposition: attachment; filename="filename.jpg"

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Disposition

追記

PDFの発行方法ですが、わざわざ一時保存先してから画面に表示させる、なんてことしなくても良いのに気づいたので、そちらのコードも作成してみました。
PDFを発行→表示する以外の部分は全く同じで良いので端折ります。

// 省略

try {
    $page = $browser->createPage();
    $page->setHtml($markUp->_toString());

    $encodedFileData = $page->pdf()->getBase64();
} catch (Throwable $e) {
    throw new ErrorException(sprintf(
        'PDFの生成に失敗しました: %s',
        $e->getMessage(),
    ));
} finally {
    // ブラウザを閉じる
    $browser->close();
}

$response = new Response(base64_decode($encodedFileData));
$response->headers->set('Content-Type', 'application/pdf');
$response->headers->set(
    'Content-Disposition',
    'inline',
);

base64のバイナリデータとして出力して、そのまま画面に表示する、という方法です。
こちらの方が、メモリ上で全ての処理が完結するのでスマートなんじゃないかなと思います。

最後に

Dockerの構築のところで、chromium-driverの部分をchromium-browserとしている記事を多く見かけたので最初はそちらで実行していたんですが、それだと上手くいかなかったです。

調べたところ、どうやらchromium-browserは昔の名前らしいのご注意ください。

参考にさせていただいたサイトや記事

https://developer.chrome.com/docs/chromium/headless?hl=ja
https://github.com/chrome-php/chrome
https://www.drupal.org/project/page_to_pdf
https://www.drupal.org/project/entity_print_chrome
https://www.drupal.org/project/entity_print_browserless_pdf
https://zenn.dev/suhyoenbae/articles/b9dd996a02abbd
https://qiita.com/sigeta/items/54f87c035b92f056816f
https://al-batross.net/2020/07/28/docker-chrome-headless/
https://stackoverflow.com/questions/47203812/package-chromium-browser-has-no-installation-candidate
https://www.php.net/manual/ja/install.pecl.php

カラビナテクノロジー デベロッパーブログ

Discussion