Skip to content

Commit

Permalink
Merge pull request #45 from brefphp/support-class-handlers
Browse files Browse the repository at this point in the history
Handle requests with the Kernel + handle events with Symfony services - Symfony Runtime integration
  • Loading branch information
mnapoli authored Dec 30, 2021
2 parents b5248c9 + 3160d41 commit 02450b7
Show file tree
Hide file tree
Showing 15 changed files with 431 additions and 43 deletions.
11 changes: 4 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ jobs:
max-parallel: 10
fail-fast: false
matrix:
php: ['7.3', '7.4', '8.0', '8.1']
sf_version: ['4.4.*', '5.0.*', '5.2.*', '5.4.*', '6.0.*']
php: ['7.4', '8.0', '8.1']
sf_version: ['5.2.*', '5.4.*', '6.0.*']
exclude:
- php: 7.3
sf_version: 6.0.*

- php: 7.4
sf_version: 6.0.*

Expand Down Expand Up @@ -48,7 +45,7 @@ jobs:
- name: Set up PHP
uses: shivammathur/[email protected]
with:
php-version: 7.3
php-version: 7.4
coverage: pcov

- name: Checkout code
Expand All @@ -59,5 +56,5 @@ jobs:

- name: Run tests
env:
PHP_VERSION: 7.3
PHP_VERSION: 7.4
run: ./vendor/bin/phpunit -v --coverage-text
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/composer.phar
/composer.lock
.phpunit.result.cache
/var/
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
This package configures Symfony to run on AWS Lambda using [Bref](https://bref.sh/).
Run Symfony on AWS Lambda using the [Bref](https://bref.sh/) runtime.

[![Build Status](https://github.com/brefphp/symfony-bridge/workflows/Tests/badge.svg)](https://github.com/brefphp/symfony-bridge/actions)
[![Latest Version](https://img.shields.io/packagist/v/bref/symfony-bridge?style=flat-square)](https://packagist.org/packages/bref/symfony-bridge)
Expand Down Expand Up @@ -95,3 +95,69 @@ class Kernel extends BrefKernel
+ }
}
```

## Handling requests in a kept-alive process without FPM

> Note: this is an advanced topic. Don't bother with this unless you know what you are doing.

To handle HTTP requests via the Symfony Kernel, without using PHP-FPM, by keeping the process alive:

```diff
# serverless.yml
functions:
app:
- handler: public/index.php
+ handler: App\Kernel
layers:
# Switch from PHP-FPM to the "function" runtime:
- - ${bref:layer.php-80-fpm}
+ - ${bref:layer.php-80}
environment:
# The Symfony process will restart every 100 requests
BREF_LOOP_MAX: 100
```

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.

## Class handlers

To handle other events (e.g. [SQS messages with Symfony Messenger](https://github.com/brefphp/symfony-messenger)) via a class name:

```diff
# serverless.yml
functions:
sqsHandler:
- handler: bin/consumer.php
+ handler: App\Service\MyService
layers:
- ${bref:layer.php-80}
```

The service will be retrieved via Symfony Runtime from the Symfony Kernel returned by `public/index.php`.

> Note: the service must be configured as **public** (`public: true`) in the Symfony configuration.

### Custom bootstrap file

If you do not have a `public/index.php` file, you can create a file that returns the kernel (or any PSR-11 container):

```php
<?php
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new App\Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
```

And configure it in `serverless.yml`:

```diff
# serverless.yml
functions:
sqsHandler:
handler: kernel.php:App\Service\MyService
```
25 changes: 16 additions & 9 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,32 @@
"autoload": {
"psr-4": {
"Bref\\SymfonyBridge\\": "src/"
}
},
"files": [
"src/bootstrap.php"
]
},
"autoload-dev": {
"psr-4": {
"Bref\\SymfonyBridge\\Test\\": "tests/"
}
},
"require": {
"php": ">=7.3",
"bref/bref": "^1.0",
"symfony/filesystem": "^4.4|^5.0|^6.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0"
"php": ">=7.4",
"bref/bref": "^1.2",
"symfony/filesystem": "^5.2|^6.0",
"symfony/http-kernel": "^5.2|^6.0",
"symfony/psr-http-message-bridge": "^2.1",
"symfony/runtime": "^5.2|^6.0"
},
"require-dev": {
"mnapoli/hard-mode": "^0.3.0",
"phpunit/phpunit": "^8.0",
"symfony/config": "^4.4|^5.0|^6.0",
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
"symfony/process": "^4.4|^5.0|^6.0"
"phpstan/phpstan": "^1.3",
"phpunit/phpunit": "^8.5.22",
"symfony/config": "^5.2|^6.0",
"symfony/dependency-injection": "^5.2|^6.0",
"symfony/framework-bundle": "^5.2|^6.0",
"symfony/process": "^5.2|^6.0"
},
"config": {
"sort-packages": true,
Expand Down
143 changes: 143 additions & 0 deletions src/HandlerResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php declare(strict_types=1);

namespace Bref\SymfonyBridge;

use Bref\Runtime\FileHandlerLocator;
use Bref\SymfonyBridge\Http\KernelAdapter;
use Bref\SymfonyBridge\Runtime\BrefRuntime;
use Exception;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelInterface;

/**
* This class resolves handlers.
*
* For example, if we configure `handler: xyz` in serverless.yml, then Bref
* will call this class to resolve `xyz` into the real Lambda handler.
*/
class HandlerResolver implements ContainerInterface
{
private ?ContainerInterface $symfonyContainer;
private FileHandlerLocator $fileLocator;

public function __construct()
{
// Bref's default handler resolver
$this->fileLocator = new FileHandlerLocator;
$this->symfonyContainer = null;
}

/**
* {@inheritDoc}
*/
public function get($id)
{
$isComposed = strpos($id, ':') !== false;

// By default we check if the handler is a file name (classic Bref behavior)
if (! $isComposed && $this->fileLocator->has($id)) {
return $this->fileLocator->get($id);
}

$service = $id;

$bootstrapFile = null;
if ($isComposed) {
[$bootstrapFile, $service] = explode(':', $id, 2);
}

// If not, we try to get the handler from the Symfony container
$handler = $this->symfonyContainer($bootstrapFile)->get($service);

// If the kernel was configured as a handler, then we wrap it to make it a valid HTTP handler for Lambda
if ($handler instanceof HttpKernelInterface) {
$handler = new KernelAdapter($handler);
}

return $handler;
}

/**
* {@inheritDoc}
*/
public function has($id): bool
{
$isComposed = strpos($id, ':') !== false;

// By default we check if the handler is a file name (classic Bref behavior)
if (! $isComposed && $this->fileLocator->has($id)) {
return true;
}

$service = $id;

$bootstrapFile = null;
if ($isComposed) {
[$bootstrapFile, $service] = explode(':', $id, 2);
}

// If not, we try to get the handler from the Symfony container
return $this->symfonyContainer($bootstrapFile)->has($service);
}

/**
* Create and return the Symfony container.
*/
private function symfonyContainer(?string $bootstrapFile = null): ContainerInterface
{
// Only create it once
if (! $this->symfonyContainer) {
$bootstrapFile = $bootstrapFile ?: 'public/index.php';

if (! file_exists($bootstrapFile)) {
throw new Exception(
"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."
);
}

$app = require $bootstrapFile;

if (! is_object($app)) {
throw new Exception(sprintf(
"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).",
$bootstrapFile,
// @phpstan-ignore-next-line
is_object($app) ? get_class($app) : gettype($app),
));
}

$projectDir = getenv('LAMBDA_TASK_ROOT') ?: null;

// Use the Symfony Runtime component to resolve the closure and get the PSR-11 container
$options = $_SERVER['APP_RUNTIME_OPTIONS'] ?? [];
if ($projectDir) {
$options['project_dir'] = $projectDir;
}
$runtime = new BrefRuntime($options);

[$app, $args] = $runtime
->getResolver($app)
->resolve();

$container = $app(...$args);

if ($container instanceof KernelInterface) {
$container->boot();
$container = $container->getContainer();
}

if (! $container instanceof ContainerInterface) {
throw new Exception(sprintf(
"The closure returned by '%s' must return either a Symfony Kernel or a PSR-11 container. Instead it returned '%s'",
$bootstrapFile,
is_object($container) ? get_class($container) : gettype($container),
));
}

$this->symfonyContainer = $container;
}

return $this->symfonyContainer;
}
}
49 changes: 49 additions & 0 deletions src/Http/KernelAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types=1);

namespace Bref\SymfonyBridge\Http;

use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\TerminableInterface;

/**
* This turns a Symfony Kernel into a PSR-15 handler.
*
* That means the Symfony Kernel can now be used by Bref (which supports PSR-15)
* to handle HTTP requests from API Gateway.
*/
class KernelAdapter implements RequestHandlerInterface
{
private HttpKernelInterface $kernel;
// PSR-15 to Symfony converters
private HttpFoundationFactory $symfonyFactory;
private PsrHttpFactory $psrFactory;

public function __construct(HttpKernelInterface $kernel)
{
$this->kernel = $kernel;
$this->symfonyFactory = new HttpFoundationFactory;
$psr17Factory = new Psr17Factory;
$this->psrFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
// From PSR-7 to Symfony
$symfonyRequest = $this->symfonyFactory->createRequest($request);

$symfonyResponse = $this->kernel->handle($symfonyRequest);

if ($this->kernel instanceof TerminableInterface) {
$this->kernel->terminate($symfonyRequest, $symfonyResponse);
}

// From Symfony to PSR-7
return $this->psrFactory->createResponse($symfonyResponse);
}
}
9 changes: 9 additions & 0 deletions src/Runtime/BrefRuntime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);

namespace Bref\SymfonyBridge\Runtime;

use Symfony\Component\Runtime\SymfonyRuntime;

class BrefRuntime extends SymfonyRuntime
{
}
14 changes: 14 additions & 0 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types=1);

use Bref\Bref;
use Bref\SymfonyBridge\HandlerResolver;

/**
* File executed when the application starts: it registers a Bref PSR-11 "handler resolver".
*
* This is what Bref will use to turn handler names (strings defined in serverless.yml/AWS Lambda)
* into classes that can handle the Lambda events.
*/
if (class_exists(Bref::class)) {
Bref::setContainer(static fn () => new HandlerResolver);
}
7 changes: 7 additions & 0 deletions tests/Fixtures/MyService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php declare(strict_types=1);

namespace Bref\SymfonyBridge\Test\Fixtures;

class MyService
{
}
Loading

0 comments on commit 02450b7

Please sign in to comment.