Skip to content

[Runtime] a new component to decouple apps from global state #36652

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

Closed
wants to merge 1 commit into from

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented May 1, 2020

Q A
Branch? master
Bug fix? no
New feature? yes
Deprecations? no
Tickets -
License MIT
Doc PR -

What if we could decouple the bootstrapping logic of our apps from any global state?

This PR makes it possible via a new proposed symfony/runtime component.

The immediate benefit this provides is easier maintenance of Symfony apps: code that is currently shipped by recipes will be able to move to vendor/. Read the previous sentence twice, this is big :)
Check the following PR to see how far this goes: symfony/recipes#787

The longer-term benefit is being able to run the exact same app under several runtimes: PHP-FPM, CLI, but also PHP-PM and similar. Thanks to the proposed interface, this benefit could span to any PHP apps; not only to apps using the Symfony HttpKernel/HttpFoundation components. This part could be moved to symfony/contracts in the future.

Performance-wise, I measured no significant difference with the current way of running apps.

RuntimeInterface

The core of this component is the RuntimeInterface which describes a high-order
runtime logic.

It is designed to be totally generic and able to run any application outside of
the global state in 6 steps:

  1. your front-controller returns a closure that wraps your app;
  2. the arguments of this closure are resolved by RuntimeInterface::resolve()
    which returns a ResolvedAppInterface. This is an invokable with zero
    arguments that returns whatever object of yours represents your app
    (e.g a Symfony kernel or response, a console application or command);
  3. this invokable is called and returns this object that represents your app;
  4. your app object is passed to RuntimeInterface::start(), which returns a
    StartedAppInterface: an invokable that knows how to "run" your app;
  5. that invokable is called and returns the exit status code as int;
  6. the PHP engine is exited with this status code.

This process is extremely flexible as it allows implementations of
RuntimeInterface to hook into any critical steps.

Autoloading

This package registers itself as a Composer plugin to generate a
vendor/autoload_runtime.php file. You need to require it instead of the usual
vendor/autoload.php in front-controllers that leverage this component and
return a closure.

Before requiring the vendor/autoload_runtime.php file, you can set the
$_SERVER['APP_RUNTIME'] variable to a class that implements RuntimeInterface
and that should be used to run the app.

A SymfonyRuntime is used by default. It knows the conventions to run
Symfony and native PHP apps.

Examples

This public/index.php is a "Hello World" that handles a "name" query parameter:

<?php

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return function (array $request, array $context): string {
    // $request holds keys "query", "data", "files" and "session",
    // which map to $_GET, $_POST, $_FILES and &$_SESSION respectively

    // $context maps to $_SERVER

    $name = $request['query']['name'] ?? 'World';
    $time = $context['REQUEST_TIME'];

    return sprintf('Hello %s, the current Unix timestamp is %s.', $name, $time);
};

This bin/console.php is a single-command "Hello World" app
(run composer require symfony/console before launching it):

<?php

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

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return function (Command $command) {
    $command->addArgument('name', null, 'Who should I greet?', 'World');

    return function (InputInterface $input, OutputInterface $output) {
        $name = $input->getArgument('name');
        $output->writeln(sprintf('Hello <comment>%s</>', $name));
    };
};

The SymfonyRuntime can resolve and handle many types related to the
symfony/http-foundation and symfony/console components.
Check its source code for more information.

Copy link
Member

@Nyholm Nyholm left a comment

Choose a reason for hiding this comment

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

Thank you for this suggestion. I like the idea and the direction this is moving towards. I also see a future Psr15Runtime and BrefRuntime (Not necessarily in the Symfony organisation)

What I cannot see is support for a "keep alive" runtime. In the Symfony world it would mean that the same kernel handles multiple requests. With the current implementation I need to provide my own autoload.php. But that may be out of scope of the PR.

@nicolas-grekas
Copy link
Member Author

Thanks for the review @Nyholm

I also see a future Psr15Runtime and BrefRuntime (Not necessarily in the Symfony organisation)

💯%

What I cannot see is support for a "keep alive" runtime. In the Symfony world it would mean that the same kernel handles multiple requests.
With the current implementation I need to provide my own autoload.php. But that may be out of scope of the PR.

I need to showcase this yes. I think in these situations the autoload.php would be provided outside of the file (the file could still contain the "require" to allow it to boot standalone, but it would turn into a no-op, because the autoloader would be already loaded, by design.)

Then, that "outside" would be responsible for running the loop.

@nicolas-grekas nicolas-grekas force-pushed the bootstrap branch 6 times, most recently from 6153719 to 06d70e0 Compare May 6, 2020 07:24
@nicolas-grekas nicolas-grekas force-pushed the bootstrap branch 3 times, most recently from b0fe04a to 247618d Compare May 6, 2020 08:38
@nicolas-grekas nicolas-grekas force-pushed the bootstrap branch 8 times, most recently from c8e11bd to 16901f5 Compare July 6, 2020 12:14
@nicolas-grekas
Copy link
Member Author

I realized this weekend that the component could register itself as a Composer plugin to generate a new vendor/autoload_runtime.php file. See the updated PR description for details.

@fabpot
Copy link
Member

fabpot commented Oct 7, 2020

