PHPのgetRealPathでファイルパスの代わりにプロジェクトルートが取れてしまった話

スズキです。 Webアプリの画面からファイル添付してサーバに送ることってありますよね。今回は、サーバ側のPHPプログラムでリクエストから添付ファイルを取り出そうとしたら、思ってたのと全然違う挙動になってしまった話です。

やろうとしたこと

やろうとしたことは超シンプルです。
  • HTTPのmultipart/form-dataで送られてきたファイルをリネームして保存する
  • ファイルが無ければエラーにする

サンプルコード(修正前)

PHP+Symfonyのコードです。例外処理などはごっそり省略。
/**
 * ファイル保存
 */
public function uploadFile(Request $request)
{
    // ①リクエストからファイルを取り出す
    $file = $request->files->get('file');
    
    // ②ファイルが一時的に保存されているパスを取得
    $tmpFilePath = $file->getRealPath();
    
    // ③ファイル存在チェック
    if (!$tmpFilePath) {
        return 'ファイルが見つかりません';
    }
    
    // ④ファイル名をリネーム
    $newFilePath = $this->makeNewPath($tmpFilePath);
    rename($tmpFilePath, $newFilePath);

    // ⑤ファイル保存
    $this->saveFile($tmpFilePath);

    return '成功しました';
}

起きたこと

②でファイルパスを取得したとき、通常なら、 // $tmpFilePath : ‘/tmp/phpucYAwE’ といった一時ファイルのパスが取得できるのですが、PHPのアップロードサイズ上限値(php.iniで設定する値)を超えたファイルを送ってみると、
// $tmpFilePath : '/opt/myWebApp/public'
といったように、なぜかWebアプリのルートディレクトリパスが取得されてしまいました。そして、③のファイル存在チェックでもエラー判定できず、そのまま④でアプリのルートディレクトリがリネームされてアプリが動かなくなる事態に…

対処

②でのファイルパス取得にて「getRealPath」を「getPathname」に変えたところ、アップロードサイズ上限を超えたファイルの場合に、期待通りに空文字が取得できるようになりました。
ファイルサイズが大きくない場合は、「getRealPath」と「getPathname」は同じパスを返してくれたので、問題なさそうです。
// $tmpFilePath : '' ←ファイルサイズ上限オーバーのときは空文字が返ってくる(期待通り)

サンプルコード(修正後)

ディレクトリパスが取得されてしまうと悲惨なので、②のパス取得部分だけでなく、安全のため③のファイル存在チェックの条件も増やしています。
/**
 * ファイル保存
 */
public function uploadFile(Request $request)
{
    // ①リクエストからファイルを取り出す
    $file = $request->files->get('file');
    
    // ②ファイルが一時的に保存されているパスを取得
    $tmpFilePath = $file->getPathname(); // getRealPath ではなく getPathname を使用
    
    // ③ファイル存在チェック
    if (!$tmpFilePath || is_dir($tmpFilePath)) { // パスがディレクトリだった場合もエラーにしておく
        return 'ファイルが見つかりません';
    }
    
    // ④ファイル名をリネーム
    $newFilePath = $this->makeNewPath($tmpFilePath);
    rename($tmpFilePath, $newFilePath);

    // ⑤ファイル保存
    $this->saveFile($tmpFilePath);

    return '成功しました';
}
 

原因推測

「getPathname」と「getRealPath」はどちらも似た説明になっていますが、「getRealPath」は絶対パスを取得するようです。 「getRealPath」が絶対パスに変換する処理の中で、空文字のパスがプロジェクトルートと解釈されたのかな…と推測しています。 リクエストに添付ファイル自体が無ければ、①の $file = $request->files->get('file'); のところでnullが取得されて特に被害は発生しないのですが、サイズ上限オーバーの場合は中途半端にファイルが存在する感じになっていそうです。 今回は事例紹介まで。

Integer型の変数がNullかどうかチェックするのはなぜ?

クロハです。 前回のPHPの初歩的な部分に躓いた話に引き続いて、たぶん今回もプログラム経験の浅い人向けの内容です。

事の発端

唐突なのですがJavaのソースの改修をしているときに次のような条件分岐に改修する必要な場面に遭遇しました。

