diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bf9e3b5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +* text=auto + +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml export-ignore +/.php_cs.dist.php export-ignore \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6a42e3c --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +The Leaf Code of Conduct can be found in the [Leaf website](https://leafphp.dev/coc). \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..9dd4119 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contribution Guide + +The Leaf Contribution Guide can be found in the [Leaf website](https://leafphp.dev/community/contributing.html). \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6710e6e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ + + +## Description + + + +## Related Issue + + + \ No newline at end of file diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml new file mode 100644 index 0000000..dd4a929 --- /dev/null +++ b/.github/workflows/php-cs-fixer.yml @@ -0,0 +1,23 @@ +name: Check & fix styling + +on: [push] + +jobs: + php-cs-fixer: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Run PHP CS Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=.php_cs.dist.php --allow-risky=yes + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Fix styling \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7f40cfa..6fa59a2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ .composer composer.lock package-lock.json +.phpunit.result.cache +.php-cs-fixer.cache vendor/ test/ @@ -14,4 +16,4 @@ Thumbs.db *.swp # phpstorm -.idea/* +.idea/* \ No newline at end of file diff --git a/.php_cs.dist.php b/.php_cs.dist.php new file mode 100644 index 0000000..027edf8 --- /dev/null +++ b/.php_cs.dist.php @@ -0,0 +1,34 @@ +in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => false, + 'trailing_comma_in_multiline' => true, + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ], + 'single_trait_insert_per_statement' => true, + ]) + ->setFinder($finder); \ No newline at end of file diff --git a/LICENSE b/LICENSE index fa2d244..045e091 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Michael Darko-Duodu +Copyright (c) 2022 Michael Darko-Duodu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.json b/composer.json index cf1cf56..da13f49 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,11 @@ "leafs/anchor": "^1.1" }, "require-dev": { - "pestphp/pest": "^1.21" - } -} + "pestphp/pest": "^1.21", + "friendsofphp/php-cs-fixer": "^3.0" + }, + "scripts": { + "format": "vendor/bin/php-cs-fixer fix --config=.php_cs.dist.php --allow-risky=yes", + "test": "vendor/bin/pest" + } +} \ No newline at end of file diff --git a/src/App.php b/src/App.php index edd6f08..fd60e1a 100755 --- a/src/App.php +++ b/src/App.php @@ -17,292 +17,292 @@ */ class App extends Router { - /** - * Leaf container instance - * @var \Leaf\Helpers\Container - */ - protected $container; - - /** - * Callable to be invoked on application error - */ - protected $errorHandler; - - /******************************************************************************** - * Instantiation and Configuration - *******************************************************************************/ - - /** - * Constructor - * @param array $userSettings Associative array of application settings - */ - public function __construct(array $userSettings = []) - { - $this->setupErrorHandler(); - - if (count($userSettings) > 0) { - Config::set($userSettings); - } - - if (class_exists('\Leaf\Anchor\CSRF')) { - if (!Anchor\CSRF::token()) { - Anchor\CSRF::init(); - } - - if (!Anchor\CSRF::verify()) { - $csrfError = Anchor\CSRF::errors()['token']; - Http\Response::status(400); - echo Exception\General::csrf($csrfError); - exit(); - } - } - - $this->container = new \Leaf\Helpers\Container(); - - $this->setupDefaultContainer(); - - if (class_exists('\Leaf\BareUI')) { - View::attach(\Leaf\BareUI::class, 'template'); - } - - $this->loadViewEngines(); - } - - protected function setupErrorHandler() - { - if ($this->config('debug')) { - $debugConfig = [E_ALL, 1, ['\Leaf\Exception\General', 'handleErrors'], false]; - } else { - $debugConfig = [0, 0, ['\Leaf\Exception\General', 'defaultError'], true]; - } - - error_reporting($debugConfig[0]); - ini_set('display_errors', (string) $debugConfig[1]); - - $this->setErrorHandler($debugConfig[2], $debugConfig[3]); - } - - /** - * Set a custom error screen. - * - * @param callable|array $handler The function to be executed - */ - public function setErrorHandler($handler, bool $wrapper = true) - { - $errorHandler = $handler; - - if ($wrapper) { - $errorHandler = function ($errno, $errstr = '', $errfile = '', $errline = '') use ($handler) { - $exception = Exception\General::toException($errno, $errstr, $errfile, $errline); - Http\Response::status(500); - call_user_func_array($handler, [$exception]); - exit(); - }; - } - - set_error_handler($errorHandler); - } - - /** - * This method adds a method to the global leaf instance - * Register a method and use it globally on the Leaf Object - */ - public function register($name, $value) - { - $this->container->singleton($name, $value); - } - - public function loadViewEngines() - { - $views = View::$engines; - - if (count($views) > 0) { - foreach ($views as $key => $value) { - $this->container->singleton($key, function () use ($value) { - return $value; - }); - } - } - } - - private function setupDefaultContainer() - { - // Default request - $this->container->singleton('request', function () { - return new \Leaf\Http\Request(); - }); - - // Default response - $this->container->singleton('response', function () { - return new \Leaf\Http\Response(); - }); - - // Default headers - $this->container->singleton('headers', function () { - return new \Leaf\Http\Headers(); - }); - - if ($this->config('log.enabled')) { - if (class_exists('Leaf\Log')) { - // Default log writer - $this->container->singleton('logWriter', function ($c) { - $logWriter = Config::get('log.writer'); - - $file = $this->config('log.dir') . $this->config('log.file'); - - return is_object($logWriter) ? $logWriter : new \Leaf\LogWriter($file, $this->config('log.open') ?? true); - }); - - // Default log - $this->container->singleton('log', function ($c) { - $log = new \Leaf\Log($c['logWriter']); - $log->enabled($this->config('log.enabled')); - $log->level($this->config('log.level')); - - return $log; - }); - } - } - - // Default mode - (function () { - $mode = $this->config('mode'); - - if (_env('APP_ENV')) { - $mode = _env('APP_ENV'); - } - - if (_env('LEAF_MODE')) { - $mode = _env('LEAF_MODE'); - } - - if (isset($_ENV['LEAF_MODE'])) { - $mode = $_ENV['LEAF_MODE']; - } else { - $envMode = getenv('LEAF_MODE'); - - if ($envMode !== false) { - $mode = $envMode; - } - } - - $this->config('mode', $mode); - })(); - - Config::set('app', [ - 'instance' => $this, - 'container' => $this->container, - ]); - } - - public function __get($name) - { - return $this->container->get($name); - } - - public function __set($name, $value) - { - $this->container->set($name, $value); - } - - public function __isset($name) - { - return $this->container->has($name); - } - - public function __unset($name) - { - $this->container->remove($name); - } - - /** - * Configure Leaf Settings - * - * This method defines application settings and acts as a setter and a getter. - * - * If only one argument is specified and that argument is a string, the value - * of the setting identified by the first argument will be returned, or NULL if - * that setting does not exist. - * - * If only one argument is specified and that argument is an associative array, - * the array will be merged into the existing application settings. - * - * If two arguments are provided, the first argument is the name of the setting - * to be created or updated, and the second argument is the setting value. - * - * @param string|array $name If a string, the name of the setting to set or retrieve. Else an associated array of setting names and values - * @param mixed $value If name is a string, the value of the setting identified by $name - * @return mixed The value of a setting if only one argument is a string - */ - public function config($name, $value = null) - { - if ($value === null && is_string($name)) { - return Config::get($name); - } - - Config::set($name, $value); - } - - /******************************************************************************** - * Logging - *******************************************************************************/ - - /** - * Get application log - * - * @return \Leaf\Log|null|void - */ - public function logger() - { - if (!$this->log) { - trigger_error('You need to enable logging to use this feature! Set log.enabled to true and install the logger module'); - } - - return $this->log; - } - - /******************************************************************************** - * Application Accessors - *******************************************************************************/ - - /** - * Get the Request Headers - * @return \Leaf\Http\Headers - */ - public function headers() - { - return $this->headers; - } - - /** - * Get the Request object - * @return \Leaf\Http\Request - */ - public function request() - { - return $this->request; - } - - /** - * Get the Response object - * @return \Leaf\Http\Response - */ - public function response() - { - return $this->response; - } + /** + * Leaf container instance + * @var \Leaf\Helpers\Container + */ + protected $container; + + /** + * Callable to be invoked on application error + */ + protected $errorHandler; + + /******************************************************************************** + * Instantiation and Configuration + *******************************************************************************/ + + /** + * Constructor + * @param array $userSettings Associative array of application settings + */ + public function __construct(array $userSettings = []) + { + $this->setupErrorHandler(); + + if (count($userSettings) > 0) { + Config::set($userSettings); + } + + if (class_exists('\Leaf\Anchor\CSRF')) { + if (!Anchor\CSRF::token()) { + Anchor\CSRF::init(); + } + + if (!Anchor\CSRF::verify()) { + $csrfError = Anchor\CSRF::errors()['token']; + Http\Response::status(400); + echo Exception\General::csrf($csrfError); + exit(); + } + } + + $this->container = new \Leaf\Helpers\Container(); + + $this->setupDefaultContainer(); + + if (class_exists('\Leaf\BareUI')) { + View::attach(\Leaf\BareUI::class, 'template'); + } + + $this->loadViewEngines(); + } + + protected function setupErrorHandler() + { + if ($this->config('debug')) { + $debugConfig = [E_ALL, 1, ['\Leaf\Exception\General', 'handleErrors'], false]; + } else { + $debugConfig = [0, 0, ['\Leaf\Exception\General', 'defaultError'], true]; + } + + error_reporting($debugConfig[0]); + ini_set('display_errors', (string) $debugConfig[1]); + + $this->setErrorHandler($debugConfig[2], $debugConfig[3]); + } + + /** + * Set a custom error screen. + * + * @param callable|array $handler The function to be executed + */ + public function setErrorHandler($handler, bool $wrapper = true) + { + $errorHandler = $handler; + + if ($wrapper) { + $errorHandler = function ($errno, $errstr = '', $errfile = '', $errline = '') use ($handler) { + $exception = Exception\General::toException($errno, $errstr, $errfile, $errline); + Http\Response::status(500); + call_user_func_array($handler, [$exception]); + exit(); + }; + } + + set_error_handler($errorHandler); + } + + /** + * This method adds a method to the global leaf instance + * Register a method and use it globally on the Leaf Object + */ + public function register($name, $value) + { + $this->container->singleton($name, $value); + } + + public function loadViewEngines() + { + $views = View::$engines; + + if (count($views) > 0) { + foreach ($views as $key => $value) { + $this->container->singleton($key, function () use ($value) { + return $value; + }); + } + } + } + + private function setupDefaultContainer() + { + // Default request + $this->container->singleton('request', function () { + return new \Leaf\Http\Request(); + }); + + // Default response + $this->container->singleton('response', function () { + return new \Leaf\Http\Response(); + }); + + // Default headers + $this->container->singleton('headers', function () { + return new \Leaf\Http\Headers(); + }); + + if ($this->config('log.enabled')) { + if (class_exists('Leaf\Log')) { + // Default log writer + $this->container->singleton('logWriter', function ($c) { + $logWriter = Config::get('log.writer'); + + $file = $this->config('log.dir') . $this->config('log.file'); + + return is_object($logWriter) ? $logWriter : new \Leaf\LogWriter($file, $this->config('log.open') ?? true); + }); + + // Default log + $this->container->singleton('log', function ($c) { + $log = new \Leaf\Log($c['logWriter']); + $log->enabled($this->config('log.enabled')); + $log->level($this->config('log.level')); + + return $log; + }); + } + } + + // Default mode + (function () { + $mode = $this->config('mode'); + + if (_env('APP_ENV')) { + $mode = _env('APP_ENV'); + } + + if (_env('LEAF_MODE')) { + $mode = _env('LEAF_MODE'); + } + + if (isset($_ENV['LEAF_MODE'])) { + $mode = $_ENV['LEAF_MODE']; + } else { + $envMode = getenv('LEAF_MODE'); + + if ($envMode !== false) { + $mode = $envMode; + } + } + + $this->config('mode', $mode); + })(); + + Config::set('app', [ + 'instance' => $this, + 'container' => $this->container, + ]); + } + + public function __get($name) + { + return $this->container->get($name); + } + + public function __set($name, $value) + { + $this->container->set($name, $value); + } + + public function __isset($name) + { + return $this->container->has($name); + } + + public function __unset($name) + { + $this->container->remove($name); + } + + /** + * Configure Leaf Settings + * + * This method defines application settings and acts as a setter and a getter. + * + * If only one argument is specified and that argument is a string, the value + * of the setting identified by the first argument will be returned, or NULL if + * that setting does not exist. + * + * If only one argument is specified and that argument is an associative array, + * the array will be merged into the existing application settings. + * + * If two arguments are provided, the first argument is the name of the setting + * to be created or updated, and the second argument is the setting value. + * + * @param string|array $name If a string, the name of the setting to set or retrieve. Else an associated array of setting names and values + * @param mixed $value If name is a string, the value of the setting identified by $name + * @return mixed The value of a setting if only one argument is a string + */ + public function config($name, $value = null) + { + if ($value === null && is_string($name)) { + return Config::get($name); + } + + Config::set($name, $value); + } + + /******************************************************************************** + * Logging + *******************************************************************************/ + + /** + * Get application log + * + * @return \Leaf\Log|null|void + */ + public function logger() + { + if (!$this->log) { + trigger_error('You need to enable logging to use this feature! Set log.enabled to true and install the logger module'); + } + + return $this->log; + } + + /******************************************************************************** + * Application Accessors + *******************************************************************************/ + + /** + * Get the Request Headers + * @return \Leaf\Http\Headers + */ + public function headers() + { + return $this->headers; + } + + /** + * Get the Request object + * @return \Leaf\Http\Request + */ + public function request() + { + return $this->request; + } + + /** + * Get the Response object + * @return \Leaf\Http\Response + */ + public function response() + { + return $this->response; + } /** * Create mode-specific code - * + * * @param string $mode The mode to run code in * @param callable $callback The code to run in selected mode. */ public static function script($mode, $callback) { - static::hook('router.before', function () use($mode, $callback) { - $appMode = Config::get('mode') ?? 'development'; + static::hook('router.before', function () use ($mode, $callback) { + $appMode = Config::get('mode') ?? 'development'; if ($mode === $appMode) { return $callback(); @@ -310,98 +310,99 @@ public static function script($mode, $callback) }); } - /******************************************************************************** - * Helper Methods - *******************************************************************************/ - - /** - * Get the absolute path to this Leaf application's root directory - * - * This method returns the absolute path to the Leaf application's - * directory. If the Leaf application is installed in a public-accessible - * sub-directory, the sub-directory path will be included. This method - * will always return an absolute path WITH a trailing slash. - * - * @return string - */ - public function root() - { - return rtrim($_SERVER['DOCUMENT_ROOT'], '/') . rtrim($this->request->getRootUri(), '/') . '/'; - } - - /** - * Clean current output buffer - */ - protected function cleanBuffer() - { - if (ob_get_level() !== 0) { - ob_clean(); - } - } - - /** - * Halt - * - * Stop the application and immediately send the response with a - * specific status and body to the HTTP client. This may send any - * type of response: info, success, redirect, client error, or server error. - * - * @param int $status The HTTP response status - * @param string $message The HTTP response body - */ - public static function halt($status, $message = '') - { - if (ob_get_level() !== 0) { - ob_clean(); - } - - Http\Headers::status($status); - Http\Response::markup($message); - - exit(); - } - - /** - * Stop - * - * The thrown exception will be caught in application's `call()` method - * and the response will be sent as is to the HTTP client. - * - * @throws \Leaf\Exception\Stop - */ - public function stop() - { - throw new \Leaf\Exception\Stop(); - } - - /** - * Pass - * - * The thrown exception is caught in the application's `call()` method causing - * the router's current iteration to stop and continue to the subsequent route if available. - * If no subsequent matching routes are found, a 404 response will be sent to the client. - * - * @throws \Leaf\Exception\Pass - */ - public function pass() - { - $this->cleanBuffer(); - throw new \Leaf\Exception\Pass(); - } - - /** - * Evade CORS errors - * - * Cors handler - * - * @param $options Config for cors - */ - public function cors($options = []) - { - if (class_exists('Leaf\Http\Cors')) { - Http\Cors::config($options); - } else { - trigger_error('Cors module not found! Run `composer require leafs/cors` to install the CORS module. This is required to configure CORS.'); - } - } + /******************************************************************************** + * Helper Methods + *******************************************************************************/ + + /** + * Get the absolute path to this Leaf application's root directory + * + * This method returns the absolute path to the Leaf application's + * directory. If the Leaf application is installed in a public-accessible + * sub-directory, the sub-directory path will be included. This method + * will always return an absolute path WITH a trailing slash. + * + * @return string + */ + public function root() + { + return rtrim($_SERVER['DOCUMENT_ROOT'], '/') . rtrim($this->request->getRootUri(), '/') . '/'; + } + + /** + * Clean current output buffer + */ + protected function cleanBuffer() + { + if (ob_get_level() !== 0) { + ob_clean(); + } + } + + /** + * Halt + * + * Stop the application and immediately send the response with a + * specific status and body to the HTTP client. This may send any + * type of response: info, success, redirect, client error, or server error. + * + * @param int $status The HTTP response status + * @param string $message The HTTP response body + */ + public static function halt($status, $message = '') + { + if (ob_get_level() !== 0) { + ob_clean(); + } + + Http\Headers::status($status); + Http\Response::markup($message); + + exit(); + } + + /** + * Stop + * + * The thrown exception will be caught in application's `call()` method + * and the response will be sent as is to the HTTP client. + * + * @throws \Leaf\Exception\Stop + */ + public function stop() + { + throw new \Leaf\Exception\Stop(); + } + + /** + * Pass + * + * The thrown exception is caught in the application's `call()` method causing + * the router's current iteration to stop and continue to the subsequent route if available. + * If no subsequent matching routes are found, a 404 response will be sent to the client. + * + * @throws \Leaf\Exception\Pass + */ + public function pass() + { + $this->cleanBuffer(); + + throw new \Leaf\Exception\Pass(); + } + + /** + * Evade CORS errors + * + * Cors handler + * + * @param $options Config for cors + */ + public function cors($options = []) + { + if (class_exists('Leaf\Http\Cors')) { + Http\Cors::config($options); + } else { + trigger_error('Cors module not found! Run `composer require leafs/cors` to install the CORS module. This is required to configure CORS.'); + } + } } diff --git a/src/Config.php b/src/Config.php index 24b30b3..ee3bf91 100644 --- a/src/Config.php +++ b/src/Config.php @@ -11,80 +11,80 @@ */ class Config { - protected static $settings = [ - 'app' => ['down' => false, 'instance' => null], - 'mode' => 'development', - 'debug' => true, - 'log' => [ - 'writer' => null, - 'level' => null, - 'enabled' => false, - 'dir' => __DIR__ . '/../../../../storage/logs/', - 'file' => 'log.txt', - 'open' => true, - ], - 'http' => ['version' => '1.1'], - 'views' => ['path' => null, 'cachePath' => null], - ]; + protected static $settings = [ + 'app' => ['down' => false, 'instance' => null], + 'mode' => 'development', + 'debug' => true, + 'log' => [ + 'writer' => null, + 'level' => null, + 'enabled' => false, + 'dir' => __DIR__ . '/../../../../storage/logs/', + 'file' => 'log.txt', + 'open' => true, + ], + 'http' => ['version' => '1.1'], + 'views' => ['path' => null, 'cachePath' => null], + ]; - /** - * Set configuration value(s) - * - * @param string|array $item The config(s) to set - * @param mixed $value The value for config. Ignored if $item is an array. - */ - public static function set($item, $value = null) - { - if (is_string($item)) { - if (!strpos($item, '.')) { - static::$settings[$item] = $value; - } else { - static::$settings = array_merge( - static::$settings, - static::mapConfig($item, $value) - ); - } - } else { - foreach ($item as $k => $v) { - static::set($k, $v); - } - } - } + /** + * Set configuration value(s) + * + * @param string|array $item The config(s) to set + * @param mixed $value The value for config. Ignored if $item is an array. + */ + public static function set($item, $value = null) + { + if (is_string($item)) { + if (!strpos($item, '.')) { + static::$settings[$item] = $value; + } else { + static::$settings = array_merge( + static::$settings, + static::mapConfig($item, $value) + ); + } + } else { + foreach ($item as $k => $v) { + static::set($k, $v); + } + } + } - /** - * Map nested config to their parents recursively - */ - protected static function mapConfig(string $item, $value = null) - { - $config = explode('.', $item); + /** + * Map nested config to their parents recursively + */ + protected static function mapConfig(string $item, $value = null) + { + $config = explode('.', $item); - if (count($config) > 2) { - trigger_error('Nested config can\'t be more than 1 level deep'); - } + if (count($config) > 2) { + trigger_error('Nested config can\'t be more than 1 level deep'); + } - return [$config[0] => array_merge( - static::$settings[$config[0]] ?? [], - [$config[1] => $value] - )]; - } + return [$config[0] => array_merge( + static::$settings[$config[0]] ?? [], + [$config[1] => $value] + )]; + } - /** - * Get configuration - * - * @param string|null $item The config to get. Returns all items if nothing is specified. - */ - public static function get($item = null) - { - if ($item) { - $items = explode('.', $item); + /** + * Get configuration + * + * @param string|null $item The config to get. Returns all items if nothing is specified. + */ + public static function get($item = null) + { + if ($item) { + $items = explode('.', $item); - if (count($items) > 1) { - return static::$settings[$items[0]][$items[1]]; - } + if (count($items) > 1) { + return static::$settings[$items[0]][$items[1]] ?? null; + } - return static::$settings[$item] ?? null; - } + return static::$settings[$item] ?? null; + } - return static::$settings; - } + return static::$settings; + } } diff --git a/src/Exception/General.php b/src/Exception/General.php index 67da01a..082cd61 100755 --- a/src/Exception/General.php +++ b/src/Exception/General.php @@ -4,7 +4,7 @@ namespace Leaf\Exception; -use \Leaf\Http\Response; +use Leaf\Http\Response; /** * Stop Exception @@ -22,7 +22,7 @@ class General extends \Exception public function __construct($throwable) { - $this->response = new Response; + $this->response = new Response(); $this->handleException($throwable); } @@ -123,7 +123,7 @@ protected static function renderBody($exception) ['
The application could not run because of the following error:
'; $body .= 'App is under maintainance, please check back soon.
' - ); + 'Oops!', + 'App is under maintainance, please check back soon.
' + ); } /** @@ -198,9 +198,9 @@ public static function defaultDown() public static function default404() { echo static::errorMarkup( - '404', - 'The page you are looking for could not be found.
' - ); + '404', + 'The page you are looking for could not be found.
' + ); } /** diff --git a/src/Helpers/Container.php b/src/Helpers/Container.php index 416cb78..f4551ae 100755 --- a/src/Helpers/Container.php +++ b/src/Helpers/Container.php @@ -8,234 +8,234 @@ * Leaf service container * ---- * A simple container solution for your leaf apps - * + * * @since v2.0 */ class Container { - /** - * Key-value array of arbitrary data - * @var array - */ - protected $data = []; - - /** - * Constructor - * @param array $items Pre-populate set with this key-value array - */ - public function __construct($items = []) - { - $this->replace($items); - } - - /** - * Normalize data key - * - * Used to transform data key into the necessary - * key format for this set. Used in subclasses - * like \Leaf\Http\Headers. - * - * @param string $key The data key - * @return mixed The transformed/normalized data key - */ - protected function normalizeKey($key) - { - return $key; - } - - /** - * Set data key to value - * @param string $key The data key - * @param mixed $value The data value - */ - public function set($key, $value) - { - $this->data[$this->normalizeKey($key)] = $value; - } - - /** - * Get data value with key - * @param string $key The data key - * @param mixed $default The value to return if data key does not exist - * @return mixed The data value, or the default value - */ - public function get($key, $default = null) - { - if ($this->has($key)) { - $isInvokable = is_object($this->data[$this->normalizeKey($key)]) && method_exists($this->data[$this->normalizeKey($key)], '__invoke'); - - return $isInvokable ? $this->data[$this->normalizeKey($key)]($this) : $this->data[$this->normalizeKey($key)]; - } - - return $default; - } - - /** - * Add data to set - * @param array $items Key-value array of data to append to this set - */ - public function replace($items) - { - foreach ($items as $key => $value) { - $this->set($key, $value); // Ensure keys are normalized - } - } - - /** - * Set the content-type of response to json - */ - public function contentJson() - { - $this->replace(['Content-Type' => 'text/json']); - } - - /** - * Set the content-type of response to html - */ - public function contentHtml() - { - $this->replace(['Content-Type' => 'text/html']); - } - - /** - * Fetch set data - * @return array This set's key-value data array - */ - public function all() - { - return $this->data; - } - - /** - * Fetch set data keys - * @return array This set's key-value data array keys - */ - public function keys() - { - return array_keys($this->data); - } - - /** - * Does this set contain a key? - * @param string $key The data key - * @return boolean - */ - public function has($key) - { - return array_key_exists($this->normalizeKey($key), $this->data); - } - - /** - * Remove value with key from this set - * @param string $key The data key - */ - public function remove($key) - { - unset($this->data[$this->normalizeKey($key)]); - } - - /** - * Property Overloading - */ - - public function __get($key) - { - return $this->get($key); - } - - public function __set($key, $value) - { - $this->set($key, $value); - } - - public function __isset($key) - { - return $this->has($key); - } - - public function __unset($key) - { - $this->remove($key); - } - - /** - * Clear all values - */ - public function clear() - { - $this->data = []; - } - - /** - * Array Access - */ - - public function offsetExists($offset) - { - return $this->has($offset); - } - - public function offsetGet($offset) - { - return $this->get($offset); - } - - public function offsetSet($offset, $value) - { - $this->set($offset, $value); - } - - public function offsetUnset($offset) - { - $this->remove($offset); - } - - /** - * Countable - */ - public function count() - { - return count($this->data); - } - - /** - * IteratorAggregate - */ - public function getIterator() - { - return new \ArrayIterator($this->data); - } - - /** - * Ensure a value or object will remain globally unique - * - * @param string $key The value or object name - * @param \Closure $value The closure that defines the object - * - * @return mixed - */ - public function singleton($key, $value) - { - $this->set($key, function ($c) use ($value) { - static $object; - - if (null === $object) { - $object = $value($c); - } - - return $object; - }); - } - - /** - * Protect closure from being directly invoked - * @param \Closure $callable A closure to keep from being invoked and evaluated - * @return \Closure - */ - public function protect(\Closure $callable) - { - return function () use ($callable) { - return $callable; - }; - } + /** + * Key-value array of arbitrary data + * @var array + */ + protected $data = []; + + /** + * Constructor + * @param array $items Pre-populate set with this key-value array + */ + public function __construct($items = []) + { + $this->replace($items); + } + + /** + * Normalize data key + * + * Used to transform data key into the necessary + * key format for this set. Used in subclasses + * like \Leaf\Http\Headers. + * + * @param string $key The data key + * @return mixed The transformed/normalized data key + */ + protected function normalizeKey($key) + { + return $key; + } + + /** + * Set data key to value + * @param string $key The data key + * @param mixed $value The data value + */ + public function set($key, $value) + { + $this->data[$this->normalizeKey($key)] = $value; + } + + /** + * Get data value with key + * @param string $key The data key + * @param mixed $default The value to return if data key does not exist + * @return mixed The data value, or the default value + */ + public function get($key, $default = null) + { + if ($this->has($key)) { + $isInvokable = is_object($this->data[$this->normalizeKey($key)]) && method_exists($this->data[$this->normalizeKey($key)], '__invoke'); + + return $isInvokable ? $this->data[$this->normalizeKey($key)]($this) : $this->data[$this->normalizeKey($key)]; + } + + return $default; + } + + /** + * Add data to set + * @param array $items Key-value array of data to append to this set + */ + public function replace($items) + { + foreach ($items as $key => $value) { + $this->set($key, $value); // Ensure keys are normalized + } + } + + /** + * Set the content-type of response to json + */ + public function contentJson() + { + $this->replace(['Content-Type' => 'text/json']); + } + + /** + * Set the content-type of response to html + */ + public function contentHtml() + { + $this->replace(['Content-Type' => 'text/html']); + } + + /** + * Fetch set data + * @return array This set's key-value data array + */ + public function all() + { + return $this->data; + } + + /** + * Fetch set data keys + * @return array This set's key-value data array keys + */ + public function keys() + { + return array_keys($this->data); + } + + /** + * Does this set contain a key? + * @param string $key The data key + * @return bool + */ + public function has($key) + { + return array_key_exists($this->normalizeKey($key), $this->data); + } + + /** + * Remove value with key from this set + * @param string $key The data key + */ + public function remove($key) + { + unset($this->data[$this->normalizeKey($key)]); + } + + /** + * Property Overloading + */ + + public function __get($key) + { + return $this->get($key); + } + + public function __set($key, $value) + { + $this->set($key, $value); + } + + public function __isset($key) + { + return $this->has($key); + } + + public function __unset($key) + { + $this->remove($key); + } + + /** + * Clear all values + */ + public function clear() + { + $this->data = []; + } + + /** + * Array Access + */ + + public function offsetExists($offset) + { + return $this->has($offset); + } + + public function offsetGet($offset) + { + return $this->get($offset); + } + + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } + + public function offsetUnset($offset) + { + $this->remove($offset); + } + + /** + * Countable + */ + public function count() + { + return count($this->data); + } + + /** + * IteratorAggregate + */ + public function getIterator() + { + return new \ArrayIterator($this->data); + } + + /** + * Ensure a value or object will remain globally unique + * + * @param string $key The value or object name + * @param \Closure $value The closure that defines the object + * + * @return mixed + */ + public function singleton($key, $value) + { + $this->set($key, function ($c) use ($value) { + static $object; + + if (null === $object) { + $object = $value($c); + } + + return $object; + }); + } + + /** + * Protect closure from being directly invoked + * @param \Closure $callable A closure to keep from being invoked and evaluated + * @return \Closure + */ + public function protect(\Closure $callable) + { + return function () use ($callable) { + return $callable; + }; + } } diff --git a/src/Middleware.php b/src/Middleware.php index 76a661b..6a2ca0b 100755 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -13,62 +13,62 @@ */ abstract class Middleware { - /** - * @var \Leaf\App Reference to the primary application instance - */ - protected $app; + /** + * @var \Leaf\App Reference to the primary application instance + */ + protected $app; - /** - * @var mixed Reference to the next downstream middleware - */ - protected $next; + /** + * @var mixed Reference to the next downstream middleware + */ + protected $next; - /** - * Set next middleware - * - * This method injects the next downstream middleware into - * this middleware so that it may optionally be called - * when appropriate. - * - * @param \Leaf\Middleware - */ - final public function setNextMiddleware($nextMiddleware) - { - $this->next = $nextMiddleware; - } + /** + * Set next middleware + * + * This method injects the next downstream middleware into + * this middleware so that it may optionally be called + * when appropriate. + * + * @param \Leaf\Middleware + */ + final public function setNextMiddleware($nextMiddleware) + { + $this->next = $nextMiddleware; + } - /** - * Get next middleware - * - * This method retrieves the next downstream middleware - * previously injected into this middleware. - * - * @return \Leaf\Middleware - */ - final public function getNextMiddleware() - { - return $this->next; - } + /** + * Get next middleware + * + * This method retrieves the next downstream middleware + * previously injected into this middleware. + * + * @return \Leaf\Middleware + */ + final public function getNextMiddleware() + { + return $this->next; + } - /** - * Call the next middleware - */ - final public function next() - { - $nextMiddleware = $this->next; + /** + * Call the next middleware + */ + final public function next() + { + $nextMiddleware = $this->next; - if (!$nextMiddleware) { - return; - } + if (!$nextMiddleware) { + return; + } - $nextMiddleware->call(); - } + $nextMiddleware->call(); + } - /** - * Call - * - * Perform actions specific to this middleware and optionally - * call the next downstream middleware. - */ - abstract public function call(); + /** + * Call + * + * Perform actions specific to this middleware and optionally + * call the next downstream middleware. + */ + abstract public function call(); } diff --git a/src/View.php b/src/View.php index fba0417..a3d8083 100644 --- a/src/View.php +++ b/src/View.php @@ -14,33 +14,33 @@ */ class View { - public static $engines = []; - - /** - * Attach view engine to Leaf view - * - * @param mixed $className The class to attach - * @param string|null $name The key to save view engine with - */ - public static function attach($className, $name = null) - { - $class = new $className; - static::$engines[$name ?? static::getDiIndex($class)] = $class; - Config::set("views.engine", $name ?? static::getDiIndex($class)); - } - - private static function getDiIndex($class) - { - $className = strtolower(get_class($class)); - - $fullName = explode("\\", $className); - $className = $fullName[count($fullName) - 1]; - - return $className; - } - - public static function __callstatic($name, $arguments) - { - return static::$engines[$name]; - } + public static $engines = []; + + /** + * Attach view engine to Leaf view + * + * @param mixed $className The class to attach + * @param string|null $name The key to save view engine with + */ + public static function attach($className, $name = null) + { + $class = new $className(); + static::$engines[$name ?? static::getDiIndex($class)] = $class; + Config::set("views.engine", $name ?? static::getDiIndex($class)); + } + + private static function getDiIndex($class) + { + $className = strtolower(get_class($class)); + + $fullName = explode("\\", $className); + $className = $fullName[count($fullName) - 1]; + + return $className; + } + + public static function __callstatic($name, $arguments) + { + return static::$engines[$name]; + } } diff --git a/src/functions.php b/src/functions.php index 906b71b..29542d3 100755 --- a/src/functions.php +++ b/src/functions.php @@ -3,22 +3,22 @@ declare(strict_types=1); if (!function_exists('app')) { - /** - * Return the Leaf instance - * - * @return Leaf\App - */ - function app() - { - $app = Leaf\Config::get("app")["instance"] ?? null; + /** + * Return the Leaf instance + * + * @return Leaf\App + */ + function app() + { + $app = Leaf\Config::get("app")["instance"] ?? null; - if (!$app) { - $app = new Leaf\App; - Leaf\Config::set("app", ["instance" => $app]); - } + if (!$app) { + $app = new Leaf\App(); + Leaf\Config::set("app", ["instance" => $app]); + } - return $app; - } + return $app; + } } if (!function_exists('_env')) { diff --git a/tests/app.test.php b/tests/app.test.php index 4d3e0f3..4eb68ac 100644 --- a/tests/app.test.php +++ b/tests/app.test.php @@ -1,130 +1,138 @@ request())->toBeInstanceOf(\Leaf\Http\Request::class); - expect(app()->response())->toBeInstanceOf(\Leaf\Http\Response::class); - expect(app()->headers())->toBeInstanceOf(\Leaf\Http\Headers::class); + expect(app()->request())->toBeInstanceOf(\Leaf\Http\Request::class); + expect(app()->response())->toBeInstanceOf(\Leaf\Http\Response::class); + expect(app()->headers())->toBeInstanceOf(\Leaf\Http\Headers::class); }); test('app mode', function () { - app()->config('app.down', false); + app()->config('app.down', false); - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/'; - app()->config('mode', 'TEST'); - app()->get('/', function () {}); - app()->script('TEST', function () { - app()->config('app.down', true); - }); + app()->config('mode', 'TEST'); + app()->get('/', function () { + }); + app()->script('TEST', function () { + app()->config('app.down', true); + }); - app()->run(); + app()->run(); - expect(app()->config('mode'))->toBe('TEST'); - expect(app()->config('app.down'))->toBe(true); + expect(app()->config('mode'))->toBe('TEST'); + expect(app()->config('app.down'))->toBe(true); }); test('set error handler', function () { - app()->config('app.down', false); + app()->config('app.down', false); - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/'; - // create an error to trigger error handler - app()->get('/', function () {$app;}); + // create an error to trigger error handler + app()->get('/', function () { + $app; + }); - app()->setErrorHandler(function () { - app()->config('app.down', true); - }); + app()->setErrorHandler(function () { + app()->config('app.down', true); + }); - app()->run(); + app()->run(); - expect(app()->config('app.down'))->toBe(true); + expect(app()->config('app.down'))->toBe(true); }); test('set 404', function () { - app()->config('app.down', false); + app()->config('app.down', false); - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/'; - app()->set404(function () { - app()->config('app.down', true); - }); + app()->set404(function () { + app()->config('app.down', true); + }); - app()->run(); + app()->run(); - expect(app()->config('app.down'))->toBe(true); + expect(app()->config('app.down'))->toBe(true); }); test('leaf middleware', function () { - $app = new Leaf\App; - app()->config('app.down', false); - - class AppMid extends \Leaf\Middleware { - public function call() - { - app()->config('app.down', true); - $this->next(); - } - } - - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['REQUEST_URI'] = '/'; - - app()->use(new AppMid); - app()->get('/', function () {}); - app()->run(); - - expect(app()->config('app.down'))->toBe(true); + $app = new Leaf\App(); + app()->config('app.down', false); + + class AppMid extends \Leaf\Middleware + { + public function call() + { + app()->config('app.down', true); + $this->next(); + } + } + + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/'; + + app()->use(new AppMid()); + app()->get('/', function () { + }); + app()->run(); + + expect(app()->config('app.down'))->toBe(true); }); test('in-route middleware', function () { - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/'; - $app = new Leaf\App; - $app->config('app.down', false); + $app = new Leaf\App(); + $app->config('app.down', false); - $m = function () use($app) { - $app->config('app.down', true); - }; + $m = function () use ($app) { + $app->config('app.down', true); + }; - $app->get('/', ['middleware' => $m, function () {}]); - $app->run(); + $app->get('/', ['middleware' => $m, function () { + }]); + $app->run(); - expect($app->config('app.down'))->toBe(true); + expect($app->config('app.down'))->toBe(true); }); test('before route middleware', function () { - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/'; - $app = new Leaf\App; + $app = new Leaf\App(); - $app->config('inTest', 'true'); - $app->before('GET', '/', function () use($app) { - $app->config('inTest', 'false'); - }); - $app->get('/', function () {}); - $app->run(); + $app->config('inTest', 'true'); + $app->before('GET', '/', function () use ($app) { + $app->config('inTest', 'false'); + }); + $app->get('/', function () { + }); + $app->run(); - expect($app->config('inTest'))->toBe('false'); + expect($app->config('inTest'))->toBe('false'); }); test('before router middleware', function () { - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['REQUEST_URI'] = '/test'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/test'; + + $app = new Leaf\App(); - $app = new Leaf\App; - - $app->config('inTest2', 'true'); + $app->config('inTest2', 'true'); - $app->before('GET', '/.*', function () use($app) { - $app->config('inTest2', 'false'); - }); - $app->get('/test', function () {}); - $app->run(); + $app->before('GET', '/.*', function () use ($app) { + $app->config('inTest2', 'false'); + }); + $app->get('/test', function () { + }); + $app->run(); - expect($app->config('inTest2'))->toBe('false'); + expect($app->config('inTest2'))->toBe('false'); }); diff --git a/tests/config.test.php b/tests/config.test.php index 059cf37..4867f2b 100644 --- a/tests/config.test.php +++ b/tests/config.test.php @@ -1,51 +1,51 @@ config('mode'); + $appMode = app()->config('mode'); - expect($appMode)->toBe($testMode); + expect($appMode)->toBe($testMode); }); test('centralized config after init', function () { - $testMode = 'down'; + $testMode = 'down'; - app()->config('mode', $testMode); + app()->config('mode', $testMode); - $appMode = \Leaf\Config::get('mode'); + $appMode = \Leaf\Config::get('mode'); - expect($appMode)->toBe($testMode); + expect($appMode)->toBe($testMode); }); test('nested config', function () { - app()->config('app.key', '2'); + app()->config('app.key', '2'); - $appConfig = app()->config('app'); + $appConfig = app()->config('app'); - expect(isset($appConfig['key']))->toBeTrue(); - expect($appConfig['key'])->toBe('2'); - expect(app()->config('app.key'))->toBe('2'); + expect(isset($appConfig['key']))->toBeTrue(); + expect($appConfig['key'])->toBe('2'); + expect(app()->config('app.key'))->toBe('2'); }); test('nested config (array)', function () { - app()->config(['app.key' => '2']); + app()->config(['app.key' => '2']); - $appConfig = app()->config('app'); + $appConfig = app()->config('app'); - expect(isset($appConfig['key']))->toBeTrue(); - expect($appConfig['key'])->toBe('2'); - expect(app()->config('app.key'))->toBe('2'); + expect(isset($appConfig['key']))->toBeTrue(); + expect($appConfig['key'])->toBe('2'); + expect(app()->config('app.key'))->toBe('2'); }); test('nested config (custom group)', function () { - app()->config(['home.key' => '2']); + app()->config(['home.key' => '2']); - $homeConfig = app()->config('home'); + $homeConfig = app()->config('home'); - expect(isset($homeConfig['key']))->toBeTrue(); - expect($homeConfig['key'])->toBe('2'); - expect(app()->config('home.key'))->toBe('2'); + expect(isset($homeConfig['key']))->toBeTrue(); + expect($homeConfig['key'])->toBe('2'); + expect(app()->config('home.key'))->toBe('2'); }); diff --git a/tests/container.test.php b/tests/container.test.php index 40cbc33..3861bac 100644 --- a/tests/container.test.php +++ b/tests/container.test.php @@ -1,33 +1,33 @@ request)->toBeInstanceOf(\Leaf\Http\Request::class); - expect($app->response)->toBeInstanceOf(\Leaf\Http\Response::class); - expect($app->headers)->toBeInstanceOf(\Leaf\Http\Headers::class); + expect($app->request)->toBeInstanceOf(\Leaf\Http\Request::class); + expect($app->response)->toBeInstanceOf(\Leaf\Http\Response::class); + expect($app->headers)->toBeInstanceOf(\Leaf\Http\Headers::class); }); test('set container item', function () { - $app = new \Leaf\App; + $app = new \Leaf\App(); - $app->register('req', function () { - return new \Leaf\Http\Request(); - }); + $app->register('req', function () { + return new \Leaf\Http\Request(); + }); - expect($app->req)->toBeInstanceOf(\Leaf\Http\Request::class); + expect($app->req)->toBeInstanceOf(\Leaf\Http\Request::class); }); test('directly set container item', function () { - $app = new \Leaf\App; + $app = new \Leaf\App(); - $app->item = 1; + $app->item = 1; - expect($app->item)->toBe(1); + expect($app->item)->toBe(1); }); test('remove container item', function () { - unset(app()->request); + unset(app()->request); - expect(app()->request)->toBeFalsy(); + expect(app()->request)->toBeFalsy(); }); diff --git a/tests/functional.test.php b/tests/functional.test.php index 0bf918b..0dd0bbb 100644 --- a/tests/functional.test.php +++ b/tests/functional.test.php @@ -1,20 +1,20 @@ toBeInstanceOf(\Leaf\App::class); + expect(app())->toBeInstanceOf(\Leaf\App::class); }); test('shared app instance', function () { - $app = new \Leaf\App(); - $app->item = 1; + $app = new \Leaf\App(); + $app->item = 1; - expect(app()->item)->toBe(1); + expect(app()->item)->toBe(1); }); test('functional mode response', function () { - expect(app()->response())->toBeInstanceOf(\Leaf\Http\Response::class); + expect(app()->response())->toBeInstanceOf(\Leaf\Http\Response::class); }); test('functional mode request', function () { - expect(app()->request())->toBeInstanceOf(\Leaf\Http\Request::class); + expect(app()->request())->toBeInstanceOf(\Leaf\Http\Request::class); }); diff --git a/tests/view.test.php b/tests/view.test.php index e04aa70..41c4b8d 100644 --- a/tests/view.test.php +++ b/tests/view.test.php @@ -1,34 +1,35 @@ toBeInstanceOf(TView::class); + expect(Leaf\View::tview())->toBeInstanceOf(TView::class); }); test('view attach with name', function () { - Leaf\View::attach(TView::class, 'named'); + Leaf\View::attach(TView::class, 'named'); - expect(Leaf\View::named())->toBeInstanceOf(TView::class); + expect(Leaf\View::named())->toBeInstanceOf(TView::class); }); test('access attached view props', function () { - Leaf\View::attach(TView::class, 'named'); + Leaf\View::attach(TView::class, 'named'); - expect(Leaf\View::named()::$num)->toBe(TView::$num); + expect(Leaf\View::named()::$num)->toBe(TView::$num); }); test('access attached view methods', function () { - Leaf\View::attach(TView::class, 'named'); + Leaf\View::attach(TView::class, 'named'); - expect(Leaf\View::named()->test())->toBe(TView::test()); + expect(Leaf\View::named()->test())->toBe(TView::test()); });