[PHP] チートシート

私の作業用として、インストール方法やよく使うコード、TIPS、トラブルシューティング的なものをまとめました。

公式サイト・インストール

パッケージ管理システム

PHPのパッケージ管理システムは「Composer」です。インストールはこちらからダウンロードしたインストーラーで可能です。

パッケージのインストールは以下のコマンドで行えます。

composer install aws/aws-sdk-php -vvv
  • -vvv:インストール状況を詳細表示します

アップデート

composer update aws/aws-sdk-php -vvv

設定変更

composer config platform-check false -vvv
  • platform-check falseがcomposer.jsonに追加され再構成されます

プロキシ環境下での実行ではプロキシの設定を行います。(以下はWindowsの例です)

set HTTP_PROXY=http://account:password@host:port
set HTTPS_PROXY=http://account:password@host:port
set HTTP_PROXY_REQUEST_FULLURI=1
set HTTPS_PROXY_REQUEST_FULLURI=0
  • パスワードに記号が入る場合はURLエンコードをしてください

ワンライナー

rem UTCの日時を日本時間の日時に変換して出力
set VAR=2022-03-24 16:09:46
php -r "$dt = new DateTime(getenv('VAR'), new DateTimeZone('UTC')); $dt->setTimeZone(new DateTimeZone('Asia/Tokyo')); print($dt->format('Y-m-d H:i:s'));"

文字列操作

正規表現

メタ文字