// Hogehoge.hogeId はInteger型の変数 private boolean IsHoge(Hogehoge hogeEntity) { Integer hogeId = 0; if(hogeEntity.hogeId != null) { hogeId = hogeEntity.hogeId; } /** 以下割愛 **/ }

内容としてはhogeId に0を代入しておいて引数のhogeEntityクラスのhogeIdがNullじゃなければhogeIdに代入するというものです。他のプログラムの書き方を見ていて思いついた処理ですね。 ちなみにこの割愛している部分の処理でDBに接続してレコードを検索するのですがフレームワークの都合でhogeIdがNullだとwhere条件句からhogeIdの指定条件が消えてしまうのでhogeIdが異なっても他の条件に一致していればレコードがヒットしてしまうという事情があり上記の条件分岐と代入処理が必要でした。 この時「int」と「Integer」の違いを知らなかった私は「Integerってつまりintでしょ?nullにしようにも0しか入らなくない?」と最初は思っていました。
その後、「そもそもintにnull入れようとするとNullPointerExceptionが出るから0にすらならない..?」とか色々自分の中でも矛盾が発生したので気になって調べた、というのが今回の事の発端です。

「int」と「Integer」の違いとは

ざっくりと書くと
  • int → プリミティブ型
  • Integer → 参照型(intのラッパークラス)
ここで上記の条件の理由が分かった人はプログラミングを基礎からちゃんと理解できてる人だと思います。(というか「int null なぜ」とかググったりしないですよね..) 話を戻します。 まずラッパークラスについてですがこれも言い換えれば「基本のデータ型やオブジェクトを使いやすくするためにメソッドなどを追加したクラス」です。 あまりいい例が思いつかなかったのですが洗濯機を例にします。 洗濯の工程として 対象を投入する→洗う→干す(乾燥する)→畳む を想定すると 普通の洗濯機(基本のデータ型)では投入された対象を洗う機能しかないとなると乾燥以降の手順を自前で行う必要があります。 しかし乾燥機能付きの洗濯機(洗濯機のラッパークラスとする)であれば洗う→乾燥までの工程を洗濯機側でやってくれるので人間は対象の投入と畳む工程をやればよい、という感じですね。文明の利器バンザイ。 次にプリミティブ型と参照型についてですが、 プリミティブ型→値を持つ 参照型→メモリ上の値が格納されているアドレスを持つ という違いがあります。 こちらの記事 に良い具体例があったので引用させていただくと
int a = 1;
int b = a; // bにはaの値:1が格納される
a = 2;

Systemout.println(a); // 2 が出力される
Systemout.println(b); // 1 が出力される

int[] a = {1, 2, 3};
int[] b = a;
a[0] = 0;

Systemout.println(a[0]); // 0 が出力される
Systemout.println(b[0]); // 0 が出力される
int[] b = a の箇所ではint配列aと同じアドレスを見ているためaの値の変更が反映されていますね。 雑な結論ではありますが値が格納されているアドレスを見ているので「値の入っている場所(アドレス)が指定されていない」という状態が参照型におけるNullなのだと個人的に解釈しました。

終わりに

さて、そろそろ話を締めます。 今回学んだことを踏まえて発端となった条件分岐を考えると 引数のHogehogeエンティティのhogeIdがNullの場合は Hogehoge.hogeId に入る値のアドレスがない=アドレスが指定されていない=Null となることがあり得るということでした。 自分の備忘録的な部分がメインですが同じような疑問を持っていた方の助けになればと思います。

目当てのIPアドレスを探せ

ある日、ネットワーク内につないだ機器のIPアドレスが現地で分からなくなったので、調べてほしいと依頼がありました。当日は力技で何とかしましたが、もっと楽にできることあったので、振り返ってみます。

前提条件

幸いなことに、今回問題のネットワークでは、以下の点が分かっていました。
  • 現地の機器をつないでいるネットワークへアクセスできるサーバーに、本社オフィスからアクセスできた。
  • 探すべきIPアドレス帯は第3オクテットまで固定、かつ第4オクテットの範囲が決まっている。
  • IPアドレスを指定してwgetが成功することで、そのIPが当たりと言える。
