Skip to content

Commit 369fe4c

Browse files
author
Ace Nassri
authored
feat(functions): add Firestore reactive sample (GoogleCloudPlatform#1292)
1 parent 09f4528 commit 369fe4c

File tree

6 files changed

+341
-0
lines changed

6 files changed

+341
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"require": {
3+
"google/cloud-functions-framework": "^0.7.1",
4+
"google/cloud-firestore": "^1.18",
5+
"grpc/grpc": "^v1.27.0"
6+
},
7+
"scripts": {
8+
"start": [
9+
"Composer\\Config::disableProcessTimeout",
10+
"FUNCTION_SIGNATURE_TYPE=cloudevent FUNCTION_TARGET=firebaseReactive php -S localhost:${PORT:-8080} vendor/bin/router.php"
11+
]
12+
},
13+
"require-dev": {
14+
"google/cloud-logging": "^1.21"
15+
}
16+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
/**
3+
* Copyright 2021 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+
// [START functions_firebase_reactive]
19+
20+
use Google\Cloud\Firestore\FirestoreClient;
21+
use Google\CloudFunctions\CloudEvent;
22+
23+
function firebaseReactive(CloudEvent $cloudevent)
24+
{
25+
$log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');
26+
$data = $cloudevent->getData();
27+
28+
$resource = $data['value']['name'];
29+
30+
$db = new FirestoreClient();
31+
32+
$docPath = explode('/documents/', $resource)[1];
33+
34+
$affectedDoc = $db->document($docPath);
35+
36+
$curValue = $data['value']['fields']['original']['stringValue'];
37+
$newValue = strtoupper($curValue);
38+
39+
if ($curValue !== $newValue) {
40+
fwrite($log, 'Replacing value: ' . $curValue . ' --> ' . $newValue . PHP_EOL);
41+
42+
$affectedDoc->set(['original' => $newValue]);
43+
} else {
44+
// Value is already upper-case
45+
// Don't perform another write (it might cause an infinite loop)
46+
fwrite($log, 'Value is already upper-case.' . PHP_EOL);
47+
}
48+
}
49+
// [END functions_firebase_reactive]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
; [START functions_php_grpc]
2+
; The gRPC PHP extension is installed but disabled by default.
3+
; See this page for a list of available extensions:
4+
; https://cloud.google.com/functions/docs/concepts/php-runtime
5+
6+
extension=grpc.so
7+
; [END functions_php_grpc]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
xml version="1.0" encoding="UTF-8"?>
2+
17+
<phpunit bootstrap="../../testing/bootstrap.php" convertWarningsToExceptions="false">
18+
<testsuites>
19+
<testsuite name="Cloud Functions Firebase Firestore Reactive Test Suite">
20+
<directory>testdirectory>
21+
<exclude>vendorexclude>
22+
testsuite>
23+
testsuites>
24+
<logging>
25+
<log type="coverage-clover" target="build/logs/clover.xml"/>
26+
logging>
27+
<filter>
28+
<whitelist>
29+
<directory suffix=".php">.directory>
30+
<exclude>
31+
<directory>./vendordirectory>
32+
exclude>
33+
whitelist>
34+
filter>
35+
phpunit>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
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+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\FirebaseReactive\Test;
21+
22+
use Google\Cloud\Firestore\FirestoreClient;
23+
use Google\Cloud\Logging\LoggingClient;
24+
use Google\Cloud\TestUtils\CloudFunctionDeploymentTrait;
25+
use Google\Cloud\TestUtils\EventuallyConsistentTestTrait;
26+
use PHPUnit\Framework\TestCase;
27+
28+
/**
29+
* Class DeployTest.
30+
*
31+
* This test is not run by the CI system.
32+
*
33+
* To skip deployment of a new function, run with "GOOGLE_SKIP_DEPLOYMENT=true".
34+
* To skip deletion of the tested function, run with "GOOGLE_KEEP_DEPLOYMENT=true".
35+
*/
36+
class DeployTest extends TestCase
37+
{
38+
use CloudFunctionDeploymentTrait;
39+
use EventuallyConsistentTestTrait;
40+
41+
/** @var string */
42+
private static $entryPoint = 'firebaseReactive';
43+
44+
/** @var string */
45+
private static $functionSignatureType = 'cloudevent';
46+
47+
/** @var string */
48+
private static $collectionName = 'messages';
49+
50+
/** @var string */
51+
private static $documentName = 'taco';
52+
53+
/** @var LoggingClient */
54+
private static $loggingClient;
55+
56+
/** @var FirestoreClient */
57+
private static $firestoreClient;
58+
59+
/**
60+
* Deploy the Cloud Function, called from DeploymentTrait::deployApp().
61+
*
62+
* Overrides CloudFunctionDeploymentTrait::doDeploy().
63+
*/
64+
private static function doDeploy()
65+
{
66+
$project = self::requireEnv('GOOGLE_PROJECT_ID');
67+
68+
$resource = sprintf(
69+
'projects/%s/databases/(default)/documents/%s/%s',
70+
$project,
71+
self::$collectionName,
72+
self::$documentName
73+
);
74+
$event = 'providers/cloud.firestore/eventTypes/document.write';
75+
76+
return self::$fn->deploy([
77+
'--trigger-resource' => $resource,
78+
'--trigger-event' => $event
79+
], '');
80+
}
81+
82+
public function dataProvider()
83+
{
84+
$data = uniqid();
85+
$expected = 'Replacing value: ' . $data . ' --> ' . strtoupper($data);
86+
return [
87+
[
88+
'data' => ['original' => $data],
89+
'expected' => $expected
90+
],
91+
];
92+
}
93+
94+
/**
95+
* @dataProvider dataProvider
96+
*/
97+
public function testFirebaseReactive(array $data, string $expected): void
98+
{
99+
// Trigger storage upload.
100+
$objectUri = $this->updateFirestore(
101+
self::$collectionName,
102+
self::$documentName,
103+
$data
104+
);
105+
106+
// Give event and log systems a head start.
107+
// If log retrieval fails to find logs for our function within retry limit, increase sleep time.
108+
sleep(30);
109+
110+
$fiveMinAgo = date(\DateTime::RFC3339, strtotime('-5 minutes'));
111+
$this->processFunctionLogs($fiveMinAgo, function (\Iterator $logs) use ($expected) {
112+
// Concatenate all relevant log messages.
113+
$actual = '';
114+
foreach ($logs as $log) {
115+
$info = $log->info();
116+
if (isset($info['textPayload'])) {
117+
$actual .= $info['textPayload'];
118+
}
119+
}
120+
121+
// Only testing one property to decrease odds the expected logs are
122+
// split between log requests.
123+
$this->assertStringContainsString($expected, $actual);
124+
});
125+
}
126+
127+
/**
128+
* Update a value in Firebase Realtime Database (RTDB).
129+
*
130+
* @param string $document The Firestore document to modify.
131+
* @param string $collection The Firestore collection to modify.
132+
* @param string $data The key-value pair to set the specified collection to.
133+
*
134+
* @throws \RuntimeException
135+
*/
136+
private function updateFirestore(
137+
string $document,
138+
string $collection,
139+
array $data
140+
): void {
141+
if (empty(self::$firestoreClient)) {
142+
self::$firestoreClient = new FirestoreClient();
143+
}
144+
145+
self::$firestoreClient
146+
->collection(self::$collectionName)
147+
->document(self::$documentName)
148+
->set($data);
149+
}
150+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
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+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\FirebaseReactive\Test;
21+
22+
use PHPUnit\Framework\TestCase;
23+
use Google\CloudFunctions\CloudEvent;
24+
use Google\Cloud\TestUtils\CloudFunctionLocalTestTrait;
25+
26+
/**
27+
* Class IntegrationTest.
28+
*
29+
* Integration Test for firebaseReactive.
30+
*/
31+
class IntegrationTest extends TestCase
32+
{
33+
use CloudFunctionLocalTestTrait;
34+
35+
/** @var string */
36+
private static $entryPoint = 'firebaseReactive';
37+
38+
/** @var string */
39+
private static $functionSignatureType = 'cloudevent';
40+
41+
/** @var string */
42+
private static $value = 'value';
43+
44+
public function dataProvider()
45+
{
46+
return [
47+
[
48+
'cloudevent' => CloudEvent::fromArray([
49+
'id' => uniqid(),
50+
'source' => 'firebase.googleapis.com',
51+
'specversion' => '1.0',
52+
'type' => 'google.cloud.firestore.document.v1.created',
53+
'data' => [
54+
'value' => [
55+
'fields' => [
56+
'original' => [
57+
'stringValue'=> self::$value
58+
]
59+
],
60+
'name' => '/documents/some_collection/blah',
61+
],
62+
],
63+
])
64+
],
65+
];
66+
}
67+
68+
/**
69+
* @dataProvider dataProvider
70+
*/
71+
public function testFirebaseFirestore(
72+
CloudEvent $cloudevent
73+
): void {
74+
// Send an HTTP request using CloudEvent.
75+
$resp = $this->request($cloudevent);
76+
77+
// The Cloud Function logs all data to stderr.
78+
$actual = self::$localhost->getIncrementalErrorOutput();
79+
80+
// Verify the data value is logged by the function.
81+
$expected = strtoupper(self::$value);
82+
$this->assertStringContainsString($expected, $actual);
83+
}
84+
}

0 commit comments

Comments
 (0)