Skip to content

[Console] Bind the closure (code) to the Command if possible #14431

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 23, 2015

Conversation

lyrixx
Copy link
Member

@lyrixx lyrixx commented Apr 21, 2015

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets ~
License MIT
Doc PR ~

This allow this kind of code:

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

require __DIR__.'/vendor/autoload.php';

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

$application = new Application();

$application->add((new Command('process')))
    ->setDescription('Play all other commands')
    ->setCode(function (InputInterface $input, OutputInterface $output) use ($application) {
        $application = $this->getApplication();
        $help = $application->find('help');
        $output->writeln($help->getHelp());
    })
;
$application->run();

@xabbuh xabbuh added the Console label Apr 21, 2015
@stof
Copy link
Member

stof commented Apr 21, 2015

how does this behave in case the closure passed as argument was already bound previously ?

@lyrixx
Copy link
Member Author

lyrixx commented Apr 21, 2015

Symfony is the last one to bind, so symfony wins:

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

require __DIR__.'/vendor/autoload.php';

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

$application = new Application();

$code = function (InputInterface $input, OutputInterface $output) use ($application) {
    $output->writeln('===================');
    $application = $this->getApplication();
    $help = $application->find('help');
    $output->writeln($help->getHelp());
    $output->writeln('===================');
};

$code = $code->bindTo($application);

$application->add((new Command('process')))
    ->setDescription('Play all other commands')
    ->setCode($code)
;
$application->run();

=>

php test.php  process
===================
The %command.name% command displays help for a given command:

  php %command.full_name% list

You can also output the help in other formats by using the --format option:

  php %command.full_name% --format=xml list

To display the list of available commands, please use the list command.
===================

(yeah, we can do crazy things with closure binding....)

@@ -284,6 +284,10 @@ public function setCode($code)
throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.');
}

if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the PHP version check necessary? In PHP 5.3 the instanceof check will return false, as the Closure class doesn't exist.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, but what if someone define a user-land Closure class ?

IHMO, it's safer to keep the version check (and will be removed in SF3.0 anyway)

edit: I did not know Closure exists since php 5.3

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Closure class exists since 5.3, but Closure::bind has been introduced in 5.4, so the check is required :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha! :)

@lyrixx lyrixx force-pushed the command-bind-this branch from b256312 to 1400f4e Compare April 22, 2015 08:28
@nicolas-grekas
Copy link
Member

I'd bind only if the closure is not already bound, so that we keep BC

@stof
Copy link
Member

stof commented Apr 22, 2015

The JS way to bind closures is better IMO, by making sure that the first one binding a closure wins, letting the creator of the closure can control how it is called without having to bother with code using the closure.

Is there a way to detect whether the closure was bound explicitly in PHP ?

@lyrixx
Copy link
Member Author

lyrixx commented Apr 22, 2015

Is there a way to detect whether the closure was bound explicitly in PHP ?

yes, http://php.net/manual/en/reflectionfunctionabstract.getclosurethis.php

        if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) {
            $r = new \ReflectionFunction($code);
            if (null === $r->getClosureThis()) {
                $code = \Closure::bind($code, $this);
            }
        }

but I'm not able to unbind a closure in tests ... (even if the doc says it's possible)

@lyrixx lyrixx force-pushed the command-bind-this branch from 1400f4e to 256de36 Compare April 22, 2015 09:15
@lyrixx
Copy link
Member Author

lyrixx commented Apr 22, 2015

Here we go, should be ok now...

public function getSetCodeBindToClosureTests()
{
return array(
array(true, 'not binded'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: bound, not binded

@lyrixx lyrixx force-pushed the command-bind-this branch 3 times, most recently from 6050e76 to 71d7c25 Compare April 22, 2015 09:24
@@ -284,6 +284,13 @@ public function setCode($code)
throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.');
}

if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) {
$r = new \ReflectionFunction($code);
if (null === $r->getClosureThis()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when it's null should we throw an exception ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ? This is precisely the case where we add a new feature

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok I understand now :D , getClosureThis is not documented, so it would be nice if we add a comment that explain why we use it

@nicolas-grekas
Copy link
Member

👍

@lyrixx lyrixx force-pushed the command-bind-this branch from 71d7c25 to ff4424a Compare April 22, 2015 09:30
@lyrixx
Copy link
Member Author

lyrixx commented Apr 23, 2015

@stof Is it ok for you?

@stof
Copy link
Member

stof commented Apr 23, 2015

👍

@stof
Copy link
Member

stof commented Apr 23, 2015

Thanks @lyrixx.

@stof stof merged commit ff4424a into symfony:2.8 Apr 23, 2015
stof added a commit that referenced this pull request Apr 23, 2015
…ssible (lyrixx)

This PR was merged into the 2.8 branch.

Discussion
----------

[Console] Bind the closure (code) to the Command if possible

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | ~
| License       | MIT
| Doc PR        | ~

This allow this kind of code:

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

require __DIR__.'/vendor/autoload.php';

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

$application = new Application();

$application->add((new Command('process')))
    ->setDescription('Play all other commands')
    ->setCode(function (InputInterface $input, OutputInterface $output) use ($application) {
        $application = $this->getApplication();
        $help = $application->find('help');
        $output->writeln($help->getHelp());
    })
;
$application->run();
```

Commits
-------

ff4424a [Console] Bind the closure (code) to the Command if possible
@fabpot fabpot mentioned this pull request Nov 16, 2015
@lyrixx lyrixx deleted the command-bind-this branch December 8, 2015 15:21
fabpot added a commit that referenced this pull request Dec 13, 2016
This PR was squashed before being merged into the 2.8 branch (closes #20847).

Discussion
----------

[Console] fixed BC issue with static closures

| Q             | A
| ------------- | ---
| Branch?       | 2.8
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #20845
| License       | MIT
| Doc PR        | n/a

Static closures were unable to be used in Command::setCode since #14431.  This change fixes the BC break and ensures static closures can still be used.

Edit: It seems the inability to bind static closures was considered a feature in PHP5 but was considered a bug by PHP7.  As such, the tests need to work around this fact - but the code can remain the same.  This code change can be tidied/removed once Symfony is PHP7+ only.

Discussion here:
https://bugs.php.net/bug.php?id=64761
https://bugs.php.net/bug.php?id=68792

Commits
-------

3247308 [Console] fixed BC issue with static closures
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants