LaravelのTopページを作る話

今回はLaravelのtopページを考える激烈シンプル系でした。
はい、はじめは….w 結局一日仕事;;

はじめに、機能を考えてみました

  • /にアクセスしてHTMLを表示する
  • サンプルだけなのでそのリンクとBlogのリンクを書く

そだけですが、もう少しTickyにしましょう。

HTMLをstaticに直書きではなく、YAMLファイルに書いた内容を書き出します
つまりはEntityを作って今までの理想型へ!(桃源郷は存在しません)

そんな感じで試験を…

動的ページなので結果は200を確認するだけでOK

<?php
namespace Tests\Feature;
use Tests\TestCase;
class TopTest extends TestCase
{
    public function testBasicTest(){
        $response = $this->get('/');
        $response->assertStatus(200);
    }
}

っとまずはと考えましたが….
理想型をまとめてみますと、こんな感じ

私的には、この理想型をWeb系の開発には想定しています。四角内はLaraelのMVCっとなり、CをextendsしてContorollerのアクセス。
ControllerはLaravel範疇と考えて、ControllerServiceを作ります

上層はややこしくなりそうなので下層からはじめましょう!

第一章 YAMLはみんなの味方

はい、試験用に作ったYAMLファイルから行きましょうか

storage/files/utils/yaml.yml

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

ま、前回の流用、dateとcommentを取得します…
前回の流用…. 流用… コピペ?重複… Noooooooo!!!!そんなバグの入るところなんて私は許しません!と言う事で、YAMLを読み取るライブラリを作って動作保証を行える範囲を広げます。

はい、まずは試験!!配列で中身は、date,commentです。

Unit/utils/file/YAMLReaderTest.php

<?php
namespace Tests\Unit\utils\file;

use App\utils\file\YAMLReader;
use Tests\TestCase;

class YAMLReaderTest extends TestCase
{
    /**
     * YAMLReader::readだけのしけん
     */
    public function test_read(){
        $data = YAMLReader::read(storage_path('files/utils/yaml.yml'));

        $this->assertCount(3,$data);
        $this->assertEquals('2020/11/22 22:00',$data[0]['date']);
        $this->assertEquals('リース',$data[0]['comment']);

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

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

    }
}

はい、これでOKです。

ソースを書くと…

utils/file/YAMLReader.php

<?php
namespace App\utils\file;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;

/**
 * YAMLファイル読み込みツール
 * Class YAMLReader
 * @package App\utils\file
 */
class YAMLReader
{
    /**
     * ファイルを読み取ります
     * @param string $file ファイルパス
     * @return array 読込データ
     * @throws ParseException If the YAML is not valid
     */
    public static function read(string $file){
        return Yaml::parse(file_get_contents($file));
    }
}

… 1行で成立していましたw
ただ、file_get_contentsでデータを取っているのでちょっとだけロジカルな部分はあるんでしょうねw

動作の保証は出来たので…

今回管理させるEntity(YAMLファイル)はこんな感じです

storage/files/top/information.yml

-
  title : Hello World
  url : hello/
  create : 2020年11月21日 6:30 PM
  latest : 2020年11月21日 6:30 PM
  blog : http://35.78.51.61/archives/2152
-
  title : Say Hello World
  url : say/
  create : 2020年11月22日 11:39 AM
  latest : 2020年11月22日 11:39 AM
  blog : http://35.78.51.61/archives/2156
-
  title : laravelにてファイルからhtmlを作成する話
  url : read/
  create : 2020年11月23日 1:23 AM
  latest : 2020年11月23日 1:23 AM
  blog : http://35.78.51.61/archives/2156

少しだけ複雑ですが、一緒です

data[2][‘comment’]こんな配列で使うのはキライなのでclass化させます
information.ymlファイル内容をクラス化するので…

Classes/Repository/Top/Information.php

<?php
namespace App\Classes\Repository\Top;

/**
 * YAMLファイルに書いている情報クラス
 * Class Information
 * @package App\Classes\Repository\Top
 */
class Information
{
    /**
     * @var string タイトル
     */
    public $title;
    /**
     * @var string 対象ページの提供URL
     */
    public $url;
    /**
     * @var string 作成日時
     */
    public $create;
    /**
     * @var string 最終更新日時
     */
    public $latest;
    /**
     * @var string 書いているブログ
     */
    public $blog;
}

うん、これでOKさっそく試験です!プロデューサーさん!

Unit/Repository/Top/InformationRepositoryTest.php

