laravelにてファイルからhtmlを作成する話

今回はシンプルにcsvやyamlファイルからデータを読み取りHMTLにて表示する話です。

ま、bladeは今回の説明範囲外ですが… と管理責任範囲を言ってみますw
うん、良くある話ですが、ここまではサービス – ここまではシステムって言う折衷箇所が発生します!

要するに、<html><head><title>の部分まで、内部のロジック作っているシステムまでが関与しますか?

もちろんNOです!

それは営業・サービス系のお仕事です。html位だれでもいぢれるでしょ?w

っと入っても、blade.phpの敷居が…. しらねぇ〜!勉強しろ!

さて、今回はこんな感じで始まりますw

どうすれば、システムから手離れできるか?

そりゃ、動的な部分を洗い出せばOKでしょう
あとは静的な部分は営業・サービスさんにお願いします。

簡単にbladeを考えてみる
今回はCSV,YAMLからデータを読み出して、結果を返します。
シンプルに、dateとstringにしましょう
string,string
これを、表にしましょう

<table>
  <thead>
    <tr>
      <th>日付</th>
      <th>コメント</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2020/11/22</th>
      <td>初回です、よろしく</td>
    </tr>
  </tbody>
</table>

って感じで表示しましょう
もちろん、tbodyは配列で続いてきます….

はいっ!ここまで来ると、先に出ますね?
画面定義書w
うん、一致するはずです!!です!すっ…

簡単にclass化するとこんな感じです

<?php
namespace App\Classes\Views\Packages\elements;
class ReadTableLineElement{
    public $date;
    public $comment;
}

ですよねぇ〜w

さて、このページにこれを当てはめるのでそのクラスはこんな感じ

<?php
namespace App\Classes\Views\Packages;
use App\Classes\Views\Packages\elements\ReadTableLineElement;
class ReadViewPackage{
    /**
     * @var ReadTableLineElement[] 結果テーブル配列
     */
    public $table = array();
}

うん、シンプル!bladeを作りますが、シンプルに…

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <title>read result</title>
</head>
<body>
<h1>result.</h1>
<table>
    <thead>
    <tr>
        <th>日付</th>
        <th>コメント</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <th>2020/11/22</th>
        <td>初回です、よろしく</td>
    </tr>
    </tbody>
</table>
</body>
</html>

値ねぇ〜w
さっそくリクエストのtable要素の繰り返しをして、それぞれを当てはめます

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <title>read result</title>
</head>
<body>
<h1>result.</h1>
<table>
    <thead>
    <tr>
        <th>日付</th>
        <th>コメント</th>
    </tr>
    </thead>
    <tbody>
@foreach($table as $tableElement)
    <tr>
        <th>{{ $tableElement->date }}</th>
        <td>{{ $tableElement->comment }}</td>
    </tr>
@endforeach
    </tbody>
</table>
</body>
</html>

ま、解説するまでもないシンプルです

簡単なアクセス引数とサービスをまとめておきます

/read/csv はCSVを使ったサービス
/read/yml はyamlファイルを使ったサービス
/read/ 試験用の変数で作ったサービス

プログラムソースに直接値をほりこむ方式から全体のわだちを作ります。

流れ全体を考えると…

/read -> なんりゃかのController -> とりあえずControllerService -> 値を取得するService

そんな流れかな?

まず、Controllerを考えますが、今回はreadって感じなので、ReadControllerってします。

Http/Controllers/ReadController.php

<?php
namespace App\Http\Controllers;
class ReadController extends Controller{
}

うん。で… 何もないののメソッドと考えると… indexかなw
indexで来たのを先のクラスを作って返します…
ControllerServiceも作りますのでちょっぴり複雑です

Classes/Services/Controllers/ReadControllerService/Request.php
Classes/Services/Controllers/ReadControllerService/Response.php

とりあえず、サービスのI/Oはまだ決まっていないので空にしました。
Response.phpとReadViewPackage.phpが入っているはずですし、viewの名前が入るわけですから下のようになります

