— y2sunlight 2020-05-06
関連記事
まずは、apricotのアプリを作る為に以下を準備します。
以下に示すようにプロジェクトフォルダ下に、アプリ用のフォルダ app を作成し、その下に7つのフォルダ(Controllers, Exceptions, Foundation, Helpers, Middleware, Models, Services)を作成します。また、Controllersの下にはInterceptorsフォルダを作成します。
apricot [プロジェクト] | ├── app [アプリ] | | | ├── Controllers [コントローラ] | | | | | └── Interceptors [インターセプター] | | | ├── Exceptions [例外] | ├── Foundation [基盤] | ├── Helpers [ヘルパー] | ├── Middleware [ルミドルウェア] | ├── Models [モデル] | └── Services [サービス] (予約)
以下のようにcomposer.jsonを編集します。
/apricot
{ "autoload" : { "psr-4" : { "App\\" : "app/", "Core\\" : "core/" }, }
autoload.psr-4
の中に App名前空間へのマッピングを追加オートローディングファイルを更新する為に、プロジェクトフォルダで以下のコマンドを実行します。
composer dump-autoload
アプリ用のヘルパー関数を作る準備として 表示用のHelperクラスを作成しておきます。必要に応じて、関数やクラスを追加していきます。
/apricot/app/Helpers
<?php namespace App\Helpers; class ViewHelper { /** * 日時のフォーマット * @param string $datetime * @param string $format * @return string */ static function formatDatetime(string $datetime, string $format='Y-m-d'):string { return date($format, strtotime($datetime)); } }
完全修飾クラス名を短くコーディングする為に、クラスのエイリアスを作成します。このエイリアスは、特にHTMLテンプレートでの使用を想定しています。
以下に示す初期設定ファイル(aliases.setup.php)を config/setup フォルダの下に作成して下さい。
/apricot/config/setup
<?php //------------------------------------------------------------------- // ビューテンプレートで使うクラスエイリアスを登録 //------------------------------------------------------------------- return function():bool { $aliases = [ /* Core */ 'Input' => \Core\Input::class, 'QueryString' => \Core\QueryString::class, 'Session' => \Core\Session::class, 'Flash' => \Core\Flash::class, 'Cookie' => \Core\Cookie::class, 'Config' => \Core\Config::class, 'Log' => \Core\Log::class, 'Debug' => \Core\Debug::class, 'DebugBar' => \Core\DebugBar::class, 'ErrorBag' => \Core\Foundation\ErrorBag::class, /* App */ 'ViewHelper' => \App\Helpers\ViewHelper::class, ]; // Creates an alias for a class foreach($aliases as $alias_name => $original_class) { class_alias($original_class, $alias_name); } return true; // Must return true on success };
上で作った aliases.setup.php をアプリケーションの設定ファイル(app.php)に追加します。
/apricot/config
<?php return [ 'setup' =>[ config_dir('setup/whoops.setup.php'), /* Error handler(whoops) */ config_dir('setup/bladeone.setup.php'), /* View template (BladeOne) */ config_dir('setup/aliases.setup.php'), /* Class aliases for view template and so on */ ], 'middleware' =>[], 'auth' =>[], 'csrf' =>[], ];
アプリの作成過程で使用する共通の言語テキストを準備します。個別の画面については、その都度に追加して行きます。
/apricot/assets/lang/ja
<?php return [ 'app'=>[ 'title'=>env('APP_NAME'), 'menu'=>[ 'menu1'=>'menu1', 'menu2'=>'Menu2', 'menu3'=>'Menu3', 'logout'=>'Logout', 'about_me'=>'About Me', ], ], 'success' => [ 'db' => [ 'insert' => 'データを登録しました', 'update' => 'データを更新しました', 'delete' => 'データを削除しました', ], ], 'error'=>[ 'unknown'=>'エラーが発生しました', 'db' => [ 'access' => 'データアクセスが失敗しました', 'insert' => 'データの登録が失敗しました', 'update' => 'データの更新が失敗しました', 'delete' => 'データの削除が失敗しました', 'optimisstic_lock'=>'データが他のユーザによって更新されています', ], ], ];
まず最初に、アプリで発生する例外を様々な例外クラスのベースとなる ApplicationException を Exceptionクラス から派生させて作ります。
/apricot/app/Exceptions
<?php namespace App\Exceptions; use Exception; /** * アプリケーション例外 */ class ApplicationException extends Exception { /** * User error message * @var string */ private $user_message; /** * Create ApplicationException * @param string $user_message * @param string $internal_message * @param int $code * @param \Throwable $previous */ public function __construct(string $user_message=null, $internal_message=__CLASS__, int $code = 0, \Throwable $previous = null) { $this->user_message = isset($user_message) ? $user_message : __('messages.error.unknown'); parent::__construct($internal_message, $code, $previous); } /** * Get user error message * @return string */ public function getUserMessage() { return $this->user_message; } }
getUserMessage()
メソッドで取得できます。__()
はトランスレータ Core\Lang::get() を呼び出しています。トランスレータへの引数 'messages.error.unknown' は言語テキストのキーです(ドット表記)。次に、ApplicationException から継承した楽観的ロック例外クラス OptimissticLockException を作ります。
/apricot/app/Exceptions
<?php namespace App\Exceptions; /** * 楽観的ロック例外 */ class OptimissticLockException extends ApplicationException { /** * Create Optimisstic Lock * @param string $message * @param \Exception $previous */ public function __construct() { parent::__construct(__('messages.error.db.optimisstic_lock'), __CLASS__); } }
__()
はトランスレータ Core\Lang::get() を呼び出しています。トランスレータへの引数 'messages.error.db.optimisstic_lock' は言語テキストのキーです(ドット表記)。このようにして、アプリで発生する例外は ApplicationException から継承して作るようにします。
apricotで採用しているテンプレートエンジン BladeOne の文法に従ってアプリ全体のレイアウトを作ります。また、cssとJavaScriptファイルも準備します。
アプリ全体で使用するHTMLテンプレートを示します。このテンプレートではbootstrapとjqueryを使用し、レスポンシブデザインを採用しています。
/apricot/assets/view
<!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> <!-- stylesheet --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link href="{{url_ver('css/main.css')}}" rel="stylesheet"> <!-- Scripts --> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> <script src="{{url_ver('js/main.js')}}"></script> {!! DebugBar::renderHead() !!} @stack('scripts') </head> <body> <nav class="navbar navbar-expand-md navbar-dark bg-dark"> <a class="navbar-brand" href="{{route()}}">{{__('messages.app.title')}}</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item"><a class="nav-link" href="{{route('stub/1')}}">{{__('messages.app.menu.menu1')}}</a></li> <li class="nav-item"><a class="nav-link" href="{{route('stub/2')}}">{{__('messages.app.menu.menu2')}}</a></li> <li class="nav-item"><a class="nav-link" href="{{route('stub/3')}}">{{__('messages.app.menu.menu3')}}</a></li> <li class="nav-item"><a class="nav-link" href="http://y2sunlight.com/ground/doku.php?id=apricot:top" target="_blank">{{__('messages.app.menu.about_me')}}</a></li> </ul> {{-- @if(app('auth.menu',false)) <ul class="navbar-nav ml-auto"> <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre> {{AuthUser::getUser()->account}} <span class="caret"></span> </a> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="{{route('logout')}}">{{__('messages.app.menu.logout')}}</a> </div> </ul> @endif --}} </div> </nav> <main class="mt-3 mb-5"> <div class="container"> <div class="row justify-content-center"> <div class="col-md-12"> <h1>@yield('title')</h1> @yield('content') {{-- error --}} @if($errors->count()) <div class="alert alert-danger mt-3"> @foreach($errors as $key=>$value) {{$value}}<br> @endforeach </div> @endif {{-- msg --}} @if(Flash::has('msg')) <div class="alert alert-info mt-3"> {{Flash::get('msg')}} </div> @endif </div> </div> </div> </main> <footer class="fixed-bottom bg-secondary text-center text-light py-1">{{env('APP_NAME')}} © 2020 y2sunlight</footer> {!! DebugBar::render() !!} </body> </html>
<meta name='viewport'>
と <title>
を設定しますmian.css
、main.js
)@stack('scripts')
を使って、個々の画面用のJavaSctiptを差し込みます<nav>
にメニューバーを表示します<main>
にコンテンツを表示します
@yield('title')
を使って、個々の画面のタイトルを生成します@yield('content')
を使って、個々の画面のコンテンツを生成します@if
, @foreach
はPHPの if, foreach と同じ機能を持つ制御ディレクティブです$errors
が空でない場合はエラー表示しますmsg
があるときはメッセージを表示します__()
はトランスレータ Core\Lang::get() を呼び出しています。トランスレータへの引数は言語テキスト(assests/lang/ja/messages.php)のキーです(ドット表記)。url_ver()
はHTMLリソースをバージョニング対応するApplicationクラスのヘルパー関数です。?v={version no}
などのクエリ文字列を追加する事を指します)route()
はルートパスをURLに変換するApplicationクラスのヘルパー関数です。user()
はログインユーザのエンティティを呼び出します(後述のユーザ認証で説明)。env()
は環境設定を取得するヘルパー関数です。<nav>
の中の@if(app('auth.menu',false))
の部分は後述のユーザ認証で説明します。この段階ではコメントアウトされています。
HTMLテンプレートに関しては本編BladeOneのリンクを項を参照して下さい。
Apricotで使用するCSSを以下に示します。このCSSは public/css 下に配置します。
/apricot/public/css
@charset "UTF-8"; .form-control::-webkit-input-placeholder{color: #c6c6c6;} /* Google Chrome, Safari, Opera 15+, Android, iOS */ .form-control:-ms-input-placeholder, /* IE 10+ */ .form-control::-ms-input-placeholder{color: #c6c6c6;} .form-control::placeholder{color: #c6c6c6;} /* default #6c757d; */ .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { margin: 0.25em 0 1em 0; } .h1, h1 { font-size: 1.75rem } .h2, h2 { font-size: 1.5rem } .h3, h3 { font-size: 1.25rem } .h4, h4 { font-size: 1.0rem } .h5, h5 { font-size: 0.75rem } .h6, h6 { font-size: 0.5rem } .container { padding: 0px 3px; !important; overflow-x: hidden !important; /* x方向の余白を削除 */ }
現バージョンのApricotではJavascriptを使っていませんが、カスタマイズ時には使用すると思われるので空のファイルを作っておきます。このJavascriptは public/js 下に配置します。
/apricot/public/js
// It's empty now.
コアクラスにはコントローラのベースクラスとして BaseController がありますが、これを直接継承するのではなく、アプリ用にもう一つコントローラのベースクラスを作ります。現段階でこのクラスは何もしていませんが、後でここにトランザクション用のコードを追加します。
/apricot/app/Foundation
<?php namespace App\Foundation; use Core\Foundation\BaseController; /** * コントローラ */ class Controller extends BaseController { }
アプリの準備として最後にスタブ画面を作り、ルーティングからコントローラアクションの起動までをテストしてみます。
以下のように、config/routes.php にスタブコントローラのルートを追加して下さい。
/apricot/config
<?php //------------------------------------------------------------------- // Route Definition Callback //------------------------------------------------------------------- return function (FastRoute\RouteCollector $r) { $base = Core\Application::getInstance()->getRouteBase(); $r->addGroup($base, function (FastRoute\RouteCollector $r) use($base) { // Home // TODO: Stub Version $r->get('/', function() use($base){ header("Location: " . $base.'/stub'); }); // Stub $r->get('/stub[/{no:\d+}]', 'StubController@index'); }); };
ルーティング設定に関してはFastRouteを参照して下さい。
上述のアプリ用のコントローラベースを継承してスタブコントローラを作ります。
/apricot/app/Controllers
<?php namespace App\Controllers; use App\Foundation\Controller; /** * Stubコントローラ */ class StubController extends Controller { /** * Stub Page * @return \Core\Foundation\Response */ public function index(int $no=null) { $title = "Stub {$no}"; $messages = []; return render('stub',['title'=>$title,'messages'=>$messages]); } }
以下にスタブコントローラのindexアクションでレンダリングしているHTMLテンプレートを示します。
/apricot/assets/views
{{-- 親レイアウト --}} @extends('layout') {{-- 追加スクリプト --}} @push('scripts') @endpush {{-- タイトル --}} @section('title', $title) {{-- コンテンツ --}} @section('content') @if(!empty($messages)) @foreach($messages as $message) <p>{{$message}}</p> @endforeach @endif @endsection
extends
ディレクティブで layout.blade.php を継承しています。push
ディレクティブで layoutテンプレートの scripts
スタック にコードを追加しています。section
で layoutテンプレートの title
と content
セクションを生成しています。@if
, @foreach
はPHPの if, foreach と同じ機能を有するディレクティブです。HTMLテンプレートに関しては本編BladeOneのリンクを項を参照して下さい。
ここまでの実装で一度実行してみましょう。ブラウザ上で以下のURLにアクセスしてみて下さい。
http://localhost/ws2019/apricot/public/
次の画面が表示されます
■ [menu1],[menu2],[menu3]を押すとそれぞれのスタブ画面が表示されます。
■ [About Me]を押すと y2sunlight.com が開きます。