<?php
namespace Tests\Unit\Repository\Top;
use App\Repository\Top\InformationRepository;
use Tests\TestCase;

class InformationRepositoryTest extends TestCase
{
    public function test_findAll(){
        $repository = new InformationRepository();
        $results = $repository->findAll();
        $this->assertEquals('Hello World',$results[0]->title);
        $this->assertEquals('hello/',$results[0]->url);

        $this->assertEquals('Say Hello World',$results[1]->title);
        $this->assertEquals('say/',$results[1]->url);

        $this->assertEquals('laravelにてファイルからhtmlを作成する話',$results[2]->title);
        $this->assertEquals('read/',$results[2]->url);
    }

}

先の内容からtitle,urlをチェックします。
アクセスメソッドは、私が大好き、全部取る、findAllですw
余談ですが、findは引数にプライマリキーを使う感じです
returnは、findAllがInformation[] .findは Informationって感じすね

インタフェイスを考えるとこんな感じです

Repository/RepositoryAccessible.php

<?php
namespace App\Repository;

/**
 * リポジトリのアクセサーブルインタフェイス
 * Interface ReadRepositoriable
 * @package App\Repository\Read
 */
interface RepositoryAccessible{
    /**
     * 全権取得
     * @return array データ
     */
    public function findAll() : array;
}

今回作成するInformationRepositoryはYAMLファイルを読み取る系のリポジトリなのでもう少し大きな考えYAML抽象クラスを作りました。

Repository/AbstractYAMLRepository.php

<?php
namespace App\Repository;

use App\utils\file\YAMLReader;

/**
 * Class YAMLRepository
 * @package App\Repository
 */
abstract class AbstractYAMLRepository implements RepositoryAccessible
{
    /**
     * @param $file
     * @return array
     */
    public function readYAML($file){
        return YAMLReader::read($file);
    }
}

はい、先ほどのアクセスインタフェイスの実装と共用のreadYAMLファイル読み取りってのを実装します。ま、抽象なんですがw

実際のInformationRepositoryクラスは

Repository/Top/InformationRepository.php

<?php
namespace App\Repository\Top;

use App\Classes\Repository\Top\Information;
use App\Repository\AbstractYAMLRepository;
/**
 * topページ情報リポジトリ
 * Class InformationRepository
 * @package App\Repository\Top
 */
class InformationRepository extends AbstractYAMLRepository
{
    /**
     * ファイルのすべての情報を取得します
     * @return array Informationの配列
     */
    public function findAll(): array
    {
        $yaml = $this->readYAML(storage_path('files/top/information.yml'));
        $results = array();
        foreach ($yaml as $line) {
            $results[] = $this->read_line($line);
        }
        return $results;
    }
    /**
     * 行データをタイトルエンティティに変換します
     * @param array $line YAML行データ
     * @return Information タイトルエンティティ
     */
    private function read_line(array $line){
        $information = new Information();
        $information->title = $line['title'];
        $information->url = $line['url'];
        $information->create = $line['create'];
        $information->latest = $line['latest'];
        $information->blog = $line['blog'];
        return $information;
    }

}

うん、findAllでアクセスして行単位に行データをエンティティクラスに変換していきます。それを返すってのです。

第二章 それでもサービスは続く

次に、エンティティを使うサービスを作ります。

古典芸能的に、topにアクセスだからTopControllerとなり、サービスはTopControllerServiceとなるわけですw

んで?もちろん試験です

Unit/Services/Controllers/TopControllerServiceTest.php

<?php
namespace Tests\Unit\Services\Controllers;

use App\Classes\Services\Controllers\TopControllerService\Request;
use App\Classes\Services\Controllers\TopControllerService\Response;
use App\Repository\Top\InformationRepository;
use App\Services\Controllers\TopControllerService;
use Tests\TestCase;

class TopControllerServiceTest extends TestCase
{

    public function test_serve(){
        $informationRepository =  new InformationRepository();
        $service = new TopControllerService( $informationRepository );

        $request = new Request();
        $response = $service->serve($request);

        $this->assertEquals('top',$response->view_name);
        $package = $response->package;
        $results = $package->titles;
        $this->assertEquals('Hello World',$results[0]->title);
        $this->assertEquals('hello/',$results[0]->url);

        $this->assertEquals('Say Hello World',$results[1]->title);
        $this->assertEquals('say/',$results[1]->url);

        $this->assertEquals('laravelにてファイルからhtmlを作成する話',$results[2]->title);
        $this->assertEquals('read/',$results[2]->url);

    }
}