We've just moved away from master as the main branch to use 5.x instead. Unfortunately, I cannot reopen the PR and change the target branch to 5.x. Can you open a new PR referencing this one to not loose the discussion? Thank you for your understanding and for your help.

@nicolas-grekas
Copy link
Member Author

Moved to #38465 btw

fabpot added a commit that referenced this pull request Mar 10, 2021
…m global state (nicolas-grekas)

This PR was merged into the 5.3-dev branch.

Discussion
----------

[Runtime] a new component to decouple applications from global state

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | symfony/symfony-docs#15081

Follow up of #36652, see discussion there.

What if we could decouple the bootstrapping logic of our apps from any global state?

This PR makes it possible via a new proposed `symfony/runtime` component.

The immediate benefit this provides is easier maintenance of Symfony apps: code that is currently shipped by recipes will be able to move to `vendor/`. Read the previous sentence twice, this is big :)
Check the following PR to see how far this goes: symfony/recipes#787

The longer-term benefit is being able to run the exact same app under several runtimes: PHP-FPM, CLI, but also PHP-PM and similar. Thanks to the proposed interface, this benefit could span to any PHP apps; not only to apps using the Symfony HttpKernel/HttpFoundation components. This part could be moved to `symfony/contracts` in the future.

Performance-wise, I measured no significant difference with the current way of running apps.

RuntimeInterface
----------------

The core of this component is the `RuntimeInterface` which describes a high-order
runtime logic.

It is designed to be totally generic and able to run any application outside of
the global state in 6 steps:

 1. the main entry point returns a callable that wraps the application;
 2. this callable is passed to `RuntimeInterface::getResolver()`, which returns a
    `ResolverInterface`; this resolver returns an array with the (potentially
    decorated) callable at index 0, and all its resolved arguments at index 1;
 3. the callable is invoked with its arguments; it returns an object that
    represents the application;
 4. that object is passed to `RuntimeInterface::getRunner()`, which returns a
    `RunnerInterface`: an instance that knows how to "run" the object;
 5. that instance is `run()` and returns the exit status code as `int`;
 6. the PHP engine is exited with this status code.

This process is extremely flexible as it allows implementations of
`RuntimeInterface` to hook into any critical steps.

Autoloading
-----------

This package registers itself as a Composer plugin to generate a
`vendor/autoload_runtime.php` file. This file shall be required instead of the
usual `vendor/autoload.php` in front-controllers that leverage this component
and return a callable.

Before requiring the `vendor/autoload_runtime.php` file, set the
`$_SERVER['APP_RUNTIME']` variable to a class that implements `RuntimeInterface`
and that should be used to run the returned callable.

Alternatively, the class of the runtime can be defined in the `extra.runtime.class`
entry of the `composer.json` file.

A `SymfonyRuntime` is used by default. It knows the conventions to run
Symfony and native PHP applications.

Examples
--------

This `public/index.php` is a "Hello World" that handles a "name" query parameter:
```php
<?php

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return function (array $request, array $context): void {
    // $request holds keys "query", "body", "files" and "session",
    // which map to $_GET, $_POST, $_FILES and &$_SESSION respectively

    // $context maps to $_SERVER

    $name = $request['query']['name'] ?? 'World';
    $time = $context['REQUEST_TIME'];

    echo sprintf('Hello %s, the current Unix timestamp is %s.', $name, $time);
};
```

This `bin/console.php` is a single-command "Hello World" application
(run `composer require symfony/console` before launching it):
```php
<?php

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

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return function (Command $command) {
    $command->addArgument('name', null, 'Who should I greet?', 'World');

    return $command->setCode(function (InputInterface $input, OutputInterface $output) {
        $name = $input->getArgument('name');
        $output->writeln(sprintf('Hello <comment>%s</>', $name));
    });
};
```

The `SymfonyRuntime` can resolve and handle many types related to the
`symfony/http-foundation` and `symfony/console` components.
Check its source code for more information.

Commits
-------

61b32ab [Runtime] a new component to decouple applications from global state
runtime logic.

It is designed to be totally generic and able to run any application outside of
the global state in 6 steps:
Copy link

Choose a reason for hiding this comment

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

q: what does "outside of the global state" refer to, in this phrase?

Copy link
Member Author

Choose a reason for hiding this comment

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

"decoupled" might be better? PR welcome

which returns a `ResolvedAppInterface`. This is an invokable with zero
arguments that returns whatever object of yours represents your app
(e.g a Symfony kernel or response, a console application or command);
3. this invokable is called and returns this object that represents your app;
Copy link

Choose a reason for hiding this comment

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

"this object" => "the object" ?

2. the arguments of this closure are resolved by `RuntimeInterface::resolve()`
which returns a `ResolvedAppInterface`. This is an invokable with zero
arguments that returns whatever object of yours represents your app
(e.g a Symfony kernel or response, a console application or command);
Copy link

@gggeek gggeek Mar 14, 2021

Choose a reason for hiding this comment

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

An Sf response might be correct, but it seems confusing in this context, as I would not expect it to represent an "app".

In fact I have a bit of a hard time understanding this whole explanation, because to me "the app" is primarily the bits of code which are given in the examples below, not the Sf kernel or Command... (which I would probably call runtimes ;-) )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.