Calling Silex backend from command line. Creating SAAS command line tools

Sometimes we need to create command line tools. We can build those tools using different technologies. In Symfony world there’s Symfony Console. I feel very confortable using it. But if we want to distribute our tool we will need to face with one “problem”. User’ll need to have PHP installed. It sounds trivial but it isn’t installed in every computer. We can use nodeJs to build our tool. Nowadays nodeJs is a de-facto standard but we still have the problem. Another “problem” is how to distribute new version of our tool. Problems everywhere.

Software as a service tools are great. We can build a service (a web based service for example) and we can even monetize our service with one kind of paid-plan or another. With our SAAS we don’t need to worry about redistribute our software within each release. But, what happens when our service is a command line one?

Imagine for example that we’re going to build one service to convert text to uppercase (I thing this idea will become me rich, indeed 🙂

We can create one simple Silex example to convert to upper case strings:

<?php
include __DIR__ . "/../vendor/autoload.php";

use Silex\Application;
use Symfony\Component\HttpFoundation\Request;

$app = new Application();
$app->post("/", function (Request $request) {
    return strtoupper($request->getContent());
});
$app->run();

And now we only to call this service from the command line. We can use curl for example and convert one file content to upper case:

cat myfile.txt | curl -d @- localhost:8080 > MYFILE.txt

You can see the example in my github account here

Advertisement

Building a small microframework with PHP (Part 2). Command line interface

In my last post we spoke about building a small microframework with PHP. The main goal of this kind of framework was to be able to map urls to plain PHP classes and become those classes easily testeable with PHPUnit. Now we’re going to take a step forward. Sometimes we need to execute the script from command line, for example if we need to use them in a crontab file. OK. We can use curl and perform a simple http call to the webserver. But it’s pretty straightforward to create a command line interface (CLI) for our microframework. Zend Framework and Symfony has a great console tools. I’ve used Zend_Console_Getopt in a project and is really easy to use and well documented. But now we’re going to build a command line interface from scratch.

We are going to reuse a lot of code from the earlier post, because of that we are going to encapsulate the code in a class (DRY).

We will use the getopt function from PHP’s function set.

$sortOptions = "";
$sortOptions .= "c:";
$sortOptions .= "f:";
$sortOptions .= "v::";
$sortOptions .= "h::";

$options = getopt($sortOptions);

Then we need to take the paramaters from $GLOBALS[‘argv’] superglobal according with the options. I use the following hack to prepare $GLOBALS[‘argv’]:

foreach( $options_array as $o => $a ) {
    while($k=array_search("-". $o. $a, $GLOBALS['argv'])) {
        if($k) {
            unset($GLOBALS['argv'][$k]);
        }
    }
    while($k=array_search("-" . $o, $GLOBALS['argv'])) {
        if($k) {
            unset($GLOBALS['argv'][$k]);
            unset($GLOBALS['argv'][$k+1]);
        }
    }
}
$GLOBALS['argv'] = array_merge($GLOBALS['argv']);

And now we get the params into $param_arr array.

$param_arr = array();
$lenght = count((array)$GLOBALS['argv']);
if ($lenght > 0) {
    for ($i = 1; $i < $lenght; $i++) {
        if (isset($GLOBALS['argv'][$i])) {
            list($paramName, $paramValue) = explode("=", $GLOBALS['argv'][$i], 2);
            $param_arr[$paramName] = $paramValue;
        }
    }
}

Now we can get className and functionName:

$className    = !array_key_exists('c', $options) ?: $options['c'];
$functionName = !array_key_exists('f', $options) ?: $options['f'];

We add a “usage” parameter:

if (array_key_exists('h', $options)) {
     $usage = <<<USAGE
Usage: cli [options] [-c] <class> [-f] <function> [args...]

Options:
-h Print this help
-v verbose mode
\n
USAGE;
    echo $usage;
    exit;
}

Now we can invoke the function with call_user_func_array

return call_user_func_array(array($className, $functionName), $this->realParams);

As you can see, instead of using $param_arr as parameters array, We need to create an extra $realParams array. The aim of this realParams arrays is to use call_user_func_array with named parameters. In getRealParams function we use reflection to see what are the real parameters of our function and use only those parameters form in the correct order instead. With this trick we will allow to the user to use the parameters in the his desired order, and without forcing to use the real order of our function.

    private function getRealParams($params)
    {
        $realParams = array();
        $class = new ReflectionClass(new $this->className);
        $reflect = $class->getMethod($this->functionName);

        foreach ($reflect->getParameters() as $i => $param) {
            $pname = $param->getName();
            if ($param->isPassedByReference()) {
                /// @todo shall we raise some warning?
            }
            if (array_key_exists($pname, $params)) {
                $realParams[] = $params[$pname];
            } else if ($param->isDefaultValueAvailable()) {
                $realParams[] = $param->getDefaultValue();
            } else {
                throw new Exception("{$this->className}::{$this->functionName}() param: missing param: {$pname}");
            }
        }
        return $realParams;
    }

And now we can use our microframework from the command line:

./cli -c 'Demo\Foo' -f hello
Hello

./cli -c Demo\\Foo -f getUsers
["Gonzalo","Peter"]

./cli -c Demo\\Foo -f helloName name=Gonzalo surname=Ayuso
Hello Gonzalo Ayuso

Full code on github