<?php
namespace App\Classes\Services\Controllers\ReadControllerService;
use App\Classes\Views\Packages\ReadViewPackage;
class Response{
    /**
     * @var string view名
     */
    public $view_name = 'read';
    /**
     * @var ReadViewPackage ページ構成要素
     */
    public $package = null;
}

うん、これでOK今回は説明を省くためコメントも入れましたw

これでサービスの応答が作成できました。
次は… 要求… Request… ないですねw

<?php
namespace App\Classes\Services\Controllers\ReadControllerService;
/**
 * Readクラスサービス要求
 * Class Request
 * @package App\Classes\Services\Controllers\ReadControllerService
 */
class Request{

}

これはこれで良い

構築と思ったらが、次は結果Outputを作ります

storage/tests/Feature/read/csv.html

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <title>read result</title>
</head>
<body>
<h1>result.</h1>
<table>
    <thead>
    <tr>
        <th>日付</th>
        <th>コメント</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <th>2020/11/22 22:00</th>
        <td>リース</td>
    </tr>
    <tr>
        <th>2020/11/22 23:00</th>
        <td>並ぶ</td>
    </tr>
    <tr>
        <th>2020/11/23 00:00</th>
        <td>アトラクション</td>
    </tr>
    </tbody>
</table>
</body>
</html>

とりあえず、いろいろ結果がこれになればOK

第零章 試験用の変数を使ったサービス

はじめに…試験を書きます!とりあえず!ビールで!

    public function test_index(){
        $response = $this->get('/read');
        $response->assertStatus(200);
        $contents = \File::get(storage_path('tests/Feature/read/index.html'));

        $this->assertEquals($contents,$response->getContent());
    }

File::get以外は普通ですね。
File::getは他の記事を漁ってください、ファイル内容を取ってくる物です
これで試験とか言うのは無しで…

これでInput / Outputの試験ができあがりました!
内容はありませんが;;

次は、routeを書きましょう!/routeをアクセスするとReadController@indexです

Route::get('/read', [ReadController::class,'index']);

※ 今回も説明が必要ですが、getはデフォルトメソッドはindexなのでメソッド名を省きました

※ indexは省けません!https://qiita.com/nagamoridaiki/items/12f2ff2a50ea4a13c84d

でっ!
app/Http/Controllers/ReadController.php
を埋めます…
index出来たら先の内容を埋める配列を作る…

    public function index(){
        $vewPackage = new ReadViewPackage();

        $vewPackage->table = array();
        $element = new ReadTableLineElement();
        $element->date = '2020/11/22 22:00';
        $element->comment = 'リース';
        $vewPackage->table[] = $element;
        $element = new ReadTableLineElement();
        $element->date = '2020/11/22 23:00';
        $element->comment = '並ぶ';
        $vewPackage->table[] = $element;
        $element->date = '2020/11/23 00:00';
        $element->comment = 'アトラクション';
        $vewPackage->table[] = $element;        
    }

こいつを返せば問題ないとは思いますが、もう少しトリッキーにします(あとの2つも含めて考えます。)

要求を作成する!
して、コントローラサービスに処理を渡す

class ReadController extends Controller{
    public $serviceController = null;
    public function __construct(ReadControllerService $serviceController){
        $this->serviceController = $serviceController;
    }

    public function index(){
        $serviceRequest = new Request();
        $response = $this->serviceController->route($serviceRequest);
        return view($response->view_name,(array)$response->package);
    }
}
class ReadControllerService
{
    public function route(Request $request){
        $vewPackage = new ReadViewPackage();

        $vewPackage->table = array();
        $element = new ReadTableLineElement();
        $element->date = '2020/11/22 22:00';
        $element->comment = 'リース';
        $vewPackage->table[] = $element;

        $element = new ReadTableLineElement();
        $element->date = '2020/11/22 23:00';
        $element->comment = '並ぶ';
        $vewPackage->table[] = $element;

        $element = new ReadTableLineElement();
        $element->date = '2020/11/23 00:00';
        $element->comment = 'アトラクション';
        $vewPackage->table[] = $element;

        $response  = new Response();
        $response->view_name = 'read';
        $response->package = $vewPackage;
        return $response;
    }
}

