目次

Apricot フロントエンド

y2sunlight 2020-07-29

Apricot ドキュメント に戻る

目次


HTMLテンプレート

テンプレートエンジン

Apricotでは、テンプレートエンジンにLaravelと同じBladeを使用しています。実際に使用しているライブラリは BladeOne です。BladeOneは、Blade のスタンドアロンバージョンです。

テンプレートの詳しい使用方法については、以下のドキュメントを参照して下さい。


レンダリング

Apricotでは、BladeOneを直接使う代わりに、それをラップしてシングルトンにしたViewクラスを使用します。ViewシングルトンはBladeOneと同じメソッド使用できますが、Apricotで主に使用するのはrun()メソッドだけです。

使用法: View::{メソッド}

メソッド機能
string run(string $view, array $variables = [])テンプレートエンジンの実行

Apricotのアプリケーションでは、直接Viewシングルトンを使用する代わりに、ボイラープレートrender() を使って、HTMLをレンダリングします( 即ち、render()の内部でView::run()が使用されています )。以下はユーザコントローラのindexアクションの例です。

public function index()
{
    $users = $this->user->findAll();
    return render("user.index", ["users"=>$users]);
}

render() の第1引数にはHTMLテンプレート名、第2引数にはテンプレート変数を[変数名 ⇒ 値]の形で渡します。

テンプレートファイルは、/your-project/assets/view/ に配置します。一般的にテンプレートファイルは、/your-project/assets/view/{path}/{name}.blade.php として保存します。{path} はサブディレクトリ下に配置したい場合のオプションで、コントローラ名に関連した名前を付けるのが一般的でしょう。{name}はテンプレート名です。上例のテンプレートファイルは、/your-project/assets/view/user/index.blade.php に配置し、テンプレート名は user.index になります。

以下は、テンプレートファイル index.blade.php からの抜粋です。render()メソッドで与えらてたテンプレート変数 $users を使って各要素( $user )を取り出し、ユーザデータをレンダリングしています。

user.index.php
@foreach($users as $user)
<tr data-href="{{route("user/{$user->id}/edit")}}">
    <td>{{ $user->id }}</td>
    <td>{{ $user->account }}</td>
    <td>{{ $user->email }}</td>
    <td>{{ ViewHelper::formatDatetime($user->created_at) }}</td>
</tr>
@endforeach

render()関数の実装

render() 関数は、Apricotコア部分下の helpser/boilerplates.php の中で以下のように実装されています。

boilerplates.php
/**
 * Renders HTML via the given template and returns a response object.
 *
 * @param string $view Template name
 * @param array $variables An array of template variables
 * @return \Apricot\Foundation\Response\RenderResponse
 */
function render(string $view=null, array $variables=[]):Apricot\Foundation\Response\RenderResponse
{
    $variables['errors'] = errors();
    $html = isset($view) ? Apricot\View::run($view, $variables) : null;
    return new Apricot\Foundation\Response\RenderResponse($html);
}

render() 関数の2つの引数は View シングルトンのrun()メソッドに渡わたされ、テンプレートを介してHTMLをレンダリングし、その結果を使ってレンダーレスポンスを生成して返しています。ボイラープレート errors() については次項を参照して下さい。


テンプレート変数 $errors

$errorsrender() ボイラープレートによって自動的に作られるテンプレート変数です。$errorserrors() ボイラープレートによって取得されます。

// テンプレート変数 $errors
['errors' => errors()]

errors() ボイラープレートはフラッシュに保存されているエラーバッグを返します(このフラッシュデータの名前は 'errors' です)。$errors はテンプレートの中で次のようにして使用されます:

@if($errors->count())
    @foreach($errors as $key=>$value)
         {{$value}}<br>
    @endforeach
@endif


ビューヘルパー

Aprocotのアプリ部分では、ヘルパークラスを /your-project/app/Helpers に配置しています。

テンプレート内で使用するヘルパークラスはビューヘルパーと呼ばれ、以下は、初期実装されている ViewHelperクラス で、文字列の日付をフォーマットするメソッドだけが実装されています。必要に応じて、ここにメソッドを追加して下さい。

/your-project/app/Helpers

ViewHelper.php
<?php
namespace App\Helpers;
 
/**
 * View Helper
 */
class ViewHelper
{
    /**
     * Returns a formatted date string.
     *
     * This method is an example of a view helper.
     *
     * @param string $datetime
     * @param string $format
     * @return string
     */
    static function formatDatetime(string $datetime, string $format='Y-m-d'):string
    {
        return date($format, strtotime($datetime));
    }
}


クラスエイリアス

Apricotでは、シングルトンとビューヘルパーのエイリアスを作っています。これによって、完全修飾クラス名を短くコーディングできます。クラスエイリアスの設定については、「Apricot 配置と構成」を参照して下さい。

以下はテンプレート内でのクラスエイリアスの使用例です。この例では、フラッシュにキーがmsgのデータがあればそれを出力しています。

@if(Flash::has('msg'))
<div class="message">{{Flash::get('msg')}}</div>
@endif


テンプレートファイル

テンプレートファイルは、/your-project/assets/views/ の下に配置し、ファイル名は .blade.php で終わる必要があります。例えば、/your-project/assets/views/{path}/{name}.blade.php として保存した場合、テンプレート名は、{path}.{name} になります。

Apricotのアプリ部分として提供しているテンプレートファイルの構成を以下に示します。

your-project
 |
 ├── assets
 |    ├── views
 |    |    |
 |    |    ├── error  [エラーページ用]
 |    |    |    |
 |    |    |    ├── layout.blade.php     [エラーページ用のマスターレイアウト]
 |    |    |    └── exception.blade.php  [集約例外コントローラ用]
 |    |    |
 |    |    ├── user  [ユーザコントローラ用]
 |    |    |    |
 |    |    |    ├── create.blade.php  [ユーザ新規登録ページ]
 |    |    |    ├── edit.blade.php    [ユーザ編集ページ]
 |    |    |    └── index.blade.php   [ユーザリストページ]
 |    |    |
 |    |    ├── home.blade.php    [ホームコントローラ用]
 |    |    ├── layout.blade.php  [通常ページ用のマスターレイアウト]
 |    |    ├── login.blade.php   [ログイン認証コントローラ用]
 |    |    └── stub.blade.php    [スタブコントローラ用]

1つのコントローラーが複数のHTMLテンプレートを持っている場合、ユーザコントローラのように、サブディレクトリ―にはコントローラ名に関連した名前付けを推奨します。上のユーザ新規登録ページのテンプレート名は user.create です。

layout.blade.php ログイン認証とエラーページ以外のマスターレイアウトとして使用されます(次項参照)。


マスターレイアウト

通常のアプリケーションでは、ページの全体的なレイアウトの中に個別のレイアウトを表示します。この全体的なレイアウトをマスターレイアウトと呼びます。以下は、マスターレイアウトのテンプレート ( layout.blade.php ) の例です。

/your-project/assets/views

layout.blade.php
{{-- This is an example of master layout --}}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{__('messages.app.title')}}</title>
    <!-- Here are stylesheets and scripts such as jquery and bootstrap -->
    {!! DebugBar::renderHead() !!}
    @stack('scripts')
</head>
<body>
    <nav>
      <!-- Here is the navigation menu bar-->
    </nav>
    <main>
        <h1>@yield('title')</h1>
        @yield('content')
 
        {{-- error --}}
        @if($errors->count())
        <div class="alert">
            @foreach($errors as $key=>$value)
                 {{$value}}<br>
            @endforeach
        </div>
        @endif
    </main>
 
    <footer>{{env('APP_NAME')}} &copy; 2020 y2sunlight</footer>
    {!! DebugBar::render() !!}
</body>
</html>

マスターレイアウトの例に従ってBladeによるテンプレートの書き方を確認してみましょう。

コメント

Bladeのコメントは {{-- --}} 表記を使います。このコメント表記はHTMLを出力しません。

{{-- This is an example of master layout --}}

データ表示

Bladeからデータを表示するには、{{ }} 表記を使います。これは自動的にhtmlspecialchars()を呼び出して、HTMLをエスケープしXSS攻撃を防ぎます。以下は、ヘルパー関数 __() による翻訳文字列とテンプレート変数の表示例です。

<title>{{__('messages.app.title')}}</title>
{{$value}}<br>

HTMLをエスケープしたくない場合は以下のように、{!! !!} 表記を使います。これは デバッグ出力 用のヘッダーと本体部分をレンダリングしている例です。

{!! DebugBar::renderHead() !!}
{!! DebugBar::render() !!}

セクションの宣言

Bladeでは、@で始まるディレクティブと呼ばれる構文があります。@yield@section ディレクティブは対で使用されます。親テンプレートの @yield で名前付きのセクションを宣言し、子テンプレートの@sectionでそこに表示するHTMLを定義します。以下の例では、マスターレイアウトでのタイトルとコンテンツセクションを宣言しています。@sectionの例は、次項を参照して下さい。

@yield('title')
@yield('content')

スタックの宣言

@stack@push ディレクティブは、テンプレートにスタック構造を与えます。親テンプレートの @stackで名前付きのスタックを宣言し、子テンプレートの@pushでそこに表示するHTMLをプッシュします。@yield - @section との違いは、必要に応じてHTMLを何度もプッシュできます。以下の例では、マスターレイアウトのHEAD要素の中にスクリプトのスタックを宣言しています。@pushの例は、次項を参照して下さい。

@stack('scripts')

制御構文

