diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index b2adc48a14..04464fb557 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -36,6 +36,7 @@
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__)
+ ->exclude(['generated'])
)
;
diff --git a/spanner/composer.json b/spanner/composer.json
index efc487c7d5..f06d93f93f 100755
--- a/spanner/composer.json
+++ b/spanner/composer.json
@@ -1,5 +1,11 @@
{
"require": {
- "google/cloud-spanner": "^1.74"
+ "google/cloud-spanner": "^1.97"
+ },
+ "autoload": {
+ "psr-4": {
+ "GPBMetadata\\": "generated/GPBMetadata",
+ "Testing\\": "generated/Testing"
+ }
}
}
diff --git a/spanner/data/user.pb b/spanner/data/user.pb
new file mode 100644
index 0000000000..24d5e09203
--- /dev/null
+++ b/spanner/data/user.pb
@@ -0,0 +1,14 @@
+
+¡
+data/user.prototesting.data"
+User
+id (Rid
+name ( Rname
+active (Ractive4
+address (2.testing.data.User.AddressRaddress3
+Address
+city ( Rcity
+state ( Rstate"H
+Book
+title ( Rtitle*
+author (2.testing.data.UserRauthorbproto3
\ No newline at end of file
diff --git a/spanner/data/user.proto b/spanner/data/user.proto
new file mode 100644
index 0000000000..9fd405ecab
--- /dev/null
+++ b/spanner/data/user.proto
@@ -0,0 +1,42 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package testing.data;
+
+message User {
+
+ int64 id = 1;
+
+ string name = 2;
+
+ bool active = 3;
+
+ message Address {
+
+ string city = 1;
+
+ string state = 2;
+ }
+
+ Address address = 4;
+}
+
+
+message Book {
+ string title = 1;
+
+ User author = 2;
+}
diff --git a/spanner/generated/GPBMetadata/Data/User.php b/spanner/generated/GPBMetadata/Data/User.php
new file mode 100644
index 0000000000..6cafee1118
--- /dev/null
+++ b/spanner/generated/GPBMetadata/Data/User.php
@@ -0,0 +1,25 @@
+internalAddGeneratedFile(
+ "\x0A\xEA\x01\x0A\x0Fdata/user.proto\x12\x0Ctesting.data\"\x85\x01\x0A\x04User\x12\x0A\x0A\x02id\x18\x01 \x01(\x03\x12\x0C\x0A\x04name\x18\x02 \x01(\x09\x12\x0E\x0A\x06active\x18\x03 \x01(\x08\x12+\x0A\x07address\x18\x04 \x01(\x0B2\x1A.testing.data.User.Address\x1A&\x0A\x07Address\x12\x0C\x0A\x04city\x18\x01 \x01(\x09\x12\x0D\x0A\x05state\x18\x02 \x01(\x09\"9\x0A\x04Book\x12\x0D\x0A\x05title\x18\x01 \x01(\x09\x12\"\x0A\x06author\x18\x02 \x01(\x0B2\x12.testing.data.Userb\x06proto3"
+ , true);
+
+ static::$is_initialized = true;
+ }
+}
+
diff --git a/spanner/generated/Testing/Data/Book.php b/spanner/generated/Testing/Data/Book.php
new file mode 100644
index 0000000000..380fd237f7
--- /dev/null
+++ b/spanner/generated/Testing/Data/Book.php
@@ -0,0 +1,96 @@
+testing.data.Book
+ */
+class Book extends \Google\Protobuf\Internal\Message
+{
+ /**
+ * Generated from protobuf field string title = 1;
+ */
+ protected $title = '';
+ /**
+ * Generated from protobuf field .testing.data.User author = 2;
+ */
+ protected $author = null;
+
+ /**
+ * Constructor.
+ *
+ * @param array $data {
+ * Optional. Data for populating the Message object.
+ *
+ * @type string $title
+ * @type \Testing\Data\User $author
+ * }
+ */
+ public function __construct($data = NULL) {
+ \GPBMetadata\Data\User::initOnce();
+ parent::__construct($data);
+ }
+
+ /**
+ * Generated from protobuf field string title = 1;
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Generated from protobuf field string title = 1;
+ * @param string $var
+ * @return $this
+ */
+ public function setTitle($var)
+ {
+ GPBUtil::checkString($var, True);
+ $this->title = $var;
+
+ return $this;
+ }
+
+ /**
+ * Generated from protobuf field .testing.data.User author = 2;
+ * @return \Testing\Data\User|null
+ */
+ public function getAuthor()
+ {
+ return $this->author;
+ }
+
+ public function hasAuthor()
+ {
+ return isset($this->author);
+ }
+
+ public function clearAuthor()
+ {
+ unset($this->author);
+ }
+
+ /**
+ * Generated from protobuf field .testing.data.User author = 2;
+ * @param \Testing\Data\User $var
+ * @return $this
+ */
+ public function setAuthor($var)
+ {
+ GPBUtil::checkMessage($var, \Testing\Data\User::class);
+ $this->author = $var;
+
+ return $this;
+ }
+
+}
+
diff --git a/spanner/generated/Testing/Data/User.php b/spanner/generated/Testing/Data/User.php
new file mode 100644
index 0000000000..f093dff02c
--- /dev/null
+++ b/spanner/generated/Testing/Data/User.php
@@ -0,0 +1,150 @@
+testing.data.User
+ */
+class User extends \Google\Protobuf\Internal\Message
+{
+ /**
+ * Generated from protobuf field int64 id = 1;
+ */
+ protected $id = 0;
+ /**
+ * Generated from protobuf field string name = 2;
+ */
+ protected $name = '';
+ /**
+ * Generated from protobuf field bool active = 3;
+ */
+ protected $active = false;
+ /**
+ * Generated from protobuf field .testing.data.User.Address address = 4;
+ */
+ protected $address = null;
+
+ /**
+ * Constructor.
+ *
+ * @param array $data {
+ * Optional. Data for populating the Message object.
+ *
+ * @type int|string $id
+ * @type string $name
+ * @type bool $active
+ * @type \Testing\Data\User\Address $address
+ * }
+ */
+ public function __construct($data = NULL) {
+ \GPBMetadata\Data\User::initOnce();
+ parent::__construct($data);
+ }
+
+ /**
+ * Generated from protobuf field int64 id = 1;
+ * @return int|string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Generated from protobuf field int64 id = 1;
+ * @param int|string $var
+ * @return $this
+ */
+ public function setId($var)
+ {
+ GPBUtil::checkInt64($var);
+ $this->id = $var;
+
+ return $this;
+ }
+
+ /**
+ * Generated from protobuf field string name = 2;
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Generated from protobuf field string name = 2;
+ * @param string $var
+ * @return $this
+ */
+ public function setName($var)
+ {
+ GPBUtil::checkString($var, True);
+ $this->name = $var;
+
+ return $this;
+ }
+
+ /**
+ * Generated from protobuf field bool active = 3;
+ * @return bool
+ */
+ public function getActive()
+ {
+ return $this->active;
+ }
+
+ /**
+ * Generated from protobuf field bool active = 3;
+ * @param bool $var
+ * @return $this
+ */
+ public function setActive($var)
+ {
+ GPBUtil::checkBool($var);
+ $this->active = $var;
+
+ return $this;
+ }
+
+ /**
+ * Generated from protobuf field .testing.data.User.Address address = 4;
+ * @return \Testing\Data\User\Address|null
+ */
+ public function getAddress()
+ {
+ return $this->address;
+ }
+
+ public function hasAddress()
+ {
+ return isset($this->address);
+ }
+
+ public function clearAddress()
+ {
+ unset($this->address);
+ }
+
+ /**
+ * Generated from protobuf field .testing.data.User.Address address = 4;
+ * @param \Testing\Data\User\Address $var
+ * @return $this
+ */
+ public function setAddress($var)
+ {
+ GPBUtil::checkMessage($var, \Testing\Data\User\Address::class);
+ $this->address = $var;
+
+ return $this;
+ }
+
+}
+
diff --git a/spanner/generated/Testing/Data/User/Address.php b/spanner/generated/Testing/Data/User/Address.php
new file mode 100644
index 0000000000..d2391e7a62
--- /dev/null
+++ b/spanner/generated/Testing/Data/User/Address.php
@@ -0,0 +1,86 @@
+testing.data.User.Address
+ */
+class Address extends \Google\Protobuf\Internal\Message
+{
+ /**
+ * Generated from protobuf field string city = 1;
+ */
+ protected $city = '';
+ /**
+ * Generated from protobuf field string state = 2;
+ */
+ protected $state = '';
+
+ /**
+ * Constructor.
+ *
+ * @param array $data {
+ * Optional. Data for populating the Message object.
+ *
+ * @type string $city
+ * @type string $state
+ * }
+ */
+ public function __construct($data = NULL) {
+ \GPBMetadata\Data\User::initOnce();
+ parent::__construct($data);
+ }
+
+ /**
+ * Generated from protobuf field string city = 1;
+ * @return string
+ */
+ public function getCity()
+ {
+ return $this->city;
+ }
+
+ /**
+ * Generated from protobuf field string city = 1;
+ * @param string $var
+ * @return $this
+ */
+ public function setCity($var)
+ {
+ GPBUtil::checkString($var, True);
+ $this->city = $var;
+
+ return $this;
+ }
+
+ /**
+ * Generated from protobuf field string state = 2;
+ * @return string
+ */
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ /**
+ * Generated from protobuf field string state = 2;
+ * @param string $var
+ * @return $this
+ */
+ public function setState($var)
+ {
+ GPBUtil::checkString($var, True);
+ $this->state = $var;
+
+ return $this;
+ }
+
+}
+
diff --git a/spanner/src/create_database_with_proto_columns.php b/spanner/src/create_database_with_proto_columns.php
new file mode 100644
index 0000000000..e305ff2506
--- /dev/null
+++ b/spanner/src/create_database_with_proto_columns.php
@@ -0,0 +1,81 @@
+instanceName($projectId, $instanceId);
+
+ $operation = $databaseAdminClient->createDatabase(
+ new CreateDatabaseRequest([
+ 'parent' => $instance,
+ 'create_statement' => sprintf('CREATE DATABASE `%s`', $databaseId),
+ 'proto_descriptors' => $fileDescriptorSet,
+ 'extra_statements' => [
+ 'CREATE PROTO BUNDLE (' .
+ 'testing.data.User,' .
+ 'testing.data.User.Address,' .
+ 'testing.data.Book' .
+ ')',
+ 'CREATE TABLE Users (' .
+ 'Id INT64,' .
+ 'User `testing.data.User`,' .
+ 'Books ARRAY<`testing.data.Book`>,' .
+ ') PRIMARY KEY (Id)'
+ ],
+ ])
+ );
+
+ print('Waiting for operation to complete...' . PHP_EOL);
+ $operation->pollUntilComplete();
+
+ printf('Created database %s on instance %s' . PHP_EOL, $databaseId, $instanceId);
+}
+// [END spanner_create_database_with_proto_columns]
+
+// The following 2 lines are only needed to run the samples
+require_once __DIR__ . '/../../testing/sample_helpers.php';
+\Google\Cloud\Samples\execute_sample(__FILE__, __NAMESPACE__, $argv);
diff --git a/spanner/src/insert_data_with_proto_columns.php b/spanner/src/insert_data_with_proto_columns.php
new file mode 100644
index 0000000000..bcb826006b
--- /dev/null
+++ b/spanner/src/insert_data_with_proto_columns.php
@@ -0,0 +1,92 @@
+instance($instanceId)->database($databaseId);
+
+ $address = (new User\Address())
+ ->setCity('San Francisco')
+ ->setState('CA');
+ $user = (new User())
+ ->setName('Test User ' . $userId)
+ ->setAddress($address);
+
+ $book1 = new Book([
+ 'title' => 'Book 1',
+ 'author' => new User(['name' => 'Author of Book 1']),
+ ]);
+ $book2 = new Book([
+ 'title' => 'Book 2',
+ 'author' => new User(['name' => 'Author of Book 2']),
+ ]);
+
+ $books = [
+ // insert using the proto message
+ $book1,
+ // insert using the Proto wrapper class
+ new Proto(
+ base64_encode($book2->serializeToString()),
+ 'testing.data.Book'
+ ),
+ ];
+
+ $transaction = $database->transaction(['singleUse' => true])
+ ->insertBatch('Users', [
+ ['Id' => $userId, 'User' => $user, 'Books' => $books],
+ ]);
+ $transaction->commit();
+
+ print('Inserted data.' . PHP_EOL);
+}
+// [END spanner_insert_data_with_proto_columns]
+
+// The following 2 lines are only needed to run the samples
+require_once __DIR__ . '/../../testing/sample_helpers.php';
+\Google\Cloud\Samples\execute_sample(__FILE__, __NAMESPACE__, $argv);
diff --git a/spanner/src/list_instance_configs.php b/spanner/src/list_instance_configs.php
index d795c3aa3d..5d588b6b13 100644
--- a/spanner/src/list_instance_configs.php
+++ b/spanner/src/list_instance_configs.php
@@ -37,7 +37,7 @@
*
* @param string $projectId The Google Cloud project ID.
*/
-function list_instance_configs(string $projectId = null): void
+function list_instance_configs(string $projectId): void
{
$instanceAdminClient = new InstanceAdminClient();
$projectName = InstanceAdminClient::projectName($projectId);
diff --git a/spanner/src/query_data_with_proto_columns.php b/spanner/src/query_data_with_proto_columns.php
new file mode 100644
index 0000000000..2ae1795805
--- /dev/null
+++ b/spanner/src/query_data_with_proto_columns.php
@@ -0,0 +1,81 @@
+instance($instanceId)->database($databaseId);
+
+ $userProto = (new User())
+ ->setName('Test User ' . $userId);
+
+ $results = $database->execute(
+ 'SELECT * FROM Users, UNNEST(Books) as Book '
+ . 'WHERE User.name = @user.name '
+ . 'AND Book.title = @bookTitle',
+ [
+ 'parameters' => [
+ 'user' => $userProto,
+ 'bookTitle' => 'Book 1',
+ ],
+ ]
+ );
+ foreach ($results as $row) {
+ /** @var User $user */
+ $user = $row['User']->get();
+ // Print the decoded Protobuf message as JSON
+ printf('User: %s' . PHP_EOL, $user->serializeToJsonString());
+ /** @var Proto $book */
+ foreach ($row['Books'] ?? [] as $book) {
+ // Print the raw row value
+ printf('Book: %s (%s)' . PHP_EOL, $book->getValue(), $book->getProtoTypeFqn());
+ }
+ }
+ // [END spanner_query_data_with_proto_columns]
+}
+
+// The following 2 lines are only needed to run the samples
+require_once __DIR__ . '/../../testing/sample_helpers.php';
+\Google\Cloud\Samples\execute_sample(__FILE__, __NAMESPACE__, $argv);
diff --git a/spanner/test/spannerProtoTest.php b/spanner/test/spannerProtoTest.php
new file mode 100644
index 0000000000..dc64dfcf00
--- /dev/null
+++ b/spanner/test/spannerProtoTest.php
@@ -0,0 +1,133 @@
+ self::$projectId,
+ ]);
+
+ self::$instanceId = 'proto-test-' . time() . rand();
+ self::$databaseId = 'proto-db-' . time() . rand();
+ self::$instance = $spanner->instance(self::$instanceId);
+
+ // Create the instance for testing
+ $operation = $spanner->createInstance(
+ $spanner->instanceConfiguration('regional-us-central1'),
+ self::$instanceId,
+ [
+ 'displayName' => 'Proto Test Instance',
+ 'nodeCount' => 1,
+ 'labels' => [
+ 'cloud_spanner_samples' => true,
+ ]
+ ]
+ );
+ $operation->pollUntilComplete();
+ }
+
+ public function testCreateDatabaseWithProtoColumns()
+ {
+ $output = $this->runFunctionSnippet('create_database_with_proto_columns', [
+ self::$projectId,
+ self::$instanceId,
+ self::$databaseId
+ ]);
+
+ $this->assertStringContainsString('Waiting for operation to complete...', $output);
+ $this->assertStringContainsString(sprintf('Created database %s on instance %s', self::$databaseId, self::$instanceId), $output);
+ }
+
+ /**
+ * @depends testCreateDatabaseWithProtoColumns
+ */
+ public function testInsertDataWithProtoColumns()
+ {
+ $output = $this->runFunctionSnippet('insert_data_with_proto_columns', [
+ self::$instanceId,
+ self::$databaseId,
+ 1 // User ID
+ ]);
+
+ $this->assertEquals('Inserted data.' . PHP_EOL, $output);
+ }
+
+ /**
+ * @depends testInsertDataWithProtoColumns
+ */
+ public function testQueryDataWithProtoColumns()
+ {
+ $output = $this->runFunctionSnippet('query_data_with_proto_columns', [
+ self::$instanceId,
+ self::$databaseId,
+ 1 // User ID
+ ]);
+
+ $this->assertStringContainsString('User:', $output);
+ $this->assertStringContainsString('Test User 1', $output);
+ $this->assertStringContainsString('Book:', $output);
+ $this->assertStringContainsString('testing.data.Book', $output);
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ if (self::$instance->exists()) {
+ // Clean up database
+ $database = self::$instance->database(self::$databaseId);
+ if ($database->exists()) {
+ $database->drop();
+ }
+ self::$instance->delete();
+ }
+ }
+}