Skip to content

Commit 4e59cea

Browse files
authored
feat: uses guzzle/psr and psr-7 instead of http-foundation (GoogleCloudPlatform#28)
1 parent 94523da commit 4e59cea

12 files changed

+249
-39
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"description": "Google Cloud Functions Framework for PHP",
44
"license": "Apache-2.0",
55
"require": {
6-
"symfony/http-foundation": "^4.1"
6+
"guzzlehttp/psr7": "^1.6",
7+
"psr/http-message": "^1.0"
78
},
89
"autoload": {
910
"psr-4": {

router.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
* limitations under the License.
1616
*/
1717

18+
use Google\CloudFunctions\Emitter;
19+
use Google\CloudFunctions\Invoker;
20+
1821
/**
1922
* Determine the autoload file to load.
2023
*/
@@ -68,6 +71,7 @@
6871

6972
$signatureType = getenv('FUNCTION_SIGNATURE_TYPE', true) ?: 'http';
7073

71-
$invoker = new Google\CloudFunctions\Invoker($target, $signatureType);
72-
$invoker->handle()->send();
74+
$invoker = new Invoker($target, $signatureType);
75+
$response = $invoker->handle();
76+
(new Emitter())->emit($response);
7377
})();

src/BackgroundFunctionWrapper.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
namespace Google\CloudFunctions;
1919

20-
use Symfony\Component\HttpFoundation\Request;
21-
use Symfony\Component\HttpFoundation\Response;
20+
use GuzzleHttp\Psr7\Response;
21+
use Psr\Http\Message\ResponseInterface;
22+
use Psr\Http\Message\ServerRequestInterface;
2223
use RuntimeException;
2324

2425
class BackgroundFunctionWrapper extends FunctionWrapper
@@ -28,9 +29,9 @@ public function __construct(callable $function)
2829
parent::__construct($function);
2930
}
3031

31-
public function execute(Request $request): Response
32+
public function execute(ServerRequestInterface $request): ResponseInterface
3233
{
33-
$event = json_decode($request->getContent(), true);
34+
$event = json_decode((string) $request->getBody(), true);
3435
if (json_last_error() != JSON_ERROR_NONE) {
3536
throw new RuntimeException('Could not parse request body: ' . json_last_error_msg());
3637
}

src/Emitter.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
/**
3+
* Copyright 2019 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace Google\CloudFunctions;
19+
20+
use Psr\Http\Message\ResponseInterface;
21+
22+
class Emitter
23+
{
24+
/**
25+
* Formats the response and sends to the web server.
26+
*
27+
* @param ResponseInterface $response
28+
*/
29+
public function emit(ResponseInterface $response): void
30+
{
31+
// Only send headers if they have not already been sent
32+
if (!headers_sent()) {
33+
$this->statusLine($response);
34+
$this->headers($response);
35+
}
36+
37+
// Send the body.
38+
echo $response->getBody();
39+
}
40+
41+
private function statusLine(ResponseInterface $response): void
42+
{
43+
$statusCode = $response->getStatusCode();
44+
$reasonPhrase = $response->getReasonPhrase();
45+
$statusLine = sprintf(
46+
'HTTP/%s %s%s',
47+
$response->getProtocolVersion(),
48+
$statusCode,
49+
$reasonPhrase ? ' ' . $reasonPhrase : ''
50+
);
51+
$this->header($statusLine, true, $statusCode);
52+
}
53+
54+
private function headers(ResponseInterface $response): void
55+
{
56+
$statusCode = $response->getStatusCode();
57+
58+
foreach ($response->getHeaders() as $header => $values) {
59+
$name = ucwords($header, '-');
60+
$isCookie = $name === 'Set-Cookie';
61+
$first = true;
62+
foreach ($values as $value) {
63+
// Replace headers for first value only, except for cookies
64+
$replace = $first && !$isCookie;
65+
$first = false;
66+
$this->header($name . ':' . $value, $replace, $statusCode);
67+
}
68+
}
69+
}
70+
71+
/**
72+
* Function used to test header output in unit tests.
73+
*/
74+
protected function header(
75+
string $headerLine,
76+
bool $replace,
77+
int $statusCode
78+
): void {
79+
header($headerLine, $replace, $statusCode);
80+
}
81+
}

