8000 [Runtime] a new component to decouple applications from global state · symfony/symfony@768c50b · GitHub
[go: up one dir, main page]

Skip to content

Commit 768c50b

Browse files
[Runtime] a new component to decouple applications from global state
1 parent 20bb3cb commit 768c50b

Some content is hidden

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

42 files changed

+1399
-1
lines changed

.gitattributes

Lines 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

Lines 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

Lines 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",
@@ -190,6 +191,10 @@
190191
"symfony/contracts": "2.3.x-dev"
191192
}
192193
}
194+
},
195+
{
196+
"type": "path",
197+
"url": "src/Symfony/Component/Runtime"
193198
}
194199
],
195200
"minimum-stability": "dev"

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

Lines 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)
Lines 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
Lines 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
Lines 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
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
*
27+
* The app-callable can declare arguments among either:
28+
* - "array $context" to get a local array similar to $_SERVER;
29+
* - "array $argv" to get the command line arguments when running on the CLI;
30+
* - "array $request" to get a local array with keys "query", "data", "files" and
31+
* "session", which map to $_GET, $_POST, $FILES and &$_SESSION respectively.
32+
*
33+
* It should return a Closure():int|string|null or an instance of RunnerInterface.
34+
*
35+
* In debug mode, the runtime registers a strict error handler
36+
* that throws exceptions when a PHP warning/notice is raised.
37+
*
38+
* @author Nicolas Grekas <p@tchwork.com>
39+
*/
40+
class GenericRuntime implements RuntimeInterface
41+
{
42+
private $debug;
43+
44+
public function __construct(array $options = [])
45+
{
46+
if ($this->debug = $options['debug'] ?? true) {
47+
$errorHandler = new BasicErrorHandler($this->debug);
48+
set_error_handler($errorHandler);
49+
}
50+
}
51+
52+
/**
53+
* {@inheritdoc}
54+
*/
55+
public function getResolver(callable $callable): ResolverInterface
56+
{
57+
if (!$callable instanceof \Closure) {
58+
$callable = \Closure::fromCallable($callable);
59+
}
60+
61+
$function = new \ReflectionFunction($callable);
62+
$parameters = $function->getParameters();
63+
64+
$arguments = function () use ($parameters) {
65+
$arguments = [];
66+
67+
try {
68+
foreach ($parameters as $parameter) {
69+
$type = $parameter->getType();
70+
$arguments[] = $this->getArgument($parameter, $type instanceof \ReflectionNamedType ? $type->getName() : null);
71+
}
72+
} catch (\InvalidArgumentException $e) {
73+
if (!$parameter->isOptional()) {
74+
throw $e;
75+
}
76+
}
77+
78+
return $arguments;
79+
};
80+
81+
if ($this->debug) {
82+
return new DebugClosureResolver($callable, $arguments);
83+
}
84+
85+
return new ClosureResolver($callable, $arguments);
86+
}
87+
88+
/**
89+
* {@inheritdoc}
90+
*/
91+
public function getRunner(object $application): RunnerInterface
92+
{
93+
if ($application instanceof RunnerInterface) {
94+
return $application;
95+
}
96+
97+
if (!\is_callable($application)) {
98+
throw new \LogicException(sprintf('"%s" doesn\'t know how to handle apps of type "%s".', get_debug_type($this), get_debug_type($application)));
99+
}
100+
101+
if (!$application instanceof \Closure) {
102+
$application = \Closure::fromCallable($application);
103+
}
104+
105+
if ($this->debug && ($r = new \ReflectionFunction($application)) && $r->getNumberOfRequiredParameters()) {
106+
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()));
107+
}
108+
109+
return new ClosureRunner($application);
110+
}
111+
112+
/**
113+
* @return mixed
114+
*/
115+
protected function getArgument(\ReflectionParameter $parameter, ?string $type)
116+
{
117+
if ('array' === $type) {
118+
switch ($parameter->name) {
119+
case 'context':
120+
$context = $_SERVER;
121+
122+
if ($_ENV && !isset($_SERVER['PATH']) && !isset($_SERVER['Path'])) {
123+
$context += $_ENV;
124+
}
125+
126+
return $context;
127+
128+
case 'argv':
129+
return $_SERVER['argv'] ?? [];
130+
131+
case 'request':
132+
return [
133+
'query' => $_GET,
134+
'data' => $_POST,
135+
'files' => $_FILES,
136+
'session' => &$_SESSION,
137+
];
138+
}
139+
}
140+
141+
if (RuntimeInterface::class === $type) {
142+
return $this;
143+
}
144+
145+
$r = $parameter->getDeclaringFunction();
146+
147+
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)));
148+
}
149+
}
Lines 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+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
use Symfony\Component\Runtime\RuntimeInterface;
22+
use Symfony\Component\Runtime\SymfonyRuntime;
23+
24+
/**
25+
* @author Nicolas Grekas <p@tchwork.com>
26+
*
27+
* @internal
28+
*/
29+
class ComposerPlugin implements PluginInterface, EventSubscriberInterface
30+
{
31+
/**
32+
* @var Composer
33+
*/
34+
private $composer;
35+
36+
/**
37+
* @var IOInterface
38+
*/
39+
private $io;
40+
41+
private static $activated = false;
42+
43+
public function activate(Composer $composer, IOInterface $io): void
44+
{
45+
self::$activated = true;
46+
$this->composer = $composer;
47+
$this->io = $io;
48+
}
49+
50+
public function deactivate(Composer $composer, IOInterface $io): void
51+
{
52+
self::$activated = false;
53+
}
54+
55+
public function uninstall(Composer $composer, IOInterface $io): void
56+
{
57+
@unlink($composer->getConfig()->get('vendor-dir').'/autoload_runtime.php');
58+
}
59+
60+
public function updateAutoloadFile(): void
61+
{
62+
$vendorDir = $this->composer->getConfig()->get('vendor-dir');
63+
64+
if (!is_file($autoloadFile = $vendorDir.'/autoload.php')
65+
|| false === $extra = $this->composer->getPackage()->getExtra()['runtime'] ?? []
66+
) {
67+
return;
68+
}
69+
70+
$fs = new Filesystem();
71+
$projectDir = \dirname(realpath(Factory::getComposerFile()));
72+
73+
if (null === $autoloadTemplate = $extra['autoload_template'] ?? null) {
74+
$autoloadTemplate = __DIR__.'/autoload_runtime.template';
75+
} else {
76+
if (!$fs->isAbsolutePath($autoloadTemplate)) {
77+
$autoloadTemplate = $projectDir.'/'.$autoloadTemplate;
78+
}
79+
80+
if (!is_file($autoloadTemplate)) {
81+
throw new \InvalidArgumentException(sprintf('File "%s" defined under "extra.runtime.autoload_template" in your composer.json file not found.', $this->composer->getPackage()->getExtra()['runtime']['autoload_template']));
82+
}
83+
}
84+
741A 85+
$projectDir = $fs->makePathRelative($projectDir, $vendorDir);
86+
$nestingLevel = 0;
87+
88+
while (0 === strpos($projectDir, '../')) {
89+
++$nestingLevel;
90+
$projectDir = substr($projectDir, 3);
91+
}
92+
93+
if (!$nestingLevel) {
94+
$projectDir = '__'.'DIR__.'.var_export('/'.$projectDir, true);
95+
} else {
96+
$projectDir = 'dirname(__'."DIR__, $nestingLevel)".('' !== $projectDir ? var_export('/'.$projectDir, true) : '');
97+
}
98+
99+
$runtimeClass = $extra['class'] ?? SymfonyRuntime::class;
100+
101+
if (SymfonyRuntime::class !== $runtimeClass && !is_subclass_of($runtimeClass, RuntimeInterface::class)) {
102+
throw new \InvalidArgumentException(sprintf('Class "%s" listed under "extra.runtime.class" in your composer.json file '.(class_exists($runtimeClass) ? 'should implement "%s".' : 'not found.'), $runtimeClass, RuntimeInterface::class));
103+
}
104+
105+
if (!\is_array($runtimeOptions = $extra['options'] ?? [])) {
106+
throw new \InvalidArgumentException('The "extra.runtime.options" entry in your composer.json file must be an array.');
107+
}
108+
109+
$code = strtr(file_get_contents($autoloadTemplate), [
110+
'%project_dir%' => $projectDir,
111+
'%runtime_class%' => var_export($runtimeClass, true),
112+
'%runtime_options%' => '['.substr(var_export($runtimeOptions, true), 7, -1)." 'project_dir' => {$projectDir},\n]",
113+
]);
114+
115+
file_put_contents(substr_replace($autoloadFile, '_runtime', -4, 0), $code);
116+
}
117+
118+
public static function getSubscribedEvents(): array
119+
{
120+
if (!self::$activated) {
121+
return [];
122+
}
123+
124+
return [
125+
ScriptEvents::POST_AUTOLOAD_DUMP => 'updateAutoloadFile',
126+
];
127+
}
128+
}

0 commit comments

Comments
 (0)
0