Initial commit

This commit is contained in:
Chris 2021-12-07 17:26:34 +01:00
commit 0c9fd2e892
15 changed files with 674 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/composer.lock
/vendor
/.phpunit.cache
/*.phar
/.spark
/.spark.json

72
README.md Normal file
View File

@ -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.

7
bin/spark Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env php
<?php
require_once __DIR__."/../vendor/autoload.php";
$app = new Spark\SparkApplication();
$app->run();

40
composer.json Normal file
View File

@ -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"
}
}

6
runtime/SparkPlug.php Normal file
View File

@ -0,0 +1,6 @@
<?php
abstract class SparkPlug
{
abstract public function load();
}

83
runtime/functions.php Normal file
View File

@ -0,0 +1,83 @@
<?php
use Spark\Commands\Command;
use Spark\Environment\Environment;
use Spark\Resource\ResourceType;
use Spark\SparkApplication;
$HELPERS = [];
function object(...$data) {
return (object)$data;
}
function register_helper(string $name, callable $helper) {
global $HELPERS;
$HELPERS[$name] = $helper;
}
function helper(string $name, ...$args) {
global $HELPERS;
if (!array_key_exists($name, $HELPERS)) {
fprintf(STDERR, "error: No helper %s registered", $name);
return false;
}
$helper = $HELPERS[$name];
return $helper(...$args);
}
function register_plugin(string $name, SparkPlug $plugin) {
$refl = new ReflectionClass($plugin);
$psr4 = object(
namespace: $refl->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);
}

38
src/Commands/Command.php Normal file
View File

@ -0,0 +1,38 @@
<?php
namespace Spark\Commands;
use Flare\SparkApplication;
use Spark\Environment\Environment;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command as CommandCommand;
abstract class Command extends CommandCommand
{
public function getEnvironment(): Environment
{
/** @var SparkApplication */
$app = $this->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);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Spark\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Spark\Commands\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name:'repl', description:'Interactive REPL for PHP')]
class ReplCommand extends Command
{
protected function configure()
{
$this->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("<info>".var_export($ret,true)."</>");
dump($ret);
} catch (\Throwable $t) {
$output->writeln("<error>".$t->getMessage()."</>");
}
}
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Spark\Commands;
use Spark\SparkApplication;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name:'resources', description:'List defined resources')]
class ResourcesCommand extends Command
{
protected function configure()
{
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$env = $this->getEnvironment();
$app = $this->getApplication();
/** @var SparkApplication $app */
$resources = $app->getResourceManager();
$types = $resources->getAllResourceTypes();
$named = $resources->getAllNamedResources();
$output->writeln("<options=bold>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("<options=bold>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;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Spark\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Spark\Commands\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name:'run', description:'Run a script')]
class RunCommand extends Command
{
protected function configure()
{
$this->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;
}
}

View File

@ -0,0 +1,135 @@
<?php
namespace Spark\Environment;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Spark\SparkApplication;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class Environment
{
private bool $loaded = false;
private array $config;
public function __construct(array $config)
{
$this->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;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Spark\Plugin;
use SparkPlug;
class PluginManager
{
private $plugins = [];
public function registerPlugin(string $name, SparkPlug $plugin)
{
$this->plugins[$name] = $plugin;
}
public function initializePlugins()
{
foreach ($this->plugins as $plugin) {
$plugin->load();
}
}
public function getPlugin(string $name)
{
return $this->plugins[$name];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Spark\Resource;
use ReflectionClass;
class ResourceManager
{
private $resourceTypes = [];
private $namedResources = [];
public function registerResourceType(string $name, string $type)
{
$refl = new ReflectionClass($type);
$this->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;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Spark\Resource;
abstract class ResourceType
{
abstract public function info();
}

73
src/SparkApplication.php Normal file
View File

@ -0,0 +1,73 @@
<?php
namespace Spark;
use Psr\Log\LoggerInterface;
use Spark\Environment\Environment;
use Spark\Plugin\PluginManager;
use Spark\Resource\ResourceManager;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Logger\ConsoleLogger;
use Symfony\Component\Console\Output\OutputInterface;
class SparkApplication extends Application
{
public static SparkApplication $instance;
private Environment $environment;
private ResourceManager $resourceManager;
private PluginManager $pluginManager;
private LoggerInterface $logger;
public function __construct()
{
parent::__construct("Spark", "dev");
self::$instance = $this;
$this->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);
}
}