これなら何とかなりそうです。

当日の手段:力業

当日は仕方なかったので、こんなシェルスクリプトを書いて、見つかったらCtrl+Cで対応しました。
wget -t 1 -T 3 --spider http://X.X.X.1/hogehoge;
wget -t 1 -T 3 --spider http://X.X.X.2/hogehoge;
・・・
wget -t 1 -T 3 --spider http://X.X.X.99/hogehoge;
wget -t 1 -T 3 --spider http://X.X.X.100/hogehoge;
IP1個につき1回wgetし、3秒待機するコマンドを第4オクテットとして考えられる範囲分(上の例なら100行分)を書いています。URLの存在チェックができれば十分だったので、–spiderを付けました。最初から1行ずつ実行していくだけのコマンドです。 コマンド自体は表計算ソフトで量産できますが、都度作るのは煩雑です。一発でできるようにしたいです。

次回からはこれを使う:ワンライナー

xargsを使うことで、ワンライナーで実行できます。
$ seq 1 100 | xargs -I@ wget -t 1 -T 3 --spider http://X.X.X.@/hogehoge;
seqコマンドで生成された数を、xargs以下に渡しています。X.X.X.@ のアットマーク分がxargsの引数で変化し順次実行していきます。これで目標とした検索が実現できます。 1から順に確認して見つけたらCtrl+Cで止めるのは、力業と同じです。

Ctrl+Cを使わない方法はあるか

Ctrl+Cで止めない方法も考えたいと思って、for文でコマンドを試作してみました。
## 1から順番に数を調べて、3の倍数になったらループを抜ける例。
for v in `seq 1 10`; do if [ $((v % 3)) -eq 0 ]; then echo fuga; break; else echo $v; fi; done
実際書いてみると、単語が込み入って見づらいと感じました。これなら、素直にシェルスクリプトファイルにした方が見やすそうです。よりよい方法があるかもしれませんが、今回はこれ以上の調査を見送ります。

余談

「ワンライナー」という言葉を使いましたが、似た感じの言葉で「リニア」があります。「ライナー」と「リニア」の違いが気になって調べてみました。
  • ライナー(liner)
    • 定期運行する交通機関
    • 「京成スカイライナー」「おはようライナー」の「ライナー」はこちらの意味
  • リニア(linear)
    • 一直線に伸びた感じの意味
    • リニアモーターカー linear motor car
    • 線形代数学 linear algebra

C#でTesseract 5

矢野です。
社内向けに作成していたC#アプリケーションで利用するTesseractのバージョンを4系から5系に更新した概要を簡単にメモしておきます。Tesseractに限らずC#での画像認識に関する情報って、Pythonなどに比べると日本語でも英語でもかなり少ないですよね。ビジネスロジックをつくっていく段階であれば、もちろん他のプログラミング言語向けの情報でも大いに参考になるのですけれど、最初の一歩して「とりあえず動くようにする」ところがその言語なり、ライブラリなりの固有の手順が必要で、そういうところで引っ掛かりがちです。 .NET Framework, .NET向けに作成されたTesseractのラッパーライブラリとしてよく使われているのは恐らくcharlesw/tesseractだと思います。私もこれまでお世話になってきていたのですが、残念ながらTesseract 4.1までしかサポートされていません。今すぐ5系が必要な状況というわけではないですが、余力のある時に乗り換えておいた方が良いこともあるだろうと思って、対応したライブラリを探してみました。 上記のcharleswさんのところから派生したSicos1977/TesseractOCRを導入することで、ほんの少しのコードの修正でTesseract 4系から5系へ移行することができました。
NuGetパッケージマネージャーからTesseractOCRをインストール
Visual Studioからであれば、NuGetパッケージとして簡単にインストールできます。 以降前のcharlesw/tesseract向けのコードは、非常に簡素に書くとこんな感じ。

using Tesseract;

// 中略

// PictureBoxに表示
pictureBox1.Image = new Bitmap("image\\lipsum.png");

// 文字認識
var engine = new TesseractEngine("tessdataフォルダのパス", "eng");
using (var pix = Pix.LoadFromFile("image\\lipsum.png"))
{
    var page = engine.Process(pix);

    // 結果表示
    textBox1.Text = page.GetText();
}


