building-for-php-cli



building-for-php-cli

0 3


building-for-php-cli

Slides for "Building for the PHP Command Line Interface"

On Github stevegrunwell / building-for-php-cli

Building for the PHP Command Line Interface

Steve Grunwell@stevegrunwell

stevegrunwell.com/slides/php-cli

Who am I?

  • Senior Web Engineer @ 10up
  • Open-source contributor
  • Husband + (new) father
  • Coffee roaster

Why the CLI?

Benefits

  • No reliance on the browser
    • Publicly-discoverable interfaces
    • Timeouts
  • Script using the same libraries as your application
  • PHP developers don't have to learn Bash
    #!/usr/bin/env php
    <?php
      // Do stuff
    

How does it run?

Two ways to invoke PHP on the CLI:

# 1. Pass the file to the php binary
$ php my-command.php
# 2. Execute the file directly if using the php shebang.
$ chmod +x my-command.php
$ ./my-command.php

When might I use them?

  • Data migrations and transformations
  • Maintenance scripts
  • Actions that should only be available via a console
    • Scaffolding
    • Other code changes
  • "#YOLO scripts"

Best Practices

Composability

Good CLI commands should be composable!

Rule of Composition

Developers should write programs that can communicate easily with other programs. This rule aims to allow developers to break down projects into small, simple programs rather than overly complex monolithic programs.

Eric S. Raymond, The Art of Unix Programming

Don't assume anything!

  • Your script could be run on a wide variety of systems
  • Check that system commands work before calling them

Know the current Server API

Scripts can use php_sapi_name() (or the PHP_SAPI constant) to determine the Server API being used.

// Make sure this script is being run over the PHP CLI!
if ('cli' !== php_sapi_name()) {
    return;
}

Rule of Silence

Developers should design programs so that they do not print unnecessary output. This rule aims to allow other programs and developers to pick out the information they need from a program's output without having to parse verbosity.

Eric S. Raymond, The Art of Unix Programming

Garbage Collection

  • Unset objects when they're no longer necessary
  • Be judicious with caching
  • Watch the size of arrays that are constantly growing

In the Wild

Drush

drush.org

  • Short for "Drupal Shell"
  • One of the OG CLI tools for PHP CMSs
  • Manage themes, modules, system updates, etc.

WP-CLI

wp-cli.org

  • Install core, themes, plugins, etc.
  • Manage posts, terms, users, and more
  • Inspect and maintain cron, caches, and transients
  • Extensible for themes + plugins

Artisan

laravel.com

  • The underlying CLI for Laravel
  • Scaffold new models, views, controllers, seeds, and more
  • Built atop the Symfony Console
  • Enables third-party packages to register new commands

Joomla Console

joomlatools.com/developer/tools/console

  • CLI framework for Joomla!
  • Manage Sites, Extensions, Databases, and Apache vhosts
  • List, purge, and clear cache contents

Writing your own scripts

Symfony Console Component

  • Robust library for writing PHP CLI scripts
  • Helpers for all sorts of input and output options
  • Designed to be easily tested
  • Integrates with other Symfony components (naturally)

symfony.com/doc/current/components/console

PHP-CLI Tools

  • Functions to handle input and output, including progress indicators
  • Tabular and Tree displays for output
  • Maintained by the WP-CLI team

github.com/wp-cli/php-cli-tools

$argv

  • Retrieves the arguments passed to the PHP script as an array.
  • The script name will be included, so this array should never be empty.

$argc

  • Returns the number of arguments passed to a script.
  • The script name will be included, so this number should always be >= 1

Using $argc and $argv

# ArgExample.php
printf('There were %d arguments passed to PHP:' . PHP_EOL, $argc);
print_r($argv);
$ php ArgExample.php abc 123
There were 3 arguments passed to PHP:
Array
(
  [0] => ArgExample.php
  [1] => abc
  [2] => 123
)

System commands

  • PHP can natively do a lot of common Unix operations: copy, chmod, chown, etc.
  • Can also execute arbitrary system operations!

Executing System Commands

exec() Returns the last line of output as a string. shell_exec() returns full output as a string. Can also be represented using the backtick operator:
`ls -al` === shell_exec('ls -al')
If you require multiple lines of output, use shell_exec()

(Even More) System Commands

system() Flush the output buffer after each line passthru() Returns the raw output. Great for working with binary files or interactive output If you require multiple lines of output, use shell_exec()

Escaping shell commands

escapeshellcmd()

Escapes any meta-characters that could be used to execute arbitrary commands

escapeshellarg()

$name = 'steve && rm -rf /';

# Oh no, $name isn't being escaped!
exec('greet-user ' . $name);
> Hello, steve # proceeded by your system being destroyed