src/FunctionWrapper.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818
namespace Google\CloudFunctions;
1919

20-
use Symfony\Component\HttpFoundation\Request;
21-
use Symfony\Component\HttpFoundation\Response;
20+
use Psr\Http\Message\ResponseInterface;
21+
use Psr\Http\Message\ServerRequestInterface;
2222

2323
abstract class FunctionWrapper
2424
{
@@ -31,5 +31,7 @@ public function __construct(callable $function, array $signature = null)
3131
// TODO: validate function signature, if present.
3232
}
3333

34-
abstract public function execute(Request $request): Response;
34+
abstract public function execute(
35+
ServerRequestInterface $request
36+
): ResponseInterface;
3537
}

src/HttpFunctionWrapper.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
namespace Google\CloudFunctions;
1919

20-
use Symfony\Component\HttpFoundation\Request;
21-
use Symfony\Component\HttpFoundation\Response;
20+
use Psr\Http\Message\ResponseInterface;
21+
use Psr\Http\Message\ServerRequestInterface;
22+
use GuzzleHttp\Psr7\Response;
2223

2324
class HttpFunctionWrapper extends FunctionWrapper
2425
{
@@ -27,19 +28,17 @@ public function __construct(callable $function)
2728
parent::__construct($function);
2829
}
2930

30-
public function execute(Request $request): Response
31+
public function execute(ServerRequestInterface $request): ResponseInterface
3132
{
32-
$path = $request->getPathInfo();
33+
$path = $request->getUri()->getPath();
3334
if ($path == '/robots.txt' || $path == '/favicon.ico') {
34-
$response = new Response();
35-
$response->setStatusCode(404);
36-
return $response;
35+
return new Response(404);
3736
}
3837

3938
$response = call_user_func($this->function, $request);
4039

4140
if (is_string($response)) {
42-
$response = new Response($response);
41+
$response = new Response(200, [], $response);
4342
}
4443

4544
return $response;

src/Invoker.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
namespace Google\CloudFunctions;
1919

2020
use InvalidArgumentException;
21-
use Symfony\Component\HttpFoundation\Request;
22-
use Symfony\Component\HttpFoundation\Response;
21+
use GuzzleHttp\Psr7\ServerRequest;
22+
use Psr\Http\Message\ResponseInterface;
23+
use Psr\Http\Message\ServerRequestInterface;
2324

2425
class Invoker
2526
{
@@ -42,10 +43,11 @@ public function __construct(callable $target, string $signatureType)
4243
}
4344
}
4445

