DrupalでHeadless Chromeを実行する
目標
- 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って何?
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"
追記
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
は昔の名前らしいのご注意ください。
参考にさせていただいたサイトや記事

株式会社 カラビナテクノロジーは「命綱や支点を素早く確実に繋ぐカラビナ。そんなカラビナのような役割をテクノロジーで実現したい」という想いのもと、福岡で設立。 主にシステム開発・アプリ開発・ Webサイト制作を行っています。採用情報→karabiner.tech/recruit/requirements/
Discussion