Call PHP-CLI script without Shell from PHP - php

I want to run some vendor-scripts of Composer from my PHP-script.
Calling each command takes long although the command itself finishes quick. I assume creating a new shell by shell_exec() takes some time.
I wanted to call the PHP-scripts directly via the require keyword but changing the global $argv to contain the parameters for a script does not apply to the called script. Is $argv implicitly immutable across script-files or do I have another error in my way of thinking?
Here is some sample code (should be executed via CLI, not tested):
namespace Foo;
class Bar
{
public static function call_cs_fixer()
{
$GLOBALS['argv'] = [
'/path/to/vendor/bin/php-cs-fixer',
'fix',
'--config',
'"/path/to/.php-cs-fixer.php"',
'"/path/to/project"',
];
return require $GLOBALS['argv'][0];
}
}
echo \Foo\Bar::call_cs_fixer();

Is $argv implicitly immutable across script-files or do I have another error in my way of thinking?
That depends on how the script/utility works that you try to invoke. Which means you can't expect it to work stable and I would refrain from it unless you know it has this interface. As you don't know it - otherwise you would not ask the question that way - throw that idea into the bin in this case.
I assume creating a new shell by shell_exec() takes some time.
This may be (we can't look into your system configuration), but if it is a linux system this is very likely not the case.
In practice, the use of a new shell sub-process to invoke the tooling is the much, much better way to do things here. This is also how composer(1) invokes scripts (see Scripts) - unless they are bound as (static) methods - and is always true for the composer exec command.
The reason is that you can control not only the command line arguments much better but also the working directory and the environment parameters (a.k.a. environment variables or environment in short), compare proc_open(php). The standard streams are available as well.
As you're running in context of composer, and if you have access to the sources of it (e.g. you bind a composer script or hook in your composer.json configuration), you can use the process components that ship with composer itself (its all PHP), it has quite some utility in there.
If you just want to start lightly, I found the passthru(php) function a good fit for quickly getting started:
// the command you'd like to execute
$command = '/path/to/vendor/bin/php-cs-fixer';
$args = [
'fix',
'--config',
'/path/to/.php-cs-fixer.php',
'/path/to/project'
];
// build the command-line
$commandLine = sprintf(
'%s %s',
$command,
implode(' ', array_map('escapeshellarg', $args))
);
// execute
$result = passthru($commandLine, $exitStatus);
// be verbose and give some debug info
fprintf(
STDERR,
"debug: command %s exited with status %d\n",
$commandLine,
$exitStatus
);
// throw on exit status != 0, a convention only but you often want this
if (false === $result || $existStatus !== 0) {
throw new \RuntimeException(sprintf(
'command "%s" exited with non-zero status %d (result=%s).\n',
addcslashes($commandLine, "\0..\37\"\\\177..\377"),
$exitStatus,
var_export($result, true)
), (int)$exitStatus);
}

Related

CakePHP 3 - executing multiple Commands from 1 Command and logging errors if they occur

I'm building an application in CakePHP 3.8 which uses Console Commands to execute several processes.
These processes are quite resource intensive so I've written them with Commands because they would easily time-out if executed in a browser.
There are 5 different scripts that do different tasks: src/Command/Stage1Command.php,
... src/Command/Stage5Command.php.
The scripts are being executed in order (Stage 1 ... Stage 5) manually, i.e. src/Command/Stage1Command.php is executed with:
$ php bin/cake.php stage1
All 5 commands accept one parameter - an ID - and then perform some work. This has been set up as follows (the code in buildOptionsParser() exists in each command):
class Stage1Command extends Command
{
protected function buildOptionParser(ConsoleOptionParser $parser)
{
$parser->addArgument('filter_id', [
'help' => 'Filter ID must be passed as an argument',
'required' => true
]);
return $parser;
}
}
So I can execute "Stage 1" as follows, assuming 428 is the ID I want to pass.
$ php bin/cake.php stage1 428
Instead of executing these manually, I want to achieve the following:
Create a new Command which loops through a set of Filter ID's and then calls each of the 5 commands, passing the ID.
Update a table to show the outcome (success, error) of each command.
For (1) I have created src/Command/RunAllCommand.php and then used a loop on my table of Filters to generate the IDs, and then execute the 5 commands, passing the ID. The script looks like this:
namespace App\Command;
use Cake\ORM\TableRegistry;
// ...
class RunAllCommand extends Command
{
public function execute(Arguments $args, ConsoleIo $io)
{
$FiltersTable = TableRegistry::getTableLocator()->get('Filters');
$all_filters = $FiltersTable->find()->toArray();
foreach ($all_filters as $k => $filter) {
$io->out($filter['id']);
// execute Stage1Command.php
$command = new Stage1Command(['filter_id' => $filter['id']]);
$this->executeCommand($command);
// ...
// execute Stage5Command.php
$command5 = new Stage5Command(['filter_id' => $filter['id']]);
$this->executeCommand($command5);
}
}
}
This doesn't work. It gives an error:
Filter ID must be passed as an argument
I can tell that the commands are being called because these are my own error messages from buildOptionsParser().
This makes no sense because the line $io->out($filter['id']) in RunAllCommand.php is showing that the filter IDs are being read from my database. How do you pass an argument in this way? I'm following the docs on Calling Other Commands (https://book.cakephp.org/3/en/console-and-shells/commands.html#calling-other-commands).
I don't understand how to achieve (2). In each of the Commands I've added code such as this when an error occurs which stops execution of the rest of that Command. For example if this gets executed in Stage1Command it should abort and move to Stage2Command:
// e.g. this code can be anywhere in execute() in any of the 5 commands where an error occurs.
$io->error('error message');
$this->abort();
If $this->abort() gets called anywhere I need to log this into another table in my database. Do I need to add code before $this->abort() to write this to a database, or is there some other way, e.g. try...catch in RunAllCommand?
Background information: The idea with this is that RunAllCommand.php would be executed via Cron. This means that the processes carried out by each Stage would occur at regular intervals without requiring manual execution of any of the scripts - or passing IDs manually as command parameters.
The arguments sent to the "main" command are not automatically being passed to the "sub" commands that you're invoking with executeCommand(), the reason for that being that they might very well be incompatible, the "main" command has no way of knowing which arguments should or shouldn't be passed. The last thing you want is a sub command do something that you haven't asked it to do just because of an argument that the main command makes use of.
So you need to pass the arguments that you want your sub commands to receive manually, that would be the second argument of \Cake\Console\BaseCommand::executeCommand(), not the command constructor, it doesn't take any arguments at all (unless you've overwritten the base constructor).
$this->executeCommand($stage1, [$filter['id']]);
Note that the arguments array is not associative, the values are passed as single value entries, just like PHP would receive them in the $argv variable, ie:
['positional argument value', '--named', 'named option value']
With regards to errors, executeCommand() returns the exit code of the command. Calling $this->abort() in your sub command will trigger an exception, which is being catched in executeCommand() and has its code returned just like the normal exit code from your sub command's execute() method.
So if you just need to log a failure, then you could simply evaluate the return code, like:
$result = $this->executeCommand($stage1, [$filter['id']]);
// assuming your sub commands do always return a code, and do not
// rely on `null` (ie no return value) being treated as success too
if ($result !== static::CODE_SUCCESS) {
$this->log('Stage 1 failed');
}
If you need additional information to be logged, then you could of course log inside of your sub commands where that information is available, or maybe store error info in the command and expose a method to read that info, or throw an exception with error details that your main command could catch and evaluate. However, throwing an exception would not be overly nice when running the commands standalone, so you'll have to figure what the best option is in your case.

