commit 0c9fd2e892623b69e3896bb1106ac2bb145ecc75 Author: Christopher Vagnetoft Date: Tue Dec 7 17:26:34 2021 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b385f21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/composer.lock +/vendor +/.phpunit.cache +/*.phar +/.spark +/.spark.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..629b94d --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Spark: Ignite your development workflow + +Spark is a utility to help with everything from various mundane tasks to complex +database migrations and project deployment. + +## Installation + +Download `spark.phar` and make it executable. If desired, alias `spark=spark.phar`. +You may also want to alias `sparksh='spark repl'`. + +## Using Spark + +Spark expects a configuration file to either be found at `./.spark.json` or +`./.spark/spark.json` relative to the project root. The `./.spark` directory +will always be used for auxillary configuration, so the placement is fully up +to you. + +On its own it doesn't do much except provide a command interface to its inside. +The magic can be found in preloading: + +*spark.json* +``` +{ + "preload": [ "./.spark/plugins/*", "./.spark/autoload.php" ] +} +``` + +The preloader will go over each of the defined rules and attempt to load them +in one of two ways, if applicable: + +1. Files with a `.php`-extension will be loaded directly. +2. Directories having a `sparkplug.php` file will be loaded as plugins. + +The advantages of writing your extensions as flat files: + +- Simple interface +- Quickly register resources for other parts of Spark +- All code evaluated on load (can be a caveat!) + +The advantage of writing your extensions as plugins: + +- Object-oriented interface +- Delayed evaluation of code, ensuring dependencies are loaded + +### Scripts + +Using scripts is the simplest way to leverage Spark: + +*spark.json* +``` +{ + ... + "scripts": { + "hello": "./.spark/hello.php", + "world": "echo 'World'", + "greet": [ + "@hello", + "@world" + ] + } +} +``` + +`.php`-files are executed in-process, and as such have access to any registered +resources, resource types and plugins. + +### Resources + +Resources are wrappers around database connections and such, providing a cleaner +interface to its innards. + + diff --git a/bin/spark b/bin/spark new file mode 100755 index 0000000..240495c --- /dev/null +++ b/bin/spark @@ -0,0 +1,7 @@ +#!/usr/bin/env php +run(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5e41ee2 --- /dev/null +++ b/composer.json @@ -0,0 +1,40 @@ +{ + "name": "noccylabs/spark", + "description": "Energize your development workflow", + "type": "application", + "license": "MIT", + "authors": [ + { + "name": "Christopher Vagnetoft", + "email": "cvagnetoft@gmail.com" + } + ], + "bin": [ + "bin/spark" + ], + "autoload": { + "psr-4": { + "Spark\\": "src/", + "": "runtime/" + }, + "files": [ + "runtime/functions.php" + ] + }, + "extra": { + "phar": { + "output": "spark.phar" + } + }, + "require": { + "php": "^8.0", + "ext-zip": "^1.19", + "ext-xml": "^8.0", + "symfony/console": "^6.0", + "symfony/expression-language": "^6.0", + "symfony/finder": "^6.0", + "symfony/process": "^6.0", + "psr/log": "^3.0", + "symfony/var-dumper": "^6.0" + } +} diff --git a/runtime/SparkPlug.php b/runtime/SparkPlug.php new file mode 100644 index 0000000..4bf0671 --- /dev/null +++ b/runtime/SparkPlug.php @@ -0,0 +1,6 @@ +getNamespaceName()."\\", + path: dirname($refl->getFileName())."/" + ); + spl_autoload_register(function ($class) use ($psr4) { + if (str_starts_with($class, $psr4->namespace)) { + $part = substr($class, strlen($psr4->namespace)); + $file = $psr4->path . strtr($part, "\\", DIRECTORY_SEPARATOR).".php"; + if (file_exists($file)) { + require_once $file; + } + } + }); + SparkApplication::$instance->getPluginManager()->registerPlugin($name, $plugin); +} + +function get_plugin(string $name) { + return SparkApplication::$instance->getPluginManager()->getPlugin($name); +} + +function register_command(Command $command) { + SparkApplication::$instance->add($command); +} + +function get_environment(): Environment { + return SparkApplication::$instance->getEnvironment(); +} + +function register_resource_type(string $name, string $type) { + SparkApplication::$instance->getResourceManager()->registerResourceType($name, $type); +} + +function create_resource(string $name, string $type, ...$options) { + return SparkApplication::$instance->getResourceManager()->createNamedResource($name, $type, $options); +} + +function get_resource(string $name) { + return SparkApplication::$instance->getResourceManager()->getNamedResource($name); +} + +function resource(string $name) { + return get_resource($name); +} + +function read_config($file=null) { + if (!$file) return; + $abs = get_environment()->getConfigDirectory() . "/" . $file; + if (!file_exists($abs)) { + //fprintf(STDERR, "warning: Can't read config file %s\n", $abs); + return []; + } + return (array)json_decode(file_get_contents($abs), true); +} diff --git a/src/Commands/Command.php b/src/Commands/Command.php new file mode 100644 index 0000000..e95dcde --- /dev/null +++ b/src/Commands/Command.php @@ -0,0 +1,38 @@ +getApplication(); + if (!$app) return null; + return $app->getEnvironment(); + } + + public function loadEnvironment(string $path) + { + /** @var SparkApplication */ + $app = $this->getApplication(); + $app->loadEnvironment($path); + } + + public function createEnvironment(string|null $path=null) + { + if (empty($path)) { + $path = getcwd(); + } + /** @var SparkApplication */ + $app = $this->getApplication(); + $app->createEnvironment($path); + $app->loadEnvironment($path); + } + +} \ No newline at end of file diff --git a/src/Commands/ReplCommand.php b/src/Commands/ReplCommand.php new file mode 100644 index 0000000..7673f2d --- /dev/null +++ b/src/Commands/ReplCommand.php @@ -0,0 +1,41 @@ +addOption("list", null, InputOption::VALUE_NONE, "List the available scripts"); + $this->addArgument("script", InputArgument::OPTIONAL, "The script too run (see --list)"); + $this->addArgument("args", InputArgument::OPTIONAL|InputArgument::IS_ARRAY, "Arguments to the script"); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $env = $this->getEnvironment(); + + while (true) { + $cmd = readline("repl> "); + if ($cmd) readline_add_history($cmd); + try { + $ret = @eval("return {$cmd};"); + //$output->writeln(json_encode($ret,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); + //$output->writeln("".var_export($ret,true).""); + dump($ret); + } catch (\Throwable $t) { + $output->writeln("".$t->getMessage().""); + } + } + + return Command::SUCCESS; + } +} diff --git a/src/Commands/ResourcesCommand.php b/src/Commands/ResourcesCommand.php new file mode 100644 index 0000000..cdd3e2b --- /dev/null +++ b/src/Commands/ResourcesCommand.php @@ -0,0 +1,57 @@ +getEnvironment(); + $app = $this->getApplication(); + + /** @var SparkApplication $app */ + $resources = $app->getResourceManager(); + + $types = $resources->getAllResourceTypes(); + $named = $resources->getAllNamedResources(); + + $output->writeln("Resource Types:"); + $table = new Table($output); + $table->setStyle("compact"); + $table->setHeaders([ "Type", "Class" ]); + $table->setColumnWidth(0, 10); + $map = []; + foreach ($types as $type=>$class) { + $map[$class] = $type; + $table->addRow([ $type, $class ]); + } + $table->render(); + + $output->writeln(""); + + $output->writeln("Named Resources:"); + $table = new Table($output); + $table->setStyle("compact"); + $table->setHeaders([ "Name", "Type", "Info" ]); + $table->setColumnWidth(0, 10); + $table->setColumnWidth(1, 6); + foreach ($named as $name=>$class) { + $table->addRow([ $name, $map[get_class($class)], $class->info() ]); + } + $table->render(); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commands/RunCommand.php b/src/Commands/RunCommand.php new file mode 100644 index 0000000..f9de0b7 --- /dev/null +++ b/src/Commands/RunCommand.php @@ -0,0 +1,35 @@ +addOption("list", null, InputOption::VALUE_NONE, "List the available scripts"); + $this->addArgument("script", InputArgument::OPTIONAL, "The script too run (see --list)"); + $this->addArgument("args", InputArgument::OPTIONAL|InputArgument::IS_ARRAY, "Arguments to the script"); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $env = $this->getEnvironment(); + + if ($input->getOption("list")) { + $output->writeln($env->getDefinedScripts()); + return Command::SUCCESS; + } elseif ($script = $input->getArgument('script')) { + $env->runScript($script, $input->getArgument('args'), $input, $output); + } + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Environment/Environment.php b/src/Environment/Environment.php new file mode 100644 index 0000000..0c1ad48 --- /dev/null +++ b/src/Environment/Environment.php @@ -0,0 +1,135 @@ +config = $config; + } + + public function getProjectDirectory(): string + { + return $this->config['project_dir']; + } + + public function getConfigDirectory(): string + { + return $this->config['config_dir']; + } + + public function getDefinedScripts(): array + { + return array_keys($this->config['scripts']??[]); + } + + public function runScript(string $name, array $args, InputInterface $input, OutputInterface $output) + { + $script = $this->config['scripts'][$name]; + + if (is_string($script)) { + // call script directly + if (str_ends_with($script, '.php')) { + $GLOBALS['args'] = $args; + $GLOBALS['output'] = $output; + $base = $this->getProjectDirectory(); + if (!file_exists($base ."/". $script)) { + fprintf(STDERR, "error: Could not find script file %s\n", $base."/".$script); + return; + } + include $base . "/" . $script; + } else { + passthru($script); + } + } + } + + public function loadEnvironment() + { + if ($this->loaded) { + return; + } + + if (!array_key_exists('project_dir', $this->config)) { + return; + } + + // $this->logger->info("Loading environment..."); + $preloads = []; + $root = $this->config['project_dir']; + foreach ((array)$this->config['preload']??[] as $preload) { + if (str_contains($preload,'*')) { + array_push($preloads, ...glob($root."/".$preload)); + } else { + array_push($preloads, $preload); + } + } + + foreach ($preloads as $item) { + if (!str_starts_with($item, "/")) { + $item = $this->getProjectDirectory() . "/" . $item; + } + if (is_file($item)) { + // $this->logger->debug("Preloading file {$item}"); + try { + include_once($item); + } catch (\Throwable $t) { + fprintf(STDERR, "error: Error preloading %s: %s in %s on line %d\n", $item, $t->getMessage(), $t->getFile(), $t->getLine()); + //$this->logger->error("Error preloading {$item}: {$t->getMessage()} in {$t->getFile()} on line {$t->getLine()}"); + } + } elseif (is_dir($item) && file_exists($item."/sparkplug.php")) { + //$this->logger->debug("Preloading plugin {$item}"); + try { + include_once($item."/sparkplug.php"); + } catch (\Throwable $t) { + fprintf(STDERR, "error: Error preloading plugin %s: %s in %s on line %d\n", $item, $t->getMessage(), $t->getFile(), $t->getLine()); + //$this->logger->error("Error preloading plugin {$item}: {$t->getMessage()} in {$t->getFile()} on line {$t->getLine()}"); + } + } else { + fprintf(STDERR, "warning: Could not preload %s\n", $item); + //$this->logger->warning("Could not preload {$item}"); + } + } + + SparkApplication::$instance->getPluginManager()->initializePlugins(); + } + + public static function createFromDirectory(string|null $directory=null): Environment + { + $directory = $directory ?? getcwd(); + $candidates = [ $directory . "/.spark.json", $directory . "/.spark/spark.json" ]; + $config = []; + while ($candidate = array_shift($candidates)) { + if (!file_exists($candidate)) { continue; } + $json = file_get_contents($candidate); + if (!$json || json_last_error()) { + throw new \RuntimeException("Error parsing {$candidate}: ".json_last_error_msg()); + } + $config = json_decode($json, true); + $config['project_dir'] = realpath($directory); + $config['config_dir'] = realpath($directory)."/.spark"; + $config['config_file'] = $candidate; + break; + } + + $env = new Environment($config); + + return $env; + } + + + +} diff --git a/src/Plugin/PluginManager.php b/src/Plugin/PluginManager.php new file mode 100644 index 0000000..7fc8444 --- /dev/null +++ b/src/Plugin/PluginManager.php @@ -0,0 +1,27 @@ +plugins[$name] = $plugin; + } + + public function initializePlugins() + { + foreach ($this->plugins as $plugin) { + $plugin->load(); + } + } + + public function getPlugin(string $name) + { + return $this->plugins[$name]; + } +} \ No newline at end of file diff --git a/src/Resource/ResourceManager.php b/src/Resource/ResourceManager.php new file mode 100644 index 0000000..75171a5 --- /dev/null +++ b/src/Resource/ResourceManager.php @@ -0,0 +1,46 @@ +resourceTypes[$name] = $type; + } + + public function createResource(string $type, array $options) + { + $resource = new $this->resourceTypes[$type]($options); + return $resource; + } + + public function createNamedResource(string $name, string $type, array $options) + { + $resource = $this->createResource($type, $options); + $this->namedResources[$name] = $resource; + return $resource; + } + + public function getNamedResource(string $name) + { + return $this->namedResources[$name] ?? null; + } + + public function getAllNamedResources(): array + { + return $this->namedResources; + } + + public function getAllResourceTypes(): array + { + return $this->resourceTypes; + } +} \ No newline at end of file diff --git a/src/Resource/ResourceType.php b/src/Resource/ResourceType.php new file mode 100644 index 0000000..924a83a --- /dev/null +++ b/src/Resource/ResourceType.php @@ -0,0 +1,8 @@ +resourceManager = new ResourceManager(); + $this->pluginManager = new PluginManager(); + + $this->environment = Environment::createFromDirectory(); + $this->environment->loadEnvironment(); + + $this->add(new Commands\RunCommand()); + $this->add(new Commands\ResourcesCommand()); + $this->add(new Commands\ReplCommand()); + + } + + public function getPluginManager(): PluginManager + { + return $this->pluginManager; + } + + public function getEnvironment(): Environment + { + // if (empty($this->environment)) { + // $this->environment = Environment::createFromDirectory(); + // $this->environment->setLogger($this->logger); + // } + return $this->environment; + } + + public function getResourceManager(): ResourceManager + { + return $this->resourceManager; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function doRun(InputInterface $input, OutputInterface $output) + { + $this->logger = new ConsoleLogger($output); + parent::doRun($input, $output); + } + +}