Apricot は PHPによるWebアプリケーションのフレームワークで、比較的小規模なアプリケーションの構築を素早く行えることを目的としています。Apricot は Laravel に大きな影響を受けていますが、Laravelようなフルスタックなフレームワークではなく、軽量なコアを持ったフレームワークです。
Apricotでは、フレームワークの主要な部分だけを独自に実装し、他のコンポーネントは、秀作な外部ライブラリーを取り込んで開発されています。テンプレートエンジンにはLaravelと同じBladeを使用している他、環境設定のPhpdotenv、ログ出力のMonolog、そしてエラーハンドラーのwhoopsもLaravelと同じコンポーネントを使用しています。その他でもLaravelと同じ識別子やコーディングスタイルを使用しているので、Laravelユーザには見慣れたコードになっています。
一般的なWebアプリケーションのフレームワークでは、リクエストルータ、リクエストクラス、レスポンスクラス、ORM、テンプレートエンジン、ミドルウェアやコントローラの基底クラス、DIコンテナ―、ロガー、エラーハンドラー、そしてそれ以外の様々な機能を持ったライブラリーや部品などから構成されており、Laravelのようなフルスタック・フレームワークでは、これらが1つのフレームワークとしてパッケージ化されています。
これらのフルスタック・フレームワークの利用は、中規模以上のアプリケーション開発では、開発効率や保守性に関して大きな効果が期待できますが、比較的小規模なアプリケーション開発では、フレームワークに関連する学習、調査、ファイルの編集、またはフレームワークのルールに従ったカスタマイズに多くの時間を費やされる場合も少なくありません。Apricot はこのような問題を解決するために、フレームワークのコアを出来るだけ軽量にし、カスタマイズ可能な部分をできるだけフレームワークの外側(即ち、アプリケーション部分)に配置するようにしました。
この問題解決により、比較的小規模なアプリケーション開発に於いて、アプリケーション全体のコードの肥大化を抑止し、また外部ライブラリーの相互運用性を高める効果も期待できます。フレームワークの選択は、開発すべきアプリケーションの規模と特性に依存すべきで、Apricotの存在がフレームワークの選択肢を広げることを期待しています。
Apricot はコア(Core)とアプリ(App)の2つの部分から構成されていますが、上記の理由により、Apricot のコアは Webアプリケーションの中心的な部分であるリクエストからレスポンスまでの以下に示す機能を中心に構成されています。
そして、これらの主要機能に加え、次の機能をApricot 独自で実装しています。
これら以外のコア機能は、以下の外部ライブラリーを使用しています。
また、運用時のデバッグツールとして以下のライブラリーを組み込んでいます。
アプリ部分はコアの機能を使って実装されます。アプリ部分の実装は、コントローラとミドルウェアの具象化が主な作業になります。これらの基底クラスは、コア部分に存在します。コントローラは、クライアントからの特定のリクエストを処理してレスポンスを生成するクラスです。ミドルウェアは一般に全てのまたは限定的なリクエストを幅広く処理するクラスで、コントローラによって形成されるアプリケーション層からは分離された存在です。両者は共にリクエストを処理して、最終的に何れかのクラスがレスポンスを生成します。
Apricotでは、これらのコンポーネントがリクエストを処理する過程で使用するデータモデルに関連するコンポーネントをアプリ部分に配置します。データモデルの基底クラスはApricot独自で実装されていますが、他のコンポーネントには、以下の外部ライブラリーを使用しています。
これらのデータモデル関連のコンポーネントは、単なるサンプルに過ぎず、Apricotユーザは好みに応じていつでも変更することができます。また、必要に応じて他の外部ライブラリーから必要なコンポーネントを追加して最適なアプリケーションを構成するようにして下さい。汎用的な独自のアプリケーション構成が完成したら、それを新しいフレームワークとしてライブラリー化し、同種のアプリケーションに適用することもできます。
Apricotのアプリ部分の名前空間は、\App
です。アプリのソースコードはプロジェクトディレクトリー下の app
に配置されます。一方、コア部分の名前空間は、\Apricot
です。これは、Apricotをプロジェクトとしてインストールした場合は、プロジェクトディレクトリー下の core
に配置され、ライブラリーとしてインストールした場合は、Composerのルールに従い、vendor/y2sunlight/apricot/core
の下に配置されます。
Apricotでは、コントローラ、ミドルウェアそしてテンプレートの中でフレームワークを呼び出す簡便な方法として、ボイラープレートとシングルトンを提供しています。
ボイラープレートとは良く使うフレームワークのコードパターンを関数として使用できるようにしたヘルパー関数です。例えば:
$app_name = env('APP_NAME'); $log_name = config('monolog.name');
env()
は環境変数の取得を、config()
はドット表記で指定された構成変数の値を取得するボイラープレートです。ボイラープレートについては、このドキュメントのユーティリティーの章を参照して下さい。
以下は、ログ出力を行うシングルトンの例です。
Log::error($message);
Logクラスは、monologのLoggerクラスをラップしたシングルトンで、Log::error()
はエラーレベルのログを出力するメソッドです。このコーディングは Laravelではお馴染みのファザードと同じスタイルをしています。Apricotのシングルトンは全てこのような静的アクセススタイルでメソッドを利用できるようになっています。
この他にもリクエストを取得する、Input、Session、Cookie、またはテンプレートエンジンのViewなどApricotでは様々なシングルトンが存在します。こららのシングルトンは、アプリケーションのどこからでも使用することができます。
このように、Apricotでは、ボイラープレートとシングルトンを使用した素朴で簡単なスタイルでコーディングができるようになっています。
Apricotの概要を知る為に、クライアントのリクエストを受け取ってからApricotがレスポンスを返すまでを、ユーザ登録アプリケーションを例に説明します。
リクエストルータは、アプリケーションのエンドポイントがどの コントローラ@アクション
を実行するのかを決定します。Apricotでは、リクエストルータに FastRouteを使用しています。ルーティングの設定は、config/routes.php
ファイルを編集することで行います。
以下は、ユーザ登録ページの CRUD操作(create, read, update, and delete)に対するルーティングの例です。
<?php /** * This file contains callback for route definitions. */ return function (FastRoute\RouteCollector $r) { /** @var string $base route base path */ $base = Apricot\Application::getInstance()->getRouteBase(); // Creates a route group with a common prefix. $r->addGroup($base, function (FastRoute\RouteCollector $r) use($base) { // User $r->get ('/users', 'UserController@index'); $r->get ('/user/create', 'UserController@create'); $r->post('/user/insert', 'UserController@insert'); $r->get ('/user/{id:\d+}/edit', 'UserController@edit'); $r->post('/user/{id:\d+}/update', 'UserController@update'); $r->post('/user/{id:\d+}/delete', 'UserController@delete'); }); };
ミドルウェアは、ルーティングによって決定されたコントローラの前後でリクエストを処理し、必要に応じてレスポンスを生成することができます。ミドルウェアがレスポンスを生成した場合、コントローラのリクエスト処理は行われません。以下は、セッションを利用してユーザ認証を行うミドルウェア(セッション認証)の例です。
<?php namespace App\Middleware\Auth; use Apricot\Foundation\Response; use Apricot\Foundation\Invoker; use Apricot\Foundation\Middleware\Middleware; use App\Foundation\Security\AuthUser; /** * Middleware - Session authentication */ class SessionAuth implements Middleware { /** * @var array List of controllers to exclude */ private $exclude = [ 'AuthController', ]; /** * {@inheritDoc} * @see \Apricot\Foundation\Middleware\Middleware::invoke() */ public function process(Invoker $next): Response { // When excluded controller. if (in_array(controllerName(), $this->exclude)) { return $next->invoke(); } // Verifys whether user is authenticated. if (AuthUser::verify()) { return $next->invoke(); } // Redirect to login page If not authenticated. return redirect(route('login')); } }
アプリ部分に実装されるミドルウェアの全ての具象クラスは、process() メソッドを実装する必要があります。
ミドルウェアは全てのコントローラに介入しますが、$exclude変数に登録されているコントローラは除外します。AuthUser
は認証ユーザクラスのシングルトンで、AuthUser::verify()
はユーザ認証をチェックするメソッドです。ユーザ認証に成功すると次のインポーカー( $next ) にミドルウェアまたはコントローラ@アクションをインボークさせ、失敗した場合は、ログインページ( route('login') )にリダイレクトします。
Apricotでは、以下のミドルウェアがアプリとして app/Middleware 内に初期実装されています。
これらは必要に応じて直接カスタマイズできます。また、アプリケーションの設定ファイル config/app.php を編集することにより新しいミドルウェアを追加することができます。
<?php /** * This file contains application settings. */ return [ 'middleware' =>[ \App\Middleware\AccessLog::class, /* Access log */ \App\Middleware\VerifyCsrfToken::class, /* Verify CSRF Token */ \App\Middleware\Auth\SessionAuth::class, /* Session authentication */ \App\Middleware\InputConverter::class, /* Input Converter */ \App\Middleware\MyMiddleware::class, /* New MyMiddleware */ ], ];
ルーティングによって設定されているエンドポイントがアクセスされ、ミドルウェアによってレスポンスが生成されなかった場合、Apricotはコントローラ@アクションをインボークします。コントローラの全ての具象クラスはApricotのアプリ部分の App\Foundation\Controller
を継承し、このControllersクラスは、コア部分の Apricot\Foundation\BaseController
を継承しています。コントローラはプロジェクトディレクトリー下の app/Controllers
に配置する必要があります( 名前空間は\App\Controllers
です )。
コントローラの継承関係
ConcreteController => Controller => BaseController
BaseController クラスの主な機能はコントローラーにインターセプターを登録する事です。また、Controller クラスにはアクションにトランザクションをサポートさせる機能があります。
以下はユーザコントローラの例です。ここには、コンストラクタと空のアクションメソッドを示します。
<?php namespace App\Controllers; use App\Foundation\Controller; use App\Models\User; /** * User Controller */ class UserController extends Controller { /** * @var \App\Models\User */ private $user; /** * Creates a user controller. */ public function __construct(User $user) { // User Model $this->user = $user; // Registers interceptors. $this->intercept('insert', 'UserInterceptor@insert'); $this->intercept('update', 'UserInterceptor@update'); // Registers transactional actions. $this->transactional('insert','update','delete'); } public function index(){} /* Users list page. */ public function create(){} /* User registration page. */ public function insert(){} /* Inserts a user record. */ public function edit(int $id){} /* User edit page. */ public function update(int $id){} /* Updates a user record. */ public function delete(int $id){} /* Deletes a user record. */ }
コントローラのコンストラクタは、Auto Wiring 機能をサポートします。これは、コンストラクター引数の型ヒントを調べることにより、オブジェクトとそのすべての依存関係を再帰的に自動的に解決する機能です。但し、注入できるのはオブジェクト型の変数だけです。Auto Wiring には外部ライブラリーの League/Container を使用しています。
Auto Wiring 機能で使用する引数には、一般的にモデルクラスやサービスクラスを指定します。ここでは、ユーザモデルをコンストラクター引数に指定し、それをメンバ変数に格納しています。
また、上のコンストラクタではコントローラのアクションにインターセプターを登録したり、アクションをトランザクション化していますが、それらについては、以下の項を参照して下さい。
通常のフレームワークでは、CGIやPHP環境からアプリケーションに到達したすべてのデータ($_SERVER、$_GET、$_POST、$_FILES、$_SESSIONなどの変数)を1つのリクエストクラスにカプセル化します。リクエストクラスでは、これらのデータに加え、フラッシュと呼ばれる、一度だけ保存されるセッション変数(次の画面の遷移のときまで保存される変数)もサポートされています。
Apricotにはリクエストクラスがありません。その代わりに、個々の変数をカプセル化したクラスのシングルトンを持っています。リクエスト取得用に以下のシングルトンがあります:
これらはシングルトンなので、アプリケーションのどこからでもリクエストを取得することができます。以下は、ユーザコントローラのinsertアクションでリクエストを取得している例です。
/** * Inserts a user record. * * @return \Apricot\Foundation\Response */ public function insert() { $inputs = Input::all(); // Do something. // ... }
Input
はフォームから送信データ(入力変数)を保持するクラスのシングルトンで、Input::all()
は全ての入力変数を取得するメソッドです(このコーディングは Laravelではお馴染みのファザードと同じスタイルをしています)。Apricotのシングルトンは全てこのような静的アクセススタイルでメソッドを利用できるようになっています。
以下は、ユーザコントローラの編集フォームを表示するアクションの例です。
/** * User edit page. * * @return \Apricot\Foundation\Response */ public function edit(int $id) { // Finds By the primary key $user = $this->user->findOne($id); if ($user!==false) { return render("user.edit", ["user"=>$user]); } else { return redirect(route("users"))->withOldErrors(); } }
URIに含まれるユーザID( $id )からユーザモデル( $this->user )を使って、ユーザ情報を取得しています。findOne() は、主キーによる検索を行うモデルのメソッドです。
ユーザ情報の取得が成功した場合、ボイラープレート render() を呼び出して、HTMLをレンダリングします。render() の第1引数にはHTMLテンプレート名、第2引数にはテンプレート変数を[変数名 ⇒ 値]の形で渡します。ユーザ情報の取得が失敗した場合、エラー情報を添付して( withOldErrors() )、users
にリダイレクトしています。
アクションの戻り値はレスポンスオブジェクトです。レスポンスには、この例のように、レンダーレスポンスとリダイレクトレスポンスの2つがあり、Aprocotはアクションの戻り値がどちらのタイプのレスポンスかによって適切なレスポンスを生成します。
ユーザコントローラのコンストラクタには次のようなコードがあります。
// Registers interceptors. $this->intercept('insert', 'UserInterceptor@insert'); $this->intercept('update', 'UserInterceptor@update');
$this->intercept()
はベースコントローラクラスのメソッドで、アクションにインターセプターを登録します。例えば、上の insert アクションの例では、このアクションに UserInterceptorクラスのinsertメソッドをインターセプターとして登録しています。インターセプターはプロジェクトディレクトリー下の app/Controllers/Interceptors に配置する必要があります( 名前空間は\app\Controllers\Interceptors です )。
インターセプターの目的はアクションに信頼性のある入力データを渡すことで、主な用途には以下のようなものがあります。
インターセプターはエラーがない場合は、voidを返しますが、バリデーションエラーなどがあった場合はレスポンスオブジェクトを返して以降のアクションを中止することができます。
以下にインターセプター( UserInterceptor@insert )の例を示します。インターセプターの第1引数はコントローラーのインスタンスで、その後にアクションと同じの引数が続きます。このメソッドの場合はコントローラの intser アクションに引数が無いので、intser インターセプター引数は1つだけです。
<?php namespace App\Controllers\Interceptors; use Apricot\Input; use App\Foundation\Controller; /** * User Interceptor */ class UserInterceptor { /** * Interceptor for insert method. * * @return void|\Apricot\Foundation\Response return Response if failed */ public function insert(Controller $controller) { $inputs = Input::all(); // Validation // ... // Removes unnecessary inputs Input::remove('password_confirmation'); } }
この例では、バリデーション後に、Input::remove()
を使用して、バリデーションだけで使用する入力変数を削除しています。完全なインターセプターの例は、次の項を参照して下さい。
Apricotでは サーバ側のバリデーションにValitron を使用しています。以下に、前項で示したインターセプター( UserInterceptor@insert )の完全な例を示します。
/** * Interceptor for insert method. * * @return void|\Apricot\Foundation\Response return Response if failed */ public function insert(Controller $controller) { $inputs = Input::all(); // Validation $v =(new \Valitron\Validator($inputs)) ->rule('required', ['account','password']) ->labels(inputLabels('messages.user.create')); if(!$v->validate()) { $errorBag = new ValidatorErrorBag($v->errors()); return redirect(back())->withInputs()->withErrors($errorBag); } // Removes unnecessary inputs Input::remove('password_confirmation'); }
この例では、Input::all()
で入力変数を取得した後に、Validator
のインスタンスを生成しています。
Validatorの rule()
メソッドは入力変数に検証ルールを適用します。この例では、必須入力を意味するrequired
ルールを入力変数の account と password に適用しています。また、labels()
メソッドはエラーメッセージで使う項目名を設定しています。
バリデーションの実行は validate()
メソッドで行います。バリデーションが失敗した時、withInputs()
で入力変数を、withErrors() でバリデーションのエラーバッグをフラッシュ変数に保存します。そして、redirect()で前画面にリダイレクトするレスポンスオブジェクトを生成し、それを返します。Apricotは、インターセプターがレスポンスオブジェクトを返した時、コントローラーアクションを呼び出さずに、そのレスポンスをクライアントに返します。
バリデーションの詳細は Valitron の README をご覧下さい。
ユーザコントローラのコンストラクタには次のようなコードがあります。
// Registers transactional actions. $this->transactional('insert','update','delete');
$this->transactional()
はアクションをトランザクション化するコントローラクラスのメソッドです。上の例では insert、update そして deleteアクションをトランザクション化しています。
Apricotは、アクションを呼び出す前にトランザクションを開始し、アクションが例外をスローしなかった場合、トランザクションが成功したものとみなして、そのトランザクションをコミットします。一方、アクションが ApplicationException
例外をスローした場合、Apricotは、それをキャッチしてエラーログを出力して、アクションに代わって前画面に戻るリダイレクトレスポンスを生成します。
Apricotの個々のモデルクラスは、\App\Foundation\Model
から継承して作りますが、このクラスはカスタマイズされることを前提としています。初期に実装されている Model
クラスは ORMに Idiorm を使用し、以下のメソッドを実装しています。
これらの実装では、アプリケーションで使用する全てのテーブルが以下のカラムを持っていることが仮定されています。
このように、初期実装されている Model
クラスには幾つかの前提があるので、アプリケーションに合わせてカスタマイズまたは再作成した方が良いかもしれません。また、ORMも Idiorm 以外のものを使うことができます。
以下は、初期実装されている Model
クラスを継承したユーザモデルの例です。個々のモデルクラスは、\App\Models
の下に配置されます。
<?php namespace App\Models; use App\Foundation\Model; use ORM; /** * User Model */ class User extends Model { /** * {@inheritDoc} * @see \App\Foundation\Model::insert() */ public function insert(array $inputs):ORM { // Encrypt the password that is required for new registration. $inputs['password'] = password_hash($inputs['password'], PASSWORD_DEFAULT); return parent::insert($inputs); } /** * {@inheritDoc} * @see \App\Foundation\Model::update() */ public function update($id, array $inputs):ORM { // Updates a password only if entered if(empty($inputs['password'])) unset($inputs['password']); if(array_key_exists('password', $inputs)) { // Encrypts the entered password $inputs['password'] = password_hash($inputs['password'], PASSWORD_DEFAULT); } return parent::update($id, $inputs); } }
ユーザモデルでは、insert() と update() メソッドをオーバーラードしています。insert() では入力された password
を暗号化してデータベースに保存しています。update() では入力されたpassword
が入力されていない場合は、入力変数から除外し、そうでない場合は、暗号化してデータベースに保存しています。
Apricotでは、テンプレートエンジンにLaravelと同じBladeを使用しています。実際に使用しているライブラリは BladeOne です。BladeOneは、Blade のスタンドアロンバージョンです。
以下は、ユーザコントロールの index アクションの例です。
public function index() { $users = $this->user->findAll(); return render("user.index", ["users"=>$users]); }
ここでは、ボイラープレートrender()
を使って、HTMLをレンダリングし、それをレスポンスとして返しています。render() の第1引数にはHTMLテンプレート名、第2引数にはテンプレート変数を[変数名 ⇒ 値]の形で渡します。
以下に、テンプレートの例( user.index )を示します。
{{-- Parent layout --}} @extends('layout') {{-- title --}} @section('title', __('messages.user.index.title')) {{--content --}} @section('content') <div class="table-responsive"> <table class="table table-hover"> <thead> <tr> <th>{{__('messages.user.index.id')}}</th> <th>{{__('messages.user.index.account')}}</th> <th>{{__('messages.user.index.email')}}</th> <th>{{__('messages.user.index.created_at')}}</th> </tr> </thead> <tbody> @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 </tbody> </table> </div> <button type="button" id="btn_new" class="btn btn-secondary" onclick="location.href='{{route('user/create')}}'">{{__('messages.user.index.btn_new')}}</button> @endsection
テンプレートファイルは、プロジェクトディレクトリ下の assets/views/
に配置します。一般的にテンプレートファイルは、assets/views/{path}/{name}.blade.php
として保存します。{path}
はサブディレクトリ下に配置したい場合のオプションですが、コントローラ名に関連した名前付けが一般的でしょう。上例のテンプレートファイルは、assets/views/user/index.blade.php
に配置しているので、テンプレート名は user.index
になります。
上の例で見られる、@extends、@section 及び @endsection、@foreach 及び @endforeach は Blade のディレクティブです。また、{{ $name }}
は $name を表示するための Blade の構文です。詳しくは以下のドキュメントを参照して下さい。
__( $key )
はトランスレータを呼び出すApricotのボイラープレートです。詳しくは次項の多言語化を参照して下さい。また、ViewHelper
はテンプレートヘルパーを実装するためのクラスで、app/Helpers/
の下に配置されています。ViewHelper::formatDatetime()
は日付を指定の書式で返すヘルパー関数です。
テンプレートヘルパーやシングルトン( Input, Session, Flashクラス など )はクラスアリアスが登録されているので、PHPのuse演算子や完全修飾した名前空間を使用することなく、テンプレートの中で使用できます。クラスアリアスの登録は config/setup/aliases.setup.php
の中で以下のようにされています。
<?php /** * Registers the class alias used in the view template */ return function():bool { $aliases = [ /* Apricot */ 'Input' => Apricot\Input::class, 'QueryString' => Apricot\QueryString::class, 'Session' => Apricot\Session::class, 'Flash' => Apricot\Flash::class, 'Cookie' => Apricot\Cookie::class, 'Config' => Apricot\Config::class, 'Log' => Apricot\Log::class, 'Debug' => Apricot\Debug::class, 'DebugBar' => Apricot\DebugBar::class, 'ErrorBag' => Apricot\Foundation\ErrorBag::class, /* App */ 'ViewHelper' => App\Helpers\ViewHelper::class, 'ValidatorErrorBag' => App\Foundation\ValidatorErrorBag::class, 'AuthUser' => App\Foundation\Security\AuthUser::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 };
Apricotは多言語をサポートしています。Lang
シングルトンの Lang::get()
メソッドはドット表記で指定されたキーから各言語用のメッセージを取得します。Apricotがどの言語を選択するかは、ブラウザで使用されている言語と準備されている言語ファイルによって自動的に決定されます。デフォルトの言語は、環境変数 APP_LANG
で指定してます。この変数が指定されていない場合は英語になります。
$message = Lang::get('messages.user.index.account');
Lang::get()
メソッドはボイラープレートを使って次のようにも記述できます。
$message = __('messages.user.index.account');
各言語用のメッセージはプロジェクトディレクトリー下の assets/lang/{言語コード}/
に配置された言語ファイルの中に保存され、そのファイル名がドット表記の最初のキーとなります。即ち、messages
で始まるキーを持つメッセージは 各言語毎に以下のファイルに保存されます。
your-project [プロジェクトディレクトリー] | ├── assets | | | ├── lang | | | | | ├── ja | | | ├── messages.php | | | | | ├── en | | | ├── messages.php
以下に、messages.php の例を示します。
<?php return [ 'user'=>[ 'index'=> [ 'title'=>'ユーザ一覧', 'id'=>'ID', 'account'=>'アカウント', 'email'=>'メールアドレス', 'note'=>'備考', 'created_at'=>'登録日', 'btn_new'=>'新規', ], ], ];