これを、Sicos1977/TesseractOCR向けに修正すると、

using TesseractOCR;

// 中略

// PictureBoxに表示
pictureBox1.Image = new Bitmap("image\\lipsum.png");

// 文字認識
var engine = new Engine("tessdataフォルダのパス", "eng");
using (var pix = TesseractOCR.Pix.Image.LoadFromFile("image\\lipsum.png"))
{
    var page = engine.Process(pix);

    // 結果表示
    textBox1.Text = page.Text;
 }


名前空間とクラス名、オブジェクトの構成を少し修正するだけで、ほぼそのまま使えました。
分かりにくいけど上が認識対象の画像、下がTesseract 5.2での認識結果です。
えっ? たったこれだけ? そうなんです、たったこれだけでした。その先は、これまでのTesseract 4系向けにつくっていたのと同じようにビジネスロジックをつくり込んでいくことができました(同時期にEAST text detectorを組み込んでみたりもしたけど、それは別の話)。これまで未経験の技術とか、「たかが」ライブラリのアップデートとか、どうしても気後れしちゃうこともありますが、一歩踏み出してしまえば「たったこれだけ」で進めちゃうこともよくあるよね、というお話でした。

illustratorからSVGを出力してHTMLに表示する

株式会社イメージ・マジックの技術ブログ、今回の担当のsoenoです。
最近SVGを触ることがあり、いろいろできるんだなあという印象です。
手軽に使えるということでボタンをillustratorからSVGを書き出して実際に使うまでの流れを紹介します。

illustratorからSVGを書き出す

  1. illustratorでボタンを作成し、ボタンの要素をグループ化します。(要素を選択し右クリック)



  2. グループ化した要素をもう一度右クリックし、書き出し用に追加(単一のアセット)します。



  3. アセットの書き出しタブの中の書き出し設定のセレクトボックスでSVGを指定します。



  4. アセットの書き出しパネルの右下のボタンから書き出しを選択。


  5. 書き出したい場所を選んで保存。

HTMLに埋め込む

HTMLでイメージの参照先に保存したSVGを指定する(下のパスは同一階層にある場合の指定)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>svg追加</title>
</head>
<body>
    <img src="btn.svg" width="79" height="35">
</body>
</html> 


上のコードをブラウザで表示させた時

最後に

上の例ではillustratorから書き出したSVGをHTMLに組み込む場合の流れを書きました。
illustratorからの書き出し方にもSVGの埋め込み方にも他の方法があります。
表示方法でできることが変わったりもします。
(SVGをHTMLに埋め込めばjavascriptから見た目を変更できる等)
また、SVG自体も使うプロパティーでいろいろな表現ができます。
プロパティーはブラウザの対応状況にばらつきがあるので使用時には少し注意がいりそうです。
自サイトのブラウザの対応範囲と、代替え手法が用意できるかなど、
様子見しながら使う感じになるのではないかと思います。

IllustratorのVer UPによるPNG読込挙動について

はじめに

こんにちは、イメージ・マジックのもあいです。
弊社では印刷用データの自動処理でPhotoshopやIllustratorのスクリプトを使用する事があるのですが、最近でIllustratorのバージョンアップを行ったら正常に動作しなくなったと言うことがありました。
その事象について備忘録として記事を残しておくことにします。

事象

具体的にどうなったのかというと、Illustratorに配置している画像がずれていてかつ大きさが小さくなるという事象でした。 報告を受けて使用しているIllustratorのバージョンを報告者に確認したところ26.1.0/26.2.1は問題が発生しないが、26.5だと発生するとのことでした。こういう事象が発生したときにバージョンを確認するとよくあるのが「最新版」という報告ですが、それが無かっただけでも良かったと思っています。
ただ、それでも不明なバージョンもあったので、Adobe Creative Cloudでバージョンダウングレードテストも行って、結果としては26.2まではOKで26.3/26.4/26.5はNGでした。

原因