assertは分かりますがRequestは?
そう、サービス利用のためのリクエスト、そして、結果のResponseを今回は作成しました。要するに、アクセスに制限を付けました。
おいらを使うにはこれだけの条件が必要じゃ! -> Request
おまえに与える情報はこれじゃ! -> Response
# class名縮めすぎたかな…

Classes/Services/Controllers/TopControllerService/Request.php

<?php
namespace App\Classes\Services\Controllers\TopControllerService;

/**
 * サービス利用のための要求リクエスト
 * Class Request
 * @package App\Classes\Services\Controllers\TopControllerService
 */
class Request extends \App\Classes\Services\Controllers\AbstractViewPackageControllerService\Response
{

}

Classes/Services/Controllers/TopControllerService/Response.php

<?php
namespace App\Classes\Services\Controllers\TopControllerService;

use App\Classes\Views\Packages\Top\ViewPackage;

/**
 * サービス利用結果の応答
 * Class Response
 * @package App\Classes\Services\Controllers\TopControllerService
 */
class Response extends \App\Classes\Services\Controllers\AbstractViewPackageControllerService\Response
{
    /**
     * @var string 結果表示view名
     */
    public $view_name = null;
    /**
     * @var ViewPackage ページ構成要素
     */
    public $package = null;

}

はい、要求無し、返却は次のページとデータです。

今後を考えてこんな冗長的な構成です。
# 今回は意味はありません

はい、そして、この要求、結果とYAMLデータの結果を考えて試験を構築すると

<?php
namespace Tests\Unit\Services\Controllers;

use App\Classes\Services\Controllers\TopControllerService\Request;
use App\Classes\Services\Controllers\TopControllerService\Response;
use App\Repository\Top\InformationRepository;
use App\Services\Controllers\TopControllerService;
use Tests\TestCase;

class TopControllerServiceTest extends TestCase
{
    private $service;
    public function setUp(): void
    {
        parent::setUp();

        $informationRepository =  new InformationRepository_mock();
        $this->service = new TopControllerService( $informationRepository );
    }

    public function test_serve(){

        $request = new Request();
        $response = $this->service->serve($request);

        $this->assertEquals('top',$response->view_name);
        $package = $response->package;
        $results = $package->titles;
        $this->assertEquals('Hello World',$results[0]->title);
        $this->assertEquals('hello/',$results[0]->url);

        $this->assertEquals('Say Hello World',$results[1]->title);
        $this->assertEquals('say/',$results[1]->url);

        $this->assertEquals('laravelにてファイルからhtmlを作成する話',$results[2]->title);
        $this->assertEquals('read/',$results[2]->url);

    }
}
class InformationRepository_mock extends InformationRepository{
    public function __construct()
    {
        parent::__construct('tests/Unit/Service/Controllers/top/information.yml');
    }
}

はい、複雑そうに見えますが、

  • リポジトリを作って
  • 試験対象のコントローラサービスを作ります
  • 空っぽの要求を作って
  • サービスを利用します
  • 結果、内容を評価していきます

シンプルです、ここで、ポイントを言うと、今回はリポジトリをモック(木偶)を作っています。

え?と思われますが、これが提供中のサービスの場合は如何でしょうか?
DBアクセスだったら… 何て考えると手前で作っているファイルが結果が分かっている物を返すと言うのが試験では便利です

実際にコントローラサービスを作ると

<?php


namespace App\Services\Controllers;


use App\Classes\Repository\Top\Information;
use App\Classes\Services\Controllers\TopControllerService\Request;
use App\Classes\Services\Controllers\TopControllerService\Response;
use App\Classes\Views\Packages\Top\Title;
use App\Classes\Views\Packages\Top\ViewPackage;
use App\Repository\Top\InformationRepository;

/**
 * TOPコントローラサービス
 * Class TopControllerService
 * @package App\Services\Controllers
 */
class TopControllerService
{
    /**
     * 情報リポジトリ
     * @var InformationRepository
     */
    private $informationRepository ;

    public function __construct(InformationRepository $informationRepository)
    {
        $this->informationRepository = $informationRepository;
    }

    /**
     * サービス
     * @param Request $request コントローラサービスリクエストサービス
     * @return Response レスポンス
     */
    public function serve(Request $request):Response{
        $response = new Response();

        $response->view_name = 'top';
        $response->package = $this->makeViewPackage();

        return $response;
    }

