Skip to content

Commit 02450b7

Browse files
authored
Merge pull request #45 from brefphp/support-class-handlers
Handle requests with the Kernel + handle events with Symfony services - Symfony Runtime integration
2 parents b5248c9 + 3160d41 commit 02450b7

15 files changed

Lines changed: 431 additions & 43 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@ jobs:
1010
max-parallel: 10
1111
fail-fast: false
1212
matrix:
13-
php: ['7.3', '7.4', '8.0', '8.1']
14-
sf_version: ['4.4.*', '5.0.*', '5.2.*', '5.4.*', '6.0.*']
13+
php: ['7.4', '8.0', '8.1']
14+
sf_version: ['5.2.*', '5.4.*', '6.0.*']
1515
exclude:
16-
- php: 7.3
17-
sf_version: 6.0.*
18-
1916
- php: 7.4
2017
sf_version: 6.0.*
2118

@@ -48,7 +45,7 @@ jobs:
4845
- name: Set up PHP
4946
uses: shivammathur/setup-php@2.7.0
5047
with:
51-
php-version: 7.3
48+
php-version: 7.4
5249
coverage: pcov
5350

5451
- name: Checkout code
@@ -59,5 +56,5 @@ jobs:
5956

6057
- name: Run tests
6158
env:
62-
PHP_VERSION: 7.3
59+
PHP_VERSION: 7.4
6360
run: ./vendor/bin/phpunit -v --coverage-text

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
/composer.phar
33
/composer.lock
44
.phpunit.result.cache
5+
/var/