45-
public function handle(Request $request = null) : Response
46-
{
46+
public function handle(
47+
ServerRequestInterface $request = null
48+
) : ResponseInterface {
4749
if ($request === null) {
48-
$request = Request::createFromGlobals();
50+
$request = ServerRequest::fromGlobals();
4951
}
5052

5153
return $this->function->execute($request);

tests/BackgroundFunctionWrapperTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
use Google\CloudFunctions\BackgroundFunctionWrapper;
2121
use Google\CloudFunctions\Context;
2222
use PHPUnit\Framework\TestCase;
23-
use Symfony\Component\HttpFoundation\Request;
23+
use GuzzleHttp\Psr7\ServerRequest;
2424

2525
/**
2626
* @group gcf-framework
@@ -34,13 +34,13 @@ public function testInvalidRequestBody()
3434
$this->expectException('RuntimeException');
3535
$this->expectExceptionMessage('Could not parse request body');
3636
$backgroundFunctionWrapper = new BackgroundFunctionWrapper([$this, 'invokeThis']);
37-
$backgroundFunctionWrapper->execute(new Request());
37+
$backgroundFunctionWrapper->execute(new ServerRequest('GET', '/'));
3838
}
3939

4040
public function testHttpBackgroundFunctionWrapper()
4141
{
4242
$backgroundFunctionWrapper = new BackgroundFunctionWrapper([$this, 'invokeThis']);
43-
$request = new Request([], [], [], [], [], [], json_encode([
43+
$request = new ServerRequest('GET', '/', [], json_encode([
4444
'data' => 'foo',
4545
'context' => [
4646
'eventId' => 'abc',

tests/EmitterTest.php

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace Google\CloudFunctions\Tests;
19+
20+
use Google\CloudFunctions\Emitter;
21+
use GuzzleHttp\Psr7\Response;
22+
use PHPUnit\Framework\TestCase;
23+
24+
/**
25+
* @group gcf-framework
26+
* @runClassInSeparateProcess
27+
*/
28+
class EmitterTest extends TestCase
29+
{
30+
public function testEmit()
31+
{
32+
if (!extension_loaded('xdebug')) {
33+
$this->markTestSkipped('xdebug extension required');
34+
}
35+
$response = new Response(200, ['foo-header' => 'bar'], 'Foo');
36+
$emitter = new Emitter();
37+
ob_start();
38+
$emitter->emit($response);
39+
$headers = xdebug_get_headers();
40+
$output = ob_get_clean();
41+
42+
$this->assertEquals('Foo', $output);
43+
$this->assertContains('Foo-Header:bar', $headers);
44+
$this->assertEquals(200, http_response_code());
45+
}
46+
47+
public function testSingleHeader()
48+
{
49+
$emitter = new TestEmitter();
50+
$emitter->emit(new Response(200, ['foo-header' => 'bar']));
51+
52+
$this->assertEquals('Foo-Header:bar', $emitter->headers[1][0]);
53+
$this->assertEquals(true, $emitter->headers[1][1]);
54+
$this->assertEquals(200, $emitter->headers[1][2]);
55+
}
56+
57+
public function testRepeatHeaders()
58+
{
59+
$emitter = new TestEmitter();
60+
$emitter->emit(new Response(200, ['foo-header' => ['bar', 'baz']]));
61+
62+
$this->assertEquals('Foo-Header:bar', $emitter->headers[1][0]);
63+
$this->assertEquals(true, $emitter->headers[1][1]);
64+
$this->assertEquals(200, $emitter->headers[1][2]);
65+
66+
$this->assertEquals('Foo-Header:baz', $emitter->headers[2][0]);
67+
$this->assertEquals(false, $emitter->headers[2][1]);
68+
$this->assertEquals(200, $emitter->headers[2][2]);
69+
}
70+
71+
public function testCookies()
72+
{
73+
$emitter = new TestEmitter();
74+
$emitter->emit(new Response(200, ['Set-Cookie' => ['1', '2']]));
75+
76+
$this->assertEquals('Set-Cookie:1', $emitter->headers[1][0]);
77+
$this->assertEquals(false, $emitter->headers[1][1]);
78+
$this->assertEquals(200, $emitter->headers[1][2]);
79+
80+
$this->assertEquals('Set-Cookie:2', $emitter->headers[2][0]);
81+
$this->assertEquals(false, $emitter->headers[2][1]);
82+
$this->assertEquals(200, $emitter->headers[2][2]);
83+
}
84+
85+
public function testStatusLine()
86+
{
87+
$emitter = new TestEmitter();
88+
$emitter->emit(new Response(200));
89+
90+
$this->assertEquals('HTTP/1.1 200 OK', $emitter->headers[0][0]);
91+
$this->assertEquals(true, $emitter->headers[0][1]);
92+
$this->assertEquals(200, $emitter->headers[0][2]);
93+
}
94+
95+
public function testStatusLineEmptyReasonPhrase()
96+
{
97+
$emitter = new TestEmitter();
98+
$emitter->emit(new Response(419));
99+
100+
$this->assertEquals('HTTP/1.1 419', $emitter->headers[0][0]);
101+
$this->assertEquals(true, $emitter->headers[0][1]);
102+
$this->assertEquals(419, $emitter->headers[0][2]);
103+
}
104+
}
105+
106+
class TestEmitter extends Emitter
107+
{
108+
public $headers;
109+
110+
protected function header(
111+
string $headerLine,
112+
bool $replace,
113+
int $statusCode
114+
): void {
115+
$this->headers[] = [$headerLine, $replace, $statusCode];
116+
}
117+
}

0 commit comments

Comments
 (0)