marwanalsoltany / blend
A versatile and lightweight PHP task runner, designed with simplicity in mind.
Fund package maintenance!
Ko-Fi
Requires
- php: ^7.4|^8.0
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^9.5
README
A versatile and lightweight PHP task runner, designed with simplicity in mind.
If you like this project and would like to support its development, giving it a ⭐ would be appreciated!
Key Features
- Blazing fast
- Easy to configure
- Dependency free
About Blend
Blend is a versatile and lightweight PHP task runner, designed with simplicity in mind. Blend was created out of frustration of trying to manage multiple CLI Tools and/or Scripts with different interfaces and trying to remember their names, locations, syntax, and options/arguments. With Blend and its intuitive and powerful API you will have to do this only once, it will, well, as the name suggests blend it for you. Blend will present those CLI Tools and/or Scripts the way you like and give you the possibility to access all of them in the form of tasks from a single, beautiful, and insightful interface that will assist you, give you feedback, and suggestions throughout the process.
To keep it as portable and as simple as it can be, the Blend package consists of a single class (TaskRunner
), this class does all the magic for you.
Blend was created to be flexible, it can be installed in many ways, each way has its benefits and drawbacks. Choose the installation method that suits you and your needs best. Check out the installation section for more details.
Why does Blend exist?
Blend was developed for personal use in the first place. However, it has come so far that it deserves to be published. It may not be what you are looking for, so check it out carefully.
Installation
Using Composer:
Require Blend through Composer using:
composer require marwanalsoltany/blend
This is the recommended way to install Blend. With this installation method, Blend will be installed just like any normal Composer package. You can interact with it using either the vendor/bin/blend
executable with a config file in the current working directory, or by requiring it in a stand-alone file and supplying the config programmatically. You can of course install it globally and let it be system-wide accessible.
Using PHAR:
Download Blend PHAR archive form the releases page or using one of the commands down below:
php -r "copy('https://git.io/JEseO', 'blend');"
php -r "copy('https://github.com/MarwanAlsoltany/blend/releases/latest/download/blend.phar', 'blend');"
Or use the following command to download a specific version (replace vX.X.X
with the wished version):
php -r "copy('https://github.com/MarwanAlsoltany/blend/releases/download/vX.X.X/blend.phar', 'blend');"
With this installation method, you will get Blend as a portable PHAR archive, you can place it anywhere you want or even include it in your PATH
for easy access. With this installation method, you have to supply a config file in order to configure/customize Blend. This installation method exists for portability where Blend is not bound to a specific project and a config file is sufficient. Starting from v1.0.3
, the PHAR installation method is distinguished from other methods with the task update
(used to be called before phar:update
v1.1.0
) that will update your PHAR to the latest release available in this repository.
Using Installer:
Download Blend Setup directly from the repository, or using one of the commands down below:
php -r "copy('https://git.io/JEseR', 'setup');" && php setup
php -r "copy('https://raw.githubusercontent.com/MarwanAlsoltany/blend/master/bin/setup', 'setup');" && php setup
Using this method, the Blend executable and source will be installed in the current working directory (to take advantage of IDEs Intellisense when configuring Blend programmatically). With this installation method, you can configure Blend programmatically using the blend
executable file or by supplying a config file in CWD. This installation method exists merely for legacy projects, where Composer is not an option and programmable config is required.
Config
Blend can be configured using either of the two available config formats:
PHP Config blend.config.php
:
<?php return [ 'autoload' => null, 'merge' => true, 'executables' => [ 'php' => [ './bin/*', ], ], 'translations' => [ 'abc' => 'xyz', ], 'ansi' => true, 'quiet' => false, 'tasks' => [ 'some:task' => [ 'name' => 'some:task', 'description' => 'Some task', 'executor' => 'shell', 'executable' => 'ls', 'arguments' => '-lash', 'hidden' => false, 'disabled' => false, ], ], ];
JSON Config blend.config.json
: (Recommended)
{ "$schema": "https://raw.githubusercontent.com/MarwanAlsoltany/blend/master/config/schema.json", "autoload": null, "merge": true, "executables": { "php": [ "./bin/*" ] }, "translations": { "abc": "xyz" }, "ansi": true, "quiet": false, "tasks": { "some:task": { "name": "some:task", "description": "Some task", "executor": "shell", "executable": "ls", "arguments": "-lash", "hidden": false, "disabled": false } } }
Note: Refer to config/blend.config.php
and config/schema.json
to learn more about the expected data types. Note that JSON config has some limitations (callback tasks for example) so check out to both files.
How Does Config Loading Work?
Blend will try to load the config from the current working directory, if nothing is to be found there, it will go one level upwards and look in the parent directory and so on until it reaches the root directory, if it does not find anything there either, Blend will start without config.
Fact: Although JSON config format is recommended, PHP config has precedence. This means, if the two config formats are to be found in the same directory, the PHP config will get loaded instead of the JSON one. This is merely because the PHP config can be executed and is, therefore, more powerful.
Examples
A basic Blend executable:
#!/usr/bin/env php <?php use MAKS\Blend\TaskRunner as Blend; $blend = new Blend(); $blend->start();
A more advanced Blend executable:
#!/usr/bin/env php <?php use MAKS\Blend\TaskRunner as Blend; $blend = new Blend([ // files in "./php/bin" will be loaded as tasks and get executed using PHP 'php' => [ './php/bin/*', ], // files in "./js/bin" with the JS extension will be loaded as tasks and get executed using Node 'node' => [ './js/bin/*.js', ], ]); // available arguments: $executables, $translations, $config, $ansi, $quiet $blend->setName('My Task Runner'); $blend->setVersion('vX.X.X'); // NOTE: these tasks are for demonstration purposes only // adding a shell task $blend->addShellTask('ls', 'Lists content of CWD or the passed one.', 'ls', '-lash'); // adding a callback task $blend->addCallbackTask('whoami', null, function () { /** @var Blend $this */ $this->say('@task'); // using the @task placeholder to get a string representation of the task object }); $blend->disableTask('whoami'); // preventing the task from being ran $blend->hideTask('whoami'); // preventing the task from being listed // alternatively, you can use the makeTask() method to pass all arguments at once $blend->makeTask([ 'name' => 'runner', 'description' => null, 'executor' => Blend::CALLBACK_TASK, 'executable' => static function ($runner) { // functions that can't be bound, get the runner as the first argument $runner->say('@runner'); }, 'arguments' => null, 'hidden' => true, 'disabled' => true, ]); // extending the task runner with an additional method // NOTE: this implementation is for demonstration purposes only $blend->extend('pipe', function (string ...$tasks) { $results = []; static $previous = null; foreach ($tasks as $task) { $current = $this->getTask($task); // skip if task does not exist if (!$current) { continue; } // pass the result as an argument if the next task is a callback task if ($current->executor === Blend::CALLBACK_TASK) { $current->arguments[] = $previous; $previous = $results[$task] = $this->runTask($task); continue; } // if not a callback task, then it is a shell task (CLI command) // execute the task and cache the result for the next task $this->runTask($task); // with shell tasks, we're interested in the whole result of the task (command) // and not only its exit code, that's why we're using getExecResults() method instead $previous = $results[$task] = $this->getExecResult(); } return $results; }); // now you can use the newly created method to pipe tasks together $blend->addCallbackTask('piped:tasks:run', 'Executes piped tasks.', function () { /** @var Blend $this */ $this->say('Running piped tasks ...'); $this->pipe( 'example:task:1', 'example:task:2', 'example:task:3' // ... ); $this->say('Finished piping!'); }); $blend->sort(); $blend->start();
A real life example of a Blend executable (PHP Development Server):
#!/usr/bin/env php <?php use MAKS\Blend\TaskRunner as Blend; $blend = new Blend(); $cwd = getcwd(); $blend->addCallbackTask( 'server:start', 'Starts a PHP Development Server in CWD', function ($cwd) { /** @var Blend $this */ if (file_exists("{$cwd}/.pid.server")) { $this->say('An already started PHP Development Server has been found.'); return Blend::FAILURE; } $pid = $this->exec("php -S localhost:8000 -t {$cwd}", true); // passing true runs the command asynchronously // you can use $this->getExecResult() method to get all additional info about the executed command. $this->say("Started a PHP Development Server in the background with PID: [{$pid}]"); file_put_contents("{$cwd}/.pid.server", $pid); return Blend::SUCCESS; }, [$cwd] // passing arguments to tasks callback ); $blend->addCallbackTask( 'server:stop', 'Stops a started PHP Development Server in CWD', function ($cwd) { /** @var Blend $this */ if (!file_exists("{$cwd}/.pid.server")) { $this->say('No started PHP Development Server has been found.'); return Blend::FAILURE; } $pid = trim(file_get_contents("{$cwd}/.pid.server")); $this->exec(PHP_OS === 'WINNT' ? "tskill {$pid}" : "kill -15 {$pid}"); $this->say("Stopped PHP Development Server with PID: [{$pid}]"); unlink("{$cwd}/.pid.server"); return Blend::SUCCESS; }, [$cwd] ); $blend->addCallbackTask( 'server:restart', 'Restarts the started PHP Development Server in CWD', function () { /** @var Blend $this */ $this->say('Restarting the PHP Development Server'); $this ->setQuiet(true) // disable output temporarily ->run('server:stop') ->run('server:start') ->setQuiet(false); // enable output again // use the runTask() method instead to get the return value of the called task // return $this->runTask('server:stop') & $this->runTask('server:start'); } ); $blend->addCallbackTask( 'server:cleanup', 'Removes ".pid.server" file from CWD if available', function ($cwd) { /** @var Blend $this */ if (file_exists($file = "{$cwd}/.pid.server")) { if (unlink($file)) { $this->say('Removed ".pid.server" file successfully.'); } else { $this->say('Failed to remove ".pid.server" file!'); return Blend::FAILURE; } } else { $this->say('Nothing to clean up!'); } return Blend::SUCCESS; }, [$cwd] ); $blend->start();
Note: Blend gets its ID from the executable name that contains it ($argv[0]
). So if you were to rename the file that contains it to something else, all Blend output will reflect this new change (help message, suggestions, etc...). The environment variable and the config file name will also be expected to match the new name.
Hint: The TaskRunner
class is well documented, if you have any questions about Blend API, refer to the DocBlocks of its methods, you will probably find your answer there.
API
Here is the full API of Blend (TaskRunner
class).
Note: The full API of the TaskRunner::class
—including private and protected members— is listed here as you mostly want to extend Blend by using the TaskRunner::extend()
method which has access to the private scope.
Constants
Properties
Public Methods
Protected Methods
Private Methods
Magic Methods
License
Blend is an open-source project licensed under the MIT license.
Copyright (c) 2021 Marwan Al-Soltany. All rights reserved.