PHPの制御構造と同様に、Blade でも @if@switch@for@foreach、そして @while の制御構文が使用できます。以下は、@if と @foreach の例です。これはエラー表示用のテンプレート変数からエラー情報を表示している例です。

@if($errors->count())
<div class="alert">
    @foreach($errors as $key=>$value)
         {{$value}}<br>
    @endforeach
</div>
@endif

PHPコード

@phpディレクティブを使えば、任意のPHPコードを実行できます。

@php
    /* Do something. */
@endphp


テンプレートの継承

前項のマスターテンプレートを継承した例を以下に示します。これはユーザコントローラーのcreateアクションで、ユーザの新規登録を行うページです。

/your-project/assets/views/user

create.blade.php
{{-- Parent layout --}}
@extends('layout')
 
{{-- Additional script --}}
@push('scripts')
    <script src="{{url_ver('js/user.js')}}"></script>
@endpush
 
{{-- title --}}
@section('title', __('messages.user.create.title'))
 
{{--content --}}
@section('content')
    <form method="POST" name="fm">
        @csrf
        {{-- account --}}
        <div>
            <label for="account">{{__('messages.user.create.account')}}</label>
            <input type="text" name="account" id="account" value="{{old('account',$user->account)}}"
             placeholder="{{__('messages.user.create.hint_account')}}">
        </div>
 
        {{-- Show other input elements here --}}
        </div>
    </form>
@endsection

上の例に従ってBladeによるテンプレートの書き方を確認してみましょう。

テンプレートの継承

マスターテンプレートを継承するには、@extendsディレクティブを使用します。このディレクティブを使えばマスターテンプレートの中に個別のコンテンツを表示することができます。

@extends('layout')

スタックへのプッシュ

マスターテンプレートで宣言されている@stackに個別のコンテンツをプッシュするには、@pushディレクティブを使います。下の例では、マスターテンプレートのHEAD要素内に個別のJavascriptを差し込んでいます。

@push('scripts')
    <script src="{{url_ver('js/user.js')}}"></script>
@endpush

セクションの定義

マスターテンプレートで宣言されている@yieldに個別のコンテンツを表示するには、@sectionディレクティブを使います。以下は個別のタイトルを表示する例です。

@section('title', __('messages.user.create.title'))

また、複数行のセクションを定義するには以下の構文を使います。

@section('content')
  {!-- Display the form element here. --}
@endsection

CSRF対策

@csrfは、CSRF対策用のトークンフィールドを出力するカスタムディレクティブです。@csrfの作り方については「Bladeのセットアップ」を参照して下さい。

<form method="POST" name="fm">
    @csrf
    {!-- Display input elements here. --}
</form>


Bladeの設定

Bladeの設定ファイル

Bladeの設定ファイルは、bladeone.setting.php です。

your-project/config/setting

bladeone.setting.php
<?php
/**
 * This file contains BladeOne settings.
 */
return
[
    'template_path' => env('VIEW_TEMPLATE',assets_dir('views')),
    'compile_path' => env('VIEW_CACHE',var_dir('cache/views')),
    'mode' => \eftec\bladeone\BladeOne::MODE_AUTO,
];

実行モードが MODE_AUTO の場合、テンプレートはPHPへコンパイルされ、変更があるまで compile_path で指定された場所にキャッシュされ続けます。強制的に再コンパイルしたい場合は、キャッシュをクリアして下さい。

実行モードには以下の種類があるます:


Bladeのセットアップ

Bladeには以下のセットアップファイルが存在します。

/your-project/config/setup

bladeone.setup.php
<?php
/**
 * Initial setting of View template (Blade One)
 */
return function():bool
{
    // @now directive
    Apricot\View::directive('now', function()
    {
        return "<?php echo date('Y-m-d H:i'); ?>";
    });
 
    // @csrf directive
    Apricot\View::directive('csrf', function()
    {
        $name = Apricot\Foundation\Security\CsrfToken::CSRF_KEY;
        return '<input name="'.$name.'" type="hidden" value="{{Session(\''.$name.'\')}}">';
    });
 
    return true; // Must return true on success
};

このセットアップファイルの目的は、HTMLテンプレートに新しいカスタムディレクティブを追加することです。ここでは以下のディレクティブを追加しています。

@now

現在時刻を表示するディレクティブです。これはカスタムディレクティブのサンプルです。

@csrf

CSRF対策用のトークンフィールドを出力するディレクティブです。セッションからCSRFトークンを取得して、hidden タイプの input 要素の中に設定しています。


Bladeの拡張

Apricotで使用しているテンプレートエンジン BladeOne には、オプションの拡張ライブラリーとして BladeOneHtml があります。このライブラリーを使用すると、HTMLフォームが簡単に綺麗に作成できます。

例:

@form()
    @input(type="text" name="myform" value=$myvalue)
    @button(type="submit" value="Send")