Illustrator上で画像が小さくなったとのことでしたので、最初から解像度の問題であることを疑いましたし、それが正解でした。サーバ上で解像度(DPI値)のみを埋め込んでいたのですが、単位までは埋め込んでいなかったのが原因で、それによってIllustratorのバージョンによって解釈が異なっていることがわかりました。 Illustratorは解像度を埋め込んでいない画像については72dpiとして扱います。26.2までは解像度のみを埋め込んでも72dpi扱いで、単位を埋め込んで初めて認識します。26.3からは解像度のみを埋め込むと単位をdpiで認識していました。これが原因して今まで動いていたものが動かなくなりました。

検証

5種類の画像を用意してIllustratorの26.2と26.5に読み込ませたらどうなるかを検証しました。
下記のような5種類の画像を用意しました。
No 解像度 単位 大きさ(px) 備考
1 なし なし 800 x 80 何も埋め込んでいないバーコード画像
2 なし なし 610 x 88 1に余白をつけたバーコード画像
3 200 なし 610 x 88 1に余白をつけて解像度を付与
4 200 PixelPerInch 610 x 88 1に余白をつけて解像度/単位を付与
5 72 PixelPerInch 610 x 88 1に余白をつけて解像度/単位を付与
下記の画像ですが上記の表の画像を上から順にIllustrator 26.2.1に読み込ませました。
No4は200dpiとして認識しているので小さく表示されていますが、それ以外は一緒です。   次にIllustrator26.5で読み込ませた結果です。
No4はバージョンが異なっても同じですが、No3の解像度のみ指定している画像も200dpi扱いになって小さく表示されます。 ※上記2つの画像は同じ拡大率で表示してるものを縦に並べてスクリーンショットをとっています。

結論

画像の解像度を埋め込む場合は、解像度とともに単位も設定する。片方だけだと誤動作するので厳密に値を設定するのがよい。

ImageMagickで服の色を変える

こんにちは岡野です。
現在開発中のプロジェクトで、服の色を変更する処理を実装したので紹介します(簡易的な方法です)。

環境

ImageMagick 6.9.12-59 Q8

手順

1. 元となる画像を用意します。例:Tシャツ ホワイト。
2. 例えばネイビーへ変更するとして変更前後の色についてRGBそれぞれの差分を計算します。
ホワイト: rgb(255, 255, 255)
ネイビー: rgb(32, 47, 85)
R: 32 - 255 = -223
G: 47 - 255 = -208
B: 85 - 255 = -170
3. ImageMagickのevaluate Addに差分の値を渡して色を変換します(Q16環境では各値を256倍します)。
convert src.png \
-channel R -evaluate Add -223 \
-channel G -evaluate Add -208 \
-channel B -evaluate Add -170 \
dest.png

まとめ

ImageMagickだけでも結構良い感じになりました。将来はAIで着色したいと思います。

その他

最近Zabbixでハマったことです。グラフ内の文字が出力されないため悩んでいたのですがopen_basedirの設定不足でした(zabbix_server.logに気をとられNginxのログに気づくのが遅れた)。何か問題があった際は落ち着かないとダメだと感じました。

SymfonyとVue.jsを組み合わせるときはSymfony Formを使うのは諦めた方が良さそう

初めまして。イメージ・マジックのスズキです。
いきなりですが、本稿は『SymfonyとVue.jsを組み合わせるときは「Symfony Forms」の機能を使うのは諦めた方がいいかもしれない』ということを伝えたい、ただそれだけの記事です。『エンジニアたる者、どんなときでもまずは結論から話せ』と、物心ついた頃(嘘)から聞かされてきましたしね。
Symfonyで一般的なデータの入力や保存を行う画面をつくるなら、「Symfony Forms」という機能を利用するのが基本のようです。
公式ドキュメントでも、基本的な実装方法として紹介されています。

Symfony Formsを使うことで、コントローラー側でフォームと画面項目の定義をしておいて、画面側(テンプレート側)では自力でタグを書いたりせずに表示することができます。
試しに、テキスト項目が2つあるだけの画面を作る場合、こういう感じになるでしょうか。
// エンティティ
namespace App\Entity;

class Master
{
    private string $name = '';
    private string $description = '';

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }

    public function getDescription(): string
    {
        return $this->description;
    }

    public function setDescription(string $description): self
    {
        $this->description = $description;
        return $this;
    }
}
// フォーム
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class MasterEditType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class)
            ->add('desctiption', TextType::class)
        ;
    }
}
// コントローラー
namespace App\Controller;

