Search code examples
phpwindowscomposer-phpsymfony-process

Use Composer to run interactive script


I've written a command using Symfony Process that asks for a password:

$validPassword = false;
do {
    $question = new Question('Enter password: ');
    $question->setHidden(true);
    $question->setHiddenFallback(false);
    $password = $questionHelper->ask($input, $output, $question);
    if ($password === null || trim($password) === '') {
        continue;
    }

    try {
        doSomeTaskThatRequiresThePassword($password);
        $validPassword = true;
    } catch (\RuntimeException $e) {
        $output->writeln(
            sprintf(
                '<error>Error: %s</error>',
                $e->getMessage()
            )
        );
    }
} while (!$validPassword);

Command is invoked from a standalone file:

#!/usr/bin/env php
<?php
require __DIR__ . '/vendor/autoload.php';

use App\MyCommand;

(new MyCommand())->run();

Additionally, since I'm using Windows, I have a small wrapper:

@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0my-command
php "%BIN_TARGET%" %*

All of the above works like a charm.

I use Composer to run all the project utilities and custom commands, so I tried the same for this one:

{
    "scripts": {
        "lint": [
            "@phpcs"
        ],
        "phpcs": "phpcs",
        "phpcbf": "phpcbf",
        "my-command": "my-command with some parameters --verbose --blah blah"
    }
}

The problem is that execution is not halted at $questionHelper->ask(). I just enter an infinite loop as the do/while progresses with an empty password. I doesn't make any difference to use PowerShell or Command Prompt, or if I skip the *.bat wrapper and define the command as "php my-command".

C:\Projects\Foo>composer my-command
> php my-command with some parameters --verbose --blah blah
Enter password:
Enter password:
Enter password:
Enter password:
Enter password:
[…]

Is this an inherent limitation of Composer or there's something I need to tweak to run interactive commands?


Solution

  • Composer appears to support interactive scripts only on Linux (since Composer/2.7.9), because it needs TTY mode, which isn't supported by the Win32 APIs that PHP implements.

    However, it provides first class support for Symfony processes:

    As of Composer 2.5 scripts can also be Symfony Console Command classes, which allows you to easily run them including passing options. This is however not recommended for handling events.

    So you can drop the wrapper script altogether and invoke the command class directly:

    {
        "scripts": {
            "my-command": "\\App\\MyCommand"
        }
    }
    

    Unfortunately, this won't allow additional arguments, except if provided manually every time:

    PS C:\> composer my-command with some parameters --verbose --blah blah
    

    Surprisingly, you can predefine additional parameters if you invoke the class indirectly, so it isn't really that defining arguments isn't supported, just that nobody thought of that particular syntax:

    {
        "scripts": {
            "my-command-class": "\\App\\MyCommand",
            "my-command": "@my-command-class with some parameters --verbose --blah blah"
        }
    }
    
    PS C:\> composer my-command