From 8432bbd04d3f41b4b7bd7bf3b13995940864fd18 Mon Sep 17 00:00:00 2001 From: ddesioeleva Date: Mon, 15 Apr 2024 12:31:20 +0200 Subject: [PATCH 1/9] Update router: - it should work with http (V1) as well as httpApi (V2) events - it should work whatever is the index of the http event in events list - it should work if function is contained is a separate file with serverless syntax --- src/Router.php | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Router.php b/src/Router.php index 6024f80..b6489d8 100644 --- a/src/Router.php +++ b/src/Router.php @@ -4,6 +4,7 @@ use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\Yaml\Yaml; use function is_array; /** @@ -16,8 +17,10 @@ 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) { + + $function = self::checkIfFunctionIsIncludedBySeparateFile($functionConfig); + $pattern = self::getPatternByEvents($function['events']); if (! $pattern) { continue; @@ -33,6 +36,34 @@ public static function fromServerlessConfig(array $serverlessConfig): self return new self($routes); } + public static function checkIfFunctionIsIncludedBySeparateFile($functionConfig, $function = null) + { + //Check if function is included by separate file with ${file(./my/function/path)} syntax + if (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 + $function = reset($functionAttributes); //first element of the attributes array has the name of the function + } + return $function; + } + + public static function getPatternByEvents($events, $pattern = null) + { + //Cycle events as they could be multiple + foreach ($events as $event) { + if (isset($event['http'])) { //Search for API Gateway v1 syntax + $pattern = $event['http']; + break; + } elseif (isset($event['httpApi'])) { //Or for API Gateway v1 syntax + $pattern = $event['httpApi']; + break; + } + } + return $pattern; + } + private static function patternToString(array $pattern): string { $method = $pattern['method'] ?? '*'; From 9b07aec2152387ddce2fd8d48cf4d5ac07906178 Mon Sep 17 00:00:00 2001 From: ddesioeleva Date: Mon, 15 Apr 2024 12:32:05 +0200 Subject: [PATCH 2/9] Update handler returning lambda-context from request attribute if found --- src/Handler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Handler.php b/src/Handler.php index be9cc77..f77b6ac 100644 --- a/src/Handler.php +++ b/src/Handler.php @@ -44,7 +44,8 @@ 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); (new ResponseEmitter)->emit($response); return null; From d13731a477fa74cc9d89138065994a207810a343 Mon Sep 17 00:00:00 2001 From: ddesioeleva Date: Mon, 15 Apr 2024 13:17:28 +0200 Subject: [PATCH 3/9] Fix router comment --- src/Router.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Router.php b/src/Router.php index b6489d8..b7ff34e 100644 --- a/src/Router.php +++ b/src/Router.php @@ -56,7 +56,7 @@ public static function getPatternByEvents($events, $pattern = null) if (isset($event['http'])) { //Search for API Gateway v1 syntax $pattern = $event['http']; break; - } elseif (isset($event['httpApi'])) { //Or for API Gateway v1 syntax + } elseif (isset($event['httpApi'])) { //Or for API Gateway v2 syntax $pattern = $event['httpApi']; break; } From f81b364d9a620cf90e8351ef8aa2e1a635e4863d Mon Sep 17 00:00:00 2001 From: ddesioeleva Date: Mon, 15 Apr 2024 13:17:45 +0200 Subject: [PATCH 4/9] Update README.md with a function example --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index d8abb10..b9b8a21 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,47 @@ 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 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). + +```php +getAttributes(); + $queryParams = $event->getQueryParams(); + $parsedBody = $event->getParsedBody(); + + $message = [ + "message" =>'Go Serverless v3.0! Your function executed successfully!', + "context" => $context, + "input" => [ + "attributes"=>$attributes, + "queryParams"=>$queryParams, + "parsedBody"=>$parsedBody + ] + ]; + + $status = 200; + + return new Response($status, [], json_encode($message)); + } +} + +return new Handler(); + +``` + + ### Assets By default, static assets are served from the current directory. From 11a5c3b5d42c586538bea664298bfe548b65c0a3 Mon Sep 17 00:00:00 2001 From: ddesioeleva Date: Tue, 16 Apr 2024 08:23:44 +0200 Subject: [PATCH 5/9] Fix cs --- phpunit.xml.dist | 26 +++++++++++--------------- src/Handler.php | 2 +- src/Router.php | 31 +++++++++++++++---------------- 3 files changed, 27 insertions(+), 32 deletions(-) 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 f77b6ac..ea388ab 100644 --- a/src/Handler.php +++ b/src/Handler.php @@ -45,7 +45,7 @@ public function handleRequest(): bool|null [$handler, $request] = $router->match($request); $controller = $handler ? $container->get($handler) : new NotFound; $context = $request->getAttribute('lambda-event')?->getRequestContext(); - $response = $controller->handle($request,$context); + $response = $controller->handle($request, $context); (new ResponseEmitter)->emit($response); return null; diff --git a/src/Router.php b/src/Router.php index b7ff34e..3ab7f0c 100644 --- a/src/Router.php +++ b/src/Router.php @@ -3,8 +3,8 @@ namespace Bref\DevServer; use Psr\Http\Message\ServerRequestInterface; - use Symfony\Component\Yaml\Yaml; + use function is_array; /** @@ -18,8 +18,9 @@ public static function fromServerlessConfig(array $serverlessConfig): self { $routes = []; foreach ($serverlessConfig['functions'] as $functionConfig) { - - $function = self::checkIfFunctionIsIncludedBySeparateFile($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) { @@ -36,32 +37,30 @@ public static function fromServerlessConfig(array $serverlessConfig): self return new self($routes); } - public static function checkIfFunctionIsIncludedBySeparateFile($functionConfig, $function = null) + public static function checkIfFunctionIsIncludedByFile(string|array $functionConfig): mixed { - //Check if function is included by separate file with ${file(./my/function/path)} syntax - if (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 + //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 - $function = reset($functionAttributes); //first element of the attributes array has the name of the function + //first element of the attributes array has the name of the function + return reset($functionAttributes); } - return $function; + return $functionConfig; } - public static function getPatternByEvents($events, $pattern = null) + 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 - $pattern = $event['http']; - break; + return $event['http']; } elseif (isset($event['httpApi'])) { //Or for API Gateway v2 syntax - $pattern = $event['httpApi']; - break; + return $event['httpApi']; } } - return $pattern; } private static function patternToString(array $pattern): string From 08a556d49f09704af2bb8032461f9a472bf16bdf Mon Sep 17 00:00:00 2001 From: ddesioeleva Date: Tue, 16 Apr 2024 08:37:48 +0200 Subject: [PATCH 6/9] Add more tests --- tests/RouterTest.php | 80 +++++++++++++++++++++++++++ tests/function/hello/v1serverless.yml | 13 +++++ tests/function/hello/v2serverless.yml | 13 +++++ 3 files changed, 106 insertions(+) create mode 100644 tests/function/hello/v1serverless.yml create mode 100644 tests/function/hello/v2serverless.yml 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: '*' From 55c156d040c35505ca220c213621b711077d1060 Mon Sep 17 00:00:00 2001 From: ddesioeleva Date: Tue, 16 Apr 2024 09:00:38 +0200 Subject: [PATCH 7/9] Fix router for path parameter --- src/Router.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Router.php b/src/Router.php index 3ab7f0c..ed5c863 100644 --- a/src/Router.php +++ b/src/Router.php @@ -61,6 +61,7 @@ public static function getPatternByEvents(array $events): mixed return $event['httpApi']; } } + return null; } private static function patternToString(array $pattern): string @@ -144,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); } From aa25288939dc45617727e8595283dea9a012eaee Mon Sep 17 00:00:00 2001 From: ddesioeleva Date: Tue, 16 Apr 2024 13:13:06 +0200 Subject: [PATCH 8/9] Fix handler for php event driven runtime --- src/Handler.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Handler.php b/src/Handler.php index ea388ab..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; @@ -46,6 +47,9 @@ public function handleRequest(): bool|null $controller = $handler ? $container->get($handler) : new NotFound; $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; From 67b55f7845021fd58d265684e41eab0839cb14eb Mon Sep 17 00:00:00 2001 From: ddesioeleva Date: Tue, 16 Apr 2024 13:20:32 +0200 Subject: [PATCH 9/9] Update documentation for function example --- README.md | 74 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b9b8a21..d735356 100644 --- a/README.md +++ b/README.md @@ -23,45 +23,79 @@ 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 function of your application. +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 getAttributes(); - $queryParams = $event->getQueryParams(); - $parsedBody = $event->getParsedBody(); - - $message = [ - "message" =>'Go Serverless v3.0! Your function executed successfully!', - "context" => $context, - "input" => [ - "attributes"=>$attributes, - "queryParams"=>$queryParams, - "parsedBody"=>$parsedBody - ] + 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 + ]) ]; - $status = 200; - - return new Response($status, [], json_encode($message)); } } -return new Handler(); +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