-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/Tests export-ignore | ||
/phpunit.xml.dist export-ignore | ||
/.gitattributes export-ignore | ||
/.gitignore export-ignore |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/vendor/ | ||
/composer.lock | ||
/phpunit.xml |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Runtime; | ||
|
||
use Symfony\Component\Runtime\Internal\BasicErrorHandler; | ||
use Symfony\Component\Runtime\ResolvedApp\ClosureResolved; | ||
use Symfony\Component\Runtime\ResolvedApp\ScalarResolved; | ||
use Symfony\Component\Runtime\StartedApp\ClosureStarted; | ||
|
||
// Help opcache.preload discover always-needed symbols | ||
class_exists(ClosureResolved::class); | ||
class_exists(BasicErrorHandler::class); | ||
|
||
/** | ||
* A runtime to do bare-metal PHP without using superglobals. | ||
* | ||
* One option named "debug" is supported; it toggles displaying errors. | ||
* | ||
* The app-closure returned by the entry script must return either: | ||
* - "string" to echo the response content, or | ||
* - "int" to set the exit status code. | ||
* | ||
* The app-closure can declare arguments among either: | ||
* - "array $context" to get a local array similar to $_SERVER; | ||
* - "array $argv" to get the command line arguments when running on the CLI; | ||
* - "array $request" to get a local array with keys "query", "data", "files" and | ||
* "session", which map to $_GET, $_POST, $FILES and &$_SESSION respectively. | ||
* | ||
* The runtime sets up a strict error handler that throws | ||
* exceptions when a PHP warning/notice is raised. | ||
* | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
*/ | ||
class BaseRuntime implements RuntimeInterface | ||
{ | ||
private $debug; | ||
|
||
public function __construct(array $options = []) | ||
{ | ||
$this->debug = $options['debug'] ?? true; | ||
$errorHandler = new BasicErrorHandler($this->debug); | ||
set_error_handler($errorHandler); | ||
set_exception_handler([$errorHandler, 'handleException']); | ||
} | ||
|
||
public function resolve(\Closure $app): ResolvedAppInterface | ||
{ | ||
$arguments = []; | ||
$function = new \ReflectionFunction($app); | ||
|
||
try { | ||
foreach ($function->getParameters() as $parameter) { | ||
$arguments[] = $this->getArgument($parameter, $parameter->getType()); | ||
} | ||
} catch (\InvalidArgumentException $e) { | ||
if (!$parameter->isOptional()) { | ||
throw $e; | ||
} | ||
} | ||
|
||
$returnType = $function->getReturnType(); | ||
|
||
switch ($returnType instanceof \ReflectionNamedType ? $returnType->getName() : '') { | ||
case 'string': | ||
return new ScalarResolved(static function () use ($app, $arguments): int { | ||
echo $app(...$arguments); | ||
|
||
return 0; | ||
}); | ||
|
||
case 'int': | ||
case 'void': | ||
return new ScalarResolved(static function () use ($app, $arguments): int { | ||
return $app(...$arguments) ?? 0; | ||
}); | ||
} | ||
|
||
return new ClosureResolved($app, $arguments); | ||
} | ||
|
||
public function start(object $app): StartedAppInterface | ||
{ | ||
if (!$app instanceof \Closure) { | ||
throw new \LogicException(sprintf('"%s" doesn\'t know how to handle apps of type "%s".', get_debug_type($this), get_debug_type($app))); | ||
} | ||
|
||
if ($this->debug && (new \ReflectionFunction($app))->getNumberOfRequiredParameters()) { | ||
throw new \ArgumentCountError('Zero argument should be required by the closure returned by the app, but at least one is.'); | ||
} | ||
|
||
return new ClosureStarted($app); | ||
} | ||
|
||
protected function getArgument(\ReflectionParameter $parameter, ?\ReflectionType $type) | ||
{ | ||
$type = $type instanceof \ReflectionNamedType ? $type->getName() : ''; | ||
|
||
if (RuntimeInterface::class === $type) { | ||
return $this; | ||
} | ||
|
||
if ('array' !== $type) { | ||
throw new \InvalidArgumentException(sprintf('Cannot resolve argument "%s $%s".', $type, $parameter->name)); | ||
} | ||
|
||
switch ($parameter->name) { | ||
case 'context': | ||
$context = $_SERVER; | ||
|
||
if ($_ENV && !isset($_SERVER['PATH']) && !isset($_SERVER['Path'])) { | ||
$context += $_ENV; | ||
} | ||
|
||
return $context; | ||
|
||
case 'argv': | ||
return $_SERVER['argv'] ?? []; | ||
|
||
case 'request': | ||
return [ | ||
'query' => $_GET, | ||
'data' => $_POST, | ||
'files' => $_FILES, | ||
'session' => &$_SESSION, | ||
]; | ||
} | ||
|
||
throw new \InvalidArgumentException(sprintf('Cannot resolve array argument "$%s", did you mean "$context" or "$request"?', $parameter->name)); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
CHANGELOG | ||
========= | ||
|
||
5.2.0 | ||
----- | ||
|
||
* added the component |
58 changes: 58 additions & 0 deletions
58
src/Symfony/Component/Runtime/Internal/BasicErrorHandler.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Runtime\Internal; | ||
|
||
/** | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
* | ||
* @internal | ||
*/ | ||
class BasicErrorHandler | ||
{ | ||
public function __construct(bool $debug) | ||
{ | ||
error_reporting(-1); | ||
|
||
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { | ||
ini_set('display_errors', $debug); | ||
} elseif (!filter_var(ini_get('log_errors'), FILTER_VALIDATE_BOOLEAN) || ini_get('error_log')) { | ||
// CLI - display errors only if they're not already logged to STDERR | ||
ini_set('display_errors', 1); | ||
} | ||
|
||
if (0 <= ini_get('zend.assertions')) { | ||
ini_set('zend.assertions', 1); | ||
ini_set('assert.active', $debug); | ||
ini_set('assert.bail', 0); | ||
ini_set('assert.warning', 0); | ||
ini_set('assert.exception', 1); | ||
} | ||
} | ||
|
||
public function __invoke(int $type, string $msg, string $file, int $line): bool | ||
{ | ||
if ((E_DEPRECATED | E_USER_DEPRECATED) & $type) { | ||
return true; | ||
} | ||
|
||
if ((error_reporting() | E_ERROR | E_RECOVERABLE_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR) & $type) { | ||
throw new \ErrorException($msg, 0, $type, $file, $line); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
public function handleException(\Throwable $e): void | ||
{ | ||
echo "<pre>\n$e\n</pre>\n"; | ||
} | ||
} |
118 changes: 118 additions & 0 deletions
118
src/Symfony/Component/Runtime/Internal/ComposerPlugin.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Runtime\Internal; | ||
|
||
use Composer\Composer; | ||
use Composer\EventDispatcher\EventSubscriberInterface; | ||
use Composer\Factory; | ||
use Composer\IO\IOInterface; | ||
use Composer\Plugin\PluginInterface; | ||
use Composer\Script\ScriptEvents; | ||
use Symfony\Component\Filesystem\Filesystem; | ||
|
||
/** | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
* | ||
* @internal | ||
*/ | ||
class ComposerPlugin implements PluginInterface, EventSubscriberInterface | ||
{ | ||
/** | ||
* @var Composer | ||
*/ | ||
private $composer; | ||
|
||
/** | ||
* @var IOInterface | ||
*/ | ||
private $io; | ||
|
||
private static $activated = false; | ||
|
||
public function activate(Composer $composer, IOInterface $io) | ||
{ | ||
self::$activated = true; | ||
$this->composer = $composer; | ||
$this->io = $io; | ||
} | ||
|
||
public function deactivate(Composer $composer, IOInterface $io) | ||
{ | ||
self::$activated = false; | ||
} | ||
|
||
public function uninstall(Composer $composer, IOInterface $io) | ||
{ | ||
@unlink($composer->getConfig()->get('vendor-dir').'/autoload_runtime.php'); | ||
} | ||
|
||
public function updateAutoloadFile() | ||
{ | ||
$vendorDir = $this->composer->getConfig()->get('vendor-dir'); | ||
$autoloadFile = $vendorDir.'/autoload.php'; | ||
|
||
if (!file_exists($autoloadFile)) { | ||
return; | ||
} | ||
|
||
$projectDir = (new Filesystem())->makePathRelative(\dirname(realpath(Factory::getComposerFile())), $vendorDir); | ||
$nestingLevel = 0; | ||
|
||
while (0 === strpos($projectDir, '../')) { | ||
++$nestingLevel; | ||
$projectDir = substr($projectDir, 3); | ||
} | ||
|
||
if (!$nestingLevel) { | ||
$projectDir = '__DIR__.'.var_export('/'.$projectDir, true); | ||
} else { | ||
$projectDir = "dirname(__DIR__, $nestingLevel)".('' !== $projectDir ? var_export('/'.$projectDir, true) : ''); | ||
} | ||
|
||
$code = <<<'EOPHP' | ||
<?php | ||
|
||
// autoload.php @generated by Symfony Runtime | ||
|
||
use Symfony\Component\Runtime\SymfonyRuntime; | ||
|
||
if (true === (require_once __DIR__.'/autoload.php') || empty($_SERVER['SCRIPT_FILENAME'])) { | ||
return; | ||
} | ||
|
||
if (!($app = require $_SERVER['SCRIPT_FILENAME']) instanceof Closure) { | ||
throw \TypeError(sprintf('Invalid return value: \Closure expected, "%s" returned from "%s".', get_debug_type($app), $_SERVER['SCRIPT_FILENAME'])); | ||
} | ||
|
||
$runtime = $_SERVER['APP_RUNTIME'] ?? SymfonyRuntime::class; | ||
$runtime = new $runtime(($_SERVER['APP_RUNTIME_OPTIONS'] ?? []) + ['project_dir' => %project_dir%]); | ||
$app = $runtime->resolve($app)(); | ||
exit($runtime->start($app)()); | ||
|
||
EOPHP; | ||
|
||
file_put_contents(substr_replace($autoloadFile, '_runtime', -4, 0), strtr($code, [ | ||
'%project_dir%' => $projectDir, | ||
])); | ||
} | ||
|
||
public static function getSubscribedEvents(): array | ||
{ | ||
if (!self::$activated) { | ||
return []; | ||
} | ||
|
||
return [ | ||
ScriptEvents::POST_AUTOLOAD_DUMP => 'updateAutoloadFile', | ||
]; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Runtime\Internal; | ||
|
||
/** | ||
* @internal class that should be loaded only when symfony/dotenv is not installed | ||
*/ | ||
class MissingDotenv | ||
{ | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.