試験を実施….OK

Time: 00:00.108, Memory: 20.00 MB

OK (1 test, 2 assertions)
Process finished with exit code 0

… でっ!考えたら同じメソッドを流用するではないか!

だったら分岐をしなくては!
まずはサービスの要求を変更

<?php
namespace App\Classes\Services\Controllers\ReadControllerService;
/**
 * Readクラスサービス要求
 * Class Request
 * @package App\Classes\Services\Controllers\ReadControllerService
 */
class Request{
    /**
     * 一般的なのぉ〜
     */
    const GENERIC = 0;
    /**
     * CSVモード
     */
    const CSV = 1;
    /**
     * YAMLモード
     */
    const YAML = 2;

    /**
     * @var int 要求モード
     */
    public $type = Request::GENERIC;

    /**
     * 要求モード指定型のコンストラクタ
     * Request constructor.
     * @param int $type 要求モード
     */
    public function __construct(int $type){
        $this->type = $type;
    }
}

んで、ControllerService本体の処理の変更

    public function route(Request $request){
        $response  = new Response();
        $response->view_name = 'read';
        $response->package = $this->readViewPackage($request->type);

        return $response;
    }

ん?処理がなくなりました!w

readViewPackageにまとめましたw

    public function readViewPackage($type){
        $vewPackage = new ReadViewPackage();
        $repository = new GenericReader();
        foreach ($repository->findAll() as $readElement){
            $vewPackage->table[] = new ReadTableLineElement($readElement->date,$readElement->comment);;
        }
        return $vewPackage;
    }

はい、This is it.です。$typeは使っていませんが新しい処理Repositoryが登場しました!!!w

Repositoryとは… 調べてねw要するにファイルとかDBとかのI/Oを仕切るサービスです….我理論!

今回は、変数でもCSVでも、YAMLでも同じ結果が欲しいわけです。
それを定義します。
要求は無視して、応答のインタフェイスは…

<?php
namespace App\Classes\Repository\Read;
class ReadElement{
    /**
     * @var string 日付
     */
    public $date;
    /**
     * @var string コメント
     */
    public $comment;
}

こんな感じ。

実際のRepositoryの処理は..

<?php
namespace App\Repository\Read;
use App\Classes\Views\Packages\elements\ReadTableLineElement;
class GenericReaderRepository extends AbstractReaderRespository{
    public function findAll(){
        $results = array();
        $results[] = new ReadTableLineElement('2020/11/22 22:00', 'リース');
        $results[] = new ReadTableLineElement('2020/11/22 23:00','並ぶ');
        $results[] = new ReadTableLineElement('2020/11/23 00:00','アトラクション');
        return $results;
    }
}

ま、こんな感じPHPのレベルとしては低いですが、プログラムの構築系のレベルではちょっぴり高いですね。

いつも考えるイメージは、レゴのジョイント部分です…
師匠もそんなことを言っていました
Lollipopイメージって言ってたかな?言い得て妙だけれども、個人的にはレゴのジョインの方がぴったりきました

◇レゴ∥LEGO【ボールジョイントブロックのセット 22個】 [R54177] の落札情報詳細| ヤフオク落札価格情報 オークフリー・スマートフォン版

こんなイメージです。要求 / 応答を一致させれば、使い回しも出来ます。
ロジックを見ていただいたからなら分かりますが、変数の受け口、CSVの受け口、YAMLの受け口をそれぞれ切り換えられるように作ります。

んでもって!それをPHPUnitで書いてやります。
試験のパーツ > 実際のパーツ > 試験のパーツ
これを作ると実際のパーツは確実です

んで… 何やってたかなw

悩んで… 試験して… コミットw

第一章 CSVファイルを読み取る方法

さて、仕切り直しまして、つぎはCSVファイル内容を出力するページを作ります
まずは試験を作ります

storage/files/reads/csv.csv

2020/11/22 22:00,リース
2020/11/22 23:00,並ぶ
2020/11/23 00:00,アトラクション

はいw
ここでついて行けない人は泣いてください。

