— y2sunlight 2020-04-25
関連記事
次に、Applicationクラス以外の基本的なコアクラスを作ります。ここで作成する多くのクラスはシングルトンとして実装します。
設定管理の責任を持つクラスは Configurationクラスです。基本的な機能だけをシンプルに実装しています。
設定ファイルはconfig/setting フォルダに以下のネーミング規則で保存されます。
設定値へのアクセスには ドット表記 による「設定キー」によって行われます。設定キーの第1キーが設定ファイル名の{first_key}と一致します。以下にmonologでの例を示します。
/apricot/config/setting
<?php return [ 'name' => 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クラスの実装コードを以下に示します。ユーザはこのクラスを直接利用するのではなく、次節に示す Configクラスを使用して下さい。
/apricot/core/Foundation
<?php namespace Core\Foundation; /** * Improvised Configuration Class */ class Configuration { /** * Configurations * @var array */ protected $config = []; /** * Create Configuration */ function __construct() { // Read Configuration foreach(glob(config_dir("setting/*.setting.php")) as $file) { $arr = explode('.', basename($file)); if (is_file($file)) $this->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クラスは 上のConfigurationクラスをシングルトンにしたもので、以下のメソッドがあります。
使用法: Config::{メソッド}
メソッド | 機能 |
---|---|
bool has(string $key) | 設定キーの存在確認 |
mixed get(string $key, $default = null) | 設定値の取得 |
/apricot/core
<?php namespace Core; use Core\Foundation\Singleton; use Core\Foundation\Configuration; /** * Config Class - Configuration Wrapper * * @method static Configuration getInstance(); * @method static bool has(string $key) * @method static mixed get(string $key, $default = null) */ class Config extends Singleton { /** * Create Translation instance. * @return Configuration */ protected static function createInstance() { return new Configuration(); } }
Configクラスのget()メソッドは良く使用されるのでヘルパー関数に追加しておきます。
/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); }
ロギングは、monologをラップしたLogクラスが担当します。Logクラスはシングルトンとして実装し、以下のように使用します。機能的にはmonologと同じですが、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クラスの実装は以下のようです。
/apricot/core
<?php namespace Core; use Core\Foundation\Singleton; use Monolog\Logger; use Monolog\Formatter\LineFormatter; use Monolog\Handler\RotatingFileHandler; /** * Log Class - Monolog\Logger Wrapper * * @method static Logger getInstance(); * @method static void emergency(string $message, array $context = []) Action must be taken immediately. * @method static void alert(string $message, array $context = []) Runtime errors that do not require immediate action but should typically be logged and monitored. * @method static void critical(string $message, array $context = []) Critical conditions. * @method static void error(string $message, array $context = []) Exceptional occurrences that are not errors. * @method static void warning(string $message, array $context = []) Normal but significant events. * @method static void notice(string $message, array $context = []) Interesting events. * @method static void info(string $message, array $context = []) User logs in, SQL logs. * @method static void debug(string $message, array $context = []) Logs with an arbitrary level. * @method static void log($level, string $message, array $context = []) Logs with an arbitrary level. */ class Log extends Singleton { /** * Log Exception * @param string $level 'debug','info','notice','warning','error','critical','alert','emergency * @param \Exception $e */ public static function exception(string $level, \Exception $e) { self::getInstance()->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; } }
ロギング設定は以下のようにシンプルなものです。
/apricot/config/setting
<?php return [ 'name' => env('LOG_NAME',env('APP_NAME')), 'path' => env('LOG_PATH',var_dir('logs')), 'level'=> env('LOG_LEVEL','debug'), 'max_files'=> 0, ];
apricotでは例外ハンドラーとしてWhoopsを使います。例外ハンドラーの動作はデバッグ用と本番用で分けて実装します。
デバッグ用の例外ハンドラーにはWhoopsで提供されている PrettyPageHandlerクラス を使いますが、このクラスにはログ出力の機能がないので継承してログ出力機能を実装した PrettyErrorHandlerWithLoggerクラス を作ります。
/apricot/core/Derivations
<?php namespace Core\Derivations; /** * PrettyErrorHandler With Logger */ class PrettyErrorHandlerWithLogger extends \Whoops\Handler\PrettyPageHandler { /** * {@inheritDoc} * @see \Whoops\Handler\PrettyPageHandler::handle() */ public function handle() { // エラーログ出力 $exception = parent::getException(); \Core\Log::critical($exception->getMessage(),[$exception->getFile(),$exception->getLine(), $exception->getTraceAsString()]); parent::handle(); } }
/apricot/config/setting
<?php return [ 'debug' => env('APP_DEBUG',false), 'controller' => \App\Exceptions\UncaughtExceptionHandler::class, 'action' => 'render', ];
集約例外ハンドラーの初期設定ファイルを以下に示します。
/apricot/config/setup
<?php //------------------------------------------------------------------- // エラーハンドラー(Whoops)の初期設定 //------------------------------------------------------------------- return function():bool { $whoops = new \Whoops\Run; if(config('whoops.debug')) { //---------------------------- // デバッグ用のエラーハンドラー //---------------------------- $whoops->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 };
デバッグ機能としてphp-debugbarを導入し、設定によってこの機能がON/OFFできるようにします。
php-debugbarはサーバ側の変数をクライアント画面で表示するので、JavaScriptやCSSなどのリソース設定が必要になり、これらのリソースは公開フォルダー( public )の下に設置する必要があります。以下にその手順を示します。
debugbar
フォルダを作成しますvender/maximebf/debugbar/src/DebugBar/Resources/
の下にある全てのファイルとフォルダを、上で作ったdebugbar
フォルダの中にコピーします結果的に以下のようになります:
/apricot/public/resources/debugbar
vendor/ widgets/ debugbar.css debugbar.js openhandler.css openhandler.js widgets.css widgets.js
apricotではデバッグ出力用に、DebugBar提供の StandardDebugBar クラスを使用します。この StandardDebugBar を使うためには、次の2つのステップが必要になります:
apricotではこの2つのステップを使い易くする為に、StandardDebugBar クラスを以下のようにカスタマイズして使います。
/apricot/core/Derivations
<?php namespace Core\Derivations; use DebugBar\DataCollector\DataCollectorInterface; /** * StandardDebugBar - Inclusion of \DebugBar\StandardDebugBar Class */ Class StandardDebugBar { /** * DebugBar * @var \DebugBar\StandardDebugBar */ protected $debugBar; /** * JavascriptRenderer * @var \DebugBar\JavascriptRenderer */ protected $renderer; /** * Create custom StandardDebugBar instance. */ public function __construct() { // Create StandardDebugBar $this->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() と render() はHTMLテンプレート内で使います。
DebugBarは、上でカスタマイズしたStandardDebugBarをラップしたクラスで、シングルトンとして実装します。DebugBar クラスはデバッグ出力のレンダリングで使用します。実際のデバッグ出力は次に説明する Debug クラスが担当します。
使用法: DebugBar::{メソッド}
メソッド | 機能 |
---|---|
string renderHead() | HTMLヘッダー用のレンダリング文字列を返す |
mixed render() | HTMLボディー用のレンダリング文字列を返す |
\DataCollector\DataCollectorInterface getCollector(string $name=“messages”) | デバッグ出力用のコレクターの取得 |
DebugBar クラスの実装は以下のようです。
/apricot/core
<?php namespace Core; use Core\Foundation\Singleton; use Core\Derivations\StandardDebugBar; /** * DebugBar Class - StandardDebugBar Wrapper * * @method static SimpleDebugBar getInstance() * @method static string renderHead() * @method static mixed render() * @method static \DataCollector\DataCollectorInterface getCollector(string $name="messages") */ class DebugBar extends Singleton { /** * Create SimpleDebugBar instance. * @return \Core\Derivations\SimpleDebugBar; */ protected static function createInstance() { return new StandardDebugBar(); } }
実際にデバッグライトを行うクラスです。機能的には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クラスの実装は以下のようです。
/apricot/core
<?php namespace Core; use Core\Foundation\CallStatic; /** * Debug Class - LoggerInterface Wrapper * * @method static \Psr\Log\LoggerInterface getInstance() * @method static void debug($message, array $context = array()) * @method static void info($message, array $context = array()) * @method static void notice($message, array $context = array()) * @method static void warning($message, array $context = array()) * @method static void error($message, array $context = array()) * @method static void critical($message, array $context = array()) * @method static void alert($message, array $context = array()) * @method static void emergency($message, array $context = array()) * @method static void log($level, $message, array $context = array()) */ class Debug extends CallStatic { /** * Create Debug instance. * @return \Psr\Log\LoggerInterface */ public static function getInstance() { // DebugBarの作成 return \Core\DebugBar::getCollector('messages'); } }
/apricot/config/setting
<?php return [ 'debug' => env('APP_DEBUG',false), 'renderer' => [ 'base_url' => url('resources/debugbar'), 'base_path' => public_dir('resources/debugbar'), 'initialize' => true, 'stacked_data' => true, ], ];
HTMLテンプレートは、BladeOneをラップしたViewクラスが担当します。Viewクラスはシングルトンとして実装し、以下のように使用します。BladeOneと同じメソッド使用できますが、apricotで使用するのはrun()メソッドだけです。
使用法: View::{メソッド}
メソッド | 機能 |
---|---|
string run(string $view, array $variables = []) | テンプレートエンジンの実行 |
Viewクラスの実装は以下のようです。
/apricot/core
<?php namespace Core; use Core\Foundation\Singleton; use eftec\bladeone\BladeOne; /** * View Class - BladeOne Wrapper * * @method static BladeOne getInstance(); * @method static string run(string $view, array $variables = []) run the blade engine. It returns the result of the code. * @method static void setAuth($user = '', $role = null, $permission = []) Authentication. Sets with a user,role and permission * @method static void share($varname, $value) Adds a global variable * @method static BladeOne setOptimize($bool = false) If true then it optimizes the result (it removes tab and extra spaces). * @method static BladeOne setIsCompiled($bool = false) If false then the file is not compiled and it is executed directly from the memory. By default the value is true. It also sets the mode to MODE_SLOW. * @method static void setMode(int $mode) Set the compile mode * @method static void setFileExtension(string $fileExtension) Set the file extension for the template files. It must includes the leading dot e.g. .blade.php * @method static string getFileExtension() Get the file extension for template files. * @method static void setCompiledExtension(string $fileExtension) Set the file extension for the compiled files. Including the leading dot for the extension is required, e.g. .bladec * @method static string getCompiledExtension() Get the file extension for template files. * @method static string runString(string $string, array $data = []) run the blade engine. It returns the result of the code. * @method static void directive(string $name, callable $handler) Register a handler for custom directives. * @method static void directiveRT(string $name, callable $handler) Register a handler for custom directives for run at runtime * @method static void setErrorFunction(callable $fn) It sets the callback function for errors. It is used by @error * @method static void setCanFunction(callable $fn) It sets the callback function for authentication. It is used by @can and @cannot * @method static void setAnyFunction(callable $fn) It sets the callback function for authentication. It is used by @canany */ class View extends Singleton { /** * Create Blade instance. * @return \eftec\bladeone\BladeOne */ protected static function createInstance() { $templatePath = config('bladeone.template_path'); $compiledPath = config('bladeone.compile_path'); $mode = config('bladeone.mode'); return new BladeOne($templatePath, $compiledPath, $mode); } }
Viewクラスはテンプレートファイルのパス、コンパイル後のHTMLファイルのパス及び実行モードをBladeOneのコンストラクタに渡しているだけです。それらの値は、設定ファイル(bladeone.setting.php)から取得します。
/apricot/config/setting
<?php return [ 'template_path' => env('VIEW_TEMPLATE',assets_dir('views')), 'compile_path' => env('VIEW_CACHE',var_dir('cache/views')), 'mode' => \eftec\bladeone\BladeOne::MODE_AUTO, ];
Viewクラスには以下の初期設定ファイルが存在します。
/apricot/config/setup
<?php //------------------------------------------------------------------- // View template (BladeOne)の初期設定 //------------------------------------------------------------------- return function():bool { // @now directive \Core\View::directive('now', function() { return "<?php echo date('Y-m-d H:i'); ?>"; }); return true; // Must return true on success };
ここでは、HTMLテンプレートで使用するカスタムディレクティブを追加します。上のコードは、現在時刻を表示する @now ディレクティブの追加を行っています。CSRFトークンを出力する @csrf ディレクティブなどもここで実装する予定です。
トランスレーションは Translationクラスに実装されており、このクラスも基本的な機能だけをシンプルに作成しています。
トランスレーションで使用する言語ファイルは assets/lang フォルダに言語毎に保存されます。日本語の場合は言語コードが ja なので assets/lang/ja/ に保存されます。言語ファイルのネーミング規則は以下の通りです。
このファイルには各言語でのテキストが連想配列によって {キー}=>{テキスト} の形式で格納されています。テキストの取得には ドット表記 を使用します。キーの最初の部分は設定ファイル名の{first_key}と一致します。以下に例を示します。
/apricot/assets/lang/ja
<?php return [ 'app'=>[ '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クラスの実装コードを以下に示します。ユーザはこのクラスを直接利用するのではなく、次節に示す Langクラスを使用して下さい。
/apricot/core/Foundation
<?php namespace Core\Foundation; /** * Improvised Translation Class */ class Translation { /* * Messages */ private $messages = []; /* * Create Translation * @param string $lang */ public function __construct(string $lang='ja') { // Read Messages foreach(glob(assets_dir("lang/{$lang}/*.php")) as $file) { $arr = explode('.', basename($file)); if (is_file($file)) $this->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クラスは 上のTranslationクラスをシングルトンにしたもので、以下のメソッドがあります。
使用法: Lang::{メソッド}
メソッド | 機能 |
---|---|
bool has(string $key) | キーの存在確認 |
string get(string $key, array $params = []) | 言語テキストの取得 |
/apricot/core
<?php namespace Core; use Core\Foundation\Singleton; use Core\Foundation\Translation; /** * Lang Class - Translation Wrapper * * @method static Translation getInstance(); * @method static bool has(string $key) * @method static string get(string $key, array $params = []) */ class Lang extends Singleton { /** * Create Translation instance. * @return Translation */ protected static function createInstance() { // cf.) $_SERVER['HTTP_ACCEPT_LANGUAGE']; $lang = env('APP_LANG','ja'); return new Translation($lang); } }
上の実装では環境変数から言語コードを取得していますが、国際対応として実装する場合は、$_SERVER['HTTP_ACCEPT_LANGUAGE']
から言語コードを取得した方が良いです。
Langクラスのget()メソッドは良く使用されるのでヘルパー関数に追加しておきます。
/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クラスには以下のメソッドがあります。
メソッド | 機能 |
---|---|
__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) | エラー配列の設定 |
エラーバッグはIteratorAggregateインターフェースを実装してるのでforeach()などのIteratorを使用した構文が使用できます。但し、Countable インターフェイス は実装していないので、count関数ではなくErrorBag@countメソッドを使用して下さい。
/apricot/core/Foundation
<?php namespace Core\Foundation; /** * Error Bag Class */ class ErrorBag implements \IteratorAggregate { public const DEFAULT_NAME = 'error'; /** * Bag name * @var string */ private $name; /** * Errors * @var array */ private $errors = []; /** * Create Error bag * @param array $errors Associative array * @param string $name Bag name */ public function __construct($errors=null, string $name=self::DEFAULT_NAME) { $this->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); } }