Call artisan command from the same command

In the handle of your custom Laravel command, can you call the command again? Like this, described using sort of pseudo-code:
public function handle() {
code..
code..
$this->importantValue = $this->option('value'); //value is 'hello'
if(something) {
//call of the same command is made, but with different arguments or options
//command does stuff and ends successfully
$this->call('myself' [
'value' => 'ahoy'
];
//I expect the handle to be returned to the original command
}
var_dump($this->importantValue); //this equals 'ahoy'
}
Why is this? What does that newly called command has in common with the original within which it had been called?
EDIT: The newly called command would not reach the condition something it would not call itself again (forever). The original command seems to pick up from where it left (before calling itself the first and only time) yet it seems it has had inherited the "children's" variables.
I do think that calling Artisan::call() instead of $this->call() might avoid that problem (note that avoiding is not the same as solving)...
#t-maxx: I'm getting the exact same issue and I'm not sure that #ben understands.
I have a command that is recursive, based on an argument, depth. The depth argument is set to a protected property as one of the first steps in handle(). Then, if depth is greater than zero, it calls itself (via $this->call()), but passing $this->depth - 1. I watch each successive call and it just goes down and down and down, never plateauing or bouncing up was the recursion would allow and as one would expect.
So...while I'm not 100% sure what's going on, I'm thinking of getting the depth option once, but passing it around as a variable (versus a property on the object). This is ugly, I think, but it may be the only solution until this is recognized and resolved. On the other hand, it could be that we're both doing the wrong thing.
Calling Artisan::call() for me leads to other issues that I'd rather avoid. The command I'm working with writes to a file and I don't want a bunch of separate commands competing for the same file.
Yes, you can Programmatically Executing Commands using Artisan::call
Artisan::call('myself', [
'value' => 'ahoy'
]);

Cleanup console command on any termination

I have the following (simple) lock code for a Laravel 5.3 command:
private $hash = null;
public final function handle() {
try {
$this->hash = md5(serialize([ static::class, $this->arguments(), $this->options() ]));
$this->info("Generated signature ".$this->hash,"v");
if (Redis::exists($this->hash)) {
$this->hash = null;
throw new \Exception("Method ".$this->signature." is already running");
}
Redis::set($this->hash, true);
$this->info("Running method","vv");
$this->runMutuallyExclusiveCommand(); //Actual command is not important here
$this->cleanup();
} catch (\Exception $e) {
$this->error($e->getMessage());
}
}
public function cleanup() {
if (is_string($this->hash)) {
Redis::del($this->hash);
}
}
This works fine if the command is allowed to go through its execution cycle normally (including handling when there's a PHP exception). However the problem arises when the command is interrupted via other means (e.g. CTRL-C or when the terminal window is closed). In that case the cleanup code is not ran and the command is considered to be still "executing" so I need to manually remove the entry from the cache in order to restart it. I have tried running the cleanup code in a __destruct function but that does not seem to be called either.
My question is, is there a way to set some code to be ran when a command is terminated regardless how it was terminated?
Short answer is no. When you kill the running process, either by Ctrl-C or just closing the terminal, you terminate it. You would need to have an interrupt in your shell that links to your cleanup code, but that is way out of scope.
There are other options however. Cron jobs can be run at intermittent intervals to perform clean up tasks and other helpful things. You could also create a start up routine that runs prior to your current code. When you execute the start up routine, it could do the cleanup for you, then call your current routine. I believe your best bet is to use a cron job that simply runs at given intervals that then looks for entries in the cache that are no longer appropriate, and then cleans them. Here is a decent site to get you started with cron jobs https://www.linux.com/learn/scheduling-magic-intro-cron-linux

Console.Terminate event on a specific command

I just discovered that it exists a ConsoleEvents::TEMINATE event in Symfony.
I want to use it to execute some additional process after the command execution (and not delaying the command).
But the fact is that i want to execute some process when a specific command is finish, not for all the commands (because i think that consoleevent.terminate is fired for all the commands.
I really don't know how to do that.
Regards.
You can access instance of the command from ConsoleTerminateEvent
It's almost copy paste from documentation of Console component. with full symfony registering listener looks a little different but you should get the idea.
$dispatcher->addListener(
ConsoleEvents::TERMINATE,
function(ConsoleTerminateEvent $event) {
$command = $event->getCommand();
// if it's not the command you want
if (!$command instanceof YourDesiredCommand) {
return;
}
// put your logic here
}
);

Symfony2 command within an asynchronous subprocess

I am new on Symfony2 and I got blocked when trying to run an asynchronous command like this:
class MyCommand extends ContainerAwareCommand{
protected function configure()
{
$this
->setName('my:command')
->setDescription('My command')
->addArgument(
'country',
InputArgument::REQUIRED,
'Which country?'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$country = $input->getArgument('country');
// Obtain the doctrine manager
$dm = $this->getContainer()->get('doctrine_mongodb.odm.document_manager');
$users = $dm->getRepository('MyBundle:User')
->findBy( array('country'=>$country));
}}
That works perfectly when I call it from my command line:
php app/console my:command uk
But it doesn't work when I call it trowh a Symfony2 Process:
$process = new Process("php ../app/console my:command $country");
$process->start();
I get a database error: "[MongoWriteConcernException] 127.0.0.1:27017: not master"
I think that means that the process is not getting my database configuration...
I just want to run an asynchronous process, is there other way to do it?
Maybe a way to call the Application Command that do not require the answer to keep going ?
Maybe I need to use injection?
PS: My current command is just a test, at the end it should be an 'expensive' operation...
Well, I found out what happened...
I use multiple environments: DEV, TEST and PROD.
And I also use differents servers.
So the DEV environment is my own machine with a simple mongodb configuration.
But the TEST environment is on other server with a replica set configuration...
Now the error get full sense: "[MongoWriteConcernException] 127.0.0.1:27017: not master"
To solve it, I've just added the environment parameter (--env=) to the process and everything worked like a charm:
$process = new Process("php ../app/console my:command $country --env=test");
Actually, to get the correct environment I use this:
$this->get('kernel')->getEnvironment();
Which let's my code as follows:
$process = new Process("php ../app/console my:command $country --env=".$this->get('kernel')->getEnvironment());
Maybe is not a beautifull way to do it, but it works for me :)
Disclamer: This might be a bit overkill for what you're trying to do :)
I would choose an opposite way to do it: pthreads
First, quick examination of StackOverflow showed me a really nice example of using pthreads: Multi-threading is possible in php
Then, knowing that you could invoke your command from another command:
http://www.craftitonline.com/2011/06/calling-commands-within-commands-in-symfony2/
... lets you piece all the parts. It's a bit complicated but it does the job.
In case you want to execute your code completely async in Symfony2/3 there is AsyncServiceCallBundle for that.
You should just call it like this:
$this->get('krlove.async')->call('your_service_id', 'method_name', [$arg1, $arg2]);
Internally it uses this approach to run your code in background.

Categories