次は、シンプルに試験を作ります

    public function test_csv(){
        $response = $this->get('/read/csv');
        $response->assertStatus(200);
        $contents = \File::get(storage_path('tests/Feature/read/csv.html'));

        $this->assertEquals($contents,$response->getContent());
    }

試験しても NGです…. か?

Expected status code 200 but received 404.
Failed asserting that 200 is identical to 404.

ですねw ちょっと脱線

まずは、先のわだちから考えると… 処理に入る部分が必要です
routeを追加

Route::get('/read/csv', [ReadController::class,'csv']);

csvメソッドに行きましょう!!!
app/Http/Controllers/ReadController.php

    public function csv(){
        // ControllerServiceへの要求
        $serviceRequest = new Request(Request::CSV);
        // ControllerServiceの処理
        $response = $this->serviceController->route($serviceRequest);
        // viewの返却
        return view($response->view_name,(array)$response->package);
    }

Request::CSV以外はindex(変数型)と同じですね… (リファクタリングしたいw)

serviceController->route($serviceRequest)の部分が変わりますので書きます!

    public function route(Request $request){
        $response  = new Response();
        $response->view_name = 'read';
        $response->package = $this->readViewPackage($request->type);

        return $response;
    }

    public function readViewPackage($type){
        $vewPackage = new ReadViewPackage();
        $repository = new GenericReaderRepository();
        // switch($type)
        foreach ($repository->findAll() as $readElement){
            $vewPackage->table[] = new ReadTableLineElement($readElement->date,$readElement->comment);;
        }
        return $vewPackage;
    }

readViewPackageメソッドのswitchがヒントです。リポジトリを$typeで切り換えればOKです!
まだ、作っていませんがw

…ってちょっと悩みましたが、できあがりました
readViewPackageメソッド

    public function readViewPackage($type){
        $vewPackage = new ReadViewPackage();
        $repository = new GenericReaderRepository();
        switch($type){
            case Request::CSV :
                $repository = new CSVReaderRepository(storage_path('files/reads/csv.csv'));
                break;
        }
        foreach ($repository->findAll() as $readElement){
            $vewPackage->table[] = new ReadTableLineElement($readElement->date,$readElement->comment);;
        }
        return $vewPackage;
    }

ファイルを指定してCSVの内容を持ってきます。

ファイルの読み取りは、SplFileObjectってのを使います!

class CSVReaderRepository
{
    private $file;
    public function __construct(string $file_path)
    {
        $this->file = $file_path;
    }

    /**
     * 全件取得メソッド
     * @return ReadElement[] 結果配列
     */
    public function findAll() : array{
        $file = new \SplFileObject($this->file);
        $file->setFlags(\SplFileObject::READ_CSV |\SplFileObject::READ_AHEAD |\SplFileObject::SKIP_EMPTY |\SplFileObject::DROP_NEW_LINE);

        $results = array();
        // 一行ずつ処理
        foreach($file as $line)
        {
            $data = $line[0];
            $comment = $line[1];

            $results[] = new ReadTableLineElement($data, $comment);
        }
        return $results;
    }
}

それほど難しくないので…

これで問題なくOK!

第二章 YAMLってヤムルって読むらしいよ

次はYAMLファイルを読み込みます…

今回のターゲットYAMLファイルは以下だ!

-
  date : 2020/11/22 22:00
  comment : リース
-
  date : 2020/11/22 23:00
  comment : 並ぶ
-
  date : 2020/11/23 00:00
  comment : アトラクション

アクセスは「/read/yaml」で同じページを返します…

    /**
     * YAML疎通
     */
    public function test_yaml(){
        $response = $this->get('/read/yaml');
        $response->assertStatus(200);
        $contents = \File::get(storage_path('tests/Feature/read/yaml.html'));

        $this->assertEquals($contents,$response->getContent());
    }

結果のyaml.htmlはcsvと同じです

次はrouteを編集して、/read/yaml -> Controller@yamlに遷移させます

Route::get('/read/yaml', [ReadController::class,'yaml']);

