Skip to content

Commit 6153719

Browse files
[Runtime] a new component to decouple apps from global state
1 parent 78df4ca commit 6153719

23 files changed

+590
-0
lines changed

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"symfony/property-info": "self.version",
8080
"symfony/proxy-manager-bridge": "self.version",
8181
"symfony/routing": "self.version",
82+
"symfony/runtime": "self.version",
8283
"symfony/security-core": "self.version",
8384
"symfony/security-csrf": "self.version",
8485
"symfony/security-guard": "self.version",
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.2.0
5+
-----
6+
7+
* added the component

src/Symfony/Component/Runtime/LICENSE

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2020 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Runtime Component
2+
=================
3+
4+
Symfony Runtime decouples your apps from global state.
5+
6+
Resources
7+
---------
8+
9+
* [Documentation](https://symfony.com/doc/current/components/runtime.html)
10+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
11+
* [Report issues](https://github.com/symfony/symfony/issues) and
12+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
13+
in the [main Symfony repository](https://github.com/symfony/symfony)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
/**
15+
* @author Nicolas Grekas <p@tchwork.com>
16+
*/
17+
interface RuntimeInterface
18+
{
19+
/**
20+
* Returns a closure that returns an object holding the userland app.
21+
*/
22+
public function getApp(\Closure $app): \Closure;
23+
24+
/**
25+
* Returns a closure with no arguments that returns the exit status as int.
26+
*/
27+
public function getMain(object $app): \Closure;
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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\Console\Application;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\ArgvInput;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\ConsoleOutput;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Dotenv\Dotenv;
22+
use Symfony\Component\ErrorHandler\Debug;
23+
use Symfony\Component\ErrorHandler\ErrorHandler;
24+
use Symfony\Component\HttpFoundation\Request;
25+
use Symfony\Component\HttpFoundation\Response;
26+
use Symfony\Component\HttpKernel\HttpKernelInterface;
27+
use Symfony\Component\HttpKernel\TerminableInterface;
28+
29+
/**
30+
* @author Nicolas Grekas <p@tchwork.com>
31+
*/
32+
class SymfonyRuntime implements RuntimeInterface
33+
{
34+
private $request;
35+
private $input;
36+
private $output;
37+
private $application;
38+
private $command;
39+
40+
public function __construct(array $options = [])
41+
{
42+
if (isset($_SERVER['argv']) && class_exists(ArgvInput::class)) {
43+
$this->getInput();
44+
}
45+
46+
if ($path = $options['dotenv'] ?? true) {
47+
if (!class_exists(Dotenv::class)) {
48+
throw new \LogicException(sprintf('You cannot use "%s" as the Dotenv component is not installed. Try running "composer require symfony/dotenv".', __CLASS__));
49+
}
50+
51+
(new Dotenv())->bootEnv(\is_string($path) ? $options['dotenv'] : (\dirname(__DIR__, 3).'/.env'));
52+
}
53+
54+
if (!class_exists(ErrorHandler::class)) {
55+
throw new \LogicException(sprintf('You cannot use "%s" as the ErrorHandler component is not installed. Try running "composer require symfony/error-handler".', __CLASS__));
56+
}
57+
58+
if ($_SERVER['APP_DEBUG']) {
59+
umask(0000);
60+
Debug::enable();
61+
} else {
62+
ErrorHandler::register();
63+
}
64+
}
65+
66+
public function getApp(\Closure $app): \Closure
67+
{
68+
$arguments = [];
69+
70+
try {
71+
foreach ((new \ReflectionFunction($app))->getParameters() as $parameter) {
72+
$arguments[] = $this->getArgument($parameter);
73+
}
74+
} catch (\InvalidArgumentException $e) {
75+
if (!$parameter->isOptional()) {
76+
throw $e;
77+
}
78+
}
79+
80+
if (!$arguments) {
81+
return $app;
82+
}
83+
84+
return static function () use ($app, $arguments) {
85+
return $app(...$arguments);
86+
};
87+
}
88+
89+
public function getMain(object $app): \Closure
90+
{
91+
if ($app instanceof HttpKernelInterface) {
92+
$request = $this->request ?? $this->request = Request::createFromGlobals();
93+
94+
return static function () use ($app, $request): int {
95+
$response = $app->handle($request);
96+
$response->send();
97+
98+
if ($app instanceof TerminableInterface) {
99+
$app->terminate($request, $response);
100+
}
101+
102+
return 0;
103+
};
104+
}
105+
106+
if ($app instanceof Response) {
107+
return static function () use ($app): int {
108+
$app->send();
109+
110+
return 0;
111+
};
112+
}
113+
114+
if ($app instanceof Command) {
115+
$application = $this->application ?? $this->application = new Application();
116+
$main = $this->getMain($application);
117+
118+
return static function () use ($app, $application, $main): int {
119+
$application->setName($app->getName() ?: $application->getName());
120+
121+
if (!$app->getName() || !$application->has($app->getName())) {
122+
$app->setName($_SERVER['argv'][0]);
123+
$application->add($app);
124+
}
125+
126+
$application->setDefaultCommand($app->getName(), true);
127+
128+
return $main();
129+
};
130+
}
131+
132+
if ($app instanceof Application) {
133+
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
134+
echo 'Warning: The console should be invoked via the CLI version of PHP, not the '.\PHP_SAPI.' SAPI'.\PHP_EOL;
135+
}
136+
137+
set_time_limit(0);
138+
$input = $this->getInput();
139+
140+
return static function () use ($app, $input): int {
141+
$definition = $app->getDefinition();
142+
143+
if (!$definition->hasOption('env')) {
144+
$definition->addOption(new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', $_SERVER['APP_ENV']));
145+
}
146+
147+
if (!$definition->hasOption('no-debug')) {
148+
$definition->addOption(new InputOption('--no-debug', null, InputOption::VALUE_NONE, 'Switches off debug mode.'));
149+
}
150+
151+
return $app->run($input);
152+
};
153+
}
154+
155+
throw new \LogicException(sprintf('Cannot handle return value of type "%s".', \get_class($app)));
156+
}
157+
158+
protected function getArgument(\ReflectionParameter $parameter)
159+
{
160+
switch ($parameter->getType()->getName()) {
161+
case 'array':
162+
if ('context' !== $parameter->name) {
163+
break;
164+
}
165+
166+
return $_SERVER;
167+
168+
case Request::class:
169+
return $this->request ?? $this->request = Request::createFromGlobals();
170+
171+
case InputInterface::class:
172+
return $this->getInput();
173+
174+
case OutputInterface::class:
175+
return $this->output ?? $this->output = new ConsoleOutput();
176+
177+
case Application::class:
178+
return $this->application ?? $this->application = new Application();
179+
180+
case Command::class:
181+
return $this->command ?? $this->command = new Command();
182+
}
183+
184+
throw new \InvalidArgumentException(sprintf('Cannot resolve argument "%s $%s".', $parameter->getType()->getName(), $parameter->name));
185+
}
186+
187+
private function getInput(): ArgvInput
188+
{
189+
if (null !== $this->input) {
190+
return $this->input;
191+
}
192+
193+
$input = new ArgvInput();
194+
195+
if (null !== $env = $input->getParameterOption(['--env', '-e'], null, true)) {
196+
putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
197+
}
198+
199+
if ($input->hasParameterOption('--no-debug', true)) {
200+
putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
201+
}
202+
203+
return $this->input = $input;
204+
}
205+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SOME_VAR=foo_bar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
use Symfony\Component\Console\Application;
4+
use Symfony\Component\Console\Command\Command;
5+
use Symfony\Component\Console\Input\InputInterface;
6+
use Symfony\Component\Console\Output\OutputInterface;
7+
8+
$_SERVER['SCRIPT_FILENAME'] = __FILE__;
9+
10+
require __DIR__.'/../../vendor/symfony/runtime/autoload.php';
11+
12+
return function (array $context) {
13+
$command = new Command('go');
14+
$command->setCode(function (InputInterface $input, OutputInterface $output) use ($context) {
15+
$output->write('OK Application '.$context['SOME_VAR']);
16+
});
17+
18+
$app = new Application();
19+
$app->add($command);
20+
$app->setDefaultCommand('go', true);
21+
22+
return $app;
23+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Test Application
3+
--INI--
4+
display_errors=1
5+
--SKIPIF--
6+
<?php file_exists(__DIR__.'/../../vendor/autoload.php') || die('skip'); ?>
7+
--FILE--
8+
<?php
9+
10+
require __DIR__.'/common.inc';
11+
require __DIR__.'/application.php';
12+
13+
?>
14+
--EXPECTF--
15+
OK Application foo_bar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
use Symfony\Component\Console\Command\Command;
4+
use Symfony\Component\Console\Input\InputInterface;
5+
use Symfony\Component\Console\Output\OutputInterface;
6+
7+
$_SERVER['SCRIPT_FILENAME'] = __FILE__;
8+
9+
require __DIR__.'/../../vendor/symfony/runtime/autoload.php';
10+
11+
return function (Command $command, InputInterface $input, OutputInterface $output, array $context) {
12+
$command->setCode(function () use ($input, $output, $context) {
13+
$output->write('OK Command '.$context['SOME_VAR']);
14+
});
15+
16+
return $command;
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Test Command
3+
--INI--
4+
display_errors=1
5+
--SKIPIF--
6+
<?php file_exists(__DIR__.'/../../vendor/autoload.php') || die('skip'); ?>
7+
--FILE--
8+
<?php
9+
10+
require __DIR__.'/common.inc';
11+
require __DIR__.'/command.php';
12+
13+
?>
14+
--EXPECTF--
15+
OK Command foo_bar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
use Symfony\Component\Console\Application;
4+
use Symfony\Component\Console\Command\Command;
5+
use Symfony\Component\Console\Input\InputInterface;
6+
use Symfony\Component\Console\Output\OutputInterface;
7+
8+
$_SERVER['SCRIPT_FILENAME'] = __FILE__;
9+
10+
require __DIR__.'/../../vendor/symfony/runtime/autoload.php';
11+
12+
return function (Application $app, Command $command, array $context) {
13+
$app->setVersion('1.2.3');
14+
$app->setName('Hello console');
15+
$command->setDescription('Hello description ');
16+
$command->setName('my_command');
17+
18+
$app->add($command->setCode(function (InputInterface $input, OutputInterface $output) use ($context) {
19+
$output->write('OK Command '.$context['SOME_VAR']);
20+
}));
21+
22+
return $app;
23+
};

0 commit comments

Comments
 (0)