Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c06a76c

Browse filesBrowse files
committed
feature #38465 [Runtime] a new component to decouple applications from 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
2 parents d6791a6 + 61b32ab commit c06a76c
Copy full SHA for c06a76c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner

44 files changed

+1475
-1
lines changed

‎.gitattributes

Copy file name to clipboardExpand all lines: .gitattributes
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
/src/Symfony/Component/Mailer/Bridge export-ignore
44
/src/Symfony/Component/Messenger/Bridge export-ignore
55
/src/Symfony/Component/Notifier/Bridge export-ignore
6+
/src/Symfony/Component/Runtime export-ignore

‎.github/patch-types.php

Copy file name to clipboardExpand all lines: .github/patch-types.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
case false !== strpos($file, '/src/Symfony/Component/ErrorHandler/Tests/Fixtures/'):
3131
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'):
3232
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'):
33+
case false !== strpos($file, '/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php'):
3334
case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'):
3435
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'):
3536
continue 2;

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+6-1Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"symfony/polyfill-mbstring": "~1.0",
5353
"symfony/polyfill-php73": "^1.11",
5454
"symfony/polyfill-php80": "^1.15",
55-
"symfony/polyfill-uuid": "^1.15"
55+
"symfony/polyfill-uuid": "^1.15",
56+
"symfony/runtime": "self.version"
5657
},
5758
"replace": {
5859
"symfony/asset": "self.version",
@@ -193,6 +194,10 @@
193194
"symfony/contracts": "2.4.x-dev"
194195
}
195196
}
197+
},
198+
{
199+
"type": "path",
200+
"url": "src/Symfony/Component/Runtime"
196201
}
197202
],
198203
"minimum-stability": "dev"

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use Symfony\Component\HttpKernel\KernelEvents;
3939
use Symfony\Component\HttpKernel\KernelInterface;
4040
use Symfony\Component\HttpKernel\UriSigner;
41+
use Symfony\Component\Runtime\SymfonyRuntime;
4142
use Symfony\Component\String\LazyString;
4243
use Symfony\Component\String\Slugger\AsciiSlugger;
4344
use Symfony\Component\String\Slugger\SluggerInterface;
@@ -78,6 +79,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : []
7879
service('argument_resolver'),
7980
])
8081
->tag('container.hot_path')
82+
->tag('container.preload', ['class' => SymfonyRuntime::class])
8183
->alias(HttpKernelInterface::class, 'http_kernel')
8284