use App\Entity\Master;
use App\Form\Type\MasterEditType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class MasterEditController extends AbstractController
    public function edit(Request $request) : Response
    {
        $master = new Master();
        // ...
        
        $form = $this->createForm(MasterEditType::class, $master);

        return $this->render('master/edit.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}
<!-- 画面テンプレート(Symfony Form使用) -->
<div>
    <div>
        <label>名称</label>
        {{ form_widget(form.name) }}
    </div>
    <div>
        <label>説明</label>
        {{ form_widget(form.description) }}
    </div>
</div>
画面のテンプレートには <input type="text"> といったごく普通のHTMLタグが登場しません。Symfony formsがタグを生成してくれるのですね。

ただ、リッチなフロント画面をVue.jsでごりごり書きたいときは、<input type="text" v-model="master.name">などとVue.jsのディレクティブを入れることになるのですが、Symfony Formsを使っているとそれがしにくくなります。
そんなわけで、Vue.jsでいろいろやりたいならば、Symfonyは使ったとしてもSymfony Formsの機能は使わない方が良さそう、といった印象です。

Symfony Formsを使わずに書くなら、サーバ側ではSymfony Formの代わりに単純なオブジェクトを作ってリターンし、画面側ではVue.jsのお作法でそれを表示するような作りが良さそうです。
先ほどのコードを、Symfony Formsを使わずに(Vue.jsを使えるように)書き直すと、こんな感じでしょうか。
// 変更後のコントローラー(Symfony Formを使わない)
namespace App\Controller;

use App\Entity\Master;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class MasterEditController extends AbstractController
    public function edit(Request $request) : Response
    {
        $master = new Master();
        // ...

        return $this->render('master/edit.html.twig', [
            'master' => [
                'name' => $master->getName(),
                'description' => $master->getDescription(),
            ],
        ]);
    }
}
// 変更後の画面テンプレート(Vue.js使用)
<div>
    <div>
        <label>名称</label>
        <input type="text" v-model="mst.name">
    </div>
    <div>
        <label>説明</label>
        <input type="text" v-model="mst.description">
    </div>
</div>
<!-- Vue.jsのスクリプト部分 -->
<script>
export default {
  props: {
    master: {},
  },
  setup(props) {
    const mst = ref(props.master);
    return {
      mst,
    };
  },
}
</script>
ということで今回は、SymfonyとVue.jsを組み合わせるときのお話でした。Symfony単体やVue.js単体での使い方は情報があっても、組み合わせ方の情報はなかなか無いので書いてみました。
Web界隈はフレームワークやライブラリがたくさんあって、組み合わせるときは試行錯誤が要りますね。

PHP触ったことのないプログラマーが動的な型付けに頭を抱えた話


こんにちは!5月に入社した黒羽(くろは)です!


隔週での更新を目標に開発一同で再開しましたテックブログ、再開後としては2つ目(?)の記事になります。
いきなり社歴の浅い新人が書いているので驚かれる方もいると信じて簡単な自己紹介をしつつ、本題に入っていこうと思います。 
さて、文系の学部を出てプログラマーになって6年目も中盤に差し掛かっておりますが、大学在学中よりも時間の経過が速かった気がします。
今までに取り扱った言語としては以下のような感じです。
  • Java
  • C#
  • Python
  • javascript
  • CSS
  • HTML
  • その他にコマンドプロンプト(世の人々がプログラムと聞いてイメージするであろう『黒い画面に文字がダーッとでる』やつ)で使用するスクリプト類など…
  最後のは言語じゃなくてコマンドの塊じゃん!と言われそう イメージマジックの開発で主に使われている言語はPHPなので入社して初めてPHPを触りました。
今まで触ってきた言語とは書き味や仕様が違っていて日々調べながら習得を進めています。
今日はそんな言語仕様に振り回されてハマってしまった時の原因について調べたので(社内ではそりゃ当たり前だろといわれそうなので未来のPHP初学者に向けて)共有できればと思います。

きっかけ


イメージマジックの各プロジェクトのPHPのソースコードを眺めているとどのソースにも
declare(strict_types=1);
と出てきます。 これは何ぞやと調べてみたところ次の記事に行き当たりました。
この記事の冒頭でこのように記述されていました。
 
 
declare(strict_types=1); とは、PHP7から導入された、厳格な型検査モードの指定構文です。

厳格な型検査モード・・・・?????と思いつつ読み進めてみると更に以下の記述が。
<?php
function add(int $a, int $b): int
{
    return $a + $b;
}

var_dump(add(1.0, 2.0));
この状態で単体実行すると、int(3)が出力されます。
  この時の私の頭の中では1つの考えに支配されていました。
「int型の変数にdouble型を堂々と代入するな!!!!!!」


言語による型付け


あまりに理解ができなくて大声になりましたが気を取り直していきます。
プログラミング言語には開発者が変数や戻り値になど事前にどういう型のデータが入るかを指定する静的な型付けと実行時にコンパイラやインタプリンタがコードを解釈してよしなにデータ型を判断してくれる動的型付けがあります。
PHPは言語仕様として動的な型付けを行っています。
私はJava()静的な型付け)からプログラミングをはじめて、当時の学習用テキストにもデータ型が一致しないとエラー吐くと脅されて教えられました。
そのため、データ型が定義されている変数に異なるデータ型を突っ込まれると困ってしまうわけです。
しかもdouble同士の加算でint型で返ってくるとはこれ如何に。
上記のサンプル関数では$a=1.0≒1(int型)、$b=2.0≒2(int型)としてPHPの方で判断しているようです。
話は逸れましたがdeclare(strict_types=1);を付けてさきほどの関数を実行すると

