Skip to content
Open
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
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,81 @@ The application will be available at [http://localhost:8000/](http://localhost:8

Routes will be parsed from `serverless.yml` in the current directory.

### Function Example
You can use this template as a sample for a simple function of your application.
Context will be mapped from `lambda-context` attribute of the request as stated [here](https://bref.sh/docs/use-cases/http/advanced-use-cases#lambda-event-and-context).

This function is giving the output as per [Lambda Proxy Integration Response spec](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format)

```php
<?php

namespace App;

use Bref\Event\Handler;
use Bref\Context\Context;

require 'vendor/autoload.php';

class HelloHandler implements Handler
{
/**
* @param $event
* @param Context|null $context
* @return array
*/
public function handle($event, ?Context $context): array
{
return [
"statusCode"=>200,
"headers"=>[
'Access-Control-Allow-Origin'=> '*',
'Access-Control-Allow-Credentials'=> true,
'Access-Control-Allow-Headers'=> '*',
],
"body"=>json_encode([
"message" =>'Bref! Your function executed successfully!',
"context" => $context,
"input" => $event
])
];

}
}

return new HelloHandler();


```

And its related `serverless.yaml` part under `functions`

```yaml
hello:
runtime: provided.al2
layers:
- ${bref:layer.php-81}
handler: src/function/hello/index.php #function handler
package: #package patterns
include:
- "!**/*"
- vendor/**
- src/function/hello/**
events: #events
#keep warm event
- schedule:
rate: rate(5 minutes)
enabled: true
input:
warmer: true
#api gateway event
- http:
path: /hello #api endpoint path
method: 'GET' #api endpoint method
cors: true

```

### Assets

By default, static assets are served from the current directory.
Expand Down
26 changes: 11 additions & 15 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit colors="true"
bootstrap="./vendor/autoload.php">

<testsuites>
<testsuite name="Test suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>

<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>

<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" bootstrap="./vendor/autoload.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Test suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
7 changes: 6 additions & 1 deletion src/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Bref\Bref;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Response;
use Nyholm\Psr7Server\ServerRequestCreator;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
Expand Down Expand Up @@ -44,7 +45,11 @@ public function handleRequest(): bool|null
$request = $this->requestFromGlobals();
[$handler, $request] = $router->match($request);
$controller = $handler ? $container->get($handler) : new NotFound;
$response = $controller->handle($request);
$context = $request->getAttribute('lambda-event')?->getRequestContext();
$response = $controller->handle($request, $context);
if (is_array($response)) {
$response = new Response(200, [], $response['body']);
}
(new ResponseEmitter)->emit($response);

return null;
Expand Down
37 changes: 34 additions & 3 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Bref\DevServer;

use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Yaml\Yaml;

use function is_array;

Expand All @@ -16,8 +17,11 @@ class Router
public static function fromServerlessConfig(array $serverlessConfig): self
{
$routes = [];
foreach ($serverlessConfig['functions'] as $function) {
$pattern = $function['events'][0]['httpApi'] ?? null;
foreach ($serverlessConfig['functions'] as $functionConfig) {
//Check if function definition is included by another file
$function = self::checkIfFunctionIsIncludedByFile($functionConfig);
//Get pattern by events array
$pattern = self::getPatternByEvents($function['events']);

if (! $pattern) {
continue;
Expand All @@ -33,6 +37,33 @@ public static function fromServerlessConfig(array $serverlessConfig): self
return new self($routes);
}

public static function checkIfFunctionIsIncludedByFile(string|array $functionConfig): mixed
{
//Check if function is included by another file with ${file(./my/function/path)} syntax
if (is_string($functionConfig) && str_contains($functionConfig, '${file')) {
$init = strpos($functionConfig, '(') + 1; //path is always after an open parenthesis
$end = strpos($functionConfig, ')'); //path is always closed by a closed parenthesis
$functionFilePath = substr($functionConfig, $init, $end - $init); //get file path
$functionAttributes = Yaml::parseFile($functionFilePath, Yaml::PARSE_CUSTOM_TAGS); //parse function file yaml
//first element of the attributes array has the name of the function
return reset($functionAttributes);
}
return $functionConfig;
}

public static function getPatternByEvents(array $events): mixed
{
//Cycle events as they could be multiple
foreach ($events as $event) {
if (isset($event['http'])) { //Search for API Gateway v1 syntax
return $event['http'];
} elseif (isset($event['httpApi'])) { //Or for API Gateway v2 syntax
return $event['httpApi'];
}
}
return null;
}

private static function patternToString(array $pattern): string
{
$method = $pattern['method'] ?? '*';
Expand Down Expand Up @@ -114,7 +145,7 @@ private function addPathParameters(ServerRequestInterface $request, mixed $pathP
}

$pathRegex = $this->patternToRegex($pathPattern);
preg_match($pathRegex, $requestPath, $matches);
preg_match($pathRegex, ltrim($requestPath, '/'), $matches);
foreach ($matches as $name => $value) {
$request = $request->withAttribute($name, $value);
}
Expand Down
80 changes: 80 additions & 0 deletions tests/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,86 @@ functions:
self::assertSame('api.php', $router->match($this->request('PUT', '/tests/1'))[0]);
}

/**
* @test
*/
public function test_routing_with_api_gateway_v1_wildcard_config(): void
{
$config = <<<YAML
functions:
api:
handler: api.php
events:
- http:
method: '*'
path: '*'
YAML;

$config = Yaml::parse($config);
$router = Router::fromServerlessConfig($config);

self::assertSame('api.php', $router->match($this->request('GET', '/'))[0]);
self::assertSame('api.php', $router->match($this->request('POST', '/'))[0]);
self::assertSame('api.php', $router->match($this->request('GET', '/tests/1'))[0]);
self::assertSame('api.php', $router->match($this->request('PUT', '/tests/1'))[0]);
}

/**
* @test
*/
public function test_routing_with_api_gateway_v1_specific_method_config(): void
{
$config = <<<YAML
functions:
hello:
handler: hello.php
events:
- http:
method: 'GET'
path: '/hello'
YAML;

$config = Yaml::parse($config);
$router = Router::fromServerlessConfig($config);

self::assertSame('hello.php', $router->match($this->request('GET', '/hello'))[0]);
}

/**
* @test
*/
public function test_routing_with_api_gateway_v1_file_config(): void
{
$config = <<<YAML
functions:
- \${file(./tests/function/hello/v1serverless.yml)}
YAML;

$config = Yaml::parse($config);
$router = Router::fromServerlessConfig($config);

self::assertSame('hello.php', $router->match($this->request('GET', '/hello'))[0]);
}

/**
* @test
*/
public function test_routing_with_api_gateway_v2_file_config(): void
{
$config = <<<YAML
functions:
- \${file(./tests/function/hello/v2serverless.yml)}
YAML;

$config = Yaml::parse($config);
$router = Router::fromServerlessConfig($config);

self::assertSame('hello.php', $router->match($this->request('GET', '/'))[0]);
self::assertSame('hello.php', $router->match($this->request('POST', '/'))[0]);
self::assertSame('hello.php', $router->match($this->request('GET', '/tests/1'))[0]);
self::assertSame('hello.php', $router->match($this->request('PUT', '/tests/1'))[0]);
}

private function request(string $method, string $path): ServerRequestInterface
{
return new ServerRequest($method, $path);
Expand Down
13 changes: 13 additions & 0 deletions tests/function/hello/v1serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
hello:
handler: hello.php #function handler
events: #events
#keep warm event
- schedule:
rate: rate(5 minutes)
enabled: true
input:
warmer: true
#api gateway event
- http:
path: /hello #api endpoint path
method: 'GET' #api endpoint method
13 changes: 13 additions & 0 deletions tests/function/hello/v2serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
hello:
handler: hello.php #function handler
events: #events
#keep warm event
- schedule:
rate: rate(5 minutes)
enabled: true
input:
warmer: true
#api gateway event
- httpApi:
method: '*'
path: '*'