README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
This package configures Symfony to run on AWS Lambda using [Bref](https://bref.sh/).
1+
Run Symfony on AWS Lambda using the [Bref](https://bref.sh/) runtime.
22

33
[![Build Status](https://github.com/brefphp/symfony-bridge/workflows/Tests/badge.svg)](https://github.com/brefphp/symfony-bridge/actions)
44
[![Latest Version](https://img.shields.io/packagist/v/bref/symfony-bridge?style=flat-square)](https://packagist.org/packages/bref/symfony-bridge)
@@ -95,3 +95,69 @@ class Kernel extends BrefKernel
9595
+ }
9696
}
9797
```
98+
99+
## Handling requests in a kept-alive process without FPM
100+
101+
> Note: this is an advanced topic. Don't bother with this unless you know what you are doing.
102+
103+
To handle HTTP requests via the Symfony Kernel, without using PHP-FPM, by keeping the process alive:
104+
105+
```diff
106+
# serverless.yml
107+
108+
functions:
109+
app:
110+
- handler: public/index.php
111+
+ handler: App\Kernel
112+
layers:
113+
# Switch from PHP-FPM to the "function" runtime:
114+
- - ${bref:layer.php-80-fpm}
115+
+ - ${bref:layer.php-80}
116+
environment:
117+
# The Symfony process will restart every 100 requests
118+
BREF_LOOP_MAX: 100
119+
```
120+
121+
The `App\Kernel` will be retrieved via Symfony Runtime from `public/index.php`. If you don't have a `public/index.php`, read the next sections.
122+
123+
## Class handlers
124+
125+
To handle other events (e.g. [SQS messages with Symfony Messenger](https://github.com/brefphp/symfony-messenger)) via a class name:
126+
127+
```diff
128+
# serverless.yml
129+
130+
functions:
131+
sqsHandler:
132+
- handler: bin/consumer.php
133+
+ handler: App\Service\MyService
134+
layers:
135+
- ${bref:layer.php-80}
136+
```
137+
138+
The service will be retrieved via Symfony Runtime from the Symfony Kernel returned by `public/index.php`.
139+
140+
> Note: the service must be configured as **public** (`public: true`) in the Symfony configuration.
141+
142+
### Custom bootstrap file
143+
144+
If you do not have a `public/index.php` file, you can create a file that returns the kernel (or any PSR-11 container):
145+
146+
```php
147+
<?php
148+
149+
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
150+
151+
return function (array $context) {
152+
return new App\Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
153+
};
154+
```
155+
156+
And configure it in `serverless.yml`:
157+
158+
```diff
159+
# serverless.yml
160+
functions:
161+
sqsHandler:
162+
handler: kernel.php:App\Service\MyService
163+
```

composer.json

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,32 @@
1111
"autoload": {
1212
"psr-4": {
1313
"Bref\\SymfonyBridge\\": "src/"
14-
}
14+
},
15+
"files": [
16+
"src/bootstrap.php"
17+
]
1518
},
1619
"autoload-dev": {
1720
"psr-4": {
1821
"Bref\\SymfonyBridge\\Test\\": "tests/"
1922
}
2023
},
2124
"require": {
22-
"php": ">=7.3",
23-
"bref/bref": "^1.0",
24-
"symfony/filesystem": "^4.4|^5.0|^6.0",
25-
"symfony/http-kernel": "^4.4|^5.0|^6.0"
25+
"php": ">=7.4",
26+
"bref/bref": "^1.2",
27+
"symfony/filesystem": "^5.2|^6.0",
28+
"symfony/http-kernel": "^5.2|^6.0",
29+
"symfony/psr-http-message-bridge": "^2.1",
30+
"symfony/runtime": "^5.2|^6.0"
2631
},
2732
"require-dev": {
2833
"mnapoli/hard-mode": "^0.3.0",
29-
"phpunit/phpunit": "^8.0",
30-
"symfony/config": "^4.4|^5.0|^6.0",
31-
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
32-
"symfony/process": "^4.4|^5.0|^6.0"
34+
"phpstan/phpstan": "^1.3",
35+
"phpunit/phpunit": "^8.5.22",
36+
"symfony/config": "^5.2|^6.0",
37+
"symfony/dependency-injection": "^5.2|^6.0",
38+
"symfony/framework-bundle": "^5.2|^6.0",
39+
"symfony/process": "^5.2|^6.0"
3340
},
3441
"config": {
3542
"sort-packages": true,

src/HandlerResolver.php

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bref\SymfonyBridge;
4+
5+
use Bref\Runtime\FileHandlerLocator;
6+
use Bref\SymfonyBridge\Http\KernelAdapter;
7+
use Bref\SymfonyBridge\Runtime\BrefRuntime;
8+
use Exception;
9+
use Psr\Container\ContainerInterface;
10+
use Symfony\Component\HttpKernel\HttpKernelInterface;
11+
use Symfony\Component\HttpKernel\KernelInterface;
12+
13+
/**
14+
* This class resolves handlers.
15+
*
16+
* For example, if we configure `handler: xyz` in serverless.yml, then Bref
17+
* will call this class to resolve `xyz` into the real Lambda handler.
18+
*/
19+
class HandlerResolver implements ContainerInterface
20+
{
21+
private ?ContainerInterface $symfonyContainer;
22+
private FileHandlerLocator $fileLocator;
23+
24+
public function __construct()
25+
{
26+
// Bref's default handler resolver
27+
$this->fileLocator = new FileHandlerLocator;
28+
$this->symfonyContainer = null;
29+
}
30+
31+
/**
32+
* {@inheritDoc}
33+
*/
34+
public function get($id)
35+
{
36+
$isComposed = strpos($id, ':') !== false;
37+
38+
// By default we check if the handler is a file name (classic Bref behavior)
39+
if (! $isComposed && $this->fileLocator->has($id)) {
40+
return $this->fileLocator->get($id);
41+
}
42+
43+
$service = $id;
44+
45+
$bootstrapFile = null;
46+
if ($isComposed) {
47+
[$bootstrapFile, $service] = explode(':', $id, 2);
48+
}
49+
50+
// If not, we try to get the handler from the Symfony container
51+
$handler = $this->symfonyContainer($bootstrapFile)->get($service);
52+
53+
// If the kernel was configured as a handler, then we wrap it to make it a valid HTTP handler for Lambda
54+
if ($handler instanceof HttpKernelInterface) {
55+
$handler = new KernelAdapter($handler);
56+
}
57+
58+
return $handler;
59+
}
60+
61+
/**
62+
* {@inheritDoc}
63+
*/
64+
public function has($id): bool
65+
{
66+
$isComposed = strpos($id, ':') !== false;
67+
68+
// By default we check if the handler is a file name (classic Bref behavior)
69+
if (! $isComposed && $this->fileLocator->has($id)) {
70+
return true;
71+
}
72+
73+
$service = $id;
74+
75+
$bootstrapFile = null;
76+
if ($isComposed) {
77+
[$bootstrapFile, $service] = explode(':', $id, 2);
78+
}
79+
80+
// If not, we try to get the handler from the Symfony container
81+
return $this->symfonyContainer($bootstrapFile)->has($service);
82+
}
83+
84+
/**
85+
* Create and return the Symfony container.
86+
*/
87+
private function symfonyContainer(?string $bootstrapFile = null): ContainerInterface
88+
{
89+
// Only create it once
90+
if (! $this->symfonyContainer) {
91+
$bootstrapFile = $bootstrapFile ?: 'public/index.php';
92+
93+
if (! file_exists($bootstrapFile)) {
94+
throw new Exception(
95+
"Cannot find file '$bootstrapFile': the Bref-Symfony bridge tried to require that file to get the Symfony kernel. If your application does not have that file, follow the Bref-Symfony documentation to create and configure a file that returns the Symfony Kernel."
96+
);
97+
}
98+
99+
$app = require $bootstrapFile;
100+
101+
if (! is_object($app)) {
102+
throw new Exception(sprintf(
103+
"The '%s' file must return an anonymous function (that returns the Symfony Kernel). Instead it returned '%s'. Either edit the file to return an anonymous function, or create a separate file (follow the online documentation to do so).",
104+
$bootstrapFile,
105+
// @phpstan-ignore-next-line
106+
is_object($app) ? get_class($app) : gettype($app),
107+
));
108+
}
109+
110+
$projectDir = getenv('LAMBDA_TASK_ROOT') ?: null;
111+
112+
// Use the Symfony Runtime component to resolve the closure and get the PSR-11 container
113+
$options = $_SERVER['APP_RUNTIME_OPTIONS'] ?? [];
114+
if ($projectDir) {
115+
$options['project_dir'] = $projectDir;
116+
}
117+
$runtime = new BrefRuntime($options);
118+
119+
[$app, $args] = $runtime
120+
->getResolver($app)
121+
->resolve();
122+
123+
$container = $app(...$args);
124+
125+
if ($container instanceof KernelInterface) {
126+
$container->boot();
127+
$container = $container->getContainer();
128+
}
129+
130+
if (! $container instanceof ContainerInterface) {
131+
throw new Exception(sprintf(
132+
"The closure returned by '%s' must return either a Symfony Kernel or a PSR-11 container. Instead it returned '%s'",
133+
$bootstrapFile,
134+
is_object($container) ? get_class($container) : gettype($container),
135+
));
136+
}
137+
138+
$this->symfonyContainer = $container;
139+
}
140+
141+
return $this->symfonyContainer;
142+
}
143+
}

src/Http/KernelAdapter.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bref\SymfonyBridge\Http;
4+
5+
use Nyholm\Psr7\Factory\Psr17Factory;
6+
use Psr\Http\Message\ResponseInterface;
7+
use Psr\Http\Message\ServerRequestInterface;
8+
use Psr\Http\Server\RequestHandlerInterface;
9+
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
10+
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
11+
use Symfony\Component\HttpKernel\HttpKernelInterface;
12+
use Symfony\Component\HttpKernel\TerminableInterface;
13+
14+
/**
15+
* This turns a Symfony Kernel into a PSR-15 handler.
16+
*
17+
* That means the Symfony Kernel can now be used by Bref (which supports PSR-15)
18+
* to handle HTTP requests from API Gateway.
19+
*/
20+
class KernelAdapter implements RequestHandlerInterface
21+
{
22+
private HttpKernelInterface $kernel;
23+
// PSR-15 to Symfony converters
24+
private HttpFoundationFactory $symfonyFactory;
25+
private PsrHttpFactory $psrFactory;
26+
27+
public function __construct(HttpKernelInterface $kernel)
28+
{
29+
$this->kernel = $kernel;
30+
$this->symfonyFactory = new HttpFoundationFactory;
31+
$psr17Factory = new Psr17Factory;
32+
$this->psrFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
33+
}
34+
35+
public function handle(ServerRequestInterface $request): ResponseInterface
36+
{
37+
// From PSR-7 to Symfony
38+
$symfonyRequest = $this->symfonyFactory->createRequest($request);
39+
40+
$symfonyResponse = $this->kernel->handle($symfonyRequest);
41+
42+
if ($this->kernel instanceof TerminableInterface) {
43+
$this->kernel->terminate($symfonyRequest, $symfonyResponse);
44+
}
45+
46+
// From Symfony to PSR-7
47+
return $this->psrFactory->createResponse($symfonyResponse);
48+
}
49+
}

src/Runtime/BrefRuntime.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bref\SymfonyBridge\Runtime;
4+
5+
use Symfony\Component\Runtime\SymfonyRuntime;
6+
7+
class BrefRuntime extends SymfonyRuntime
8+
{
9+
}

src/bootstrap.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types=1);
2+
3+
use Bref\Bref;
4+
use Bref\SymfonyBridge\HandlerResolver;
5+
6+
/**
7+
* File executed when the application starts: it registers a Bref PSR-11 "handler resolver".
8+
*
9+
* This is what Bref will use to turn handler names (strings defined in serverless.yml/AWS Lambda)
10+
* into classes that can handle the Lambda events.
11+
*/
12+
if (class_exists(Bref::class)) {
13+
Bref::setContainer(static fn () => new HandlerResolver);
14+
}

tests/Fixtures/MyService.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bref\SymfonyBridge\Test\Fixtures;
4+
5+
class MyService
6+
{
7+
}

0 commit comments

Comments
 (0)