====== Apricot 各種基本コアクラス ====== --- //[[http://www.y2sunlight.com|y2sunlight]] 2020-04-25// [[apricot:top|Apricot に戻る]] 関連記事 * [[apricot:configuration|Apricot プロジェクトの作成]] * [[apricot:public|Apricot 公開フォルダ]] * Apricot コア * [[apricot:core:top|Apricot コア作成の準備]] * [[apricot:core:application-class|Apricot アプリケーションクラス]] * Apricot 各種基本コアクラス * [[apricot:core:request-class|Apricot リクエストクラス]] * [[apricot:core:response-class|Apricot レスポンスクラス]] * [[apricot:core:base-controller|Apricot ベースコントローラ]] * [[apricot:core:completion|Apricot コアの完成]] * [[apricot:app:top|Apricot アプリ]] * [[apricot:ext:middleware|Apricot 拡張]] 次に、Applicationクラス以外の基本的なコアクラスを作ります。ここで作成する多くのクラスはシングルトンとして実装します。 ---- ===== 設定管理 ===== 設定管理の責任を持つクラスは Configurationクラスです。基本的な機能だけをシンプルに実装しています。 ==== 設定ファイル ==== 設定ファイルはconfig/setting フォルダに以下のネーミング規則で保存されます。 * 設定ファイル名: {first_key}.setting.php 設定値へのアクセスには **ドット表記** による「設定キー」によって行われます。設定キーの第1キーが設定ファイル名の{first_key}と一致します。以下にmonologでの例を示します。 {{fa>folder-open-o}} ** /apricot/config/setting ** env('LOG_NAME',env('APP_NAME')), 'path' => env('LOG_PATH',var_dir('logs')), 'level'=> env('LOG_LEVEL','debug'), 'max_files'=> 0, ]; 例えば「ログの名前」を参照する設定キーは、''monolog.name'' になります。ドット表記による階層に制限はなく、monolog.phpで返す連想配列の階層が深ければ ''monolog.second_key.third_key.4th_key'' などのように深い階層も可能です。 \\ ==== Configurationクラス ==== Configurationクラスの実装コードを以下に示します。ユーザはこのクラスを直接利用するのではなく、次節に示す Configクラスを使用して下さい。 {{fa>folder-open-o}} ** /apricot/core/Foundation ** read($file, $arr[0]); } } /** * Checks if a key is present * @param string $dot Dot-notation key * @return bool */ public function has(string $dot):bool { return array_has($this->config, $dot); } /** * Get a value from the configuration * @param string $dot Dot-notation key * @param mixed $default * @return mixed */ public function get(string $dot, $default = null) { return array_get($this->config, $dot, $default); } /** * Read configuration * @param string $config_file * @param string $top_key */ private function read(string $config_file, string $top_key) { $config = require_once $config_file; if (is_array($config) && count($config)) { $this->config[$top_key] = $config; } } } \\ ==== Configクラス ==== Configクラスは 上のConfigurationクラスをシングルトンにしたもので、以下のメソッドがあります。 使用法: ** Config::{メソッド} ** ^メソッド^機能^ |bool has(string $key)|設定キーの存在確認| |mixed get(string $key, $default = null)|設定値の取得| {{fa>folder-open-o}} ** /apricot/core ** === ヘルパー関数 === Configクラスのget()メソッドは良く使用されるのでヘルパー関数に追加しておきます。 {{fa>folder-open-o}} ** /apricot/core/helpers ** /** * Get Configuration Variable * @param string $key * @param mixed $default * @return mixed configuration Variable */ function config($key, $default = null) { return Core\Config::get($key, $default); } \\ ===== ロギング ===== ロギングは、[[basic-library:monolog:2.0|monolog]]をラップしたLogクラスが担当します。Logクラスはシングルトンとして実装し、以下のように使用します。機能的にはmonologと同じですが、[[https://www.php-fig.org/psr/psr-3/|PSR-3]]に従って使います。 使用法: **Log::{メソッド}** ^メソッド^機能^ |void emergency(string $message, array $context = [])|emergencyレベルのログ| |void alert(string $message, array $context = [])|alertレベルのログ| |void critical(string $message, array $context = [])|criticalレベルのログ| |void error(string $message, array $context = [])|errorレベルのログ| |void warning(string $message, array $context = [])|warningレベルのログ| |void notice(string $message, array $context = [])|noticeレベルのログ| |void info(string $message, array $context = [])|infoレベルのログ| |void debug(string $message, array $context = [])|debugレベルのログ| |void log($level, string $message, array $context = [])|任意レベルのログ| Logクラスの実装は以下のようです。 {{fa>folder-open-o}} ** /apricot/core ** log($level, $e->getMessage(),[$e->getFile(), $e->getLine(), $e->getTraceAsString()]); } /** * Create Monolog Logger instance. * @return \Monolog\Logger */ protected static function createInstance() { $log_name = config('monolog.name'); $log_path = config('monolog.path'); $log_level = config('monolog.level'); $log_max_files = config('monolog.max_files',0); // ログハンドラーの作成 // ログフォーマット設定: ログ内の改行を許可、付加情報が空の場合無視する $log_file_name = "{$log_path}/{$log_name}.log"; $stream = new RotatingFileHandler($log_file_name, $log_max_files, $log_level); $stream->setFormatter(new LineFormatter(null, null, true, true)); // ログチャネルの作成 //////////////////////// $instance = new Logger($log_name); $instance->pushHandler($stream); return $instance; } } ロギング設定は以下のようにシンプルなものです。 * ログチャネルは1つだけ * ログは日付によってローテーションする ( 例:apricot-2020-04-01.log) \\ ==== 設定ファイル ==== {{fa>folder-open-o}} ** /apricot/config/setting ** env('LOG_NAME',env('APP_NAME')), 'path' => env('LOG_PATH',var_dir('logs')), 'level'=> env('LOG_LEVEL','debug'), 'max_files'=> 0, ]; * name --- ログの名前(既定値は 環境変数APP_NAMEの値) * path --- ログの出力パス(既定値は var/logs/) * level --- ログの出力レベル(既定値は 'debug') * max_files --- ログファイルの最大保存数(0は無制限) \\ ===== 集約例外ハンドラー ===== apricotでは例外ハンドラーとして[[basic-library:whoops:2.7|Whoops]]を使います。例外ハンドラーの動作はデバッグ用と本番用で分けて実装します。 * デバッグ用 --- ログ出力して、例外内容とスタックトレースを画面に表示する * 本番用 --- ログ出力して、ユーザ向けのエラー画面を表示する ==== Whoopsのカスタマイズ ==== デバッグ用の例外ハンドラーにはWhoopsで提供されている PrettyPageHandlerクラス を使いますが、このクラスにはログ出力の機能がないので継承してログ出力機能を実装した PrettyErrorHandlerWithLoggerクラス を作ります。 {{fa>folder-open-o}} ** /apricot/core/Derivations ** getMessage(),[$exception->getFile(),$exception->getLine(), $exception->getTraceAsString()]); parent::handle(); } } \\ ==== 設定ファイル ==== {{fa>folder-open-o}} ** /apricot/config/setting ** env('APP_DEBUG',false), 'controller' => \App\Exceptions\UncaughtExceptionHandler::class, 'action' => 'render', ]; * debug --- デバッグモード(既定値は 環境変数APP_DEBUGの値) * controller --- 本番用エラー画面のコントローラクラス(後述) * action --- 本番用エラー画面のアクションメソッド(後述) \\ ==== 初期設定ファイル==== 集約例外ハンドラーの初期設定ファイルを以下に示します。 {{fa>folder-open-o}} ** /apricot/config/setup ** pushHandler(new \Core\Derivations\PrettyErrorHandlerWithLogger); } else { //---------------------------- // 本番用のエラーハンドラー //---------------------------- $whoops->pushHandler(function($exception, $inspector, $run) { // エラーログ出力 \Core\Log::critical($exception->getMessage(),[$exception->getFile(),$exception->getLine(), $exception->getTraceAsString()]); // ユーザ向けエラー画面の表示 // TODO: ここは例外のループを抑止しなかればならない $controller = config('whoops.controller',null); $action = config('whoops.action',null); if (isset($controller)&&isset($action)) { $instance = new $controller(); $response = call_user_func_array(array($instance, $action), [$exception]); } return \Whoops\Handler\Handler::QUIT; }); } $whoops->register(); return true; // Must return true on success }; * 設定ファイル( whoops.setting.php )のdebugの値を見て、デバッグ用か本番用かを分けています。 * デバッグ用の場合は、[[#例外ハンドラーの継承|PrettyErrorHandlerWithLogger]]クラス を呼び出し * 本番用の場合は、設定ファイルで定義した controller@action を呼び出しています \\ ===== デバッグバー ===== デバッグ機能として[[basic-library:php-debugbar:1.16|php-debugbar]]を導入し、設定によってこの機能がON/OFFできるようにします。 ==== 公開用リソースの設置 ==== php-debugbarはサーバ側の変数をクライアント画面で表示するので、JavaScriptやCSSなどのリソース設定が必要になり、これらのリソースは公開フォルダー( public )の下に設置する必要があります。以下にその手順を示します。 - public/resources の下に ''debugbar'' フォルダを作成します - ''vender/maximebf/debugbar/src/DebugBar/Resources/'' の下にある全てのファイルとフォルダを、上で作った''debugbar'' フォルダの中にコピーします 結果的に以下のようになります: {{fa>folder-open-o}} ** /apricot/public/resources/debugbar ** vendor/ widgets/ debugbar.css debugbar.js openhandler.css openhandler.js widgets.css widgets.js \\ ==== DebugBarのカスタマイズ ==== apricotではデバッグ出力用に、DebugBar提供の StandardDebugBar クラスを使用します。この StandardDebugBar を使うためには、次の2つのステップが必要になります: - コレクター(Collector)を使ってデバッグ出力を行う - デバッグ出力を画面にレンダリングする apricotではこの2つのステップを使い易くする為に、StandardDebugBar クラスを以下のようにカスタマイズして使います。 {{fa>folder-open-o}} ** /apricot/core/Derivations ** debugBar = new \DebugBar\StandardDebugBar(); // Get JavascriptRenderer $base_url = config('debugbar.renderer.base_url'); $base_path = config('debugbar.renderer.base_path'); $this->renderer = $this->debugBar->getJavascriptRenderer($base_url, $base_path); $this->renderer->setEnableJqueryNoConflict(false); } /** * Renders the html to include needed assets * @return string */ public function renderHead():string { if (config('debugbar.debug')) { return $this->renderer->renderHead(); } return ''; } /** * Returns the code needed to display the debug bar * @return string */ public function render():string { if (config('debugbar.debug')) { $initialize = config('debugbar.renderer.initialize', true); $stacked_data = config('debugbar.renderer.stacked_data', true); return $this->renderer->render($initialize, $stacked_data); } return ''; } /** * Get Data Collector * @param string $name * @return DataCollectorInterface */ public function getCollector(string $name="messages"): DataCollectorInterface { return $this->debugBar->getCollector($name); } } カスタム化された StandardDebugBar クラスのコンストラクタでは、後述の設定ファイル( debugbar.setting.php )に従ってJavascriptのレンダラーを取得しています。 この StandardDebugBar クラスでは以下のメソッドが実装されています。 * renderHead() --- HTMLヘッダー用のレンダリングを行います。 * render() --- HTMLボディ用のレンダリングを行います。 * getCollector() --- デバッグ出力用のコレクターを取得します。 renderHead() と render() はHTMLテンプレート内で使います。 \\ ==== DebugBarクラス ==== DebugBarは、上でカスタマイズしたStandardDebugBarをラップしたクラスで、シングルトンとして実装します。DebugBar クラスはデバッグ出力のレンダリングで使用します。実際のデバッグ出力は次に説明する Debug クラスが担当します。 使用法: **DebugBar::{メソッド}** ^メソッド^機能^ |string renderHead()|HTMLヘッダー用のレンダリング文字列を返す| |mixed render()|HTMLボディー用のレンダリング文字列を返す| |\DataCollector\DataCollectorInterface\\ getCollector(string $name="messages")|デバッグ出力用のコレクターの取得| DebugBar クラスの実装は以下のようです。 {{fa>folder-open-o}} ** /apricot/core ** \\ ==== Debugクラス ==== 実際にデバッグライトを行うクラスです。機能的にはDebugBarのコレクター( DataCollectorInterface )と同じですが、ロギングと同様にPSR-3に従って以下のように使います。以下の関数は基本的にvar_dump()と同じように変数の内容をダンプします (これらの関数の違いは単に出力レベルが付いているだけです)。''Debug::debug($this)'' とすれば自分のメンバ変数が全てダンプされます。 使用法: ** Debug::{メソッド} ** ^メソッド^機能^ |void emergency(string $message, array $context = [])|emergencyレベル| |void alert(string $message, array $context = [])|alertレベル| |void critical(string $message, array $context = [])|criticalレベル| |void error(string $message, array $context = [])|errorレベル| |void warning(string $message, array $context = [])|warningレベル| |void notice(string $message, array $context = [])|noticeレベル| |void info(string $message, array $context = [])|infoレベル| |void debug(string $message, array $context = [])|debugレベル| |void log($level, string $message, array $context = [])|任意レベル| Debugクラスの実装は以下のようです。 {{fa>folder-open-o}} ** /apricot/core ** * 親クラスが Singletonではなく CallStatic である点に注意して下さい。 * getInstance()では、単に DebugBarオブジェクトから取得したコレクターを返しているだけです。Debug オブジェクトはDebugBarオブジェクトに包含されています。 \\ ==== 設定ファイル ==== {{fa>folder-open-o}} ** /apricot/config/setting ** env('APP_DEBUG',false), 'renderer' => [ 'base_url' => url('resources/debugbar'), 'base_path' => public_dir('resources/debugbar'), 'initialize' => true, 'stacked_data' => true, ], ]; * debug --- デバッグモード(既定値は 環境変数APP_DEBUGの値) * renderer.base_url --- DebugBarの公開用リソースのURL * renderer.base_path --- DebugBarの公開用リソースのサーバ内ディレクトリ * renderer.initialize --- 初期化コードをレンダリングするか否か(既定値はtrue) * renderer.stacked_data --- スタックデータをレンダリングするか否か(既定値はtrue) \\ ===== HTMLテンプレート ===== HTMLテンプレートは、[[basic-library:bladeone:3.37|BladeOne]]をラップしたViewクラスが担当します。Viewクラスはシングルトンとして実装し、以下のように使用します。BladeOneと同じメソッド使用できますが、apricotで使用するのはrun()メソッドだけです。 使用法: **View::{メソッド}** ^メソッド^機能^ |string run(string $view, array $variables = [])|テンプレートエンジンの実行| * $view --- テンプレート名 ( テンプレートファイル名は {$view}.blade.php になる) * $variables --- ビュー変数 (['変数名'=>'値']の形式の連想配列) * return値 --- HTMLテキストを返す Viewクラスの実装は以下のようです。 {{fa>folder-open-o}} ** /apricot/core ** Viewクラスはテンプレートファイルのパス、コンパイル後のHTMLファイルのパス及び実行モードをBladeOneのコンストラクタに渡しているだけです。それらの値は、設定ファイル(bladeone.setting.php)から取得します。 \\ ==== 設定ファイル ==== {{fa>folder-open-o}} ** /apricot/config/setting ** env('VIEW_TEMPLATE',assets_dir('views')), 'compile_path' => env('VIEW_CACHE',var_dir('cache/views')), 'mode' => \eftec\bladeone\BladeOne::MODE_AUTO, ]; * template_path --- HTMLテンプレートファイルのパス(既定値は assets/views/) * compile_path --- コンパイル後のHTMLファイルのパス(既定値は var/cache/) * mode --- 実行モード(既定値は MODE_AUTO) \\ ==== 初期設定ファイル ==== Viewクラスには以下の初期設定ファイルが存在します。 {{fa>folder-open-o}} ** /apricot/config/setup ** "; }); return true; // Must return true on success }; ここでは、HTMLテンプレートで使用するカスタムディレクティブを追加します。上のコードは、現在時刻を表示する @now ディレクティブの追加を行っています。CSRFトークンを出力する @csrf ディレクティブなどもここで実装する予定です。 \\ ===== トランスレーション ===== トランスレーションは Translationクラスに実装されており、このクラスも基本的な機能だけをシンプルに作成しています。 ==== 言語ファイル ==== トランスレーションで使用する言語ファイルは assets/lang フォルダに言語毎に保存されます。日本語の場合は言語コードが **ja** なので assets/lang/ja/ に保存されます。言語ファイルのネーミング規則は以下の通りです。 * 言語ファイル名: {first_key}.php このファイルには各言語でのテキストが連想配列によって {キー}=>{テキスト} の形式で格納されています。テキストの取得には **ドット表記** を使用します。キーの最初の部分は設定ファイル名の{first_key}と一致します。以下に例を示します。 {{fa>folder-open-o}} ** /apricot/assets/lang/ja ** [ 'title'=>env('APP_NAME'), 'menu'=>[ 'menu1'=>'Menu1', 'menu2'=>'Menu2', 'menu3'=>'Menu3', 'logout'=>'Logout', 'about_me'=>'About Me', ], ], ]; 例えば「アプリのタイトル」を参照するキーは、''messages.app.title'' に、Usersメニューを参照するには ''messages.app.menu.users'' になります。 \\ ==== Translationクラス ==== Translationクラスの実装コードを以下に示します。ユーザはこのクラスを直接利用するのではなく、次節に示す Langクラスを使用して下さい。 {{fa>folder-open-o}} ** /apricot/core/Foundation ** read($file, $arr[0]); } } /** * Checks if a key is present * @param string $key * @return bool */ public function has(string $key):bool { return array_key_exists($key, $this->messages); } /** * Get a value from the Messages. * @param string $key * @param string $params * @return string */ public function get(string $key, array $params = []):string { if ($this->has($key)) { $message = $this->messages[$key]; if (!empty($params)) { $message = str_replace(array_keys($params), array_values($params), $message); } } else { $message = $key; } return $message; } /** * Read Messages * @param string $lang_file * @param string $top_key */ private function read(string $lang_file, string $top_key) { $messages = require_once $lang_file; if (is_array($messages) && count($messages)) { $dot_arr = array_dot($messages, $top_key.'.'); $this->messages = array_merge($this->messages, $dot_arr); } } } \\ ==== Langクラス ==== Langクラスは 上のTranslationクラスをシングルトンにしたもので、以下のメソッドがあります。 使用法: ** Lang::{メソッド} ** ^メソッド^機能^ |bool has(string $key)|キーの存在確認| |string get(string $key, array $params = [])|言語テキストの取得| {{fa>folder-open-o}} ** /apricot/core ** >上の実装では環境変数から言語コードを取得していますが、国際対応として実装する場合は、''$_SERVER['HTTP_ACCEPT_LANGUAGE']'' から言語コードを取得した方が良いです。 === ヘルパー関数 === Langクラスのget()メソッドは良く使用されるのでヘルパー関数に追加しておきます。 {{fa>folder-open-o}} ** /apricot/core/helpers ** /** * Get Translated Message * @param string $key * @param string $params * @return string translated Message */ function __($key, $params = []) { return Core\Lang::get($key, $params); } > この関数名は __ です。2つ並んだアンダースコアはPythonプログラマーの間では ''dunders'' (double underscoreの意) と呼ばれ特別なクラス内メンバに付加されますが、ここではそのような意味はなくトランスレータを表す関数名としてLaravelに準じました。 \\ ===== エラーバッグ ===== エラーバッグ(ErrorBagクラス)は、入力エラーなどの業務的なエラー(例外ではないエラー)を管理する為のクラスです。 ==== ErrorBagクラス ==== エラーバッグには名前を付けることができます。エラーは連想配列で保存されバッグ内の各エラーにはキーが付いています。ErrorBagクラスには以下のメソッドがあります。 ^メソッド^機能^ |__construct($errors=null, string $name=self::DEFAULT_NAME)|エラーバッグの生成| |count(string $name=null):int|エラー数の取得| |has(string $key, string $name=self::DEFAULT_NAME):bool|キーによるエラーの存在確認| |get(string $key, string $name=self::DEFAULT_NAME)|キーによるエラーの取得| |all(string $name=null):array|全てのエラーの取得| |put($errors)|エラー配列の設定| >エラーバッグは[[https://www.php.net/manual/ja/class.iteratoraggregate.php|IteratorAggregateインターフェース]]を実装してるのでforeach()などのIteratorを使用した構文が使用できます。但し、Countable インターフェイス は実装していないので、count関数ではなくErrorBag@countメソッドを使用して下さい。 {{fa>folder-open-o}} ** /apricot/core/Foundation ** name = $name; if (isset($errors)) { $this->put($errors); } } /** * Count errors * @param string $name Bag name * @return int */ public function count(string $name=null):int { if (!isset($name) || ($this->name==$name)) { return count($this->errors); } else { return 0; } } /** * Checks if a key is present * @param string $key Error key * @param string $name Bag name * @return boolean */ public function has(string $key, string $name=self::DEFAULT_NAME):bool { if ($this->name==$name) { return array_key_exists($key, $this->errors); } return false; } /** * Get error a bag * @param string $key Error key * @param string $name Bag name * @return mixed return null if a key is not present */ public function get(string $key, string $name=self::DEFAULT_NAME) { $result = null; if ($this->name==$name) { if ($this->has($key, $name)) { return $this->errors[$key]; } } return $result; } /** * Get all errors * @param string $name Bag name * @return array */ public function all(string $name=null):array { if (!isset($name) || ($this->name==$name)) { return $this->errors; } else { return []; } } /** * Put errors * @param array $error Associative array */ public function put($errors) { $arr = is_array($errors) ? $errors : (array)$errors; $this->errors = $arr; } /** * IteratorAggregate Interface */ public function getIterator() { return new \ArrayIterator($this->errors); } } \\