From c85200dafb309448afb0e43f5880a166da77999f Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Mon, 24 Sep 2018 16:07:51 -0400 Subject: [PATCH 1/5] Add more in-depth support for object lifecycle management --- Storage/src/Bucket.php | 80 ++++++- Storage/src/Lifecycle.php | 233 +++++++++++++++++++++ Storage/src/StorageClient.php | 5 +- Storage/tests/Snippet/BucketTest.php | 42 ++++ Storage/tests/Snippet/LifecycleTest.php | 149 +++++++++++++ Storage/tests/System/ManageBucketsTest.php | 45 ++++ Storage/tests/Unit/BucketTest.php | 57 +++++ Storage/tests/Unit/LifecycleTest.php | 146 +++++++++++++ Storage/tests/Unit/StorageClientTest.php | 28 +++ 9 files changed, 783 insertions(+), 2 deletions(-) create mode 100644 Storage/src/Lifecycle.php create mode 100644 Storage/tests/Snippet/LifecycleTest.php create mode 100644 Storage/tests/Unit/LifecycleTest.php diff --git a/Storage/src/Bucket.php b/Storage/src/Bucket.php index ded30b7f2d90..99530edf8d37 100644 --- a/Storage/src/Bucket.php +++ b/Storage/src/Bucket.php @@ -779,7 +779,7 @@ public function delete(array $options = []) * configuration. * @type array $defaultObjectAcl Default access controls to apply to new * objects when no ACL is provided. - * @type array $lifecycle The bucket's lifecycle configuration. + * @type array|Lifecycle $lifecycle The bucket's lifecycle configuration. * @type array $logging The bucket's logging configuration, which * defines the destination bucket and optional name prefix for the * current bucket's logs. @@ -810,6 +810,10 @@ public function delete(array $options = []) */ public function update(array $options = []) { + if (isset($options['lifecycle']) && $options['lifecycle'] instanceof Lifecycle) { + $options['lifecycle'] = $options['lifecycle']->toArray(); + } + return $this->info = $this->connection->patchBucket($options + $this->identity); } @@ -988,6 +992,80 @@ public function name() return $this->identity['bucket']; } + /** + * Retrieves a fresh lifecycle builder. If a lifecyle configuration already + * exists on the target bucket and this builder is used, it will fully + * replace the configuration with the rules provided by this builder. + * + * This builder is intended to be used in tandem with + * {@see Google\Cloud\Storage\StorageClient::createBucket()} and + * {@see Google\Cloud\Storage\Bucket::update()}. + * + * Example: + * ``` + * use Google\Cloud\Storage\Bucket; + * + * $lifecycle = Bucket::lifecycle() + * ->addDeleteRule([ + * 'age' => 50, + * 'isLive' => true + * ]); + * $bucket->update([ + * 'lifecycle' => $lifecycle + * ]); + * ``` + * + * @see https://cloud.google.com/storage/docs/lifecycle Object Lifecycle Management API Documentation + * + * @param array $lifecycle [optional] A lifecycle configuration. Please see + * [here](https://cloud.google.com/storage/docs/json_api/v1/buckets#lifecycle) + * for the expected structure. + * @return Lifecycle + */ + public static function lifecycle(array $lifecycle = []) + { + return new Lifecycle($lifecycle); + } + + /** + * Retrieves a lifecycle builder preconfigured with the lifecycle rules that + * already exists on the bucket. Use this if you want to make updates to an + * existing configuration without removing existing rules, as would be the + * case when using {@see Google\Cloud\Storage\Bucket::lifecycle()}. + * + * This builder is intended to be used in tandem with + * {@see Google\Cloud\Storage\StorageClient::createBucket()} and + * {@see Google\Cloud\Storage\Bucket::update()}. + * + * Please note, this method may trigger a network request in order to fetch + * the existing lifecycle rules from the server. + * + * Example: + * ``` + * $lifecycle = $bucket->currentLifecycle() + * ->addDeleteRule([ + * 'age' => 50, + * 'isLive' => true + * ]); + * $bucket->update([ + * 'lifecycle' => $lifecycle + * ]); + * ``` + * + * @see https://cloud.google.com/storage/docs/lifecycle Object Lifecycle Management API Documentation + * + * @param array $options [optional] Configuration options. + * @return Lifecycle + */ + public function currentLifecycle(array $options = []) + { + return self::lifecycle( + isset($this->info($options)['lifecycle']) + ? $this->info['lifecycle'] + : [] + ); + } + /** * Returns whether the bucket with the given file prefix is writable. * Tries to create a temporary file as a resumable upload which will diff --git a/Storage/src/Lifecycle.php b/Storage/src/Lifecycle.php new file mode 100644 index 000000000000..f1918ed71f5a --- /dev/null +++ b/Storage/src/Lifecycle.php @@ -0,0 +1,233 @@ +bucket('my-bucket'); + * $lifecycle = $bucket->currentLifecycle(); + * ``` + * + * ``` + * // Or get a fresh builder by using the static factory method. + * use Google\Cloud\Storage\Bucket; + * + * $lifecycle = Bucket::lifecycle(); + * ``` + * + * @see https://cloud.google.com/storage/docs/lifecycle Object Lifecycle Management API Documentation + */ +class Lifecycle +{ + /** + * @var array + */ + private $lifecycle; + + /** + * @param array $lifecycle [optional] A lifecycle configuration. Please see + * [here](https://cloud.google.com/storage/docs/json_api/v1/buckets#lifecycle) + * for the expected structure. + */ + public function __construct(array $lifecycle = []) + { + $this->lifecycle = $lifecycle; + } + + /** + * Adds a delete rule. + * + * Example: + * ``` + * $lifecycle->addDeleteRule([ + * 'age' => 50, + * 'isLive' => true + * ]); + * ``` + * + * @param array $condition { + * The condition(s) where the rule will apply. + * + * @type int $age Age of an object (in days). This condition is + * satisfied when an object reaches the specified age. + * @type string $createdBefore A date in RFC 3339 format with only the + * date part (for instance, "2013-01-15"). This condition is + * satisfied when an object is created before midnight of the + * specified date in UTC. + * @type bool $isLive Relevant only for versioned objects. If the value + * is `true`, this condition matches live objects; if the value is + * `false`, it matches archived objects. + * @type string[] $matchesStorageClass Objects having any of the storage + * classes specified by this condition will be matched. Values + * include `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`, + * `"COLDLINE"`, `"STANDARD"`, and `"DURABLE_REDUCED_AVAILABILITY"`. + * @type int $numNewerVersions Relevant only for versioned objects. If + * the value is N, this condition is satisfied when there are at + * least N versions (including the live version) newer than this + * version of the object. + * } + * @return Lifecycle + */ + public function addDeleteRule(array $condition) + { + $this->lifecycle['rule'][] = [ + 'action' => [ + 'type' => 'Delete' + ], + 'condition' => $condition + ]; + + return $this; + } + + /** + * Adds a set storage class rule. + * + * Example: + * ``` + * $lifecycle->addSetStorageClassRule('COLDLINE', [ + * 'age' => 50, + * 'isLive' => true + * ]); + * ``` + * + * @param string $storageClass The target storage class. Values include + * `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`, `"COLDLINE"`, + * `"STANDARD"`, and `"DURABLE_REDUCED_AVAILABILITY"`. + * @param array $condition { + * The condition(s) where the rule will apply. + * + * @type int $age Age of an object (in days). This condition is + * satisfied when an object reaches the specified age. + * @type string $createdBefore A date in RFC 3339 format with only the + * date part (for instance, "2013-01-15"). This condition is + * satisfied when an object is created before midnight of the + * specified date in UTC. + * @type bool $isLive Relevant only for versioned objects. If the value + * is `true`, this condition matches live objects; if the value is + * `false`, it matches archived objects. + * @type string[] $matchesStorageClass Objects having any of the storage + * classes specified by this condition will be matched. Values + * include `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`, + * `"COLDLINE"`, `"STANDARD"`, and `"DURABLE_REDUCED_AVAILABILITY"`. + * @type int $numNewerVersions Relevant only for versioned objects. If + * the value is N, this condition is satisfied when there are at + * least N versions (including the live version) newer than this + * version of the object. + * } + * @return Lifecycle + */ + public function addSetStorageClassRule($storageClass, array $condition) + { + $this->lifecycle['rule'][] = [ + 'action' => [ + 'type' => 'SetStorageClass', + 'storageClass' => $storageClass + ], + 'condition' => $condition + ]; + + return $this; + } + + /** + * Clears out rules based on the provided condition. + * + * Example: + * ``` + * // Remove all rules. + * $lifecycle->clearRules(); + * ``` + * + * ``` + * // Remove all "Delete" based rules. + * $lifecycle->clearRules('Delete'); + * ``` + * + * @param string|callable $condition [optional] If a string is provided, it + * must be the name of the type of rule to remove (`SetStorageClass` + * or `Delete`). All rules of this type will then be cleared. When + * providing a callable you may define a custom route for how you + * would like to remove rules. The provided callable will be run + * through + * [array_filter](http://php.net/manual/en/function.array-filter.php). + * **Defaults to** `null`, clearing all assigned rules. + * @return Lifecycle + * @throws \InvalidArgumentException If a type other than a string or + * callabe is provided. + */ + public function clearRules($condition = null) + { + if (!$condition) { + $this->lifecycle = []; + return $this; + } + + if (!is_string($condition) && !is_callable($condition)) { + throw new \InvalidArgumentException( + sprintf( + 'Expected either a string or callable, instead got \'%s\'.', + gettype($condition) + ) + ); + } + + if (isset($this->lifecycle['rule'])) { + if (is_string($condition)) { + $condition = function ($rule) use ($condition) { + return $rule['action']['type'] !== $condition; + }; + } + + $this->lifecycle['rule'] = array_filter( + $this->lifecycle['rule'], + $condition + ); + + if (!$this->lifecycle['rule']) { + $this->lifecycle = []; + } + } + + return $this; + } + + /** + * @access private + * @return array + */ + public function toArray() + { + return $this->lifecycle; + } +} diff --git a/Storage/src/StorageClient.php b/Storage/src/StorageClient.php index 1c84f43a15ab..ae36e46e76fc 100644 --- a/Storage/src/StorageClient.php +++ b/Storage/src/StorageClient.php @@ -258,7 +258,7 @@ function (array $bucket) use ($userProject) { * configuration. * @type array $defaultObjectAcl Default access controls to apply to new * objects when no ACL is provided. - * @type array $lifecycle The bucket's lifecycle configuration. + * @type array|Lifecycle $lifecycle The bucket's lifecycle configuration. * @type string $location The location of the bucket. **Defaults to** * `"US"`. * @type array $logging The bucket's logging configuration, which @@ -306,6 +306,9 @@ public function createBucket($name, array $options = []) 'and we were unable to detect a default project ID.' ); } + if (isset($options['lifecycle']) && $options['lifecycle'] instanceof Lifecycle) { + $options['lifecycle'] = $options['lifecycle']->toArray(); + } $bucketUserProject = $this->pluck('bucketUserProject', $options, false); $bucketUserProject = !is_null($bucketUserProject) diff --git a/Storage/tests/Snippet/BucketTest.php b/Storage/tests/Snippet/BucketTest.php index 9831eec16899..cbfdad904f42 100644 --- a/Storage/tests/Snippet/BucketTest.php +++ b/Storage/tests/Snippet/BucketTest.php @@ -46,6 +46,23 @@ class BucketTest extends SnippetTestCase private $connection; private $bucket; + private static $expectedLifecycleData = [ + 'userProject' => null, + 'bucket' => self::BUCKET, + 'lifecycle' => [ + 'rule' => [ + [ + 'action' => [ + 'type' => 'Delete' + ], + 'condition' => [ + 'age' => 50, + 'isLive' => true + ] + ] + ] + ] + ]; public function setUp() { @@ -489,6 +506,31 @@ public function testName() $this->assertEquals(self::BUCKET, $res->output()); } + public function testLifecycle() + { + $snippet = $this->snippetFromMethod(Bucket::class, 'lifecycle'); + $snippet->addLocal('bucket', $this->bucket); + $this->connection->patchBucket(self::$expectedLifecycleData) + ->shouldBeCalled(); + $this->bucket->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + } + + public function testCurrentLifecycle() + { + $snippet = $this->snippetFromMethod(Bucket::class, 'currentLifecycle'); + $snippet->addLocal('bucket', $this->bucket); + $this->connection->getBucket(Argument::any()) + ->shouldBeCalled() + ->willReturn([]); + $this->connection->patchBucket(self::$expectedLifecycleData) + ->shouldBeCalled(); + $this->bucket->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + } + public function testIam() { $snippet = $this->snippetFromMethod(Bucket::class, 'iam'); diff --git a/Storage/tests/Snippet/LifecycleTest.php b/Storage/tests/Snippet/LifecycleTest.php new file mode 100644 index 000000000000..0294285cdf50 --- /dev/null +++ b/Storage/tests/Snippet/LifecycleTest.php @@ -0,0 +1,149 @@ + 50, + 'isLive' => true + ]; + + public function setUp() + { + $this->lifecycle = new Lifecycle; + } + + public function testClass() + { + $connection = $this->prophesize(Rest::class); + $connection->projectId() + ->willReturn(self::PROJECT_ID); + $connection->getBucket(Argument::any()) + ->willReturn([ + 'lifecycle' => ['test' => 'test'] + ]); + $client = TestHelpers::stub(StorageClient::class); + $client->___setProperty('connection', $connection->reveal()); + + $snippet = $this->snippetFromClass(Lifecycle::class); + $snippet->setLine(4, ''); + $snippet->addLocal('storage', $client); + $res = $snippet->invoke('lifecycle'); + + $this->assertInstanceOf(Lifecycle::class, $res->returnVal()); + } + + public function testClassStatic() + { + $snippet = $this->snippetFromClass(Lifecycle::class, 1); + $res = $snippet->invoke('lifecycle'); + + $this->assertInstanceOf(Lifecycle::class, $res->returnVal()); + } + + public function testAddDeleteRule() + { + $snippet = $this->snippetFromMethod(Lifecycle::class, 'addDeleteRule'); + $snippet->addLocal('lifecycle', $this->lifecycle); + + $returnVal = $snippet->invoke('lifecycle') + ->returnVal(); + + $this->assertInstanceOf(Lifecycle::class, $returnVal); + $this->assertEquals( + [ + 'rule' => [ + [ + 'action' => [ + 'type' => 'Delete' + ], + 'condition' => self::$condition + ] + ] + ], + $returnVal->toArray() + ); + } + + public function testAddSetStorageClassRule() + { + $snippet = $this->snippetFromMethod(Lifecycle::class, 'addSetStorageClassRule'); + $snippet->addLocal('lifecycle', $this->lifecycle); + + $returnVal = $snippet->invoke('lifecycle') + ->returnVal(); + + $this->assertInstanceOf(Lifecycle::class, $returnVal); + $this->assertEquals( + [ + 'rule' => [ + [ + 'action' => [ + 'type' => 'SetStorageClass', + 'storageClass' => 'COLDLINE' + ], + 'condition' => self::$condition + ] + ] + ], + $returnVal->toArray() + ); + } + + public function testClearRules() + { + $this->lifecycle + ->addDeleteRule(self::$condition); + $snippet = $this->snippetFromMethod(Lifecycle::class, 'clearRules'); + $snippet->addLocal('lifecycle', $this->lifecycle); + + $returnVal = $snippet->invoke('lifecycle') + ->returnVal(); + + $this->assertInstanceOf(Lifecycle::class, $returnVal); + $this->assertEmpty($returnVal->toArray()); + } + + public function testClearRulesWithString() + { + $this->lifecycle + ->addDeleteRule(self::$condition); + $snippet = $this->snippetFromMethod(Lifecycle::class, 'clearRules', 1); + $snippet->addLocal('lifecycle', $this->lifecycle); + + $returnVal = $snippet->invoke('lifecycle') + ->returnVal(); + + $this->assertInstanceOf(Lifecycle::class, $returnVal); + $this->assertEmpty($returnVal->toArray()); + } +} diff --git a/Storage/tests/System/ManageBucketsTest.php b/Storage/tests/System/ManageBucketsTest.php index 43aa2db09da7..470f611a758e 100644 --- a/Storage/tests/System/ManageBucketsTest.php +++ b/Storage/tests/System/ManageBucketsTest.php @@ -17,6 +17,8 @@ namespace Google\Cloud\Storage\Tests\System; +use Google\Cloud\Storage\Bucket; + /** * @group storage * @group storage-bucket @@ -69,6 +71,23 @@ public function testCreatesBucket() $this->assertEquals($options['versioning'], $bucket->info()['versioning']); } + public function testCreatesBucketWithLifeycleBuilder() + { + $lifecycle = Bucket::lifecycle() + ->addDeleteRule([ + 'age' => 500 + ]); + $name = uniqid(self::TESTING_PREFIX); + $this->assertFalse(self::$client->bucket($name)->exists()); + $bucket = self::createBucket(self::$client, $name, [ + 'lifecycle' => $lifecycle + ]); + + $this->assertTrue(self::$client->bucket($name)->exists()); + $this->assertEquals($name, $bucket->name()); + $this->assertEquals($lifecycle->toArray(), $bucket->info()['lifecycle']); + } + public function testUpdateBucket() { $options = [ @@ -82,6 +101,32 @@ public function testUpdateBucket() $this->assertEquals($options['website'], $info['website']); } + public function testUpdateBucketWithLifecycleBuilder() + { + $lifecycle = self::$bucket->currentLifecycle() + ->addDeleteRule([ + 'age' => 500 + ]); + $info = self::$bucket->update(['lifecycle' => $lifecycle]); + + $this->assertEquals($lifecycle->toArray(), $info['lifecycle']); + + $lifecycle = self::$bucket->currentLifecycle() + ->addDeleteRule([ + 'age' => 1000 + ]); + $info = self::$bucket->update(['lifecycle' => $lifecycle]); + + $this->assertEquals($lifecycle->toArray(), $info['lifecycle']); + + $lifecycle = self::$bucket->currentLifecycle() + ->clearRules('Delete'); + $info = self::$bucket->update(['lifecycle' => $lifecycle]); + + $this->assertEmpty($lifecycle->toArray()); + $this->assertArrayNotHasKey('lifecycle', $info); + } + public function testReloadBucket() { $this->assertEquals('storage#bucket', self::$bucket->reload()['kind']); diff --git a/Storage/tests/Unit/BucketTest.php b/Storage/tests/Unit/BucketTest.php index fcc671dbaf16..82330b533075 100644 --- a/Storage/tests/Unit/BucketTest.php +++ b/Storage/tests/Unit/BucketTest.php @@ -26,6 +26,7 @@ use Google\Cloud\Storage\Acl; use Google\Cloud\Storage\Bucket; use Google\Cloud\Storage\Connection\Rest; +use Google\Cloud\Storage\Lifecycle; use Google\Cloud\Storage\Notification; use Google\Cloud\Storage\StorageObject; use Prophecy\Argument; @@ -313,6 +314,31 @@ public function testUpdatesData() $this->assertTrue($bucket->info()['versioning']['enabled']); } + public function testUpdatesDataWithLifecycleBuilder() + { + $lifecycleArr = ['test' => 'test']; + $lifecycle = $this->prophesize(Lifecycle::class); + $lifecycle->toArray() + ->willReturn($lifecycleArr); + $this->connection + ->patchBucket([ + 'userProject' => null, + 'bucket' => self::BUCKET_NAME, + 'lifecycle' => $lifecycleArr + ]) + ->willReturn([ + 'lifecycle' => $lifecycleArr + ]); + $bucket = $this->getBucket(); + + $this->assertEquals( + $lifecycleArr, + $bucket->update( + ['lifecycle' => $lifecycle->reveal()] + )['lifecycle'] + ); + } + public function testGetsInfo() { $bucketInfo = [ @@ -347,6 +373,37 @@ public function testGetsName() $this->assertEquals(self::BUCKET_NAME, $bucket->name()); } + public function testLifecycle() + { + $this->assertInstanceOf(Lifecycle::class, Bucket::lifecycle()); + } + + public function testCurrentLifecycle() + { + $this->connection + ->getBucket(Argument::any()) + ->willReturn(['lifecycle' => ['test' => 'test']]); + + $this->assertInstanceOf( + Lifecycle::class, + $this->getBucket()->currentLifecycle() + ); + } + + public function testCurrentLifecycleWithCachedData() + { + $this->connection + ->getBucket(Argument::any()) + ->shouldNotBeCalled(); + + $this->assertInstanceOf( + Lifecycle::class, + $this->getBucket([ + 'lifecycle' => ['test' => ['test']] + ])->currentLifecycle() + ); + } + public function testIsWritable() { $this->connection->insertObject(Argument::any())->willReturn($this->resumableUploader); diff --git a/Storage/tests/Unit/LifecycleTest.php b/Storage/tests/Unit/LifecycleTest.php new file mode 100644 index 000000000000..a29eefc81bb3 --- /dev/null +++ b/Storage/tests/Unit/LifecycleTest.php @@ -0,0 +1,146 @@ + 50 + ]; + + public function setUp() + { + $this->lifecycle = new Lifecycle; + } + + public function testAddDeleteRule() + { + $expected = [ + 'rule' => [ + [ + 'action' => [ + 'type' => 'Delete' + ], + 'condition' => self::$condition + ] + ] + ]; + + $this->assertInstanceOf( + Lifecycle::class, + $this->lifecycle->addDeleteRule(self::$condition) + ); + $this->assertEquals($expected, $this->lifecycle->toArray()); + } + + public function testAddStorageClassRule() + { + $storageClass = 'NEARLINE'; + $expected = [ + 'rule' => [ + [ + 'action' => [ + 'type' => 'SetStorageClass', + 'storageClass' => $storageClass + ], + 'condition' => self::$condition + ] + ] + ]; + + $this->assertInstanceOf( + Lifecycle::class, + $this->lifecycle->addSetStorageClassRule($storageClass, self::$condition) + ); + $this->assertEquals($expected, $this->lifecycle->toArray()); + } + + public function testClearRules() + { + $this->lifecycle + ->addDeleteRule(self::$condition) + ->addDeleteRule(self::$condition); + + $this->assertCount( + 2, + $this->lifecycle->toArray()['rule'] + ); + + $this->assertInstanceOf( + Lifecycle::class, + $this->lifecycle->clearRules() + ); + $this->assertEmpty( + $this->lifecycle->toArray() + ); + } + + public function testClearRulesWithString() + { + $this->lifecycle + ->addDeleteRule(self::$condition) + ->addSetStorageClassRule('NEARLINE', self::$condition); + + $this->assertInstanceOf( + Lifecycle::class, + $this->lifecycle->clearRules('Delete') + ); + + $result = $this->lifecycle + ->toArray(); + + $this->assertCount(1, $result['rule']); + $this->assertEquals( + 'SetStorageClass', + current($result['rule'])['action']['type'] + ); + } + + public function testClearRulesWithCallable() + { + $this->lifecycle + ->addDeleteRule(self::$condition); + + $this->assertInstanceOf( + Lifecycle::class, + $this->lifecycle + ->clearRules(function ($rule) { + return $rule['condition']['age'] > 70; + }) + ); + $this->assertEmpty( + $this->lifecycle->toArray() + ); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testClearRulesThrowsExceptionWithInvalidType() + { + $this->lifecycle + ->clearRules(123); + } +} diff --git a/Storage/tests/Unit/StorageClientTest.php b/Storage/tests/Unit/StorageClientTest.php index b4149bfd00b6..fdc14233b48c 100644 --- a/Storage/tests/Unit/StorageClientTest.php +++ b/Storage/tests/Unit/StorageClientTest.php @@ -22,6 +22,7 @@ use Google\Cloud\Core\Upload\SignedUrlUploader; use Google\Cloud\Storage\Bucket; use Google\Cloud\Storage\Connection\Rest; +use Google\Cloud\Storage\Lifecycle; use Google\Cloud\Storage\StorageClient; use Google\Cloud\Storage\StreamWrapper; use GuzzleHttp\Psr7; @@ -132,6 +133,33 @@ public function testCreatesBucket() $this->assertInstanceOf(Bucket::class, $this->client->createBucket('bucket')); } + public function testCreatesBucketWithLifecycleBuilder() + { + $bucket = 'bucket'; + $lifecycleArr = ['test' => 'test']; + $lifecycle = $this->prophesize(Lifecycle::class); + $lifecycle->toArray() + ->willReturn($lifecycleArr); + $this->connection->projectId() + ->willReturn(self::PROJECT); + $this->connection + ->insertBucket([ + 'project' => self::PROJECT, + 'lifecycle' => $lifecycleArr, + 'name' => $bucket + ]) + ->willReturn(['name' => $bucket]); + $this->client->___setProperty('connection', $this->connection->reveal()); + + $this->assertInstanceOf( + Bucket::class, + $this->client->createBucket( + $bucket, + ['lifecycle' => $lifecycle->reveal()] + ) + ); + } + public function testRegisteringStreamWrapper() { $this->assertTrue($this->client->registerStreamWrapper()); From e2921f21c846d36f0e5e396248bf18019e08b79f Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Tue, 25 Sep 2018 15:06:39 -0400 Subject: [PATCH 2/5] clarify documentation for using a callable in clearRules --- Storage/src/Lifecycle.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Storage/src/Lifecycle.php b/Storage/src/Lifecycle.php index f1918ed71f5a..14e3c3b9b4d4 100644 --- a/Storage/src/Lifecycle.php +++ b/Storage/src/Lifecycle.php @@ -181,6 +181,9 @@ public function addSetStorageClassRule($storageClass, array $condition) * would like to remove rules. The provided callable will be run * through * [array_filter](http://php.net/manual/en/function.array-filter.php). + * The callable's argument will be a single lifecycle rule as an + * associative array. When returning true from the callable the rule + * will be preserved, and if false it will be removed. * **Defaults to** `null`, clearing all assigned rules. * @return Lifecycle * @throws \InvalidArgumentException If a type other than a string or From 816efde4f0e3021b81c0c1e21dc2d93476122ec2 Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Tue, 25 Sep 2018 16:36:28 -0400 Subject: [PATCH 3/5] code review updates --- Storage/src/Lifecycle.php | 33 ++++++++++++++++--------- Storage/tests/Snippet/LifecycleTest.php | 14 +++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/Storage/src/Lifecycle.php b/Storage/src/Lifecycle.php index 14e3c3b9b4d4..3c27e31e2b41 100644 --- a/Storage/src/Lifecycle.php +++ b/Storage/src/Lifecycle.php @@ -65,7 +65,7 @@ public function __construct(array $lifecycle = []) } /** - * Adds a delete rule. + * Adds an Object Lifecycle Delete Rule. * * Example: * ``` @@ -111,7 +111,7 @@ public function addDeleteRule(array $condition) } /** - * Adds a set storage class rule. + * Adds an Object Lifecycle Set Storage Class Rule. * * Example: * ``` @@ -161,7 +161,7 @@ public function addSetStorageClassRule($storageClass, array $condition) } /** - * Clears out rules based on the provided condition. + * Clear all Object Lifecycle rules or rules of a certain action type. * * Example: * ``` @@ -174,7 +174,16 @@ public function addSetStorageClassRule($storageClass, array $condition) * $lifecycle->clearRules('Delete'); * ``` * - * @param string|callable $condition [optional] If a string is provided, it + * ``` + * // Clear any rules which have an age equal to 50. + * $lifecycle->clearRules(function (array $rule) { + * return $rule['condition']['age'] === 50 + * ? false + * : true; + * }); + * ``` + * + * @param string|callable $action [optional] If a string is provided, it * must be the name of the type of rule to remove (`SetStorageClass` * or `Delete`). All rules of this type will then be cleared. When * providing a callable you may define a custom route for how you @@ -189,32 +198,32 @@ public function addSetStorageClassRule($storageClass, array $condition) * @throws \InvalidArgumentException If a type other than a string or * callabe is provided. */ - public function clearRules($condition = null) + public function clearRules($action = null) { - if (!$condition) { + if (!$action) { $this->lifecycle = []; return $this; } - if (!is_string($condition) && !is_callable($condition)) { + if (!is_string($action) && !is_callable($action)) { throw new \InvalidArgumentException( sprintf( 'Expected either a string or callable, instead got \'%s\'.', - gettype($condition) + gettype($action) ) ); } if (isset($this->lifecycle['rule'])) { - if (is_string($condition)) { - $condition = function ($rule) use ($condition) { - return $rule['action']['type'] !== $condition; + if (is_string($action)) { + $action = function ($rule) use ($action) { + return $rule['action']['type'] !== $action; }; } $this->lifecycle['rule'] = array_filter( $this->lifecycle['rule'], - $condition + $action ); if (!$this->lifecycle['rule']) { diff --git a/Storage/tests/Snippet/LifecycleTest.php b/Storage/tests/Snippet/LifecycleTest.php index 0294285cdf50..1dda1eeae40f 100644 --- a/Storage/tests/Snippet/LifecycleTest.php +++ b/Storage/tests/Snippet/LifecycleTest.php @@ -146,4 +146,18 @@ public function testClearRulesWithString() $this->assertInstanceOf(Lifecycle::class, $returnVal); $this->assertEmpty($returnVal->toArray()); } + + public function testClearRulesWithCallable() + { + $this->lifecycle + ->addDeleteRule(self::$condition); + $snippet = $this->snippetFromMethod(Lifecycle::class, 'clearRules', 2); + $snippet->addLocal('lifecycle', $this->lifecycle); + + $returnVal = $snippet->invoke('lifecycle') + ->returnVal(); + + $this->assertInstanceOf(Lifecycle::class, $returnVal); + $this->assertEmpty($returnVal->toArray()); + } } From 7aeb2f832ebdffdbbfc49e6d15be56e9bf637ca5 Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Tue, 25 Sep 2018 16:51:34 -0400 Subject: [PATCH 4/5] implement ArrayAccess and IteratorAggregate --- Storage/src/Lifecycle.php | 58 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/Storage/src/Lifecycle.php b/Storage/src/Lifecycle.php index 3c27e31e2b41..083217aa2b0e 100644 --- a/Storage/src/Lifecycle.php +++ b/Storage/src/Lifecycle.php @@ -47,7 +47,7 @@ * * @see https://cloud.google.com/storage/docs/lifecycle Object Lifecycle Management API Documentation */ -class Lifecycle +class Lifecycle implements \ArrayAccess, \IteratorAggregate { /** * @var array @@ -234,6 +234,21 @@ public function clearRules($action = null) return $this; } + /** + * @access private + * @return \Generator + */ + public function getIterator() + { + if (!isset($this->lifecycle['rule'])) { + return; + } + + foreach ($this->lifecycle['rule'] as $rule) { + yield $rule; + } + } + /** * @access private * @return array @@ -242,4 +257,45 @@ public function toArray() { return $this->lifecycle; } + + /** + * @access private + * @param string $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + $this->lifecycle['rule'][$offset] = $value; + } + + /** + * @access private + * @param string $offset + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->lifecycle['rule'][$offset]); + } + + /** + * @access private + * @param string $offset + */ + public function offsetUnset($offset) + { + unset($this->lifecycle['rule'][$offset]); + } + + /** + * @access private + * @param string $offset + * @return mixed + */ + public function offsetGet($offset) + { + return isset($this->lifecycle['rule'][$offset]) + ? $this->lifecycle['rule'][$offset] + : null; + } } From 429b1c52183cc331e539761a68ff477cb7434c15 Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Tue, 25 Sep 2018 17:05:42 -0400 Subject: [PATCH 5/5] Add example of iterating over existing rules --- Storage/src/Bucket.php | 9 +++++++++ Storage/tests/Snippet/BucketTest.php | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Storage/src/Bucket.php b/Storage/src/Bucket.php index 99530edf8d37..894ec978e069 100644 --- a/Storage/src/Bucket.php +++ b/Storage/src/Bucket.php @@ -1052,6 +1052,15 @@ public static function lifecycle(array $lifecycle = []) * ]); * ``` * + * ``` + * // Iterate over existing rules. + * $lifecycle = $bucket->currentLifecycle(); + * + * foreach ($lifecycle as $rule) { + * print_r($rule); + * } + * ``` + * * @see https://cloud.google.com/storage/docs/lifecycle Object Lifecycle Management API Documentation * * @param array $options [optional] Configuration options. diff --git a/Storage/tests/Snippet/BucketTest.php b/Storage/tests/Snippet/BucketTest.php index cbfdad904f42..1b4b78946371 100644 --- a/Storage/tests/Snippet/BucketTest.php +++ b/Storage/tests/Snippet/BucketTest.php @@ -531,6 +531,23 @@ public function testCurrentLifecycle() $res = $snippet->invoke(); } + public function testCurrentLifecycleIterate() + { + $snippet = $this->snippetFromMethod(Bucket::class, 'currentLifecycle', 1); + $snippet->addLocal('bucket', $this->bucket); + $this->connection->getBucket(Argument::any()) + ->shouldBeCalled() + ->willReturn(self::$expectedLifecycleData); + $this->bucket->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + + $this->assertEquals( + print_r(self::$expectedLifecycleData['lifecycle']['rule'][0], true), + $res->output() + ); + } + public function testIam() { $snippet = $this->snippetFromMethod(Bucket::class, 'iam');