Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions packages/http/src/OpaqueRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

declare(strict_types=1);

namespace Tempest\Http;

use Tempest\Container\Singleton;
use Tempest\Http\Cookie\Cookie;

#[Singleton]
final class OpaqueRequest implements Request
{
public function __construct(
private RequestHolder $requestHolder,
) {}

public array $cookies {
get {
return $this->requestHolder->request->cookies;
}
}

public array $files {
get {
return $this->requestHolder->request->files;
}
}

public array $query {
get {
return $this->requestHolder->request->query;
}
}

public string $path {
get {
return $this->requestHolder->request->path;
}
}
public RequestHeaders $headers {
get {
return $this->requestHolder->request->headers;
}
}

public array $body {
get {
return $this->requestHolder->request->body;
}
}

public ?string $raw {
get {
return $this->requestHolder->request->raw;
}
}

public string $uri {
get {
return $this->requestHolder->request->uri;
}
}

public Method $method {
get {
return $this->requestHolder->request->method;
}
}

public function has(string $key): bool
{
return $this->requestHolder->request->has($key);
}

public function hasBody(?string $key = null): bool
{
return $this->requestHolder->request->hasBody($key);
}

public function hasQuery(string $key): bool
{
return $this->requestHolder->request->hasQuery($key);
}

public function get(string $key, mixed $default = null): mixed
{
return $this->requestHolder->request->get($key, $default);
}

public function getSessionValue(string $name): mixed
{
return $this->requestHolder->request->getSessionValue($name);
}

public function getCookie(string $name): ?Cookie
{
return $this->requestHolder->request->getCookie($name);
}

public function accepts(ContentType ...$contentType): bool
{
return $this->requestHolder->request->accepts(...$contentType);
}
}
23 changes: 23 additions & 0 deletions packages/http/src/RequestHolder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Tempest\Http;

use Tempest\Container\Singleton;

#[Singleton]
final class RequestHolder
{
private(set) Request $request;

public function setRequest(Request $request): void
{
$this->request = $request;
}

public function clear(): void
{
unset($this->request);
}
}
76 changes: 76 additions & 0 deletions packages/router/src/WorkerApplication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Tempest\Router;

use Tempest\Container\Container;
use Tempest\Container\Singleton;
use Tempest\Core\Application;
use Tempest\Core\Kernel;
use Tempest\Core\Tempest;
use Tempest\Http\RequestFactory;
use Tempest\Http\RequestHolder;

#[Singleton]
final readonly class WorkerApplication implements Application
{
public function __construct(
private Container $container,
private int $maxLoops = -1,
) {}

/** @param \Tempest\Discovery\DiscoveryLocation[] $discoveryLocations */
public static function boot(string $root, array $discoveryLocations = [], int $maxLoops = -1): self
{
$container = Tempest::boot($root, $discoveryLocations);

return new self($container, $maxLoops);
}

public function run(): never
{
// Inspired from https://github.com/php-runtime/frankenphp-symfony/blob/main/src/Runner.php
// Prevent worker script termination when a client connection is interrupted
ignore_user_abort(true);

$requestFactory = $this->container->get(RequestFactory::class);
$responseSender = $this->container->get(ResponseSender::class);
$router = $this->container->get(WorkerRouter::class);
$requestHolder = $this->container->get(RequestHolder::class);

$server = array_filter($_SERVER, static fn (string $key) => ! str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY);

$handler = function () use ($server, $requestFactory, $responseSender, $router, $requestHolder): void {
// Merge the environment variables coming from DotEnv with the ones tied to the current request
$_SERVER += $server;

$psrRequest = $requestFactory->make();
$response = $router->dispatch($psrRequest);
$responseSender->send($response);
// TODO: this probably should be handled by RESET event I'm talking about below
$requestHolder->clear();
};

$loops = 0;

// it still allows to run application without frankenphp, but it will be a single request and then exit
// this is only useful for testing and development
if (! function_exists('frankenphp_handle_request')) {
$handler();

exit();
}

do {
$ret = \frankenphp_handle_request($handler);

// TODO: there should be some event to RESET state
// for example: CookieManager should reset its internal cookies state after each request

gc_collect_cycles();
} while ($ret && (-1 === $this->maxLoops || ++$loops < $this->maxLoops));

$this->container->get(Kernel::class)->shutdown();
}
}
36 changes: 36 additions & 0 deletions packages/router/src/WorkerRouter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Tempest\Router;

use Psr\Http\Message\ServerRequestInterface as PsrRequest;
use Tempest\Container\Container;
use Tempest\Http\Mappers\PsrRequestToGenericRequestMapper;
use Tempest\Http\OpaqueRequest;
use Tempest\Http\Request;
use Tempest\Http\RequestHolder;
use Tempest\Http\Response;

use function Tempest\Mapper\map;

final readonly class WorkerRouter implements Router
{
public function __construct(
private Container $container,
private Router $router,
private RequestHolder $requestHolder,
) {}

public function dispatch(Request|PsrRequest $request): Response
{
if (! $request instanceof Request) {
$request = map($request)->with(PsrRequestToGenericRequestMapper::class)->do();
}

$this->requestHolder->setRequest($request);
$this->container->singleton(Request::class, $this->container->get(OpaqueRequest::class));

return $this->router->dispatch($request);
}
}
12 changes: 12 additions & 0 deletions public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@

use Tempest\Discovery\DiscoveryLocation;
use Tempest\Router\HttpApplication;
use Tempest\Router\WorkerApplication;

require_once __DIR__ . '/../vendor/autoload.php';

if (function_exists('frankenphp_handle_request')) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a good idea, see #1792 (comment)

WorkerApplication::boot(
root: __DIR__ . '/../',
discoveryLocations: [
new DiscoveryLocation('Tests\\Tempest\\Fixtures\\', __DIR__ . '/../tests/Fixtures/'),
],
)->run();

exit();
}

HttpApplication::boot(__DIR__ . '/../', discoveryLocations: [
new DiscoveryLocation('Tests\\Tempest\\Fixtures\\', __DIR__ . '/../tests/Fixtures/'),
])->run();
Expand Down
10 changes: 10 additions & 0 deletions src/Tempest/Framework/Installers/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
declare(strict_types=1);

use Tempest\Router\HttpApplication;
use Tempest\Router\WorkerApplication;

require_once __DIR__ . '/../vendor/autoload.php';

if (function_exists('frankenphp_handle_request')) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a good idea, see #1792 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I fully agree. I guess in the final form of this PR I will just remove it, because as far as I understand from reading discord discussion, preparing tempest dockerfiles is another beast to deal with, and probably shouldn't even be handled in this PR. I made that draft-PR to mark/communicate that I'm willing to try work on worker mode support.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great, we're looking forward to your contribution! In this case, this comment might also be helpful, this is one of the known limitations.

WorkerApplication::boot(
root: __DIR__ . '/../',
maxLoops: 500, // TODO: make it configurable
)->run();

exit();
}

HttpApplication::boot(__DIR__ . '/../')->run();