    /**
     * Viewパッケージを生成します
     * @return ViewPackage
     */
    public function makeViewPackage(){
        $viewPackage = new ViewPackage();
        // タイトル情報を取得します
        $viewPackage->titles = $this->makeViewPackage_titles($this->informationRepository->findAll());

        return $viewPackage;
    }

    /**
     * タイトル情報を生成します
     * @param array $data データ
     * @return array タイトル一覧
     */
    public function makeViewPackage_titles(array $data){
        $titles = array();
        foreach ($data as $information){
            $titles[] = $this->makeViewPackage_titles_title($information);
        }
        return $titles;
    }

    /**
     * タイトル情報を入れ替えます
     * @param $information Information エンティティーのタイトル情報
     * @return Title PageViewのタイトル情報
     */
    public function makeViewPackage_titles_title(Information $information){
        $title = new Title();
        $title->title = $information->title;
        $title->url = $information->url;
        $title->blog = $information->blog;
        return $title;
    }
}

はい、先ほどのI/Oの試験だけでは足りませんね。
makeViewPackage , makeViewPackage_titles , makeViewPackage_titles_title の動作保証を取る方が良いですね

    public function test_makeViewPackage(){
        $viewPackage = $this->service->makeViewPackage();

        $this->assertTrue($viewPackage instanceof ViewPackage);
        $this->assertCount(3,$viewPackage->titles);

        $results = $viewPackage->titles;
        $this->assertEquals('Hello World',$results[0]->title);
        $this->assertEquals('hello/',$results[0]->url);

        $this->assertEquals('Say Hello World',$results[1]->title);
        $this->assertEquals('say/',$results[1]->url);

        $this->assertEquals('laravelにてファイルからhtmlを作成する話',$results[2]->title);
        $this->assertEquals('read/',$results[2]->url);
    }
    public function test_makeViewPackage_titles(){
        $params = array();
        {
            $param = new Information();
            $param->title = 'A';
            $param->url = 'url';
            $param->blog = 'blog';
            $params[] = $param;
        }
        {
            $param = new Information();
            $param->title = 'B';
            $param->url = 'url2';
            $param->blog = 'blog2';
            $params[] = $param;
        }
        $titles = $this->service->makeViewPackage_titles($params);

        $this->assertTrue($titles[0] instanceof Title);
        $this->assertEquals('A',$titles[0]->title);
        $this->assertEquals('url',$titles[0]->url);
        $this->assertEquals('blog',$titles[0]->blog);

        $this->assertTrue($titles[1] instanceof Title);
        $this->assertEquals('B',$titles[1]->title);
        $this->assertEquals('url2',$titles[1]->url);
        $this->assertEquals('blog2',$titles[1]->blog);
    }
    public function test_makeViewPackage_titles_title(){
        $params = new Information();
        $params->title = 'A';
        $params->url = 'url';
        $params->blog = 'blog';

        $title = $this->service->makeViewPackage_titles_title($params);
        $this->assertTrue($title instanceof Title);
        $this->assertEquals('A',$title->title);
        $this->assertEquals('url',$title->url);
        $this->assertEquals('blog',$title->blog);
    }

ちょっとだけ複雑系の試験になりました

最後は… Controllerとおまけ程度だが重鎮のrouteです

Http/Controllers/TopController.php

<?php
namespace App\Http\Controllers;
use App\Classes\Services\Controllers\TopControllerService\Request;
use App\Services\Controllers\TopControllerService;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;

/**
 * Class TopController
 * @package App\Http\Controllers
 */
class TopController extends Controller{
    /**
     * @var TopControllerService コントローラサービス
     */
    public $controllerService ;

    /**
     * デフォルトコンストラクタ
     * SayController constructor.
     * @param TopControllerService $controllerService コントローラサービス
     */
    public function __construct(TopControllerService $controllerService){
        $this->controllerService = $controllerService;
    }

    /**
     * /readの処理
     * @return Application|Factory|View
     */
    public function index(){
        // ControllerServiceへの要求
        $serviceRequest = new Request();
        // ControllerServiceの処理
        $response = $this->controllerService->serve($serviceRequest);
        // viewの返却
        return view($response->view_name,(array)$response->package);
    }
}

はい、先の試験とほぼ同じです!

routes/web.php

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

はい、これでアクセスすると… Webページが表示されます!bladeは除外します

はいっ!んな感じでTOPページが出来ました!

次回は… どうしようかな?