From b6b5b7fa38229ed7b5eeaec164e744ed3ac0964d Mon Sep 17 00:00:00 2001 From: Craig Morris Date: Fri, 8 Jun 2018 18:01:24 +0100 Subject: [PATCH] feat: Model API after Google's Firestore PHP library (free docs!) BREAKING CHANGE: new API --- src/Collection.php | 451 -------------------- src/CollectionReference.php | 99 +++++ src/{Document.php => DocumentReference.php} | 248 +---------- src/{Snapshot.php => DocumentSnapshot.php} | 4 +- src/Firestore.php | 6 +- src/Query.php | 0 tests/DocumentTest.php | 42 +- tests/TestCase.php | 2 +- 8 files changed, 115 insertions(+), 737 deletions(-) delete mode 100644 src/Collection.php create mode 100644 src/CollectionReference.php rename src/{Document.php => DocumentReference.php} (52%) rename src/{Snapshot.php => DocumentSnapshot.php} (99%) create mode 100644 src/Query.php diff --git a/src/Collection.php b/src/Collection.php deleted file mode 100644 index 1605132..0000000 --- a/src/Collection.php +++ /dev/null @@ -1,451 +0,0 @@ -validator = $validator ?? new Validator(); - $this->validator->validateUri($uri); - - $this->valueMapper = $valueMapper ?? new ValueMapper(null, false); - - $this->uri = $uri; - $this->apiClient = $apiClient; - $this->valueMapper = $valueMapper; - } - - /** - * The last part of the current path. - * - * For example, "ada" is the key for https://sample-app.firebaseio.com/users/ada. - * - * The key of the root Reference is null. - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#key - * - * @return string|null - */ - public function getKey() - { - $key = basename($this->getPath()); - - return '' !== $key ? $key : null; - } - - /** - * Returns the full path to a reference. - * - * @return string - */ - public function getPath(): string - { - return trim($this->uri->getPath(), '/'); - } - - /** - * The parent location of a Reference. - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#parent - * - * @throws OutOfRangeException if requested for the root Reference - * - * @return Reference - */ - public function getParent(): self - { - $parentPath = \dirname($this->getPath()); - - if ('.' === $parentPath) { - throw new OutOfRangeException('Cannot get parent of root reference'); - } - - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return new self($this->uri->withPath($parentPath), $this->apiClient, $this->validator); - } - - /** - * The root location of a Reference. - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#root - * - * @return Reference - */ - public function getRoot(): self - { - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return new self($this->uri->withPath('/'), $this->apiClient, $this->validator); - } - - /** - * Gets a Reference for the location at the specified relative path. - * - * The relative path can either be a simple child name (for example, "ada") - * or a deeper slash-separated path (for example, "ada/name/first"). - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#child - * - * @param string $path - * - * @throws InvalidArgumentException if the path is invalid - * - * @return Reference - */ - public function getDocument(string $path): Document - { - $childPath = sprintf('%s/%s', trim($this->uri->getPath(), '/'), trim($path, '/')); - - try { - return new Document($this->uri->withPath($childPath), $this->apiClient, $this->validator, $this->valueMapper); - } catch (\InvalidArgumentException $e) { - throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Generates a new Query object ordered by the specified child key. - * - * @see Query::orderByChild() - * - * @param string $path - * - * @return Query - */ - public function orderByChild(string $path): Query - { - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return $this->query()->orderByChild($path); - } - - /** - * Generates a new Query object ordered by key. - * - * @see Query::orderByKey() - * - * @return Query - */ - public function orderByKey(): Query - { - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return $this->query()->orderByKey(); - } - - /** - * Generates a new Query object ordered by child values. - * - * @see Query::orderByValue() - * - * @return Query - */ - public function orderByValue(): Query - { - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return $this->query()->orderByValue(); - } - - /** - * Generates a new Query limited to the first specific number of children. - * - * @see Query::limitToFirst() - * - * @param int $limit - * - * @return Query - */ - public function limitToFirst(int $limit): Query - { - return $this->query()->limitToFirst($limit); - } - - /** - * Generates a new Query object limited to the last specific number of children. - * - * @see Query::limitToLast() - * - * @param int $limit - * - * @return Query - */ - public function limitToLast(int $limit): Query - { - return $this->query()->limitToLast($limit); - } - - /** - * Creates a Query with the specified starting point. - * - * @see Query::startAt() - * - * @param int|float|string|bool $value $value - * - * @return Query - */ - public function startAt($value): Query - { - return $this->query()->startAt($value); - } - - /** - * Creates a Query with the specified ending point. - * - * @see Query::endAt() - * - * @param int|float|string|bool $value - * - * @return Query - */ - public function endAt($value): Query - { - return $this->query()->endAt($value); - } - - /** - * Creates a Query which includes children which match the specified value. - * - * @see Query::equalTo() - * - * @param int|float|string|bool $value - * - * @return Query - */ - public function equalTo($value): Query - { - return $this->query()->equalTo($value); - } - - /** - * Creates a Query with shallow results. - * - * @see Query::shallow() - * - * @return Query - */ - public function shallow(): Query - { - return $this->query()->shallow(); - } - - /** - * Returns the keys of a reference's children. - * - * @throws OutOfRangeException if the reference has no children with keys - * @throws ApiException if the API reported an error - * - * @return string[] - */ - public function getChildKeys(): array - { - /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ - $snapshot = $this->shallow()->getSnapshot(); - - if (\is_array($value = $snapshot->getValue())) { - return array_keys($value); - } - - throw new OutOfRangeException(sprintf('%s has no children with keys', $this)); - } - - /** - * Convenience method for {@see getSnapshot()}->getValue(). - * - * @throws ApiException if the API reported an error - * - * @return mixed - */ - public function getValue() - { - return $this->getSnapshot()->getValue(); - } - - /** - * Write data to this database location. - * - * This will overwrite any data at this location and all child locations. - * - * Passing null for the new value is equivalent to calling {@see remove()}: - * all data at this location or any child location will be deleted. - * - * @param mixed $value - * - * @throws ApiException if the API reported an error - * - * @return Reference - */ - public function set($value): self - { - $this->apiClient->set($this->uri, $value); - - return $this; - } - - /** - * Returns a data snapshot of the current location. - * - * @throws ApiException if the API reported an error - * - * @return Snapshot - */ - public function getSnapshot(): Snapshot - { - $value = $this->apiClient->get($this->uri); - - return new Snapshot($this, $value); - } - - /** - * Generates a new child location using a unique key and returns its reference. - * - * This is the most common pattern for adding data to a collection of items. - * - * If you provide a value to push(), the value will be written to the generated location. - * If you don't pass a value, nothing will be written to the database and the child - * will remain empty (but you can use the reference elsewhere). - * - * The unique key generated by push() are ordered by the current time, so the resulting - * list of items will be chronologically sorted. The keys are also designed to be - * unguessable (they contain 72 random bits of entropy). - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#push - * - * @param mixed $value - * - * @throws ApiException if the API reported an error - * - * @return Reference A new reference for the added child - */ - public function push($value = null): self - { - $newKey = $this->apiClient->push($this->uri, $value); - $newPath = sprintf('%s/%s', $this->uri->getPath(), $newKey); - - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return new self($this->uri->withPath($newPath), $this->apiClient, $this->validator); - } - - /** - * Remove the data at this database location. - * - * Any data at child locations will also be deleted. - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#remove - * - * @throws ApiException if the API reported an error - * - * @return Reference A new instance for the now empty Reference - */ - public function remove(): self - { - $this->apiClient->remove($this->uri); - - return $this; - } - - /** - * Writes multiple values to the database at once. - * - * The values argument contains multiple property/value pairs that will be written to the database together. - * Each child property can either be a simple property (for example, "name"), or a relative path - * (for example, "name/first") from the current location to the data to update. - * - * As opposed to the {@see set()} method, update() can be use to selectively update only the referenced properties - * at the current location (instead of replacing all the child properties at the current location). - * - * Passing null to {see update()} will remove the data at this location. - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#update - * - * @param array $values - * - * @throws ApiException if the API reported an error - * - * @return Reference - */ - public function update(array $values): self - { - $this->apiClient->update($this->uri, $values); - - return $this; - } - - /** - * Returns the absolute URL for this location. - * - * This method returns a URL that is ready to be put into a browser, curl command, or a - * {@see Database::getReferenceFromUrl()} call. Since all of those expect the URL - * to be url-encoded, toString() returns an encoded URL. - * - * Append '.json' to the URL when typed into a browser to download JSON formatted data. - * If the location is secured (not publicly readable), - * you will get a permission-denied error. - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#toString - * - * @return UriInterface - */ - public function getUri(): UriInterface - { - return $this->uri; - } - - /** - * Returns the absolute URL for this location. - * - * @see getUri() - * - * @return string - */ - public function __toString() - { - return (string) $this->getUri(); - } - - /** - * Returns a new query for the current reference. - * - * @return Query - */ - private function query(): Query - { - return new Query($this, $this->apiClient); - } -} diff --git a/src/CollectionReference.php b/src/CollectionReference.php new file mode 100644 index 0000000..9210fc9 --- /dev/null +++ b/src/CollectionReference.php @@ -0,0 +1,99 @@ +validator = $validator ?? new Validator(); + $this->validator->validateUri($uri); + + $this->valueMapper = $valueMapper ?? new ValueMapper(null, false); + + $this->uri = $uri; + $this->apiClient = $apiClient; + $this->valueMapper = $valueMapper; + } + + /** + * Gets a Reference for the location at the specified relative path. + * + * The relative path can either be a simple child name (for example, "ada") + * or a deeper slash-separated path (for example, "ada/name/first"). + * + * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#child + * + * @param string $path + * + * @throws InvalidArgumentException if the path is invalid + * + * @return Reference + */ + public function document(string $path): DocumentReference + { + $childPath = sprintf('%s/%s', trim($this->uri->getPath(), '/'), trim($path, '/')); + + try { + return new DocumentReference($this->uri->withPath($childPath), $this->apiClient, $this->validator, $this->valueMapper); + } catch (\InvalidArgumentException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Remove the data at this database location. + * + * Any data at child locations will also be deleted. + * + * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#remove + * + * @throws ApiException if the API reported an error + * + * @return Reference A new instance for the now empty Reference + */ + public function remove(): self + { + throw new \BadMethodCallException('Not implemented'); + } + +} diff --git a/src/Document.php b/src/DocumentReference.php similarity index 52% rename from src/Document.php rename to src/DocumentReference.php index 95cc609..c8311bb 100644 --- a/src/Document.php +++ b/src/DocumentReference.php @@ -14,7 +14,7 @@ * * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference */ -class Document +class DocumentReference { /** * @var UriInterface @@ -57,248 +57,6 @@ public function __construct(UriInterface $uri, ApiClient $apiClient, Validator $ $this->apiClient = $apiClient; } - /** - * The last part of the current path. - * - * For example, "ada" is the key for https://sample-app.firebaseio.com/users/ada. - * - * The key of the root Reference is null. - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#key - * - * @return string|null - */ - public function getKey() - { - $key = basename($this->getPath()); - - return '' !== $key ? $key : null; - } - - /** - * Returns the full path to a reference. - * - * @return string - */ - public function getPath(): string - { - return trim($this->uri->getPath(), '/'); - } - - /** - * The parent location of a Reference. - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#parent - * - * @throws OutOfRangeException if requested for the root Reference - * - * @return Reference - */ - public function getParent(): self - { - $parentPath = \dirname($this->getPath()); - - if ('.' === $parentPath) { - throw new OutOfRangeException('Cannot get parent of root reference'); - } - - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return new self($this->uri->withPath($parentPath), $this->apiClient, $this->validator); - } - - /** - * The root location of a Reference. - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#root - * - * @return Reference - */ - public function getRoot(): self - { - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return new self($this->uri->withPath('/'), $this->apiClient, $this->validator); - } - - /** - * Gets a Reference for the location at the specified relative path. - * - * The relative path can either be a simple child name (for example, "ada") - * or a deeper slash-separated path (for example, "ada/name/first"). - * - * @see https://firebase.google.com/docs/reference/js/firebase.database.Reference#child - * - * @param string $path - * - * @throws InvalidArgumentException if the path is invalid - * - * @return Reference - */ - public function getChild(string $path): self - { - $childPath = sprintf('%s/%s', trim($this->uri->getPath(), '/'), trim($path, '/')); - - try { - return new self($this->uri->withPath($childPath), $this->apiClient, $this->validator); - } catch (\InvalidArgumentException $e) { - throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Generates a new Query object ordered by the specified child key. - * - * @see Query::orderByChild() - * - * @param string $path - * - * @return Query - */ - public function orderByChild(string $path): Query - { - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return $this->query()->orderByChild($path); - } - - /** - * Generates a new Query object ordered by key. - * - * @see Query::orderByKey() - * - * @return Query - */ - public function orderByKey(): Query - { - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return $this->query()->orderByKey(); - } - - /** - * Generates a new Query object ordered by child values. - * - * @see Query::orderByValue() - * - * @return Query - */ - public function orderByValue(): Query - { - /* @noinspection ExceptionsAnnotatingAndHandlingInspection */ - return $this->query()->orderByValue(); - } - - /** - * Generates a new Query limited to the first specific number of children. - * - * @see Query::limitToFirst() - * - * @param int $limit - * - * @return Query - */ - public function limitToFirst(int $limit): Query - { - return $this->query()->limitToFirst($limit); - } - - /** - * Generates a new Query object limited to the last specific number of children. - * - * @see Query::limitToLast() - * - * @param int $limit - * - * @return Query - */ - public function limitToLast(int $limit): Query - { - return $this->query()->limitToLast($limit); - } - - /** - * Creates a Query with the specified starting point. - * - * @see Query::startAt() - * - * @param int|float|string|bool $value $value - * - * @return Query - */ - public function startAt($value): Query - { - return $this->query()->startAt($value); - } - - /** - * Creates a Query with the specified ending point. - * - * @see Query::endAt() - * - * @param int|float|string|bool $value - * - * @return Query - */ - public function endAt($value): Query - { - return $this->query()->endAt($value); - } - - /** - * Creates a Query which includes children which match the specified value. - * - * @see Query::equalTo() - * - * @param int|float|string|bool $value - * - * @return Query - */ - public function equalTo($value): Query - { - return $this->query()->equalTo($value); - } - - /** - * Creates a Query with shallow results. - * - * @see Query::shallow() - * - * @return Query - */ - public function shallow(): Query - { - return $this->query()->shallow(); - } - - /** - * Returns the keys of a reference's children. - * - * @throws OutOfRangeException if the reference has no children with keys - * @throws ApiException if the API reported an error - * - * @return string[] - */ - public function getChildKeys(): array - { - /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ - $snapshot = $this->shallow()->getSnapshot(); - - if (\is_array($value = $snapshot->getValue())) { - return array_keys($value); - } - - throw new OutOfRangeException(sprintf('%s has no children with keys', $this)); - } - - /** - * Convenience method for {@see getSnapshot()}->getValue(). - * - * @throws ApiException if the API reported an error - * - * @return mixed - */ - public function getValue() - { - return $this->getSnapshot()->getValue(); - } - /** * Write data to this database location. * @@ -342,13 +100,13 @@ public function set($value, $merge = false): self * * @return Snapshot */ - public function getSnapshot(): Snapshot + public function snapshot(): DocumentSnapshot { $value = $this->apiClient->get($this->uri); $data = $this->valueMapper->decodeValues($value['fields']); - return new Snapshot($this, [], $data, true); + return new DocumentSnapshot($this, [], $data, true); } /** diff --git a/src/Snapshot.php b/src/DocumentSnapshot.php similarity index 99% rename from src/Snapshot.php rename to src/DocumentSnapshot.php index c2371ed..48aa597 100644 --- a/src/Snapshot.php +++ b/src/DocumentSnapshot.php @@ -40,7 +40,7 @@ * $bitcoinWalletValue = $snapshot['wallet']['cryptoCurrency']['bitcoin']; * ``` */ -class Snapshot implements \ArrayAccess +class DocumentSnapshot implements \ArrayAccess { /** * @var DocumentReference @@ -75,7 +75,7 @@ class Snapshot implements \ArrayAccess * @param bool $exists Whether the document exists in the Firestore database. */ public function __construct( - Document $reference, + DocumentReference $reference, array $info, array $data, $exists diff --git a/src/Firestore.php b/src/Firestore.php index 0d3850f..19aae6b 100644 --- a/src/Firestore.php +++ b/src/Firestore.php @@ -44,10 +44,10 @@ public function __construct(UriInterface $uri, ApiClient $client) * * @return Reference */ - public function getCollection(string $path = ''): Collection + public function collection(string $path = ''): CollectionReference { try { - return new Collection(Uri::resolve($this->uri, $path), $this->client); + return new CollectionReference(Uri::resolve($this->uri, $path), $this->client); } catch (\InvalidArgumentException $e) { throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); } @@ -56,7 +56,7 @@ public function getCollection(string $path = ''): Collection /** * Returns the root collections. */ - public function getRootCollections() + public function collections() { $uri = $this->uri->withPath($this->uri->getPath() . ':listCollectionIds'); $value = $this->client->post($uri, null); diff --git a/src/Query.php b/src/Query.php new file mode 100644 index 0000000..e69de29 diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 44c4b0d..57e79c3 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -5,13 +5,13 @@ class DocumentTest extends TestCase { /** - * @var Collection + * @var CollectionReference */ private $collection; public function setUp() { - $this->collection = self::$firestore->getCollection(self::$testCollection); + $this->collection = self::$firestore->collection(self::$testCollection); } /** @@ -22,11 +22,11 @@ public function setUp() */ public function testSetAndGet() { - $doc = $this->collection->getDocument(__FUNCTION__); + $doc = $this->collection->document(__FUNCTION__); $doc->set($this->validValues()); - $snap = $doc->getSnapshot(); + $snap = $doc->snapshot(); foreach ($this->validValues() as $key => $value) { $this->assertSame($value, $snap[$key]); @@ -35,7 +35,8 @@ public function testSetAndGet() public function testSetMergeAndGet() { - $doc = $this->collection->getDocument(__FUNCTION__); + $doc = $this->collection->document(__FUNCTION__); + $doc->set([ 'first' => 'value', 'second' => 'value', @@ -52,36 +53,7 @@ public function testSetMergeAndGet() 'third' => 'new', ]; - $this->assertEquals($expected, $doc->getSnapshot()->data()); - } - - public function testPush() - { - $this->markTestIncomplete('This test has not been implemented yet.'); - - $ref = $this->ref->getChild(__FUNCTION__); - $value = 'a value'; - - $newRef = $ref->push($value); - - $this->assertSame(1, $ref->getSnapshot()->numChildren()); - $this->assertSame($value, $newRef->getValue()); - } - - public function testRemove() - { - $this->markTestIncomplete('This test has not been implemented yet.'); - - $ref = $this->ref->getChild(__FUNCTION__); - - $ref->set([ - 'first' => 'value', - 'second' => 'value', - ]); - - $ref->getChild('first')->remove(); - - $this->assertEquals(['second' => 'value'], $ref->getValue()); + $this->assertEquals($expected, $doc->snapshot()->data()); } public function validValues() diff --git a/tests/TestCase.php b/tests/TestCase.php index f7a317a..b9181d1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -36,7 +36,7 @@ public static function setUpBeforeClass() self::$testCollection = 'tests'; try { - self::$firestore->getCollection(self::$testCollection)->remove(); + self::$firestore->collection(self::$testCollection)->remove(); } catch (\Exception $e) { // assuming it just doesn't exist yet, continue with tests