PHP Fatal error: Uncaught TypeError: Argument 1 passed to add() must be of the type integer, float given, called in /Users/hiraku/sandbox/stricttypes/A.php on line 9 and defined in /Users/hiraku/sandbox/stricttypes/A.php:4 Stack trace: #0 /Users/hiraku/sandbox/stricttypes/A.php(9): add(1, 2) #1 {main} thrown in /Users/hiraku/sandbox/stricttypes/A.php on line 4

とエラーが発生します。
つまりdeclare(strict_types=1);とは動的な型付けをしているPHPで静的な型付けのように振舞わせる宣言という風に解釈できます。
ちなみにこの宣言をしているファイルを呼び出している場合のみ有効であるため、別ファイルからuse文等で参照して同じように関数を呼び出しても緩い型検査のままであるのでint型で返ってきます。
つまり引数を$a=1.0,$b=2.0とする場合、
  • declare(strict_types=1); を宣言しているファイルAのadd関数を呼び出す⇒Typeエラーが発生
  • declare(strict_types=1); を宣言しているファイルAを参照しているがdeclare(strict_types=1); を宣言していないファイルBでファイルAのadd関数を呼び出す⇒int型の戻り値が返却される。 
ということです。

最後に


つまるところ何が言いたかったのかというとカルチャーショックを受けたという話でした。
動的な型付けは手軽な一方で想定していない値が返ってくる可能性があるのでやはり多少面倒でも静的な型付けできっちりとデータ型を指定して書く方が私は好きです。(というかぱっと見で何の値が入ってくるのかソースから読み取れないと後々困ることもある)
最後に余談となりますがこの記事を書いてる間に裏どりがてらに色々調べていたのですが 「動的な型付け」と「型推論」は同じものだと思っていましたが違うもののようです。
ここら辺も少し調べてみようと思います。
ここまで読んでいただきありがとうございました。何かの参考になれば幸いです!


LaravelのCollectionパッケージでPHPの配列操作を劇的に快適にする

こんにちは、池田です。最近家の鍵をsesame4というスマートロックにしました。鍵の向きを考えずにスマートフォンをかざすだけでドアが開くというのは、スマートフォンの充電端子がType-Cになったときと、駅の改札を交通系ICで通れるようになったときを合わせたような感動があります。まあどっちも昔のことで大して覚えていないので、適当言ってるだけなんですが。

本題