文字説明
\This is a pen\\.メタ文字などのエスケープ文字。例は、文末のメタ文字 “.”をエスケープしている
^This is a pen\\.検索文字列の先頭と一致します。
$This is a pen\\.$検索文字列の末尾と一致します。
.…..改行文字以外の任意の1文字。例は、5文字の文字列
This is a (pen|book)\\.選択肢。例の“(pen|book)“の部分は、”pen”または“book”になります。
(This is a (pen|book)\\.サブパターンの開始。例の“(pen|book)“の部分は、”pen”または“book”になります。
(This is a (pen|book)\\.サブパターンの終了。例の“(pen|book)“の部分は、”pen”または“book”になります。
[[Gg]oogle文字クラス定義の開始。例の“[|g]“のような”[“と”]“で囲まれた箇所は文字クラスと呼ばれます。 ここでは”G”または“g”になるので、“Google”または“google”になります。
][0-9A-F]文字クラス定義の終了。例の“[0-9A-F]“は、数字かA~Fの文字になります。 ”[0-9A-F]+“なら16進数の検索な
?Go?gle直前の文字またはサブパターンを、0回または1回だけ繰り返します。例はGogleまたはGoogle
*Go*gle直前の文字またはサブパターンを0回以上の繰り返し。 これは、{0,} と同じです。 例では、直前の文字”o”が0回以上の繰り返しとなり、“o*”は、“o”、“oo”、“ooo”、 “oooo”、….となります。 そのため、Gogle、Google、Gooogle、Goooogle、…になります。
+Go+gle直前の文字またはサブパターンを1回以上の繰り返し。これは、{1,} と同じです。例はGoogle、Gooogle、Goooogle、…
{Go{2}gle文字数の最小/最大指定の開始。例は、“o”が2個の“Goolge”になります。
}Go{1,2}gle文字数の最小/最大指定の終了。例は、“o”が1~2個になり、“Gogle”または“Goolge”になります。

制御コード

記法説明
\aアラーム、ベル文字 (16進 07)
\f改ページ(16進 0C)
\n改行(16進 0A)
\r復帰 (carriage return) (16進 0D)
\tタブ (16進 09)
\x??16 進コードで ?? 。例 16進のFFは、0xFFまたは0xff

文字クラス

記法説明
\d20\d{2,2}[0-9]と同じ数字です。例は、2000~2099の21世紀の西暦になります。
\D^\D{4,4}数字(\d)以外の文字で、[^0-9]と同じ数字以外の文字。例は、先頭が数字以外の4文字。
\wclass\s(\w+)単語構成文字(数字、アルファベットと“_”)で、Perlが定義する単語と成り得る文字のこと。[a-zA-Z0-9_]と同じ。
\W単語構成文字(\w)以外の文字で、[^a-zA-Z0-9]と同じ
\sスペース、タブ、改行など( HT[9]、LF[10]、FF[12]、CR[13]、スペース[32] )の空白文字で、[\f\n\r\t\v]と同じになります。 ロケールを指定したマッチングを行った場合には、128から255までのコードポイントの文字 (たとえば NBSP (A0)) も空白文字とみなされる可能性があります。 例は、空白文字で区切られた2個の単語の文字列となります。
\S空白文字(\s)以外の文字。

パターン修飾子

記号PCRE 内部名前動作
iPCRE_CASELESS正規表現パターンの文字は検索対象文字列の大文字にも小文字にもマッチします。
mPCRE_MULTILINE改行 “\n”がある 複数行 の検索対象文字列を 1行 として処理します。この修飾子を指定すると、行頭メタ文字の^と 行末メタ文字の $ が検索対象文字列の行頭と行末に加えて、各改行 “\n” の直前と直後にそれぞれマッチします。
sPCRE_DOTALLメタ文字の .(ピリオド)が 改行 “\n” にもマッチするようになります。設定しない場合、改行 “\n” 以外のすべての文字にマッチします。
APCRE_ANCHORED正規表現パターンは検索対象文字列の先頭でのみマッチングするように固定されます。
DPCRE_DOLLAR_ENDONLYメタ文字 $(ドル) は検索対象文字列の最後にのみマッチします。修飾子m が設定されている場合は無視されます。
uPCRE_UTF8正規表現パターンと検索対象文字列をUTF-8として処理します。

日付・時刻操作

フォーマット

日時データのフォーマットを行います。(YYYY-MM-DD HH:MI:SS)

echo date('Y-m-d H:i:s');

日付計算

echo date('Ymd', strtotime('20190101 +1 day'));

ファイル操作

ファイル出力

下記コードはデバッグ時に使ったりしていたものです。

file_put_contents("C:/debug.txt", var_export(array(
    'url' => $_SERVER['PATH_INFO'],
), true) . "\n", FILE_APPEND);

ディレクトリ作成(再帰)

関数を探せていないため自作

public static function create_dir_tree($path)
{
    $tree = explode('/', preg_replace('#[\\\/]+#', '/', $path));
    for ($i = 0; $i < count($tree); $i ++) {
        $dir = implode('/', array_slice($tree, 0, $i + 1));
        if (! file_exists($dir)) {
            mkdir($dir, 0777, true);
        }
    }
}

ディレクトリ削除(再帰)

関数を探せていないため自作

public static function remove_dir_tree($dir)
{
    foreach (glob($dir . '/*') as $filepath) {
        if (is_dir($filepath)) {
            static::rmdir_tree($filepath, $commit_result);
            rmdir($filepath);
        } else {
            unlink($filepath);
        }
    }
    rmdir($dir);
}

ファイル収集

関数を探せていないため自作

private static function collect_files($dir, $pattern, &$filepaths)
{
    foreach (glob($dir . '/*') as $filepath) {
        if (is_dir($filepath)) {
            static::collect_files($filepath, $pattern, $filepaths);
        }
        if (preg_match('/' . $pattern . '/', $filepath) === 1) {
            array_push($filepaths, $filepath);
        }
    }
}

リファクタリング

型チェック

変数の型チェック方法です。

$value = 'test';
echo gettype($value); // "string"
  • ここによると以下の値が返るそうです
    • “boolean”
    • “integer”
    • “double” (歴史的な理由により float の場合は、“float” ではなく、 “double” が返されます)
    • “string”
    • “array”
    • “object”
    • “resource”
    • “resource (closed)” (PHP 7.2.0 以降)
    • “NULL”
    • “unknown type”

デバッグ

xDebug

Eclipse等で使うxDebugですが、自分が使っているPHPのバージョンがどのxDebugを使っていいか迷う時があると思います。そんな時は、以下のサイトのテキストエリアにphpinfo()の結果を貼り付けるとインストールすべきxDebugを教えてくれます。

バックトレース

呼び出し階層が分かるリストが出力されます。

echo debug_backtrace();

パフォーマンスチェック/チューニング

条件分岐

三項演算子は遅いらしく、普通のif文のほうが良いそうです。

ループ

for/foreachよりwhileのほうが早いそうです。

$i = 0;
while (true) {
    ++$i;
    if ($i <= 10000) {
        // TODO: 処理
    }
}
  • 条件文(上記コードの(true)の箇所)に関数を置くと遅くなるそうです

配列追加

array_pushより$xxx[] = yyy形式のほうが早いそうです。

$arr = array();
array_push($arr, 'test');
 
// 以下のように変える
$arr[] = 'test';

インクリメント/デクリメント

通常は$i++のように書きますが、++$iのように前に書いたほうが早いそうです。

時間計測

$start = new DateTime('2012-07-08 11:14:14.100000');
$end   = new DateTime('2012-07-08 11:14:15.200000'); 
print(date_diff($start, $end)->format('%s.%F'));

TIPS

エラーログの出力先をsyslogに変更(Linux)

PHPや他のミドルウェア等のエラーログを一か所で管理したいという要件が出てきた場合、Linuxだとrsyslogでエラーログを集約して特定のログファイルに出力するという方法があります。

ここではphpとrsyslogの設定変更によるエラーログの出力方法をご紹介します。

まずはphp.iniでエラーログの出力先を変更します。

error_log = syslog
syslog.ident = php
syslog.facility = local0
  • syslog.facilityはlocal0~local7まで利用可能です

rsyslog側で受け取ったファシリティのデータをどう出力するか設定します。

*.info;mail.none;authpriv.none;cron.none;local0.none    /var/log/messages
local0.err                                              /var/log/application_error_log
  • 以下を意図して上記の設定をしています
    • local0のログを/var/log/messagesに出力しない
    • local0のエラーログのみ/var/log/application_error_logに出力する

設定の反映にはrsyslogとapacheの再起動が必要です。

sudo systemctl restart rsyslog && sudo systemctl status rsyslog
sudo systemctl restart httpd && sudo systemctl status httpd

これでphpのエラーログがsyslogを通じて/var/log/application_error_logに出力されます。

なお、動作確認としては以下のようなコマンドでエラーログが出力されると思います。

php -r 'print('test');'
php -r '~'                                    // PHP Parse error
php -r '$d =& dir('/');'                      // PHP Strict Standards
php -r 'set_magic_quotes_runtime(false);'     // PHP Deprecated
php -r 'echo $undefined;'                     // PHP Notice      -> エラーじゃないので出ない
php -r 'ini_set();'                           // PHP Warning     -> エラーじゃないので出ない
php -r 'undefined();'                         // PHP Fatal error 

拡張モジュールのビルド(Windows)

PHPを利用したアプリケーション構築時に拡張モジュールの不具合や要件に合わない動作があって拡張モジュールを修正したくなったり、Windows版のビルドを誰もしておらず自分でソースコードからビルドしなくてはならないことがあると思います。

Windows環境において拡張モジュールのビルドを試したのでその記録となります。

ビルド環境の準備

Visual Studio XX Communityをダウンロードしてインストーラーを起動し、「C++によるデスクトップ開発」にチェックをつけてインストールします。

  • ここによると、修正するPHPのバージョンによってビルドに利用するVisualStudioのバージョンも変わってくるそうです
    • PHP7.0~7.1:Visual Studio 2015
    • PHP7.2~7.4:Visual Studio 2017
    • master:Visual Studio 2019

PHP SDKをダウンロードして任意のディレクトリに展開します。

  • 後述の作業に必要となるため、ここでは「C:\Users\xxxx\Downloads\php-sdk-binary-tools-master」に展開したものとして進みます

ターゲットとなるバージョンのPHPのソースコードをダウンロードして任意のディレクトリに展開します。

  • 後述の作業に必要となるため、ここでは「PHP 7.3.9 (tar.gz)」をダウンロードして「C:\Users\xxxx\Downloads\php-7.3.9」に展開したものとして進みます

(必要であれば)対象となる拡張モジュールのソースコードを修正します。私は必要があってpdo_ociを修正したり、pdo_sqlsrvのソースコードを持ってきたりしました。PHPにデフォルトで含まれている拡張モジュールであれば不要ですが、サードパーティが提供しているものだと含まれていないためextフォルダにソースコードをコピーする必要があります。

  • 上記の例で言うとpdo_ociはPHPに含まれています
  • pdo_sqlsrvは含まれていないため以下の手順が必要です

ビルド

スタートメニューから「x64 Native Tools Command Prompt for VS XX」を探して起動します。

次にPHP SDKの環境変数をセットします。

C:\Windows\System32>C:\Users\xxxx\Downloads\php-sdk-binary-tools-master\bin\phpsdk_setvars.bat

ビルドを実行します。

C:\Windows\System32>cd C:\Users\xxxx\Downloads\php-7.3.9
C:\Users\xxxx\Downloads\php-7.3.9>buildconf.bat –force
C:\Users\xxxx\Downloads\php-7.3.9>configure.bat --enable-snapshot-build --enable-debug-pack --with-oci8-12c=C:\oracleclient\instantclient\sdk,shared --with-enchant=shared --enable-com-dotnet=shared --without-analyzer --with-pdo-oci=C:\oracleclient\instantclient\sdk,shared --with-extra-libs=C:\oracleclient\instantclient --disable-calendar
C:\Users\xxxx\Downloads\php-7.3.9>nmake snap

  • オプションについてはこのあたりを見ながら指定しました
    • –enable-snapshot-build:可能な限りオプションをONにしてビルドエラーを無視してくれます
    • –enable-debug-pack:–enable-snapshot-build指定時にはこちらも指定する必要があるようです
    • –with-enchant、–enable-com-dotnet=shared、–without-analyzer:ビルドに必要だったようです
    • –disable-calendar:有効なままだとビルドできなかったため
    • –with-oci8-12c=C:\oracleclient\instantclient\sdk,shared:php_ociビルドに必要
    • –with-pdo-oci=C:\oracleclient\instantclient\sdk,shared:php_ociビルドに必要
    • –with-extra-libs=C:\oracleclient\instantclient:php_ociビルドに必要
  • ビルドオプションはNon Thread Safe版(IIS用)とThread Safe版(Apache用)で異なるため注意が必要です
    • 上記はThread Safe版でのビルド例です
    • Non Thread Safe版では以下のようにビルドします
    • configure.bat –enable-snapshot-build –enable-debug-pack –disable-zts –with-oci8-12c=C:\oracleclient\instantclient\sdk,shared –with-enchant=shared –enable-com-dotnet=shared –without-analyzer –with-pdo-oci=C:\oracleclient\instantclient\sdk,shared –with-extra-libs=C:\oracleclient\instantclient –disable-calendar

ビルドすると、Thread Safe/Non Thread Safe版でそれぞれのディレクトリに拡張モジュールが出力されています。

  • Thread Safe
    • C:\Users\xxxx\Downloads\php-7.3.9\x64\Release_TS
  • Non Thread Safe
    • C:\Users\xxxx\Downloads\php-7.3.9\x64\Release

以下のサイトを参考に作業しました。

暗号化・復号化

mcryptが主流だったもののPHP7.2で非推奨となってしまいopenssl が推奨となりました。以下はAES(CBCモード)で暗号化・復号化するコードの例です。

前提としてphp.iniでextension=opensslを有効化させておく必要があります。

// 暗号化
$encrypt = function ($data, $password) {
    /*
     * AES-128-CBCは共通鍵暗号(128bit)の暗号方法
     *
     * AESは日本政府のプロジェクト「CRYPTREC」により推奨されている暗号方法のうちの1つ
     * ※CRYPTREC: 総務省・経産省の暗号技術検討会、NICT・IPAの暗号技術評価委員会・暗号技術活用委員会で構成されている日本政府のプロジェクト
     * ※CRYPTRECにより「安全性・実装性能が確認されている」暗号技術のリスト: https://www.cryptrec.go.jp/list/cryptrec-ls-0001-2012r4.pdf
     *
     * PHP: opensslの暗号化・復号化関数でサポート
     * Python: PyCryptodomeのインストールでサポート
     *
     * https://ja.wikipedia.org/wiki/CRYPTREC
     * https://ja.wikipedia.org/wiki/Advanced_Encryption_Standard
     * php.net/manual/ja/function.openssl-encrypt.php
     * https://www.php.net/manual/ja/function.openssl-get-cipher-methods.php
     * https://dev.classmethod.jp/articles/python-crypto-libraries/
     */
    // AEC(CBCモード)にて暗号
    $encrypt_method = 'aes-128-cbc';
    $ivlen = openssl_cipher_iv_length($encrypt_method);
    $iv = openssl_random_pseudo_bytes($ivlen);
    $encrypted_data_raw = openssl_encrypt($data, $encrypt_method, $password, $options = OPENSSL_RAW_DATA, $iv);

    // 暗号化したデータをBASE64エンコードして返す
    $encrypted_data = base64_encode($iv . $encrypted_data_raw);
    return $encrypted_data;
};

$encrypted_data = $encrypt($cipher_data, 'crypt_key_12');

// 復号化
$decrypt = function ($data, $password) {
    // AEC(CBCモード)にて復号
    $encrypt_method = 'aes-128-cbc';
    $base64decoded_data = base64_decode($data);
    $ivlen = openssl_cipher_iv_length($encrypt_method);
    $iv = substr($base64decoded_data, 0, $ivlen);

    // BASE64デコードした暗号データのうち、初期ベクター以降のデータを取得し復号
    $encrypted_data = substr($base64decoded_data, $ivlen);
    $decrypted_data = openssl_decrypt($encrypted_data, $encrypt_method, $password, $options = OPENSSL_RAW_DATA, $iv);
    return $decrypted_data;
};

$decrypted_data = $decrypt($encrypted_data, 'crypt_key_12');
  • AES-128-CBCは共通鍵暗号(128bit)の暗号方法です
    • AESは日本政府のプロジェクト「CRYPTREC」により推奨されている暗号方法のうちの1つです
    • CRYPTREC: 総務省・経産省の暗号技術検討会、NICT・IPAの暗号技術評価委員会・暗号技術活用委員会で構成されている日本政府のプロジェクトです
    • CRYPTRECにより「安全性・実装性能が確認されている」暗号技術のリスト
    • PHPでは例の通りopensslの暗号化・復号化関数でサポートしています

Webビーコン用画像

時代的にWebビーコンは排除されていく方向らしいですが、以下は1×1ピクセルの透過gif画像を返すコードです。

header("Content-type: image/gif");
echo base64_decode('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==');

APCuの使い方

データベースで管理されている特定データへのアクセス頻度が高くパフォーマンス悪化のボトルネックとなることがあらかじめ想定される場合、共有メモリにkey-value形式でデータをストアできるAPCu(APC User Cache)機能が使えるかもしれません。

利用にさいしては拡張モジュールをダウンロードしてphp.iniにてapcuを有効にする必要があります。

[apcu]
apc.enabled=1
apc.shm_size=32M
apc.ttl=7200
apc.enable_cli=1
apc.serializer=php

APCuの使い方は以下です。

$acpu_key = 'test';

// キーの存在チェック
apcu_exists($acpu_key)

// データの追加
apcu_add($acpu_key, $data);

// データ取得
apcu_fetch($acpu_key);

// データ削除
apcu_delete($acpu_key);

JIS第1水準・第2水準の文字チェック

今時はUTF8で統一される文字コードですが、お役所で扱うシステムでは今なおJIS第1水準・第2水準でのデータを求められることが少なくありません。

  • IS第1水準・第2水準とは
    • 日本産業規格(JIS)によって制定されている文字コードの規格「JIS X 0208」に含まれている漢字のエリア2か所を指します
    • 文字コード「ISO-2022-JP」はこのJIS第1水準・第2水準の漢字を含んでいます
  • 文字コード「ISO-2022-JP」とは
    • エスケープシーケンスによってバンク切り替えを行うタイプの7ビットで記される文字コードです
    • 制御文字・ラテン文字・JIS第1水準・第2水準を含んでいます

以下はJIS第1水準・第2水準で利用不可の文字が含まれているかを探すコード例です。

$data = '...'; // UTF8での文字列が入っている前提

$ng_chars = array();

for ($i = 0; $i < mb_strlen($data, 'utf-8'); $i ++) {

    // 文字列中の1文字をISO-2022-JPにエンコーディングする
    $c = mb_substr($data, $i, 1, 'utf-8');
    $a = mb_convert_encoding($c, 'iso-2022-jp', 'utf-8');

    // エンコーディングできなかった文字はJIS第1・第2水準の範囲外としてNGにする
    // Note: エンコーディングできなかった文字は「?」になるため、元々「?」の文字を除外してチェック
    // - mb_check_encoding($c, 'iso-2022-jp')を試してみたが、「あ」「い」もfalseとなるため使用を中止した
    if ($c !== '?' && $a === '?') {
        array_push($ng_chars, array(
            'position' => ($i + 1),
            'charactor' => $c,
            'reason' => 'Encoding failure'
        ));
    } else {
        // エンコーディングした文字のバイナリデータを取得する
        // Note:
        // - UTF-8からISO-2022-JPにエンコーディングすると、8バイトになる(漢字シフトコードが付与されるためらしい)
        // - 先頭に1B 24 42、末尾に1B 28 42が付与され、その間にある2バイトがJIS文字コードを表すバイナリになる
        // - 例)あ: 1b 24 42 24 22 1b 28 42
        // -   い: 1b 24 42 24 24 1b 28 42
        // -    1: 31
        // 中央2バイトのうち、1バイト目がJISコードの「区」、2バイト目がJISコードの「点」となる
        // この「区」「点」を使い、JISの第1・第2水準の漢字として利用可能かをチェックする
        $h = bin2hex($a);
        if ((strlen($h)) / 2 === 1) {
            // 1バイト文字
            $d = hexdec($h);
            // LF/CRを除く制御文字以外はNGとする
            if ((0 <= $d && $d <= 9) || (11 <= $d && $d <= 12) || (14 <= $d && $d <= 31) || $d === 127) {
                array_push($ng_chars, array(
                    'position' => ($i + 1),
                    'charactor' => $c,
                    'reason' => 'Invalid control code'
                ));
            }
        } elseif ((strlen($h) / 2) === 8) {
            // 2バイト文字
            // 1~8区(記号等)、16~47区(第1水準)、48~83区(第2水準)、84区(第2水準の追加文字)以外はNGとする
            $ku = hexdec(substr($h, 6, 2)) - 32;
            $ten = hexdec(substr($h, 8, 2)) - 32;
            if (! ((1 <= $ku && $ku <= 8) || (16 <= $ku && $ku <= 47) || (48 <= $ku && $ku <= 84))) {
                array_push($ng_chars, array(
                    'position' => ($i + 1),
                    'charactor' => $c,
                    'reason' => 'Out of JIS 1/2 LEVEL range'
                ));
            }
        }
    }
}

return $ng_chars;
  • UTF-8で入力された文字列に対して1文字ずつISO-2022-JPに変換し、以下のパターンに該当するものをNG文字として配列に保持して返すコード例です
    • ISO-2022-JPに変換できなかったもの
    • 1バイト文字でLF/CR以外を除く制御文字
    • 2バイト文字でJIS第1・第2水準の漢字以外の文字

以下を参考に書きました。

UTF8(BOM)ファイルの検索

function find_bom_file($target_dir)
{
    chdir($target_dir);
    $files = glob('*');
    if (! is_array($files) || count($files) == 0) {
        return;
    }
    foreach ($files as $file) {
        $path = $target_dir . $file;
        if (is_dir($path)) {
            find_bom_file($path . '/');
        } elseif (filesize($path) >= 3) {
            $contents = file_get_contents($path);
            if (hasbom($contents)) {
                // 出力
                echo "$path\n";
            }
        }
    }
}
 
function hasbom($contents)
{
    return preg_match('/^efbbbf/', bin2hex($contents[0] . $contents[1] . $contents[2])) === 1;
}

$initial_directory = 'C:/targetdir';
find_bom_file($initial_directory . '/');

PHPからPDFを作成する

やり方は様々あると思いますが、たいてい「PHP PDF」で検索するとTCPDFによるPDF出力が上位に出てくると思います。

しかし位置を決めて文字列や図形を出力するロジックの場合、調整の難易度が高かったり修正の面倒さがあったりといまいちな気がしていました。

そこで簡単にPDF出力したいと思い、(1)Wordで作った文書を変換しながら出力、(2)HTMLをPDF変換できないか、その二つに絞って以下の組み合わせを調査しました。

  • PHPWord+LibreOffice
  • HTML+TCPDF
  • HTML+mPDF

PHPWord+LibreOffice

PHPからPHPWordでWord文書を作成しそれをLibreOfficeで変換するという方法です。以下は調査段階で分かった内容です。

  • PDF変換は可能
    • 図形は微妙にずれるものの調整可能な範囲
  • ハイパーリンクが消えてしまう問題がありました
    • LibreOfficeのPDFエクスポート時にオプション指定すればできるという記事を見ましたが、今のところ成功はしていません
    • そもそもPHPWordで作成したWordファイルをLibreOfficeで開くとハイパーリンクが消えていました
    • プレースホルダー置換方式で無理やり対応したハイパーリンクが、LibreOfficeでは対応できていないそうです
$cmd = 'soffice --headless --nologo --nofirststartwizard --convert-to pdf "C:/test.docx" --outdir "C:/";
exec($cmd, $lines, $rc);
// C:\にtest.pdfができている

HTML+TCPDF

  • 普通にPDF出力はできていました
  • HTMLの内容がそれなりにレンダリングされています
  • HTMLなのでハイパーリンクや表も普通に書けました
  • 画像はパスをローカルに変更すれば出力されていました
  • ですが、いくつか懸念点もあります
    • 帳票デザインがやや面倒、ブラウザで表示されるものとPDF出力されるものは微妙に異なるため、デザインは修正と確認をかなり繰り返さなければなりませんでした
    • デフォルトの日本語フォントでレンダリングされるとギザギザっぽくなるため、別途フォントを導入しコード上で読み込ませてからPDF出力してあげる必要があります(IPAフォント等)
    • とにかくHTMLのレンダリング結果が微妙で、サポートされているスタイルも少なすぎて使い物になりませんでした
// TCPDF生成
$pdf = new \TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
$pdf->SetPrintHeader(false);
$pdf->SetPrintFooter(false);
 
// フォント設定
$font = new \TCPDF_FONTS();
$ipafont = $font->addTTFfont('C:/ipagp.ttf');
$pdf->SetFont($ipafont, '', 10);
 
$html = file_get_contents('C:/test.html');  // html,head,body等は不要、実際に使うのはbodyの中のHTMLブロックのみ
 
// ファイル出力
$pdf->writeHTML($html, true, false, true, false, '');
$pdf->Output('C:/test.pdf', 'F');

HTML+mPDF

composerでインストールします。

php composer.phar require mpdf/mpdf
  • 日本語フォントの登録が必要でした
    • 使うフォントはmpdfのフォントファイルディレクトリに置く必要があります
    • 場所は/project/vendor/mpdf/mpdf/ttfonts
$mpdf = new \Mpdf\Mpdf(array(
    'format' => 'A4', // 用紙設定
    'orientation' => 'P', // 用紙向き
    'fontdata' => array( // フォント
        'ipa' => array(
            'R' => 'ipag.ttf'
        )
    ),
    'margin_top' => 10, // 余白(上)
    'margin_bottom' => 10, // 余白(下)
    'margin_left' => 10, // 余白(左)
    'margin_right' => 10, // 余白(右)
    '' => ''
));
$mpdf->WriteHTML(file_get_contents('C:/template.html'));
$mpdf->Output('C:/test.pdf', 'F');

HTMLのためテンプレートエンジン(例えばsmarty)を用いてパラメータを渡したり表示制御をしたりした結果のHTMLを渡すことも可能です。

結論としてはコードも書きやすくレンダリング結果も良かったので、このHTML+mPDFを採用しました。

try~catchで補足できないエラー

ざっと調べた限り以下のエラーはtry~catchで例外補足できないものだそうでした

  • メモリ不足
  • 実行時間が長い
  • 存在しないクラス、関数の呼び出し
  • 型宣言と異なるデータ型を渡す
  • セミコロン漏れなどの文法ミス

PDOによるSQL実行でエラーなのに例外が起こらない

PDO(MySQL)で該当レコードが行ロックされていた状態でselect … from … for updateを実行したところ、タイムアウトはしたもののfetchAll()で0件の配列が返ってきてしまいました。

例外が起きるものと思っていましたが、PHPのPDOにおけるデフォルトは「例外を起こさない」になっていました。そこで調査したところ、PHPのマニュアルにある通り、PHP8より前は「例外を起こさない」、PHP8から「例外を起こす」がデフォルトになっていました。

PDOのインスタンスが持つsetAttributeメソッドでエラーモードを「エラー発生時には例外を起こす(PDO::ERRMODE_EXCEPTION)」設定にすれば良かったようです。

  • エラーモードは以下の3つがありました
    • PDO::ERRMODE_SILENT
      • エラー発生時にはPDO/PDOStatementオブジェクトのerrorCode/errorInfoメソッドでエラー情報を拾うことができます
    • PDO::ERRMODE_WARNING
      • SILENTと同じようにerrorCode/errorInfoを設定するとともに、PHPのE_WARNINGを出力します
    • PDO::ERRMODE_EXCEPTION
      • 例外を起こす
try {
    $dbh = new PDO($dsn, $user, $password);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

WebDAVアクセス

cURLによるWebDAVアクセスのサンプルです。前提としてphp.iniでextension=curlが有効になっていることが必要です。

// アップロード
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://xxx/webdav/put.txt');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // curl_execの結果を文字列で返す
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 証明書の検証を行わない
curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents('C:/put.txt'));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Authorization:Basic webdavuser:webdavuserpass'
));

$response = curl_exec($ch);

// ダウンロード
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://xxx/webdav/get.txt');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // curl_execの結果を文字列で返す
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 証明書の検証を行わない
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Authorization:Basic webdavuser:webdavuserpass'
));

$response = curl_exec($ch); // get.txtの内容が取得されます