amercier/php-cli-helpers

View on GitHub
src/Cli/Helpers/Script.php

Summary

Maintainability
A
1 hr
Test Coverage
B
86%
<?php

namespace Cli\Helpers;

use \Closure;

/**
 * Cli\Helpers\Script
 * ==================
 *
 * Utility class to create scripts.
 *
 * Example
 * -------
 *
 * Let's create a script hello.php
 *
 * ```php
 * #!/usr/bin/env php
 * <?php
 *
 * require_once 'vendor/autoload.php';
 *
 * $script = new Cli\Helpers\Script();
 * $script
 *     ->setName('Hello')
 *     ->setVersion('1.0')
 *     ->setDescription('Say hello to the world or to a particular person')
 *     ->setCopyright('Copyright (c) Orange ECV 2013')
 *     ->addParameter(new Parameter('n', 'name'   , 'World'                  ), 'Set the name of the person to greet')
 *     ->addParameter(new Parameter('V', 'verbose', Parameter::VALUE_NO_VALUE), 'Increase verbosity')
 *     ->setProgram(function($parameters) {
 *         echo 'Hello, ' . $parameters['name'];
 *         if ($parameters['verbose']) {
 *             echo ' Nice to see you again :)';
 *         }
 *         echo "\n";
 *     })
 *     ->start();
 * ```
 *
 * $ hello-world.php
 * Hello, World!
 *
 * $ hello-world.php -n Alex
 * Hello, Alex!
 *
 * $ hello-world.php --verbose
 * Hello, World! Nice to see you again :)
 *
 * $ hello-world.php --version
 * Hello v1.0
 * Copyright (c) Alexandre Mercier 2014
 *
 * $ hello-world.php --help
 * Usage: hello-world.php [OPTIONS]
 *
 * Say hello to the world or to a particular person.
 *
 *   -n, --name NAME       Set the name of the person to greet
 *   -V, --verbose         Increase verbosity
 *   -h, --help            Display this help and exit.
 *   -v, --version         Output version information and exit.
 *
 * Hello
 * Copyright (c) Orange ECV 2013
 */
class Script
{
    public $name;
    public $version;
    public $description;
    public $copyright;
    public $parameters = array();
    public $parameterDescriptions = array();

    protected $parameterCallbacks = array();
    protected $program;
    protected $exceptionCatchingEnabled = true;

    public function __construct()
    {
    }

    protected function checkProperties()
    {
        if (!isset($this->name)) {
            throw new Exception\MissingScriptParameter('name');
        }
        if (!isset($this->version)) {
            throw new Exception\MissingScriptParameter('version');
        }
        if (!isset($this->description)) {
            throw new Exception\MissingScriptParameter('description');
        }
        if (!isset($this->program)) {
            throw new Exception\MissingScriptParameter('program');
        }
    }

    protected function initParameters($arguments)
    {
        if (!$this->exceptionCatchingEnabled) {
            return Parameter::getFromCommandLine($this->parameters, $arguments);
        }

        try {
            return Parameter::getFromCommandLine($this->parameters, $arguments);
        } catch (\Exception $e) {
            fwrite(STDERR, $e->getMessage() . "\n");
            exit(1);
        }
    }

    protected function processParameters($arguments)
    {
        // Get parameter values without throwing exceptions in case of missing
        // required parameter (ex: my-sctipt -h misses -u)
        $options = array();
        $this->parameters = array_reverse($this->parameters, true);
        foreach ($this->parameters as $id => $parameter) {
            try {
                $value = Parameter::getFromCommandLine(array($id => $parameter), $arguments);
                $options[$id] = $value[$id];
            } catch (Exception\MissingRequiredParameter $e) {
                $options[$id] = null;
            }
        }

        foreach ($this->parameterCallbacks as $id => $callback) {
            if ($options[$id] === true) {
                if ($callback($options, $arguments, $this) === false) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Run the program defined with #setProgram().
     *
     * If an exception is thrown during execution of the program, the error
     * message is displayed on STDERR, and the script exits with return code 0.
     * To prevent catching the exception, use #setExceptionCatchingEnabled(false).
     *
     * @param  array  $arguments  The original arguments given to the script
     * @return mixed              Returns whatever the program returns
     */
    protected function run($arguments)
    {
        $program = $this->program;
        return $program($this->initParameters($arguments), $arguments, $this);
    }

    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }

    public function setVersion($version)
    {
        $this->version = $version;
        return $this;
    }

    public function setDescription($description)
    {
        $this->description = $description;
        return $this;
    }

    public function setCopyright($copyright)
    {
        $this->copyright = $copyright;
        return $this;
    }

    public function setProgram($program)
    {
        $this->program = $program;
        return $this;
    }

    public function setExceptionCatchingEnabled($exceptionCatchingEnabled)
    {
        $this->exceptionCatchingEnabled = $exceptionCatchingEnabled;
        return $this;
    }

    public function addParameter($parameter, $description, $callback = null)
    {

        // Check whether a parameter with the same switch already exists
        foreach ($this->parameters as $p) {
            if ($p->getShort() === $parameter->getShort()) {
                throw new Exception\DuplicateScriptParameter($p->getShortSwitch(), $p);
            }
            if ($p->getLong() === $parameter->getLong()) {
                throw new Exception\DuplicateScriptParameter($p->getLongSwitch(), $p);
            }
        }

        $id = $parameter->getLong();
        $this->parameters[$id] = $parameter;
        $this->parameterDescriptions[$id] = $description;
        if ($callback) {
            if ($parameter->getDefaultValue() !== Parameter::VALUE_NO_VALUE) {
                throw new Exception\InvalidScriptParameter($parameter);
            }
            $this->parameterCallbacks[$id] = $callback;
        }

        return $this;
    }

    /**
     * Starts the program using either the script arguments, or custom ones.
     *
     * @param  array  $arguments  Arguments to pass to the script (optional).
     *                            When null, the script uses the command-line
     *                            arguments (global $argv).
     * @return mixed              Returns whatever the program returns
     */
    public function start($arguments = null)
    {

        $this->checkProperties();

        global $argv;
        $arguments = $arguments === null ? $argv : $arguments;

        try {
            $continue = $this->processParameters($arguments);
            if (!$continue) {
                return;
            }

            return $this->run($arguments);
        } catch (Exception $e) {
            if ($this->exceptionCatchingEnabled) {
                fwrite(STDERR, $e->getMessage() . "\n");
                exit(1);
            } else {
                throw $e;
            }
        }
    }
}