From b320b4228897bb5a5be9d6de6e9e54a13c3ee441 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 3 Aug 2022 07:46:13 +0000 Subject: [PATCH 01/12] injections in tasks --- src/CLI/CLI.php | 83 +++++++++++++++++++++++++++++++++++++++++- src/CLI/Task.php | 45 +++++++++++++++++++++++ tests/CLI/CLITest.php | 22 +++++++++++ tests/CLI/TaskTest.php | 15 ++++++++ 4 files changed, 163 insertions(+), 2 deletions(-) diff --git a/src/CLI/CLI.php b/src/CLI/CLI.php index 4aed97b..ab51f5d 100644 --- a/src/CLI/CLI.php +++ b/src/CLI/CLI.php @@ -16,6 +16,16 @@ class CLI */ protected $command = ''; + /** + * @var array + */ + protected array $resources = []; + + /** + * @var array + */ + protected static array $resourcesCallbacks = []; + /** * Args * @@ -142,6 +152,70 @@ public function task(string $name): Task return $task; } + /** + * If a resource has been created return it, otherwise create it and then return it + * + * @param string $name + * @param bool $fresh + * @return mixed + * @throws Exception + */ + public function getResource(string $name, bool $fresh = false): mixed + { + if ($name === 'utopia') { + return $this; + } + + if (!\array_key_exists($name, $this->resources) || $fresh || self::$resourcesCallbacks[$name]['reset']) { + if (!\array_key_exists($name, self::$resourcesCallbacks)) { + throw new Exception('Failed to find resource: "' . $name . '"'); + } + + $this->resources[$name] = \call_user_func_array(self::$resourcesCallbacks[$name]['callback'], + $this->getResources(self::$resourcesCallbacks[$name]['injections'])); + } + + self::$resourcesCallbacks[$name]['reset'] = false; + + return $this->resources[$name]; + } + + /** + * Get Resources By List + * + * @param array $list + * @return array + */ + public function getResources(array $list): array + { + $resources = []; + + foreach ($list as $name) { + $resources[$name] = $this->getResource($name); + } + + return $resources; + } + + /** + * Set a new resource callback + * + * @param string $name + * @param callable $callback + * @param array $injections + * + * @throws Exception + * + * @return void + */ + public static function setResource(string $name, callable $callback, array $injections = []): void + { + if ($name === 'utopia') { + throw new Exception("'utopia' is a reserved keyword.", 500); + } + self::$resourcesCallbacks[$name] = ['callback' => $callback, 'injections' => $injections, 'reset' => true]; + } + /** * task-name --foo=test * @@ -221,14 +295,19 @@ public function run(): self $params = []; foreach ($command->getParams() as $key => $param) { - // Get value from route or request object $value = (isset($this->args[$key])) ? $this->args[$key] : $param['default']; $this->validate($key, $param, $value); - $params[$key] = $value; + $params[$param['order']] = $value; } + foreach($command->getInjections() as $key => $injection) { + $params[$injection['order']] = $this->getResource($injection['name']); + } + + ksort($params); + // Call the callback with the matched positions as params \call_user_func_array($command->getAction(), $params); diff --git a/src/CLI/Task.php b/src/CLI/Task.php index cb6cdf4..b7bf66f 100644 --- a/src/CLI/Task.php +++ b/src/CLI/Task.php @@ -2,6 +2,7 @@ namespace Utopia\CLI; use Utopia\Validator; +use Exception; class Task { @@ -33,6 +34,15 @@ class Task */ protected $params = []; + /** + * Injections + * + * List of route required injections for action callback + * + * @var array + */ + protected array $injections = []; + /** * Labels * @@ -95,11 +105,36 @@ public function param(string $key, $default, Validator $validator, string $descr 'description' => $description, 'optional' => $optional, 'value' => null, + 'order' => count($this->params) + count($this->injections), ); return $this; } + /** + * Inject + * + * @param string $injection + * + * @throws Exception + * + * @return static + */ + public function inject(string $injection): static + { + if (array_key_exists($injection, $this->injections)) { + throw new Exception('Injection already declared for ' . $injection); + } + + $this->injections[$injection] = [ + 'name' => $injection, + 'order' => count($this->params) + count($this->injections), + ]; + + return $this; + } + + /** * Add Label * @@ -155,6 +190,16 @@ public function getParams(): array return $this->params; } + /** + * Get Injections + * + * @return array + */ + public function getInjections(): array + { + return $this->injections; + } + /** * Get Label * diff --git a/tests/CLI/CLITest.php b/tests/CLI/CLITest.php index 7746c5d..93421fb 100755 --- a/tests/CLI/CLITest.php +++ b/tests/CLI/CLITest.php @@ -136,6 +136,28 @@ public function testGetArgs() $this->assertEquals(['email' => 'me@example.com', 'list' => ['item1', 'item2']], $cli->getArgs()); } + public function testInjection() + { + ob_start(); + + $cli = new CLI(['test.php', 'build', '--email=me@example.com']); + CLI::setResource('test', fn() => 'test-value'); + + $cli->task('build') + ->inject('test') + ->param('email', null, new Text(15), 'valid email address') + ->action(function($test, $email){ + echo $test . '-' . $email; + }); + + $cli->run(); + + $result = ob_get_clean(); + + $this->assertEquals('test-value-me@example.com', $result); + + } + public function testMatch() { $cli = new CLI(['test.php', 'build2', '--email=me@example.com', '--list=item1', '--list=item2']); // Mock command request diff --git a/tests/CLI/TaskTest.php b/tests/CLI/TaskTest.php index 0b3c01b..03a644b 100755 --- a/tests/CLI/TaskTest.php +++ b/tests/CLI/TaskTest.php @@ -69,4 +69,19 @@ public function testParam() $this->assertCount(1, $this->task->getParams()); } + + public function testResources() + { + $this->assertEquals([], $this->task->getInjections()); + + $this->task + ->inject('user') + ->inject('time') + ->action(function() {}) + ; + + $this->assertCount(2, $this->task->getInjections()); + $this->assertEquals('user', $this->task->getInjections()['user']['name']); + $this->assertEquals('time', $this->task->getInjections()['time']['name']); + } } From 178e7c270cbf2035e1a393c87d6c2f3260b4c136 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 3 Aug 2022 07:52:40 +0000 Subject: [PATCH 02/12] formatting and housekeeping --- .gitignore | 3 ++- composer.json | 7 ++++++- phpcs.xml | 16 ++++++++++++++ src/CLI/CLI.php | 42 +++++++++++++++++++------------------ src/CLI/Console.php | 44 +++++++++++++++++++-------------------- src/CLI/Task.php | 6 ++++-- tests/CLI/CLITest.php | 26 +++++++++++------------ tests/CLI/ConsoleTest.php | 5 +++-- tests/CLI/TaskTest.php | 6 ++++-- tests/resources/loop.php | 6 +++--- 10 files changed, 95 insertions(+), 66 deletions(-) create mode 100644 phpcs.xml diff --git a/.gitignore b/.gitignore index 8924c80..2e18838 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ composer.lock /vendor/ -/.idea/ \ No newline at end of file +/.idea/ +.phpunit.result.cache diff --git a/composer.json b/composer.json index 6db2a02..6ecee40 100755 --- a/composer.json +++ b/composer.json @@ -10,6 +10,11 @@ "email": "eldad@appwrite.io" } ], + "scripts": { + "test": "vendor/bin/phpunit --configuration phpunit.xml < test/input.text", + "lint": "vendor/bin/phpcs", + "format": "vendor/bin/phpcbf" + }, "autoload": { "psr-4": {"Utopia\\CLI\\": "src/CLI"} }, @@ -19,7 +24,7 @@ }, "require-dev": { "phpunit/phpunit": "^9.3", - "vimeo/psalm": "4.0.1" + "squizlabs/php_codesniffer": "^3.6" }, "minimum-stability": "dev" } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..342c6bc --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,16 @@ + + + + ./src + ./tests + + + + + * + + + + * + + \ No newline at end of file diff --git a/src/CLI/CLI.php b/src/CLI/CLI.php index ab51f5d..70fbf62 100644 --- a/src/CLI/CLI.php +++ b/src/CLI/CLI.php @@ -14,7 +14,7 @@ class CLI * * @var string */ - protected $command = ''; + protected string $command = ''; /** * @var array @@ -33,7 +33,7 @@ class CLI * * @var array */ - protected $args = []; + protected array $args = []; /** * Tasks @@ -42,7 +42,7 @@ class CLI * * @var array */ - protected $tasks = []; + protected array $tasks = []; /** * Error @@ -83,7 +83,7 @@ public function __construct(array $args = []) throw new Exception('CLI tasks can only work from the command line'); } - $this->args = $this->parse((!empty($args) || !isset($_SERVER['argv'])) ? $args: $_SERVER['argv']); + $this->args = $this->parse((!empty($args) || !isset($_SERVER['argv'])) ? $args : $_SERVER['argv']); $this->error = function (Exception $error): void { Console::error($error->getMessage()); @@ -136,11 +136,11 @@ public function error(callable $callback): self /** * Task - * + * * Add a new command task - * + * * @param string $name - * + * * @return Task */ public function task(string $name): Task @@ -171,8 +171,10 @@ public function getResource(string $name, bool $fresh = false): mixed throw new Exception('Failed to find resource: "' . $name . '"'); } - $this->resources[$name] = \call_user_func_array(self::$resourcesCallbacks[$name]['callback'], - $this->getResources(self::$resourcesCallbacks[$name]['injections'])); + $this->resources[$name] = \call_user_func_array( + self::$resourcesCallbacks[$name]['callback'], + $this->getResources(self::$resourcesCallbacks[$name]['injections']) + ); } self::$resourcesCallbacks[$name]['reset'] = false; @@ -241,14 +243,14 @@ public function parse(array $args): array } } - /** - * Refer to this answer + /** + * Refer to this answer * https://stackoverflow.com/questions/18669499/php-issue-with-looping-over-an-array-twice-using-foreach-and-passing-value-by-re/18669732 */ unset($arg); foreach ($args as $arg) { - $pair = explode("=",$arg); + $pair = explode("=", $arg); $key = $pair[0]; $value = $pair[1]; $output[$key][] = $value; @@ -269,17 +271,17 @@ public function parse(array $args): array /** * Find the command that should be triggered - * + * * @return Task|null */ - public function match() + public function match(): ?Task { return isset($this->tasks[$this->command]) ? $this->tasks[$this->command] : null; } /** * Run - * + * * @return $this */ public function run(): self @@ -302,7 +304,7 @@ public function run(): self $params[$param['order']] = $value; } - foreach($command->getInjections() as $key => $injection) { + foreach ($command->getInjections() as $key => $injection) { $params[$injection['order']] = $this->getResource($injection['name']); } @@ -326,7 +328,7 @@ public function run(): self /** * Get list of all tasks - * + * * @return Task[] */ public function getTasks(): array @@ -336,7 +338,7 @@ public function getTasks(): array /** * Get list of all args - * + * * @return array */ public function getArgs(): array @@ -370,7 +372,7 @@ protected function validate(string $key, array $param, $value): void } if (!$validator->isValid($value)) { - throw new Exception('Invalid ' .$key . ': ' . $validator->getDescription(), 400); + throw new Exception('Invalid ' . $key . ': ' . $validator->getDescription(), 400); } } else { if (!$param['optional']) { @@ -378,4 +380,4 @@ protected function validate(string $key, array $param, $value): void } } } -} \ No newline at end of file +} diff --git a/src/CLI/Console.php b/src/CLI/Console.php index 0c7fbd3..2c2da1a 100644 --- a/src/CLI/Console.php +++ b/src/CLI/Console.php @@ -7,12 +7,12 @@ class Console /** * Title * - * Sets the process title visible in tools such as top and ps. + * Sets the process title visible in tools such as top and ps. * * @param string $title * @return bool */ - static public function title(string $title) + public static function title(string $title): bool { return @\cli_set_process_title($title); } @@ -25,7 +25,7 @@ static public function title(string $title) * @param string $message * @return bool|int */ - static public function log(string $message) + public static function log(string $message): int|false { return \fwrite(STDOUT, $message . "\n"); } @@ -38,7 +38,7 @@ static public function log(string $message) * @param string $message * @return bool|int */ - static public function success(string $message) + public static function success(string $message): int|false { return \fwrite(STDOUT, "\033[32m" . $message . "\033[0m\n"); } @@ -51,7 +51,7 @@ static public function success(string $message) * @param string $message * @return bool|int */ - static public function error(string $message) + public static function error(string $message): int|false { return \fwrite(STDERR, "\033[31m" . $message . "\033[0m\n"); } @@ -64,7 +64,7 @@ static public function error(string $message) * @param string $message * @return bool|int */ - static public function info(string $message) + public static function info(string $message): int|false { return \fwrite(STDOUT, "\033[34m" . $message . "\033[0m\n"); } @@ -77,7 +77,7 @@ static public function info(string $message) * @param string $message * @return bool|int */ - static public function warning(string $message) + public static function warning(string $message): int|false { return \fwrite(STDERR, "\033[1;33m" . $message . "\033[0m\n"); } @@ -90,7 +90,7 @@ static public function warning(string $message) * @param string $question * @return string */ - static public function confirm(string $question) + public static function confirm(string $question): string { if (!self::isInteractive()) { return ''; @@ -102,7 +102,7 @@ static public function confirm(string $question) $line = \trim(\fgets($handle)); \fclose($handle); - + return $line; } @@ -114,16 +114,16 @@ static public function confirm(string $question) * @param string $message * @return void */ - static public function exit(int $status = 0): void + public static function exit(int $status = 0): void { exit($status); } /** * Execute a Commnad - * + * * This function was inspired by: https://stackoverflow.com/a/13287902/2299554 - * + * * @param string $cmd * @param string $stdin * @param string $stdout @@ -131,7 +131,7 @@ static public function exit(int $status = 0): void * @param int $timeout * @return int */ - static public function execute(string $cmd, string $stdin, string &$stdout, string &$stderr, int $timeout = -1): int + public static function execute(string $cmd, string $stdin, string &$stdout, string &$stderr, int $timeout = -1): int { $cmd = '( ' . $cmd . ' ) 3>/dev/null ; echo $? >&3'; @@ -171,7 +171,7 @@ static public function execute(string $cmd, string $stdin, string &$stdout, stri \fclose($pipes[2]); \proc_close($process); - $exitCode = (int) str_replace("\n","",$status); + $exitCode = (int) str_replace("\n", "", $status); return $exitCode; } @@ -184,10 +184,10 @@ static public function execute(string $cmd, string $stdin, string &$stdout, stri /** * Is Interactive Mode? - * + * * @return bool */ - static public function isInteractive(): bool + public static function isInteractive(): bool { return ('cli' === PHP_SAPI && defined('STDOUT')); } @@ -197,7 +197,7 @@ static public function isInteractive(): bool * @param float $sleep // in seconds! * @param callable $onError */ - static public function loop(callable $callback, $sleep = 1 /* seconds */, callable $onError = null): void + public static function loop(callable $callback, $sleep = 1 /* seconds */, callable $onError = null): void { gc_enable(); @@ -206,8 +206,8 @@ static public function loop(callable $callback, $sleep = 1 /* seconds */, callab while (!connection_aborted() || PHP_SAPI == "cli") { try { $callback(); - } catch(\Exception $e) { - if($onError != null) { + } catch (\Exception $e) { + if ($onError != null) { $onError($e); } else { throw $e; @@ -217,18 +217,18 @@ static public function loop(callable $callback, $sleep = 1 /* seconds */, callab $intSeconds = intval($sleep); $microSeconds = ($sleep - $intSeconds) * 1000000; - if($intSeconds > 0) { + if ($intSeconds > 0) { sleep($intSeconds); } - if($microSeconds > 0) { + if ($microSeconds > 0) { usleep($microSeconds); } $time = $time + $sleep; if (PHP_SAPI == "cli") { - if($time >= 60 * 5) { // Every 5 minutes + if ($time >= 60 * 5) { // Every 5 minutes $time = 0; gc_collect_cycles(); //Forces collection of any existing garbage cycles } diff --git a/src/CLI/Task.php b/src/CLI/Task.php index b7bf66f..6a1fcf1 100644 --- a/src/CLI/Task.php +++ b/src/CLI/Task.php @@ -1,4 +1,5 @@ name = $name; - $this->action = function(): void {}; + $this->action = function (): void { + }; } /** @@ -213,4 +215,4 @@ public function getLabel(string $key, $default) { return (isset($this->labels[$key])) ? $this->labels[$key] : $default; } -} \ No newline at end of file +} diff --git a/tests/CLI/CLITest.php b/tests/CLI/CLITest.php index 93421fb..0188744 100755 --- a/tests/CLI/CLITest.php +++ b/tests/CLI/CLITest.php @@ -1,4 +1,5 @@ param('email', null, new Text(0), 'Valid email address') ->param('list', null, new ArrayList(new Text(256)), 'List of strings') ->action(function ($email, $list) { - echo $email.'-'.implode('-', $list); + echo $email . '-' . implode('-', $list); }); $cli->run(); @@ -98,7 +99,7 @@ public function testGetTasks() ->param('email', null, new Text(0), 'Valid email address') ->param('list', null, new ArrayList(new Text(256)), 'List of strings') ->action(function ($email, $list) { - echo $email.'-'.implode('-', $list); + echo $email . '-' . implode('-', $list); }); $cli @@ -106,7 +107,7 @@ public function testGetTasks() ->param('email', null, new Text(0), 'Valid email address') ->param('list', null, new ArrayList(new Text(256)), 'List of strings') ->action(function ($email, $list) { - echo $email.'-'.implode('-', $list); + echo $email . '-' . implode('-', $list); }); $this->assertCount(2, $cli->getTasks()); @@ -121,7 +122,7 @@ public function testGetArgs() ->param('email', null, new Text(0), 'Valid email address') ->param('list', null, new ArrayList(new Text(256)), 'List of strings') ->action(function ($email, $list) { - echo $email.'-'.implode('-', $list); + echo $email . '-' . implode('-', $list); }); $cli @@ -129,7 +130,7 @@ public function testGetArgs() ->param('email', null, new Text(0), 'Valid email address') ->param('list', null, new ArrayList(new Text(256)), 'List of strings') ->action(function ($email, $list) { - echo $email.'-'.implode('-', $list); + echo $email . '-' . implode('-', $list); }); $this->assertCount(2, $cli->getArgs()); @@ -146,16 +147,15 @@ public function testInjection() $cli->task('build') ->inject('test') ->param('email', null, new Text(15), 'valid email address') - ->action(function($test, $email){ + ->action(function ($test, $email) { echo $test . '-' . $email; }); - + $cli->run(); $result = ob_get_clean(); $this->assertEquals('test-value-me@example.com', $result); - } public function testMatch() @@ -167,7 +167,7 @@ public function testMatch() ->param('email', null, new Text(0), 'Valid email address') ->param('list', null, new ArrayList(new Text(256)), 'List of strings') ->action(function ($email, $list) { - echo $email.'-'.implode('-', $list); + echo $email . '-' . implode('-', $list); }); $cli @@ -175,7 +175,7 @@ public function testMatch() ->param('email', null, new Text(0), 'Valid email address') ->param('list', null, new ArrayList(new Text(256)), 'List of strings') ->action(function ($email, $list) { - echo $email.'-'.implode('-', $list); + echo $email . '-' . implode('-', $list); }); $this->assertEquals('build2', $cli->match()->getName()); @@ -187,7 +187,7 @@ public function testMatch() ->param('email', null, new Text(0), 'Valid email address') ->param('list', null, new ArrayList(new Text(256)), 'List of strings') ->action(function ($email, $list) { - echo $email.'-'.implode('-', $list); + echo $email . '-' . implode('-', $list); }); $cli @@ -195,9 +195,9 @@ public function testMatch() ->param('email', null, new Text(0), 'Valid email address') ->param('list', null, new ArrayList(new Text(256)), 'List of strings') ->action(function ($email, $list) { - echo $email.'-'.implode('-', $list); + echo $email . '-' . implode('-', $list); }); $this->assertEquals(null, $cli->match()); } -} \ No newline at end of file +} diff --git a/tests/CLI/ConsoleTest.php b/tests/CLI/ConsoleTest.php index e37bbc4..717f8fe 100755 --- a/tests/CLI/ConsoleTest.php +++ b/tests/CLI/ConsoleTest.php @@ -1,4 +1,5 @@ assertEquals('', $stderr); $this->assertGreaterThan(30, count(explode("\n", $stdout))); diff --git a/tests/CLI/TaskTest.php b/tests/CLI/TaskTest.php index 03a644b..e1f39f6 100755 --- a/tests/CLI/TaskTest.php +++ b/tests/CLI/TaskTest.php @@ -1,4 +1,5 @@ assertEquals([], $this->task->getInjections()); - + $this->task ->inject('user') ->inject('time') - ->action(function() {}) + ->action(function () { + }) ; $this->assertCount(2, $this->task->getInjections()); diff --git a/tests/resources/loop.php b/tests/resources/loop.php index 8e73d8d..cef0bca 100644 --- a/tests/resources/loop.php +++ b/tests/resources/loop.php @@ -2,8 +2,8 @@ use Utopia\CLI\Console; -include __DIR__.'/../../vendor/autoload.php'; +include __DIR__ . '/../../vendor/autoload.php'; -Console::loop(function() { +Console::loop(function () { echo "Hello\n"; -}); \ No newline at end of file +}); From b4f026ee2d0abf85e8620be0db5df98b689cd9b0 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 3 Aug 2022 07:57:18 +0000 Subject: [PATCH 03/12] return types --- src/CLI/Task.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CLI/Task.php b/src/CLI/Task.php index 6a1fcf1..991d0b2 100644 --- a/src/CLI/Task.php +++ b/src/CLI/Task.php @@ -10,14 +10,14 @@ class Task /** * @var string */ - protected $name = ''; + protected string $name = ''; /** * Description * * @var string */ - protected $desc = ''; + protected string $desc = ''; /** * Action Callback @@ -33,7 +33,7 @@ class Task * * @var array */ - protected $params = []; + protected array $params = []; /** * Injections @@ -51,7 +51,7 @@ class Task * * @var array */ - protected $labels = []; + protected array $labels = []; /** * Task constructor. @@ -211,7 +211,7 @@ public function getInjections(): array * @param mixed $default * @return mixed */ - public function getLabel(string $key, $default) + public function getLabel(string $key, $default): mixed { return (isset($this->labels[$key])) ? $this->labels[$key] : $default; } From 163b19949f481ea3a129c6df99df495b0d594f19 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 3 Aug 2022 08:08:24 +0000 Subject: [PATCH 04/12] update travis test to use phpcs --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3ba22f2..ef07f04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ before_script: composer install --ignore-platform-reqs script: - vendor/bin/phpunit --configuration phpunit.xml < tests/input.txt -- vendor/bin/psalm --show-info=true +- vendor/bin/phpcs -p From f40ab66a4ee412c9fc43a75fde97f3c4a78662bb Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 3 Aug 2022 08:31:51 +0000 Subject: [PATCH 05/12] implement hooks --- src/CLI/CLI.php | 100 ++++++++++++++++++-------------- src/CLI/Task.php | 148 +---------------------------------------------- 2 files changed, 59 insertions(+), 189 deletions(-) diff --git a/src/CLI/CLI.php b/src/CLI/CLI.php index 70fbf62..877f808 100644 --- a/src/CLI/CLI.php +++ b/src/CLI/CLI.php @@ -3,6 +3,7 @@ namespace Utopia\CLI; use Exception; +use Utopia\Hook; use Utopia\Validator; class CLI @@ -49,27 +50,27 @@ class CLI * * An error callback * - * @var callable + * @var Hook[] */ - protected $error; + protected $errors = []; /** * Init * * A callback function that is initialized on application start * - * @var callable[] + * @var Hook[] */ - protected $init = []; + protected array $init = []; /** * Shutdown * * A callback function that is initialized on application end * - * @var callable[] + * @var Hook[] */ - protected $shutdown = []; + protected array $shutdown = []; /** * CLI constructor. @@ -97,13 +98,13 @@ public function __construct(array $args = []) * * Set a callback function that will be initialized on application start * - * @param callable $callback - * @return $this + * @return Hook */ - public function init(callable $callback): self + public function init(): Hook { - $this->init[] = $callback; - return $this; + $hook = new Hook(); + $this->init[] = $hook; + return $hook; } /** @@ -111,13 +112,13 @@ public function init(callable $callback): self * * Set a callback function that will be initialized on application end * - * @param $callback - * @return $this + * @return Hook */ - public function shutdown(callable $callback): self + public function shutdown(): Hook { - $this->shutdown[] = $callback; - return $this; + $hook = new Hook(); + $this->shutdown[] = $hook; + return $hook; } /** @@ -125,13 +126,13 @@ public function shutdown(callable $callback): self * * An error callback for failed or no matched requests * - * @param $callback - * @return $this + * @return Hook */ - public function error(callable $callback): self + public function error(): Hook { - $this->error = $callback; - return $this; + $hook = new Hook(); + $this->errors[] = $hook; + return $hook; } /** @@ -279,6 +280,32 @@ public function match(): ?Task return isset($this->tasks[$this->command]) ? $this->tasks[$this->command] : null; } + /** + * Get Arguments + * + * @param Hook $hook + * @return array + */ + protected function getParams(Hook $hook): array + { + $params = []; + + foreach ($hook->getParams() as $key => $param) { + $value = (isset($this->args[$key])) ? $this->args[$key] : $param['default']; + + $this->validate($key, $param, $value); + + $params[$param['order']] = $value; + } + + foreach ($hook->getInjections() as $key => $injection) { + $params[$injection['order']] = $this->getResource($injection['name']); + } + + ksort($params); + return $params; + } + /** * Run * @@ -290,37 +317,24 @@ public function run(): self try { if ($command) { - foreach ($this->init as $init) { - \call_user_func_array($init, []); + foreach ($this->init as $hook) { + \call_user_func_array($hook->getAction(), $this->getParams($hook)); } - $params = []; - - foreach ($command->getParams() as $key => $param) { - $value = (isset($this->args[$key])) ? $this->args[$key] : $param['default']; - - $this->validate($key, $param, $value); - - $params[$param['order']] = $value; - } - - foreach ($command->getInjections() as $key => $injection) { - $params[$injection['order']] = $this->getResource($injection['name']); - } - - ksort($params); - // Call the callback with the matched positions as params - \call_user_func_array($command->getAction(), $params); + \call_user_func_array($command->getAction(), $this->getParams($command)); - foreach ($this->shutdown as $shutdown) { - \call_user_func_array($shutdown, []); + foreach ($this->shutdown as $hook) { + \call_user_func_array($hook->getAction(), $this->getParams($hook)); } } else { throw new Exception('No command found'); } } catch (Exception $e) { - \call_user_func_array($this->error, array($e)); + foreach($this->errors as $hook) { + self::setResource('error', fn () => $e); + \call_user_func_array($hook->getAction(), $this->getParams($hook)); + } } return $this; diff --git a/src/CLI/Task.php b/src/CLI/Task.php index 991d0b2..d29cb25 100644 --- a/src/CLI/Task.php +++ b/src/CLI/Task.php @@ -4,46 +4,15 @@ use Utopia\Validator; use Exception; +use Utopia\Hook; -class Task +class Task extends Hook { /** * @var string */ protected string $name = ''; - /** - * Description - * - * @var string - */ - protected string $desc = ''; - - /** - * Action Callback - * - * @var callable - */ - protected $action; - - /** - * Parameters - * - * List of route params names and validators - * - * @var array - */ - protected array $params = []; - - /** - * Injections - * - * List of route required injections for action callback - * - * @var array - */ - protected array $injections = []; - /** * Labels * @@ -64,79 +33,6 @@ public function __construct(string $name) }; } - /** - * Add Description - * - * @param string $desc - * @return $this - */ - public function desc($desc): self - { - $this->desc = $desc; - return $this; - } - - /** - * Add Action - * - * @param callable $action - * @return $this - */ - public function action(callable $action): self - { - $this->action = $action; - return $this; - } - - /** - * Add Param - * - * @param string $key - * @param mixed $default - * @param Validator $validator - * @param string $description - * @param bool $optional - * - * @return $this - */ - public function param(string $key, $default, Validator $validator, string $description = '', bool $optional = false): self - { - $this->params[$key] = array( - 'default' => $default, - 'validator' => $validator, - 'description' => $description, - 'optional' => $optional, - 'value' => null, - 'order' => count($this->params) + count($this->injections), - ); - - return $this; - } - - /** - * Inject - * - * @param string $injection - * - * @throws Exception - * - * @return static - */ - public function inject(string $injection): static - { - if (array_key_exists($injection, $this->injections)) { - throw new Exception('Injection already declared for ' . $injection); - } - - $this->injections[$injection] = [ - 'name' => $injection, - 'order' => count($this->params) + count($this->injections), - ]; - - return $this; - } - - /** * Add Label * @@ -162,46 +58,6 @@ public function getName(): string return $this->name; } - /** - * Get Description - * - * @return string - */ - public function getDesc(): string - { - return $this->desc; - } - - /** - * Get Action - * - * @return callable - */ - public function getAction(): callable - { - return $this->action; - } - - /** - * Get Params - * - * @return array - */ - public function getParams(): array - { - return $this->params; - } - - /** - * Get Injections - * - * @return array - */ - public function getInjections(): array - { - return $this->injections; - } - /** * Get Label * From ea4be50ad09629ebb19ca22af9fa4bc7abf41a9a Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 3 Aug 2022 08:32:02 +0000 Subject: [PATCH 06/12] format --- src/CLI/CLI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CLI/CLI.php b/src/CLI/CLI.php index 877f808..5757965 100644 --- a/src/CLI/CLI.php +++ b/src/CLI/CLI.php @@ -331,7 +331,7 @@ public function run(): self throw new Exception('No command found'); } } catch (Exception $e) { - foreach($this->errors as $hook) { + foreach ($this->errors as $hook) { self::setResource('error', fn () => $e); \call_user_func_array($hook->getAction(), $this->getParams($hook)); } From 7432bdcd43cca2dfe97eb4183b5b79f9406b091f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 3 Aug 2022 08:48:23 +0000 Subject: [PATCH 07/12] hook test --- src/CLI/CLI.php | 5 ++++ tests/CLI/CLITest.php | 59 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/CLI/CLI.php b/src/CLI/CLI.php index 5757965..6f06b64 100644 --- a/src/CLI/CLI.php +++ b/src/CLI/CLI.php @@ -394,4 +394,9 @@ protected function validate(string $key, array $param, $value): void } } } + + public static function reset(): void + { + self::$resourcesCallbacks = []; + } } diff --git a/tests/CLI/CLITest.php b/tests/CLI/CLITest.php index 0188744..3c43d79 100755 --- a/tests/CLI/CLITest.php +++ b/tests/CLI/CLITest.php @@ -29,6 +29,31 @@ public function tearDown(): void { } + public function testResources() + { + $cli = new CLI(); + CLI::setResource('rand', function () { + return rand(); + }); + CLI::setResource('first', function ($second) { + return 'first-' . $second; + }, ['second']); + CLI::setResource('second', function () { + return 'second'; + }); + $second = $cli->getResource('second'); + $first = $cli->getResource('first'); + $this->assertEquals('second', $second); + $this->assertEquals('first-second', $first); + + $resource = $cli->getResource('rand'); + + $this->assertNotEmpty($resource); + $this->assertEquals($resource, $cli->getResource('rand')); + $this->assertEquals($resource, $cli->getResource('rand')); + $this->assertEquals($resource, $cli->getResource('rand')); + } + public function testAppSuccess() { ob_start(); @@ -137,6 +162,40 @@ public function testGetArgs() $this->assertEquals(['email' => 'me@example.com', 'list' => ['item1', 'item2']], $cli->getArgs()); } + public function testHook() + { + CLI::reset(); + + $cli = new CLI(['test.php', 'build', '--email=me@example.com', '--list=item1', '--list=item2']); + + $cli + ->init() + ->action(function () { + echo '(init)-'; + }); + + $cli + ->shutdown() + ->action(function () { + echo '-(shutdown)'; + }); + + $cli + ->task('build') + ->param('email', null, new Text(0), 'Valid email address') + ->param('list', null, new ArrayList(new Text(256)), 'List of strings') + ->action(function ($email, $list) { + echo $email . '-' . implode('-', $list); + }); + + \ob_start(); + + $cli->run(); + $result = \ob_get_clean(); + + $this->assertEquals('(init)-me@example.com-item1-item2-(shutdown)', $result); + } + public function testInjection() { ob_start(); From 5608d6e2d8a55ae8acf7ffc4e062bb7ef9476ac7 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 3 Aug 2022 08:57:44 +0000 Subject: [PATCH 08/12] fix test --- composer.json | 2 +- tests/CLI/CLITest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 6ecee40..66086c7 100755 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "scripts": { - "test": "vendor/bin/phpunit --configuration phpunit.xml < test/input.text", + "test": "vendor/bin/phpunit --configuration phpunit.xml < tests/input.txt", "lint": "vendor/bin/phpcs", "format": "vendor/bin/phpcbf" }, diff --git a/tests/CLI/CLITest.php b/tests/CLI/CLITest.php index 3c43d79..ba2b579 100755 --- a/tests/CLI/CLITest.php +++ b/tests/CLI/CLITest.php @@ -31,7 +31,7 @@ public function tearDown(): void public function testResources() { - $cli = new CLI(); + $cli = new CLI(['test.php', 'build']); CLI::setResource('rand', function () { return rand(); }); From d4dccc72c84376020d1366a9348b8bf98e63797c Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 4 Aug 2022 05:52:53 +0000 Subject: [PATCH 09/12] update comments --- src/CLI/CLI.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CLI/CLI.php b/src/CLI/CLI.php index 6f06b64..7bbd91b 100644 --- a/src/CLI/CLI.php +++ b/src/CLI/CLI.php @@ -281,7 +281,8 @@ public function match(): ?Task } /** - * Get Arguments + * Get Params + * Get runtime params for the provided Hook * * @param Hook $hook * @return array From de3908706b244c342a7226b330647b18e65b3041 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 9 Oct 2022 08:25:08 +0000 Subject: [PATCH 10/12] labels now in hook --- src/CLI/Task.php | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/src/CLI/Task.php b/src/CLI/Task.php index d29cb25..736e9db 100644 --- a/src/CLI/Task.php +++ b/src/CLI/Task.php @@ -13,14 +13,6 @@ class Task extends Hook */ protected string $name = ''; - /** - * Labels - * - * List of route label names - * - * @var array - */ - protected array $labels = []; /** * Task constructor. @@ -33,21 +25,6 @@ public function __construct(string $name) }; } - /** - * Add Label - * - * @param string $key - * @param mixed $value - * - * @return $this - */ - public function label(string $key, $value): self - { - $this->labels[$key] = $value; - - return $this; - } - /** * Get Name * @@ -57,18 +34,4 @@ public function getName(): string { return $this->name; } - - /** - * Get Label - * - * Return given label value or default value if label doesn't exists - * - * @param string $key - * @param mixed $default - * @return mixed - */ - public function getLabel(string $key, $default): mixed - { - return (isset($this->labels[$key])) ? $this->labels[$key] : $default; - } } From f756e9bbd73fb14aa1a2909b7d94984c71a2ffc2 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 9 Oct 2022 08:26:37 +0000 Subject: [PATCH 11/12] remove reserved word --- src/CLI/CLI.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/CLI/CLI.php b/src/CLI/CLI.php index 7bbd91b..6731963 100644 --- a/src/CLI/CLI.php +++ b/src/CLI/CLI.php @@ -163,10 +163,6 @@ public function task(string $name): Task */ public function getResource(string $name, bool $fresh = false): mixed { - if ($name === 'utopia') { - return $this; - } - if (!\array_key_exists($name, $this->resources) || $fresh || self::$resourcesCallbacks[$name]['reset']) { if (!\array_key_exists($name, self::$resourcesCallbacks)) { throw new Exception('Failed to find resource: "' . $name . '"'); @@ -213,9 +209,6 @@ public function getResources(array $list): array */ public static function setResource(string $name, callable $callback, array $injections = []): void { - if ($name === 'utopia') { - throw new Exception("'utopia' is a reserved keyword.", 500); - } self::$resourcesCallbacks[$name] = ['callback' => $callback, 'injections' => $injections, 'reset' => true]; } From e05e9ea8d510bc8f9e69dbb11faaae2417572163 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 9 Oct 2022 08:35:02 +0000 Subject: [PATCH 12/12] update readme with hooks --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 0821434..1e9aa09 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,45 @@ And than, run from command line: php script.php command-name --email=me@example.com ``` +### Hooks + +There are three types of hooks, init hooks, shutdown hooks and error hooks. Init hooks are executed before the task is executed. Shutdown hook is executed after task is executed before application shuts down. Finally error hooks are executed whenever there's an error in the application lifecycle. You can provide multiple hooks for each stage. + +```php +require_once __DIR__ . '/../../vendor/autoload.php'; + +use Utopia\App; +use Utopia\Request; +use Utopia\Response; + +CLI::setResource('res1', function() { + return 'resource 1'; +}) + +CLI::init() + inject('res1') + ->action(function($res1) { + Console::info($res1); + }); + +CLI::error() + ->inject('error') + ->action(function($error) { + Console::error('Error occurred ' . $error); + }); + +$cli = new CLI(); + +$cli + ->task('command-name') + ->param('email', null, new Wildcard()) + ->action(function ($email) { + Console::success($email); + }); + +$cli->run(); +``` + ### Log Messages ```php