PHPでblob型でデータベースに保存した画像をHTML(twig)に出力する

# 概要
blob型でデータベースに保存したpng画像のバイナリデータを、PHP + Symfony + twigで画像として出力する方法について、調査に半日くらいかかったので、記事にまとめました。

今回は外部に画像ファイルを置けないという特殊な都合によりこの方法を行いました。データベースに画像を保存したり、[data URI scheme](http://ja.wikipedia.org/wiki/Data_URI_scheme)を使用することはよく考えて選ぶ必要があります。

# 動作環境
[EC-CUBE4 独自プラグイン開発　①Dockerで環境構築 + xdebug導入](https://qiita.com/haruna-nagayoshi/items/948b196cf4f5186641e9)
この記事と同じ。

```
EC-CUBE 4.0.3
Symfony 3.4
PHP 7.3
MySQL 5.7
```

# 結論
結論からいうと、
①バイナリデータを`stream_get_contents()`と`base64_encode()`で変換する。
②HTMLには、`<img>`タグのsrc属性に[data URI scheme](http://ja.wikipedia.org/wiki/Data_URI_scheme)とMIME-Type(どんな画像か、png or jpeg or etc...)と共に①を渡す。
を行えば、画像を表示できました。

```html
<img src="data:image/png;base64,xxxxx..." />
```

※画像に合わせてMIME-Typeの部分を変更する。

# サンプルコード
## Entity

本来は他のプロパティ・メソッドが存在しますが、ここでは省略します。

```php
<?php

namespace Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="products")
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="discriminator_type", type="string", length=255)
 * @ORM\Entity
 */
class Product extends AbstractEntity
{
    /**
     * @return string
     */
    public function displayPngImageFromString(): string
    {
        // stream_get_contents() の offset を 0 にする必要がある。 
        // 初期値の -1 だと、 displayPngImageFromString() を呼び出したときにポインタが移動したままになると推測され、
        // 2回目以降に呼び出したときにencodeされた文字列が出力されない=画像が出力されない。
        // 詳しくはPHPのマニュアルを参照。
        return base64_encode(stream_get_contents($this->getPng(), -1, 0));
    }

    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer", nullable=false, options={"unsigned"=true,"comment"="ID"})
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var resource
     *
     * @ORM\Column(name="png", type="blob", length=16777215, nullable=false, options={"comment"="png画像"})
     */
    private $png;
    
    /**
     * Get id.
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }
    
    /**
     * Set png.
     *
     * @param string $png
     *
     * @return Designtool
     */
    public function setPng($png)
    {
        $this->png = $png;

        return $this;
    }

    /**
     * Get png.
     *
     * @return resource
     */
    public function getPng()
    {
        return $this->png;
    }
}
```

## twig

画像を出力する部分だけ抜粋します。
画像をクリックするモーダルとして拡大表示される、商品一覧ページと思ってください。

```twig
{% for product in pagination %}
	{% set imageString = product.displayPngImageFromString %}
	<div class="ec-imageGrid__img">
    	<a onclick="openModal({{ product.id }})">
        	<img src="data:image/png;base64,{{ imageString }}" alt="">
    	</a>
	</div>
    <div class="background">
        <img src="data:image/png;base64,{{ imageString }}" alt="">
    </div>
{% endfor %}
```

```javascript
<script>
$(function () {
  'use strict';

  const modalBackground = $('.background');
  modalBackground.on('click', function () {
    modalBackground.fadeOut();
  });
});

  const openModal = function (id) {
    const image = $('.background');
    image.fadeIn();
    image.children('img').fadeIn();
  }
</script>
```

```css
<style type="text/css">
    .background {
        display: none;
        background-color: rgba(0, 0, 0, 0.5);
        width: 100%;
        height: 100%;
        z-index: 1;
        position: fixed;
        top: 0;
        left: 0;
     }
  
	.background img {
        width: 600px;
        height: auto;
        position: fixed;
        top: calc(50% - 290px);
        left: calc(50% - 300px);
        cursor: pointer;
    }
        
	@media screen and (max-width: 767px) {
        .background img {
            padding: 13px;
            width: auto;
            position: fixed;
            top: 20%;
            left: 0;
        }
    }
</style>
```

# URI Schemeによって画像の取得方法が異なる

通常、画像を配置する場合はこのように`<img>`タグに画像のパスを指定する。このとき、ブラウザはHTMLを読み込んだだけでは画像を表示できません。HTMLを読み込んだ後、`<img>`タグに指定された画像を取得するために、画像の数だけもう一度サーバと通信して画像を取得します。

```html
<img src="images/hoge.png" />
```

サーバと繰り返し通信を行うのは、CSSファイルなども同じです。



一方、このように `Data URI scheme`を指定した場合、画像は直接HTMLに埋め込まれます。その分、HTMLのファイルサイズは大きくなるためダウンロードに時間がかかりますが、ブラウザは画像の数だけ通信を行う必要がなくなります。


```html
<img src="data:image/png;base64,xxxxx..." />
```

よって、この方法を選択する必要がある場合は、小さな画像に対してのみ行うべきかもしれません。

Wikipediaにも`Data URI scheme`の短所・長所について説明があります。
[data URI scheme](http://ja.wikipedia.org/wiki/Data_URI_scheme)

以下、
[画像をBase64で変換して埋め込めば、HTTPリクエストがなくなり高速表示できる](https://yokonoji.work/095-image-base64)より

>しかし、Base64の扱いには注意が必要です。データサイズが37％も増加するので**ダウンロードにかかる時間は増加する**からです。そのため、データサイズの小さな画像に対してのみBase64を使うのが一般的です。

> 元のデータサイズが小さい場合は、**データサイズ増加によるダウンロード時間の増加よりも、リクエストとレスポンスの時間を削減する方が効果が大きい**が、データサイズが大きい場合は、削減したリクエストとレスポンスの時間以上にダウンロード時間が増加する可能性があるからです。

# おまけ
## blob型をもつテストデータをコピーする
今回、`fuga`テーブルにはblob型を含むテストデータが既に用意されていて、`hoge`テーブルにもblob型をもつテストデータが必要、という状況でした。
画像を`hoge`に保存する機能がまだ存在していなかったので、PhpStormやA5M2を使ってコピーしようとしたのですが、blob型のデータはコピーができませんでした。クリップボードに貼るにはデータ量が大きいからでしょうか・・・。
なので、このようなSQLを叩いてテストデータを用意しました。

```
insert into hoge
set file_data = (select png from fuga where id = 1),
    file_name = 'test.png',
    create_date = now();
```

# 参考
- [インラインで画像をHTMLに埋め込むData URLスキーム](https://www.softel.co.jp/blogs/tech/archives/2117)
  - DBから文字列を取得したものの、初めは`<img>`タグにどう渡せばよいか分からなかった。このサイトを見つけてようやく分かりました。
- [stack over flow  Display image stored in BLOB database in symfony](https://stackoverflow.com/questions/28294077/display-image-stored-in-blob-database-in-symfony)
  - ここの回答を見てEntityにメソッド追加してtwigから呼び出せばいいのかー、と気づいた。
- [PHP マニュアル stream_get_contents](https://www.php.net/manual/ja/function.stream-get-contents.php)
  - `stream_get_contents()` の `offset` を `0` にする必要がある。
    初期値の `-1` だと、 `displayPngImageFromString()` を呼び出したときにポインタが移動したままになると推測され、2回目以降に呼び出したときにencodeされた文字列が出力されない=画像が出力されない。
  -  「残りのストリームを文字列に読み込む」「既にオープンしている ストリームリソースに対して操作を行います。」という意味が分かっていない。
- [身につけておきたいWebサイト高速化テクニック #5｜リクエスト数削減テクニック01：インラインイメージ編](https://dev.classmethod.jp/articles/image-performance-tuning-03/)
  - こちらのサイトでは、画像の最適化・Webサイトの速度最適化に重きを置いて説明されています。