つぎは、そのyamlメソッドをControllerに実装します

    public function yaml(){
        // ControllerServiceへの要求
        $serviceRequest = new Request(Request::YAML);
        // ControllerServiceの処理
        $response = $this->serviceController->route($serviceRequest);
        // viewの返却
        return view($response->view_name,(array)$response->package);
    }

Request::YAML以外はcsvのコピーです… リファクタリングしたい!

次はこのなかの $this->serviceController->routeを編集します

    /**
     * Readの結果ページのデータを作成します
     * @param $type int リクエストタイプ
     * @return ReadViewPackage ページデータ
     */
    public function readViewPackage($type){
        $vewPackage = new ReadViewPackage();
        $repository = new GenericReaderRepository();
        switch($type){
            case Request::CSV :
                $repository = new CSVReaderRepository(storage_path('files/reads/csv.csv'));
                break;
        }
        foreach ($repository->findAll() as $readElement){
            $vewPackage->table[] = new ReadTableLineElement($readElement->date,$readElement->comment);;
        }
        return $vewPackage;
    }

ここがキモですね。
switchをYAML対応させる必要があります
なので、YAMLReaderRepositoryを作りますが…
まずはYAMLReaderRepositoryTestを作ります!

YAML解析は… ここが詳しい!

http://35.78.51.61/archives/2018

ごめん嘘ですw

あんまり書いていませんが、yaml扱うにはcomposerにてパッケージを追加します

composer require symfony/yaml

色々、書いていましたが、上のコマンドだけでOKです

    public function test_findAll(){
        $repository = new YAMLReaderRepository(storage_path('files/reads/yaml.yml'));
        $results = $repository->findAll();
        $this->assertEquals('2020/11/22 22:00',$results[0]->date);
        $this->assertEquals('リース',$results[0]->comment);

        $this->assertEquals('2020/11/22 23:00',$results[1]->date);
        $this->assertEquals('並ぶ',$results[1]->comment);

        $this->assertEquals('2020/11/23 00:00',$results[2]->date);
        $this->assertEquals('アトラクション',$results[2]->comment);
    }

 CSVとかと同じ書き方です。

実装!!!

class YAMLReaderRepository extends AbstractReaderRepository{    
    private $file;
    public function __construct(string $file_path){
        $this->file = $file_path;
    }

    /**
     * 全件取得メソッド
     * @return ReadElement[] 結果配列
     */
    public function findAll() : array{
        $yaml = Yaml::parse(file_get_contents($this->file));
        $results = array();
        foreach($yaml as $line)
        {
            $date = $line['date'];
            $comment = $line['comment'];

            $results[] = new ReadTableLineElement($date, $comment);
        }
        return $results;
    }
}

ふぅ〜〜 できたぁ〜〜

試験を実行すると…


Time: 00:00.182, Memory: 22.00 MB

OK (14 tests, 28 assertions)
Process finished with exit code 0

うみぃ〜〜〜〜!!!!

これでブラウザでアクセスしてみるw(今までしてないのかよ!)

おぉぉぉぉけぇぇぇぇぇ〜〜

これで世界平和に貢献できました!

はぁ〜、実は地味ネタと思っていたんですが、めっちゃヘビーでした。

次はどうしよう?DBかな?それとももう少し違うのをするかを考えます!

fin.

番外編 1. CSVにて困った系…

結論から言いましょう…

CSVじゃなくてHTMLを指定していましたよぉ〜どらえもぉ〜んw

class CSVReaderRepositoryTest extends TestCase
{

    public function test_findAll(){
        $repository = new CSVReaderRepository(storage_path('files/reads/csv.csv'));
        $results = $repository->findAll();
        $this->assertEquals('2020/11/22 22:00',$results[0]->date);
        $this->assertEquals('リース',$results[0]->comment);

        $this->assertEquals('2020/11/22 23:00',$results[1]->date);
        $this->assertEquals('並ぶ',$results[1]->comment);

        $this->assertEquals('2020/11/23 00:00',$results[2]->date);
        $this->assertEquals('アトラクション',$results[2]->comment);
    }
}

こんな試験と、loopにてprint_rを付けてやればデバグできました!