diff --git a/README.md b/README.md index d8abb10..d735356 100644 --- a/README.md +++ b/README.md @@ -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 +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. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8b3119c..d50c461 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,17 +1,13 @@ - - - - - ./tests/ - - - - - - src - - - + + + + src + + + + + ./tests/ + + diff --git a/src/Handler.php b/src/Handler.php index be9cc77..460aaa1 100644 --- a/src/Handler.php +++ b/src/Handler.php @@ -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; @@ -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; diff --git a/src/Router.php b/src/Router.php index 6024f80..ed5c863 100644 --- a/src/Router.php +++ b/src/Router.php @@ -3,6 +3,7 @@ namespace Bref\DevServer; use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\Yaml\Yaml; use function is_array; @@ -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; @@ -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'] ?? '*'; @@ -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); } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index e7c690b..7691d08 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -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 = <<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 = <<match($this->request('GET', '/hello'))[0]); + } + + /** + * @test + */ + public function test_routing_with_api_gateway_v1_file_config(): void + { + $config = <<match($this->request('GET', '/hello'))[0]); + } + + /** + * @test + */ + public function test_routing_with_api_gateway_v2_file_config(): void + { + $config = <<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); diff --git a/tests/function/hello/v1serverless.yml b/tests/function/hello/v1serverless.yml new file mode 100644 index 0000000..70a4f23 --- /dev/null +++ b/tests/function/hello/v1serverless.yml @@ -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 diff --git a/tests/function/hello/v2serverless.yml b/tests/function/hello/v2serverless.yml new file mode 100644 index 0000000..45a8521 --- /dev/null +++ b/tests/function/hello/v2serverless.yml @@ -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: '*'