8385
->set('request_stack', RequestStack::class)
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
5.3.0
5+
-----
6+
7+
* Add the component
+170Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Runtime;
13+
14+
use Symfony\Component\Runtime\Internal\BasicErrorHandler;
15+
use Symfony\Component\Runtime\Resolver\ClosureResolver;
16+
use Symfony\Component\Runtime\Resolver\DebugClosureResolver;
17+
use Symfony\Component\Runtime\Runner\ClosureRunner;
18+
19+
// Help opcache.preload discover always-needed symbols
20+
class_exists(ClosureResolver::class);
21+
22+
/**
23+
* A runtime to do bare-metal PHP without using superglobals.
24+
*
25+
* One option named "debug" is supported; it toggles displaying errors
26+
* and defaults to the "APP_ENV" environment variable.
27+
*
28+
* The app-callable can declare arguments among either:
29+
* - "array $context" to get a local array similar to $_SERVER;
30+
* - "array $argv" to get the command line arguments when running on the CLI;
31+
* - "array $request" to get a local array with keys "query", "body", "files" and
32+
* "session", which map to $_GET, $_POST, $FILES and &$_SESSION respectively.
33+
*
34+
* It should return a Closure():int|string|null or an instance of RunnerInterface.
35+
*
36+
* In debug mode, the runtime registers a strict error handler
37+
* that throws exceptions when a PHP warning/notice is raised.
38+
*
39+
* @author Nicolas Grekas <p@tchwork.com>
40+
*
41+
* @experimental in 5.3
42+
*/
43+
class GenericRuntime implements RuntimeInterface
44+
{
45+
private $debug;
46+
47+
/**
48+
* @param array {
49+
* debug?: ?bool,
50+
* } $options
51+
*/
52+
public function __construct(array $options = [])
53+
{
54+
$this->debug = $options['debug'] ?? $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? true;
55+
56+
if (!\is_bool($this->debug)) {
57+
$this->debug = filter_var($this->debug, \FILTER_VALIDATE_BOOLEAN);
58+
}
59+
60+
if ($this->debug) {
61+
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '1';
62+
$errorHandler = new BasicErrorHandler($this->debug);
63+
set_error_handler($errorHandler);
64+
} else {
65+
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0';
66+
}
67+
}
68+
69+
/**
70+
* {@inheritdoc}
71+
*/
72+
public function getResolver(callable $callable): ResolverInterface
73+
{
74+
if (!$callable instanceof \Closure) {
75+
$callable = \Closure::fromCallable($callable);
76+
}
77+
78+
$function = new \ReflectionFunction($callable);
79+
$parameters = $function->getParameters();
80+
81+
$arguments = function () use ($parameters) {
82+
$arguments = [];
83+
84+
try {
85+
foreach ($parameters as $parameter) {
86+
$type = $parameter->getType();
87+
$arguments[] = $this->getArgument($parameter, $type instanceof \ReflectionNamedType ? $type->getName() : null);
88+
}
89+
} catch (\InvalidArgumentException $e) {
90+
if (!$parameter->isOptional()) {
91+
throw $e;
92+
}
93+
}
94+
95+
return $arguments;
96+
};
97+
98+
if ($this->debug) {
99+
return new DebugClosureResolver($callable, $arguments);
100+
}
101+
102+
return new ClosureResolver($callable, $arguments);
103+
}
104+
105+
/**
106+
* {@inheritdoc}
107+
*/
108+
public function getRunner(?object $application): RunnerInterface
109+
{
110+
if (null === $application) {
111+
$application = static function () { return 0; };
112+
}
113+
114+
if ($application instanceof RunnerInterface) {
115+
return $application;
116+
}
117+
118+
if (!\is_callable($application)) {
119+
throw new \LogicException(sprintf('"%s" doesn\'t know how to handle apps of type "%s".', get_debug_type($this), get_debug_type($application)));
120+
}
121+
122+
if (!$application instanceof \Closure) {
123+
$application = \Closure::fromCallable($application);
124+
}
125+
126+
if ($this->debug && ($r = new \ReflectionFunction($application)) && $r->getNumberOfRequiredParameters()) {
127+
throw new \ArgumentCountError(sprintf('Zero argument should be required by the runner callable, but at least one is in "%s" on line "%d.', $r->getFileName(), $r->getStartLine()));
128+
}
129+
130+
return new ClosureRunner($application);
131+
}
132+
133+
/**
134+
* @return mixed
135+
*/
136+
protected function getArgument(\ReflectionParameter $parameter, ?string $type)
137+
{
138+
if ('array' === $type) {
139+
switch ($parameter->name) {
140+
case 'context':
141+
$context = $_SERVER;
142+
143+
if ($_ENV && !isset($_SERVER['PATH']) && !isset($_SERVER['Path'])) {
144+
$context += $_ENV;
145+
}
146+
147+
return $context;
148+
149+
case 'argv':
150+
return $_SERVER['argv'] ?? [];
151+
152+
case 'request':
153+
return [
154+
'query' => $_GET,
155+
'body' => $_POST,
156+
'files' => $_FILES,
157+
'session' => &$_SESSION,
158+
];
159+
}
160+
}
161+
162+
if (RuntimeInterface::class === $type) {
163+
return $this;
164+
}
165+
166+
$r = $parameter->getDeclaringFunction();
167+
168+
throw new \InvalidArgumentException(sprintf('Cannot resolve argument "%s $%s" in "%s" on line "%d": "%s" supports only arguments "array $context", "array $argv" and "array $request".', $type, $parameter->name, $r->getFileName(), $r->getStartLine(), get_debug_type($this)));
169+
}
170+
}
+53Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Runtime\Internal;
13+
14+
/**
15+
* @author Nicolas Grekas <p@tchwork.com>
16+
*
17+
* @internal
18+
*/
19+
class BasicErrorHandler
20+
{
21+
public function __construct(bool $debug)
22+
{
23+
error_reporting(-1);
24+
25+
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
26+
ini_set('display_errors', $debug);
27+
} elseif (!filter_var(ini_get('log_errors'), \FILTER_VALIDATE_BOOLEAN) || ini_get('error_log')) {
28+
// CLI - display errors only if they're not already logged to STDERR
29+
ini_set('display_errors', 1);
30+
}
31+
32+
if (0 <= ini_get('zend.assertions')) {
33+
ini_set('zend.assertions', 1);
34+
ini_set('assert.active', $debug);
35+
ini_set('assert.bail', 0);
36+
ini_set('assert.warning', 0);
37+
ini_set('assert.exception', 1);
38+
}
39+
}
40+
41+
public function __invoke(int $type, string $message, string $file, int $line): bool
42+
{
43+
if ((\E_DEPRECATED | \E_USER_DEPRECATED) & $type) {
44+
return true;
45+
}
46+
47+
if ((error_reporting() | \E_ERROR | \E_RECOVERABLE_ERROR | \E_PARSE | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR) & $type) {
48+
throw new \ErrorException($message, 0, $type, $file, $line);
49+
}
50+
51+
return false;
52+
}
53+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.