Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
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/setup-php@2.7.0
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"
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This file registers the Symfony container in Bref. It will be called for every invocation/request.

]
},
"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