1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Yiisoft\Yii\Runner\RoadRunner; |
||
6 | |||
7 | use ErrorException; |
||
8 | use Exception; |
||
9 | use JsonException; |
||
10 | use Psr\Container\ContainerExceptionInterface; |
||
11 | use Psr\Container\ContainerInterface; |
||
12 | use Psr\Container\NotFoundExceptionInterface; |
||
13 | use Psr\Http\Message\ResponseInterface; |
||
14 | use ReflectionClass; |
||
15 | use RuntimeException; |
||
16 | use Spiral\RoadRunner\Environment; |
||
17 | use Spiral\RoadRunner\Environment\Mode; |
||
18 | use Spiral\RoadRunner\Http\PSR7WorkerInterface; |
||
19 | use Temporal\Worker\Transport\HostConnectionInterface; |
||
20 | use Temporal\Worker\WorkerFactoryInterface; |
||
21 | use Throwable; |
||
22 | use Yiisoft\Definitions\Exception\CircularReferenceException; |
||
23 | use Yiisoft\Definitions\Exception\InvalidConfigException; |
||
24 | use Yiisoft\Definitions\Exception\NotInstantiableException; |
||
25 | use Yiisoft\Di\NotFoundException; |
||
26 | use Yiisoft\Di\StateResetter; |
||
27 | use Yiisoft\ErrorHandler\ErrorHandler; |
||
28 | use Yiisoft\ErrorHandler\Renderer\HtmlRenderer; |
||
29 | use Yiisoft\Log\Logger; |
||
30 | use Yiisoft\Log\Target\File\FileTarget; |
||
31 | use Yiisoft\Yii\Http\Application; |
||
32 | use Yiisoft\Yii\Runner\ApplicationRunner; |
||
33 | use Yiisoft\Yii\Runner\RoadRunner\Temporal\TemporalDeclarationProvider; |
||
34 | |||
35 | use function gc_collect_cycles; |
||
36 | use function interface_exists; |
||
37 | |||
38 | /** |
||
39 | * `RoadRunnerHttpApplicationRunner` runs the Yii HTTP application using RoadRunner. |
||
40 | */ |
||
41 | final class RoadRunnerHttpApplicationRunner extends ApplicationRunner |
||
42 | { |
||
43 | private ?ErrorHandler $temporaryErrorHandler = null; |
||
44 | private ?PSR7WorkerInterface $psr7Worker = null; |
||
45 | private bool $isTemporalEnabled = false; |
||
46 | |||
47 | /** |
||
48 | * @param string $rootPath The absolute path to the project root. |
||
49 | * @param bool $debug Whether the debug mode is enabled. |
||
50 | * @param bool $checkEvents Whether to check events' configuration. |
||
51 | * @param string|null $environment The environment name. |
||
52 | * @param string $bootstrapGroup The bootstrap configuration group name. |
||
53 | * @param string $eventsGroup The events' configuration group name. |
||
54 | * @param string $diGroup The container definitions' configuration group name. |
||
55 | * @param string $diProvidersGroup The container providers' configuration group name. |
||
56 | * @param string $diDelegatesGroup The container delegates' configuration group name. |
||
57 | * @param string $diTagsGroup The container tags' configuration group name. |
||
58 | * @param string $paramsGroup The configuration parameters group name. |
||
59 | * @param array $nestedParamsGroups Configuration group names that are included into configuration parameters group. |
||
60 | * This is needed for recursive merging of parameters. |
||
61 | * @param array $nestedEventsGroups Configuration group names that are included into events' configuration group. |
||
62 | * This is needed for reverse and recursive merge of events' configurations. |
||
63 | * |
||
64 | * @psalm-param list<string> $nestedParamsGroups |
||
65 | * @psalm-param list<string> $nestedEventsGroups |
||
66 | */ |
||
67 | 12 | public function __construct( |
|
68 | string $rootPath, |
||
69 | bool $debug = false, |
||
70 | bool $checkEvents = false, |
||
71 | ?string $environment = null, |
||
72 | string $bootstrapGroup = 'bootstrap-web', |
||
73 | string $eventsGroup = 'events-web', |
||
74 | string $diGroup = 'di-web', |
||
75 | string $diProvidersGroup = 'di-providers-web', |
||
76 | string $diDelegatesGroup = 'di-delegates-web', |
||
77 | string $diTagsGroup = 'di-tags-web', |
||
78 | string $paramsGroup = 'params-web', |
||
79 | array $nestedParamsGroups = ['params'], |
||
80 | array $nestedEventsGroups = ['events'], |
||
81 | ) { |
||
82 | 12 | parent::__construct( |
|
83 | 12 | $rootPath, |
|
84 | 12 | $debug, |
|
85 | 12 | $checkEvents, |
|
86 | 12 | $environment, |
|
87 | 12 | $bootstrapGroup, |
|
88 | 12 | $eventsGroup, |
|
89 | 12 | $diGroup, |
|
90 | 12 | $diProvidersGroup, |
|
91 | 12 | $diDelegatesGroup, |
|
92 | 12 | $diTagsGroup, |
|
93 | 12 | $paramsGroup, |
|
94 | 12 | $nestedParamsGroups, |
|
95 | 12 | $nestedEventsGroups, |
|
96 | 12 | ); |
|
97 | } |
||
98 | |||
99 | /** |
||
100 | * Returns a new instance with the specified temporary error handler instance {@see ErrorHandler}. |
||
101 | * |
||
102 | * A temporary error handler is needed to handle the creation of configuration and container instances, |
||
103 | * then the error handler configured in your application configuration will be used. |
||
104 | * |
||
105 | * @param ErrorHandler $temporaryErrorHandler The temporary error handler instance. |
||
106 | */ |
||
107 | 2 | public function withTemporaryErrorHandler(ErrorHandler $temporaryErrorHandler): self |
|
108 | { |
||
109 | 2 | $new = clone $this; |
|
110 | 2 | $new->temporaryErrorHandler = $temporaryErrorHandler; |
|
111 | 2 | return $new; |
|
112 | } |
||
113 | |||
114 | /** |
||
115 | * Returns a new instance with the specified PSR-7 worker instance {@see PSR7WorkerInterface}. |
||
116 | * |
||
117 | * @param PSR7WorkerInterface $worker The PSR-7 worker instance. |
||
118 | */ |
||
119 | 12 | public function withPsr7Worker(PSR7WorkerInterface $worker): self |
|
120 | { |
||
121 | 12 | $new = clone $this; |
|
122 | 12 | $new->psr7Worker = $worker; |
|
123 | 12 | return $new; |
|
124 | } |
||
125 | |||
126 | /** |
||
127 | * Returns a new instance with enabled temporal support. |
||
128 | */ |
||
129 | 2 | public function withTemporalEnabled(bool $value): self |
|
130 | { |
||
131 | 2 | if (!$this->isTemporalSDKInstalled()) { |
|
132 | throw new Exception('Temporal SDK is not installed. To install the SDK run `composer require temporal/sdk`.'); |
||
133 | } |
||
134 | 2 | $new = clone $this; |
|
135 | 2 | $new->isTemporalEnabled = $value; |
|
136 | 2 | return $new; |
|
137 | } |
||
138 | |||
139 | /** |
||
140 | * {@inheritDoc} |
||
141 | * |
||
142 | * @throws CircularReferenceException|ErrorException|InvalidConfigException|JsonException |
||
143 | * @throws ContainerExceptionInterface|NotFoundException|NotFoundExceptionInterface|NotInstantiableException |
||
144 | */ |
||
145 | 11 | public function run(): void |
|
146 | { |
||
147 | // Register temporary error handler to catch error while container is building. |
||
148 | 11 | $temporaryErrorHandler = $this->createTemporaryErrorHandler(); |
|
149 | 11 | $this->registerErrorHandler($temporaryErrorHandler); |
|
150 | |||
151 | 11 | $container = $this->getContainer(); |
|
152 | |||
153 | // Register error handler with real container-configured dependencies. |
||
154 | /** @var ErrorHandler $actualErrorHandler */ |
||
155 | 11 | $actualErrorHandler = $container->get(ErrorHandler::class); |
|
156 | 11 | $this->registerErrorHandler($actualErrorHandler, $temporaryErrorHandler); |
|
157 | |||
158 | 11 | $this->runBootstrap(); |
|
159 | 11 | $this->checkEvents(); |
|
160 | |||
161 | 10 | $env = Environment::fromGlobals(); |
|
162 | |||
163 | 10 | if ($env->getMode() === Mode::MODE_TEMPORAL) { |
|
164 | 2 | if (!$this->isTemporalEnabled) { |
|
165 | 1 | throw new RuntimeException( |
|
166 | 1 | 'Temporal support is disabled. You should call `withTemporalEnabled(true)` to enable temporal support.', |
|
167 | 1 | ); |
|
168 | } |
||
169 | 1 | $this->runTemporal($container); |
|
170 | 1 | return; |
|
171 | } |
||
172 | 8 | if ($env->getMode() === Mode::MODE_HTTP) { |
|
173 | 7 | $this->runRoadRunner($container); |
|
174 | 7 | return; |
|
175 | } |
||
176 | |||
177 | 1 | throw new RuntimeException(sprintf( |
|
178 | 1 | 'Unsupported mode "%s", supported modes are: "%s".', |
|
179 | 1 | $env->getMode(), |
|
180 | 1 | implode('", "', [Mode::MODE_HTTP, Mode::MODE_TEMPORAL]), |
|
181 | 1 | )); |
|
182 | } |
||
183 | |||
184 | 11 | private function createTemporaryErrorHandler(): ErrorHandler |
|
185 | { |
||
186 | 11 | if ($this->temporaryErrorHandler !== null) { |
|
187 | 1 | return $this->temporaryErrorHandler; |
|
188 | } |
||
189 | |||
190 | 10 | $logger = new Logger([new FileTarget("$this->rootPath/runtime/logs/app.log")]); |
|
191 | 10 | return new ErrorHandler($logger, new HtmlRenderer()); |
|
192 | } |
||
193 | |||
194 | /** |
||
195 | * @throws ErrorException |
||
196 | */ |
||
197 | 11 | private function registerErrorHandler(ErrorHandler $registered, ErrorHandler $unregistered = null): void |
|
198 | { |
||
199 | 11 | $unregistered?->unregister(); |
|
200 | |||
201 | 11 | if ($this->debug) { |
|
202 | 11 | $registered->debug(); |
|
203 | } |
||
204 | |||
205 | 11 | $registered->register(); |
|
206 | } |
||
207 | |||
208 | 6 | private function afterRespond( |
|
209 | Application $application, |
||
210 | ContainerInterface $container, |
||
211 | ?ResponseInterface $response, |
||
212 | ): void { |
||
213 | 6 | $application->afterEmit($response); |
|
214 | /** @psalm-suppress MixedMethodCall */ |
||
215 | 6 | $container |
|
216 | 6 | ->get(StateResetter::class) |
|
217 | 6 | ->reset(); // We should reset the state of such services every request. |
|
218 | 6 | gc_collect_cycles(); |
|
219 | } |
||
220 | |||
221 | 7 | private function runRoadRunner(ContainerInterface $container): void |
|
222 | { |
||
223 | 7 | $worker = new RoadRunnerHttpWorker($container, $this->psr7Worker); |
|
224 | |||
225 | /** @var Application $application */ |
||
226 | 7 | $application = $container->get(Application::class); |
|
227 | 7 | $application->start(); |
|
228 | |||
229 | 7 | while (true) { |
|
230 | 7 | $request = $worker->waitRequest(); |
|
231 | 7 | $response = null; |
|
232 | |||
233 | 7 | if ($request === null) { |
|
234 | 7 | break; |
|
235 | } |
||
236 | |||
237 | 6 | if ($request instanceof Throwable) { |
|
238 | 1 | $response = $worker->respondWithError($request); |
|
239 | 1 | $this->afterRespond($application, $container, $response); |
|
240 | 1 | continue; |
|
241 | } |
||
242 | |||
243 | try { |
||
244 | 5 | $response = $application->handle($request); |
|
245 | 4 | $worker->respond($response); |
|
246 | 1 | } catch (Throwable $t) { |
|
247 | 1 | $response = $worker->respondWithError($t, $request); |
|
248 | } finally { |
||
249 | 5 | $this->afterRespond($application, $container, $response); |
|
250 | } |
||
251 | } |
||
252 | |||
253 | 7 | $application->shutdown(); |
|
254 | } |
||
255 | |||
256 | 1 | private function runTemporal(ContainerInterface $container): void |
|
257 | { |
||
258 | /** @var TemporalDeclarationProvider $temporalDeclarationProvider */ |
||
259 | 1 | $temporalDeclarationProvider = $container->get(TemporalDeclarationProvider::class); |
|
260 | /** @var HostConnectionInterface $host */ |
||
261 | 1 | $host = $container->get(HostConnectionInterface::class); |
|
262 | |||
263 | /** @var WorkerFactoryInterface $factory */ |
||
264 | 1 | $factory = $container->get(WorkerFactoryInterface::class); |
|
265 | 1 | $worker = $factory->newWorker('default'); |
|
266 | |||
267 | 1 | $workflows = $temporalDeclarationProvider->getWorkflows(); |
|
268 | 1 | $activities = $temporalDeclarationProvider->getActivities(); |
|
269 | |||
270 | 1 | $worker->registerWorkflowTypes(...$workflows); |
|
271 | |||
272 | /** @psalm-suppress MixedReturnStatement,MixedInferredReturnType */ |
||
273 | 1 | $activityFactory = static fn (ReflectionClass $class): object => $container->get($class->getName()); |
|
274 | 1 | $activityFinalizer = static function () use ($container): void { |
|
275 | /** @psalm-suppress MixedMethodCall */ |
||
276 | $container |
||
277 | ->get(StateResetter::class) |
||
278 | ->reset(); // We should reset the state of such services every request. |
||
279 | gc_collect_cycles(); |
||
280 | 1 | }; |
|
281 | |||
282 | 1 | foreach ($activities as $activity) { |
|
283 | $worker->registerActivity($activity, $activityFactory); |
||
284 | } |
||
285 | 1 | $worker->registerActivityFinalizer($activityFinalizer); |
|
286 | |||
287 | /** @psalm-suppress TooManyArguments */ |
||
288 | 1 | $factory->run($host); |
|
0 ignored issues
–
show
|
|||
289 | } |
||
290 | |||
291 | 2 | private function isTemporalSDKInstalled(): bool |
|
292 | { |
||
293 | 2 | return interface_exists(WorkerFactoryInterface::class); |
|
294 | } |
||
295 | } |
||
296 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.