さて、皆さんはこの世で最も邪悪なものを知っていますか。そう、PHPでの配列操作のやりにくさですね。どれだけ配列操作がやりにくいかについては、以下の記事を読むのが良いと思います。
PHPを使いもせずDISってる君達へ 
特に記事のPHP Tips 5 : array_filterが歯抜けになるのに気をつけようの現象には、
自分「よし、配列をフィルタして最初の値を取ろう!」
array_filter([1,2,3], fn(int $num) => $num > 1)[0]
PHP「だめだよ」
自分「???」
理由:array_filterされた配列は[1 => 2, 2 => 3]になっており、0のキーは存在しないので取れないから
と何度も苦しめられました。さすがに学習した今でもこの挙動に関しては納得できず、思い出す度にFワードが出そうになります。会社のテックブログじゃなければ出してます。
また、連続で配列操作関数を使用した際のネストの深さ、読みにくさもピカイチです。こちらは先程の記事で一番最初に紹介されてますね。
これは Symfony という素晴らしいフレームワークを使っていても、如何ともし難いものがありました。先程の記事にもあったように、Symfonyでは配列をラップしたDoctrineの Collectionクラス が存在するんですが、メソッドの数が痒い所に手が届かない感じで(ソートができない!)(フラットにできない!)(ユニークにできない!)、複雑な配列操作をしようとするなら一度ネイティブの配列に戻すことが必要になります。なんか気持ち悪いですね。継承してオレオレ実装もあまりしたくありません。絶対バグを生み出すので。
ならばどうすれば良いのか……?ここで本題です。LaravelのCollectionパッケージを使いましょう。

何が良いのか

公式ページ にアクセスして、メソッド一覧を見てください。たくさんのメソッドありますね。131個あるらしいです。これだけ数があるということは、ニーズにあったメソッドも存在する可能性が高いということです。
例えば、先程挙げた3個の「ソート」「フラット」「ユニーク」は全て用意されたメソッドで可能です。また、無駄にネストを深くする必要もありません。
collect([1, [2, 3, [4, 2]]])
  ->flatten()
  ->unique()
  ->sort();
上記のように、非常にシンプルな記述をすることが可能です。
中でも、私がイチオシのメソッドは mapWithKeys です。皆さんも幼稚園くらいの頃に「以下のように連想配列に変換したいな……」と考えたことがあると思います。
↓これを
[
    [
        'name' => 'John',
        'department' => 'Sales',
        'email' => 'john@example.com',
    ],
    [
        'name' => 'Jane',
        'department' => 'Marketing',
        'email' => 'jane@example.com',
    ]
]
↓こうする
[
    'john@example.com' => 'John',
    'jane@example.com' => 'Jane',
]
ネイティブの配列操作関数でやるなら、恐らくarray_reduceでやる必要があると思います。ですが読みにくくなるので、使わないに越したことはありません。LaravelのCollectionを使えば以下のように簡単に変換することができます。
$collection = collect([
    [
        'name' => 'John',
        'department' => 'Sales',
        'email' => 'john@example.com',
    ],
    [
        'name' => 'Jane',
        'department' => 'Marketing',
        'email' => 'jane@example.com',
    ]
]);

$keyed = $collection->mapWithKeys(function ($item, $key) {
    return [$item['email'] => $item['name']];
});

$keyed->all();

/*
    [
        'john@example.com' => 'John',
        'jane@example.com' => 'Jane',
    ]
*/
別の言語だとJavaScriptは結構配列操作がやりやすいと思っているんですが、それでもこの変換をここまでシンプルにはできなかったと思うので、mapWithKeysには驚きました。 しかし、中にはLaravelを使っていない人もいるでしょう。Laravelのパッケージをまるごと入れてCollectionだけ使うしかないのでしょうか。当然そんなことはありません。以下のコマンドでこれだけ(+α)導入可能です。
composer require illuminate/support
これにより、Symfony環境であってもLaravelのCollectionの素晴らしさを享受することができます。
……え、フレームワーク使ってない?Composerなんてない?……それは……その……どうすればいいんでしょうね……?

まとめ

  • PHPネイティブの配列操作はやりにくい
  • LaravelのCollectionパッケージを使えば途端にやりやすくなる
  • 単体(+α)導入も可能