@endform()

インストール方法や使い方は、以下を参照して下さい:


多言語化

Apricotは多言語をサポートしてます。これはアプリケーションで各言語毎にメッセージをファイルを作成することによって行い、このファイルの事を言語ファイルと呼びます。

アプリケーションの実行時に、Apricotがどの言語を選択するかは、利用者がブラウザで使用している言語とアプリケーションで準備されている言語ファイルによって自動的に決定されます。デフォルトの言語は、環境変数 APP_LANG で指定してます。この変数が指定されていない場合は英語になります。


言語ファイル

言語ファイルは /your-project/assets/lang/{言語コード}/ に配置されます。言語コードは、ISO 639-1で定義された2文字のコードです。言語ファイル名は各言語共通で、内容を同じキーで定義しなければなりません。Apricotの初期実装では以下のディレクトリーと言語ファイルが用意されています。

your-project [プロジェクトディレクトリー]
 |
 ├── assets
 |   |
 |   ├── lang/
 |   |   |
 |   |   ├── en  [英語]
 |   |   |   ├── auth.php             [ログイン認証画面用]
 |   |   |   ├── messages.php         [一般的なメッセージ]
 |   |   |   └── vlucas.valitron.php  [バリデーション用]
 |   |   |
 |   |   ├── ja  [日本語]
 |   |   |   ├── auth.php
 |   |   |   ├── messages.php
 |   |   |   └── vlucas.valitron.php

メッセージの識別は、ドット表記のキーで行います。最初のキーは、言語ファイルの .php を除いたベース名です。キーの残りの部分は、それに続いてドットで区切られた階層が続きます。

以下は、言語ファイル(messages.php)の例です。このファイル内のメッセージを取得するドット表記のキーは messages.home.titlemessages.home.msg_hello です。後者のメッセージには :account という名前の付いた1つのパラメータを含んでいます。

/your-project/assets/lang/en

messages.php
<?php
return [
    'home'=>[
        'title'=>env('APP_NAME'),
        'msg_hello'=>'Hello, :account !',
    ],
];


言語メッセージの取得

Langクラス

Langクラスはドット表記で指定されたキーから各言語用のメッセージを取得するシングルトンです。このクラスは Apricot\Foundation\Translation クラスをシングルトンにしたものです。

使用法: Lang::{メソッド}

メソッド機能
string getLangCode()言語コード(ISO 639-1)の取得
bool has(string $key)キーの存在確認
string get(string $key, array $params = [])言語テキストの取得

以下は、ホームコントローラのindexアクションです。この例では、前項の例の言語ファイル( messages.php )から 'messages.home.msg_hello' のキーを持つメッセージを取得した後、それをテンプレートに渡しHTMLをレンダリングしています。

public function index()
{
    $message = Lang::get('messages.home.msg_hello', [':account'=>AuthUser::getUser()->account]);
    return render('home',['message'=>$message]);
}

ボイラープレート

Lang::get() メソッドを使用する代わりに __() ボイラープレートを使って以下のようにもコーディングできます。

public function index()
{
    $message = __('messages.home.msg_hello', [':account'=>AuthUser::getUser()->account]);
    return render('home',['message'=>$message]);
}
この関数名は __ です。2つ並んだアンダースコアはPythonプログラマーの間では dunders (double underscoreの意) と呼ばれ特別なクラス内メンバに付加されますが、ここではそのような意味はなくLaravelと同じ関数名にしました。


アセットファイルのバージョニング

通常のブラウザでは、静的なアセットファイル( .js , .css または画像ファイル)がキャッシュされる場合があります。これは、アプリケーションのアセットファイルを変更した場合に問題が発生するかもしれません。これを防ぐ手段をキャッシュ・バスティング( Cache Busting )と呼びます。

簡単なキャッシュ・バスティングの方法としては、アセットファイルのURLに現在日時をクエリー文字列として付加する方法があります。

<link href="{{url('css/main.css').'?'.time()}}" rel="stylesheet">
<script src="{{url('js/main.js').'?'.time()}}"></script>

しかし、この方法では、アクセスする度にブラウザがアセットファイルを取得してしまい、ブラウザのキャッシュ機構が上手く機能しません。従って、現在日時の代わりにアプリケーションのバージョン番号を付加すれば、この問題を解決することができます。これがアセットファイルのバージョニングです。

Apricotではバージョニング用のボイラープレート url_ver() 関数が用意さており、上のコードを以下のように変更することで、アセットファイルのバージョニングができます。

<link href="{{url_ver('css/main.css')}}" rel="stylesheet">
<script src="{{url_ver('js/main.js')}}"></script>

アプリケーションのバージョン番号は、環境変数 APP_VERSION から取得します。従って、アセットファイルのバージョニングを使用している場合は、アセットファイルを変更したら必ず APP_VERSION も変更する必要があります。