$name = 'steve && rm -rf /';

# Nice try, user!
exec('greet-user ' . escapeshellarg($name));
> Hello, steve && rm -rf / # What an odd name!

Environment Variables

$name = getenv('DEMO_NAME');

if ($name) {
    printf('Hey, I recognize you, %s!' . PHP_EOL, $name);
    $name = sprintf('My old friend, %s!', $name);

} else {
    echo "I don't know you, so I'll just call you Fred." . PHP_EOL;
    $name = 'Fred';
}

// Update DEMO_NAME and call the system's echo program.
putenv('DEMO_NAME=' . $name);
passthru('echo $DEMO_NAME');

Environment Variables

export DEMO_NAME="Steve"

> Hey, I recognize you, Steve!
> My old friend, Steve!

Otherwise:

> I don't know you, so I'll just call you Fred.
> Fred

Exit codes

  • The way we exit scripts is significant:
    • 0 = successful
    • 1 = error
  • Additional values between 2-255 have special meanings in *nix
  • If no exit status is provided, scripts will exit with the status of the last command run

Exit codes

if (! isset($argv['1'])) {
    echo "Missing required argument!";
    exit(1);
}

// Do something awesome
$ php my-script.php foo && echo "Success"
$ php my-script.php && echo "You will never see this"

Daemons

A process that continually runs in the background

while ( $run ) {
    // do something!
}

bit.ly/1S0RwGw

Examples

github.com/stevegrunwell/php-cli-examples

Symfony Console

<?php
use Symfony\Component\Console\Command\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;

class SymfonyExample extends Command
{
    // ...

Symfony Console

/**
 * Define what the command should be called and what arguments it
 * should accept.
 */
protected function configure()
{
    $this
        ->setName('symfony-example')
        ->setDescription('Greet a user by name.')
        ->addArgument(
            'name',
            InputArgument::REQUIRED,
            'The name of the user'
        );
}

Symfony Console

/**
 * Execute the command.
 *
 * @param InputInterface  $input  The input interface.
 * @param OutputInterface $output The output interface.
 */
protected function execute($input, $output)
{
    $output->writeln(sprintf(
        '<comment>Symfony says "hello", %s!</comment>',
        $input->getArgument('name')
    ));
}

Symfony Console

$ php examples/SymfonyExample.php symfony-example Steve
> Symfony says "hello", Steve!

PHP-CLI Tools

#!/usr/bin/env php
<?php

// Require dependencies.
require_once __DIR__ . '/../vendor/autoload.php';

$limit  = cli\prompt('How high should I count?', 10);
$loud   = cli\choose('Shall I shout it');
$suffix = 'y' === $loud ? '!' : '.';

for ($i = 1; $i <= $limit; $i++) {
    cli\line($i . $suffix);
}

PHP-CLI Tools

$ php examples/PHPCliToolsExample.php
$ php examples/PHPCliToolsExample.php
How high should I count? [10]: 5
Shall I shout it? [y/N]y
1!
2!
3!
4!
5!

WP-CLI: Register Command

/**
 * Example WP-CLI script.
 */
class Example_WP_CLI_Command extends WP_CLI_Command {
    // At least one public method.
}

WP_CLI::add_command( 'example-command', 'Example_WP_CLI_Command' );

WP-CLI: Command Definition

/**
 * Display the latest posts from a given user.
 *
 * ## OPTIONS
 *
 * <login>
 * : The user login to collect stats for.
 *
 * @subcommand latest-posts-by-user
 */
public function latest_posts_by_user( $args, $assoc_args ) {
    // Do something!
}

WP-CLI: Command Method

public function latest_posts_by_user( $args, $assoc_args ) {
    $user = get_user_by( 'login', $args['0'] );
    if ( ! $user ) {
        return WP_CLI::error(
            'The specified user login does not exist!'
        );
    }

    $posts  = get_posts( array( 'author' => $user->ID ) );
    $fields = array( 'ID', 'post_title', 'post_date' );

    return WP_CLI\Utils\format_items( 'table', $posts, $fields );
}

WP-CLI: In Action

$ wp example-command latest-posts-by-user admin
+-----+---------------+---------------------+
| ID  | post_title    | post_date           |
+-----+---------------+---------------------+
| 7   | A third post  | 2016-04-08 14:32:00 |
| 5   | Another post  | 2016-04-05 18:32:23 |
| 1   | Hello World!  | 2015-11-12 01:14:38 |
+-----+---------------+---------------------+

Thank You!

Steve Grunwellstevegrunwell.com10up.comnow hiring!

stevegrunwell.com/slides/php-cli joind.in/talk/ce9a4