Skip to content

Commit 78f178f

Browse files
[Runtime] a new component to decouple applications from global state
1 parent 17eaad2 commit 78f178f

Some content is hidden

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

41 files changed

+1394
-2
lines changed

.github/patch-types.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
foreach ($loader->getClassMap() as $class => $file) {
1515
switch (true) {
16-
case false !== strpos(realpath($file), '/vendor/'):
16+
case false !== strpos($file = realpath($file), '/vendor/'):
1717
case false !== strpos($file, '/src/Symfony/Bridge/PhpUnit/'):
1818
case false !== strpos($file, '/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Validation/Article.php'):
1919
case false !== strpos($file, '/src/Symfony/Component/Config/Tests/Fixtures/BadFileName.php'):
@@ -36,6 +36,7 @@
3636
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'):
3737
case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php80Dummy.php'):
3838
case false !== strpos($file, '/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures'):
39+
case false !== strpos($file, '/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php'):
3940
case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'):
4041
case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/'):
4142
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/LotsOfAttributes.php'):

composer.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"symfony/polyfill-mbstring": "~1.0",
3737
"symfony/polyfill-php73": "^1.11",
3838
"symfony/polyfill-php80": "^1.15",
39-
"symfony/polyfill-uuid": "^1.15"
39+
"symfony/polyfill-uuid": "^1.15",
40+
"symfony/runtime": "self.version"
4041
},
4142
"replace": {
4243
"symfony/asset": "self.version",
@@ -172,6 +173,10 @@
172173
{
173174
"type": "path",
174175
"url": "src/Symfony/Contracts"
176+
},
177+
{
178+
"type": "path",
179+
"url": "src/Symfony/Component/Runtime"
175180
}
176181
],
177182
"minimum-stability": "dev",

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

+2
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)
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
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
5.3.0
5+
-----
6+
7+
* added the component
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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\ResolvedCallable\ResolvedCallable;
16+
use Symfony\Component\Runtime\ResolvedCallable\ResolvedScalar;
17+
use Symfony\Component\Runtime\StartedApplication\StartedClosure;
18+
19+
// Help opcache.preload discover always-needed symbols
20+
class_exists(ResolvedCallable::class);
21+
class_exists(BasicErrorHandler::class);
22+
23+
/**
24+
* A runtime to do bare-metal PHP without using superglobals.
25+
*
26+
* One option named "debug" is supported; it toggles displaying errors.
27+
*
28+
* The app-closure returned by the entry script must return either:
29+
* - "string" to echo the response content, or
30+
* - "int" to set the exit status code.
31+
*
32+
* The app-closure can declare arguments among either:
33+
* - "array $context" to get a local array similar to $_SERVER;
34+
* - "array $argv" to get the command line arguments when running on the CLI;
35+
* - "array $request" to get a local array with keys "query", "data", "files" and
36+
* "session", which map to $_GET, $_POST, $FILES and &$_SESSION respectively.
37+
*
38+
* The runtime sets up a strict error handler that throws
39+
* exceptions when a PHP warning/notice is raised.
40+
*
41+
* @author Nicolas Grekas <p@tchwork.com>
42+
*/
43+
class GenericRuntime implements RuntimeInterface
44+
{
45+
private $debug;
46+
47+
public function __construct(array $options = [])
48+
{
49+
$this->debug = $options['debug'] ?? true;
50+
$errorHandler = new BasicErrorHandler($this->debug);
51+
set_error_handler($errorHandler);
52+
set_exception_handler([$errorHandler, 'handleException']);
53+
}
54+
55+
public function resolve(callable $callable): ResolvedCallableInterface
56+
{
57+
if (!$callable instanceof \Closure) {
58+
$callable = \Closure::fromCallable($callable);
59+
}
60+
61+
$arguments = [];
62+
$function = new \ReflectionFunction($callable);
63+
64+
try {
65+
foreach ($function->getParameters() as $parameter) {
66+
$arguments[] = $this->getArgument($parameter, $parameter->getType());
67+
}
68+
} catch (\InvalidArgumentException $e) {
69+
if (!$parameter->isOptional()) {
70+
throw $e;
71+
}
72+
}
73+
74+
$returnType = $function->getReturnType();
75+
76+
switch ($returnType instanceof \ReflectionNamedType ? $returnType->getName() : '') {
77+
case 'string':
78+
return new ResolvedScalar(static function () use ($callable, $arguments): int {
79+
echo $callable(...$arguments);
80+
81+
return 0;
82+
});
83+
84+
case 'int':
85+
case 'void':
86+
return new ResolvedScalar(static function () use ($callable, $arguments): int {
87+
return $callable(...$arguments) ?? 0;
88+
});
89+
}
90+
91+
return new ResolvedCallable($callable, $arguments);
92+
}
93+
94+
public function start(object $application): StartedApplicationInterface
95+
{
96+
if (!$application instanceof \Closure) {
97+
throw new \LogicException(sprintf('"%s" doesn\'t know how to handle apps of type "%s".', get_debug_type($this), get_debug_type($application)));
98+
}
99+
100+
if ($this->debug && (new \ReflectionFunction($application))->getNumberOfRequiredParameters()) {
101+
throw new \ArgumentCountError('Zero argument should be required by the closure returned by the app, but at least one is.');
102+
}
103+
104+
return new StartedClosure($application);
105+
}
106+
107+
/**
108+
* @return mixed
109+
*/
110+
protected function getArgument(\ReflectionParameter $parameter, ?\ReflectionType $type)
111+
{
112+
$type = $type instanceof \ReflectionNamedType ? $type->getName() : '';
113+
114+
if (RuntimeInterface::class === $type) {
115+
return $this;
116+
}
117+
118+
if ('array' !== $type) {
119+
throw new \InvalidArgumentException(sprintf('Cannot resolve argument "%s $%s": "%s" supports only arguments "$context", "$argv" and "$request" with type "array".', $type, $parameter->name, get_debug_type($this)));
120+
}
121+
122+
switch ($parameter->name) {
123+
case 'context':
124+
$context = $_SERVER;
125+
126+
if ($_ENV && !isset($_SERVER['PATH']) && !isset($_SERVER['Path'])) {
127+
$context += $_ENV;
128+
}
129+
130+
return $context;
131+
132+
case 'argv':
133+
return $_SERVER['argv'] ?? [];
134+
135+
case 'request':
136+
return [
137+
'query' => $_GET,
138+
'data' => $_POST,
139+
'files' => $_FILES,
140+
'session' => &$_SESSION,
141+
];
142+
}
143+
144+
throw new \InvalidArgumentException(sprintf('Cannot resolve array argument "$%s": "%s" supports only arguments "$context", "$argv" and "$request".', $parameter->name, get_debug_type($this)));
145+
}
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
54+
public function handleException(\Throwable $e): void
55+
{
56+
echo "<pre>\n$e\n</pre>\n";
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
use Composer\Composer;
15+
use Composer\EventDispatcher\EventSubscriberInterface;
16+
use Composer\Factory;
17+
use Composer\IO\IOInterface;
18+
use Composer\Plugin\PluginInterface;
19+
use Composer\Script\ScriptEvents;
20+
use Symfony\Component\Filesystem\Filesystem;
21+
22+
/**
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*
25+
* @internal
26+
*/
27+
class ComposerPlugin implements PluginInterface, EventSubscriberInterface
28+
{
29+
/**
30+
* @var Composer
31+
*/
32+
private $composer;
33+
34+
/**
35+
* @var IOInterface
36+
*/
37+
private $io;
38+
39+
private static $activated = false;
40+
41+
public function activate(Composer $composer, IOInterface $io): void
42+
{
43+
self::$activated = true;
44+
$this->composer = $composer;
45+
$this->io = $io;
46+
}
47+
48+
public function deactivate(Composer $composer, IOInterface $io): void
49+
{
50+
self::$activated = false;
51+
}
52+
53+
public function uninstall(Composer $composer, IOInterface $io): void
54+
{
55+
@unlink($composer->getConfig()->get('vendor-dir').'/autoload_runtime.php');
56+
}
57+
58+
public function updateAutoloadFile(): void
59+
{
60+
$vendorDir = $this->composer->getConfig()->get('vendor-dir');
61+
$autoloadFile = $vendorDir.'/autoload.php';
62+
63+
if (!file_exists($autoloadFile)) {
64+
return;
65+
}
66+
67+
$projectDir = (new Filesystem())->makePathRelative(\dirname(realpath(Factory::getComposerFile())), $vendorDir);
68+
$nestingLevel = 0;
69+
70+
while (0 === strpos($projectDir, '../')) {
71+
++$nestingLevel;
72+
$projectDir = substr($projectDir, 3);
73+
}
74+
75+
if (!$nestingLevel) {
76+
$projectDir = '__'.'DIR__.'.var_export('/'.$projectDir, true);
77+
} else {
78+
$projectDir = 'dirname(__'."DIR__, $nestingLevel)".('' !== $projectDir ? var_export('/'.$projectDir, true) : '');
79+
}
80+
81+
$code = strtr(file_get_contents(__DIR__.'/autoload_runtime.template'), [
82+
'%project_dir%' => $projectDir,
83+
]);
84+
85+
file_put_contents(substr_replace($autoloadFile, '_runtime', -4, 0), $code);
86+
}
87+
88+
public static function getSubscribedEvents(): array
89+
{
90+
if (!self::$activated) {
91+
return [];
92+
}
93+
94+
return [
95+
ScriptEvents::POST_AUTOLOAD_DUMP => 'updateAutoloadFile',
96+
];
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
* @internal class that should be loaded only when symfony/dotenv is not installed
16+
*/
17+
class MissingDotenv
18+
{
19+
}

0 commit comments

Comments
 (0)