allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s

This commit is contained in:
2026-03-30 14:54:57 +07:00
parent 66aed7c4e8
commit b5e3a778ce
21316 changed files with 2777892 additions and 13 deletions

View File

@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database;
use Beste\Json;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Query;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Uri;
use Kreait\Firebase\Exception\DatabaseApiExceptionConverter;
use Kreait\Firebase\Exception\DatabaseException;
use Psr\Http\Message\ResponseInterface;
use Throwable;
/**
* @internal
*/
class ApiClient
{
public function __construct(
private readonly ClientInterface $client,
private readonly UrlBuilder $resourceUrlBuilder,
private readonly DatabaseApiExceptionConverter $errorHandler,
) {
}
/**
* @throws DatabaseException
*/
public function get(string $path): mixed
{
$response = $this->requestApi('GET', $path);
return Json::decode((string) $response->getBody(), true);
}
/**
* @throws DatabaseException
*
* @return array<string, mixed>
*/
public function getWithETag(string $path): array
{
$response = $this->requestApi('GET', $path, [
'headers' => [
'X-Firebase-ETag' => 'true',
],
]);
$value = Json::decode((string) $response->getBody(), true);
$etag = $response->getHeaderLine('ETag');
return [
'value' => $value,
'etag' => $etag,
];
}
/**
* @throws DatabaseException
*/
public function set(string $path, mixed $value): mixed
{
$response = $this->requestApi('PUT', $path, ['json' => $value]);
return Json::decode((string) $response->getBody(), true);
}
/**
* @throws DatabaseException
*/
public function setWithEtag(string $path, mixed $value, string $etag): mixed
{
$response = $this->requestApi('PUT', $path, [
'headers' => [
'if-match' => $etag,
],
'json' => $value,
]);
return Json::decode((string) $response->getBody(), true);
}
/**
* @throws DatabaseException
*/
public function removeWithEtag(string $path, string $etag): void
{
$this->requestApi('DELETE', $path, [
'headers' => [
'if-match' => $etag,
],
]);
}
/**
* @throws DatabaseException
*/
public function updateRules(string $path, RuleSet $ruleSet): mixed
{
$rules = $ruleSet->getRules();
$encodedRules = Json::encode((object) $rules);
$response = $this->requestApi('PUT', $path, [
'body' => $encodedRules,
]);
return Json::decode((string) $response->getBody(), true);
}
/**
* @throws DatabaseException
*/
public function push(string $path, mixed $value): string
{
$response = $this->requestApi('POST', $path, ['json' => $value]);
return Json::decode((string) $response->getBody(), true)['name'];
}
/**
* @throws DatabaseException
*/
public function remove(string $path): void
{
$this->requestApi('DELETE', $path);
}
/**
* @param array<array-key, mixed> $values
*
* @throws DatabaseException
*/
public function update(string $path, array $values): void
{
$this->requestApi('PATCH', $path, ['json' => $values]);
}
/**
* @param array<string, mixed>|null $options
*
* @throws DatabaseException
*/
private function requestApi(string $method, string $path, ?array $options = []): ResponseInterface
{
$options ??= [];
$uri = new Uri($path);
$url = $this->resourceUrlBuilder->getUrl(
$uri->getPath(),
Query::parse($uri->getQuery()),
);
$request = new Request($method, $url);
try {
return $this->client->send($request, $options);
} catch (Throwable $e) {
throw $this->errorHandler->convertException($e);
}
}
}

View File

@@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database;
use Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\Filter\EndAt;
use Kreait\Firebase\Database\Query\Filter\EndBefore;
use Kreait\Firebase\Database\Query\Filter\EqualTo;
use Kreait\Firebase\Database\Query\Filter\LimitToFirst;
use Kreait\Firebase\Database\Query\Filter\LimitToLast;
use Kreait\Firebase\Database\Query\Filter\Shallow;
use Kreait\Firebase\Database\Query\Filter\StartAfter;
use Kreait\Firebase\Database\Query\Filter\StartAt;
use Kreait\Firebase\Database\Query\Sorter;
use Kreait\Firebase\Database\Query\Sorter\OrderByChild;
use Kreait\Firebase\Database\Query\Sorter\OrderByKey;
use Kreait\Firebase\Database\Query\Sorter\OrderByValue;
use Kreait\Firebase\Exception\Database\DatabaseNotFound;
use Kreait\Firebase\Exception\Database\UnsupportedQuery;
use Kreait\Firebase\Exception\DatabaseException;
use Psr\Http\Message\UriInterface;
use Stringable;
/**
* A Query sorts and filters the data at a database location so only a subset of the child data is included.
* This can be used to order a collection of data by some attribute (e.g. height of dinosaurs) as well as
* to restrict a large list of items (e.g. chat messages) down to a number suitable for synchronizing
* to the client. Queries are created by chaining together one or more of the filter methods
* defined here.
*
* Just as with a Reference, you can receive data from a Query by using the
* {@see getSnapshot()} or {@see getValue()} method. You will only receive
* Snapshots for the subset of the data that matches your query.
*/
class Query implements Stringable
{
/**
* @var Filter[]
*/
private array $filters = [];
private ?Sorter $sorter = null;
/**
* @internal
*/
public function __construct(private readonly Reference $reference, private readonly ApiClient $apiClient)
{
}
/**
* Returns the absolute URL for this location.
*
* @see getUri()
*/
public function __toString(): string
{
return (string) $this->getUri();
}
/**
* Returns a Reference to the Query's location.
*/
public function getReference(): Reference
{
return $this->reference;
}
/**
* Returns a data snapshot of the current location.
*
* @throws UnsupportedQuery if an error occurred
*/
public function getSnapshot(): Snapshot
{
$uri = $this->getUri();
$pathAndQuery = $uri->getPath().'?'.$uri->getQuery();
try {
$value = $this->apiClient->get($pathAndQuery);
} catch (DatabaseNotFound $e) {
throw $e;
} catch (DatabaseException $e) {
throw new UnsupportedQuery($this, $e->getMessage(), $e->getCode(), $e->getPrevious());
}
if ($this->sorter !== null) {
$value = $this->sorter->modifyValue($value);
}
foreach ($this->filters as $filter) {
$value = $filter->modifyValue($value);
}
return new Snapshot($this->reference, $value);
}
/**
* Convenience method for {@see getSnapshot()}->getValue().
*
* @throws UnsupportedQuery if an error occurred
*/
public function getValue(): mixed
{
return $this->getSnapshot()->getValue();
}
/**
* Creates a Query with the specified ending point.
*
* The ending point is inclusive, so children with exactly
* the specified value will be included in the query.
*
* @param scalar $value
*/
public function endAt($value): self
{
return $this->withAddedFilter(new EndAt($value));
}
/**
* Creates a Query with the specified ending point (exclusive).
*
* @param scalar $value
*/
public function endBefore($value): self
{
return $this->withAddedFilter(new EndBefore($value));
}
/**
* Creates a Query which includes children which match the specified value.
*
* @param scalar $value
*/
public function equalTo($value): self
{
return $this->withAddedFilter(new EqualTo($value));
}
/**
* Creates a Query with the specified starting point (inclusive).
*
* @param scalar $value
*/
public function startAt($value): self
{
return $this->withAddedFilter(new StartAt($value));
}
/**
* Creates a Query with the specified starting point (exclusive).
*
* @param scalar $value
*/
public function startAfter($value): self
{
return $this->withAddedFilter(new StartAfter($value));
}
/**
* Generates a new Query limited to the first specific number of children.
*/
public function limitToFirst(int $limit): self
{
return $this->withAddedFilter(new LimitToFirst($limit));
}
/**
* Generates a new Query object limited to the last specific number of children.
*/
public function limitToLast(int $limit): self
{
return $this->withAddedFilter(new LimitToLast($limit));
}
/**
* Generates a new Query object ordered by the specified child key.
*
* Queries can only order by one key at a time. Calling orderBy*() multiple times on
* the same query is an error.
*
* @throws UnsupportedQuery if the query is already ordered
*/
public function orderByChild(string $childKey): self
{
return $this->withSorter(new OrderByChild($childKey));
}
/**
* Generates a new Query object ordered by key.
*
* Sorts the results of a query by their ascending key value.
*
* Queries can only order by one key at a time. Calling orderBy*() multiple times on
* the same query is an error.
*
* @throws UnsupportedQuery if the query is already ordered
*/
public function orderByKey(): self
{
return $this->withSorter(new OrderByKey());
}
/**
* Generates a new Query object ordered by child values.
*
* If the children of a query are all scalar values (numbers or strings), you can order the results
* by their (ascending) values.
*
* Queries can only order by one key at a time. Calling orderBy*() multiple times on
* the same query is an error.
*
* @throws UnsupportedQuery if the query is already ordered
*/
public function orderByValue(): self
{
return $this->withSorter(new OrderByValue());
}
/**
* This is an advanced feature, designed to help you work with large datasets without needing to download
* everything. Set this to true to limit the depth of the data returned at a location. If the data at
* the location is a JSON primitive (string, number or boolean), its value will simply be returned.
*
* If the data snapshot at the location is a JSON object, the values for each key will be
* truncated to true.
*
* @see https://firebase.google.com/docs/reference/rest/database/#section-param-shallow
*/
public function shallow(): self
{
return $this->withAddedFilter(new Shallow());
}
/**
* 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.
*/
public function getUri(): UriInterface
{
$uri = $this->reference->getUri();
if ($this->sorter !== null) {
$uri = $this->sorter->modifyUri($uri);
}
foreach ($this->filters as $filter) {
$uri = $filter->modifyUri($uri);
}
return $uri;
}
private function withAddedFilter(Filter $filter): self
{
$query = clone $this;
$query->filters[] = $filter;
return $query;
}
private function withSorter(Sorter $sorter): self
{
if ($this->sorter !== null) {
throw new UnsupportedQuery($this, 'This query is already ordered.');
}
$query = clone $this;
$query->sorter = $sorter;
return $query;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query;
/**
* @internal
*/
interface Filter extends Modifier
{
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Filter;
use Beste\Json;
use Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
final class EndAt implements Filter
{
use ModifierTrait;
public function __construct(private readonly bool|float|int|string $value)
{
}
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'endAt', Json::encode($this->value));
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Filter;
use Beste\Json;
use Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
final class EndBefore implements Filter
{
use ModifierTrait;
public function __construct(private readonly int|float|string|bool $value)
{
}
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'endBefore', Json::encode($this->value));
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Filter;
use Beste\Json;
use Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
final class EqualTo implements Filter
{
use ModifierTrait;
public function __construct(private readonly bool|float|int|string $value)
{
}
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'equalTo', Json::encode($this->value));
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
final class LimitToFirst implements Filter
{
use ModifierTrait;
private readonly int $limit;
public function __construct(int $limit)
{
if ($limit < 1) {
throw new InvalidArgumentException('Limit must be 1 or greater');
}
$this->limit = $limit;
}
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'limitToFirst', $this->limit);
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
final class LimitToLast implements Filter
{
use ModifierTrait;
private readonly int $limit;
public function __construct(int $limit)
{
if ($limit < 1) {
throw new InvalidArgumentException('Limit must be 1 or greater');
}
$this->limit = $limit;
}
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'limitToLast', $this->limit);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
final class Shallow implements Filter
{
use ModifierTrait;
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'shallow', 'true');
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Filter;
use Beste\Json;
use Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
final class StartAfter implements Filter
{
use ModifierTrait;
public function __construct(private readonly int|float|string|bool $value)
{
}
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'startAfter', Json::encode($this->value));
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Filter;
use Beste\Json;
use Kreait\Firebase\Database\Query\Filter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
final class StartAt implements Filter
{
use ModifierTrait;
public function __construct(private readonly int|float|string|bool $value)
{
}
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'startAt', Json::encode($this->value));
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query;
use Psr\Http\Message\UriInterface;
/**
* @internal
*/
interface Modifier
{
/**
* Modifies the given URI and returns it.
*/
public function modifyUri(UriInterface $uri): UriInterface;
/**
* Modifies the given value and returns it.
*/
public function modifyValue(mixed $value): mixed;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query;
use GuzzleHttp\Psr7\Query;
use Psr\Http\Message\UriInterface;
use function array_merge;
/**
* @internal
*/
trait ModifierTrait
{
public function modifyValue(mixed $value): mixed
{
return $value;
}
protected function appendQueryParam(UriInterface $uri, string $key, mixed $value): UriInterface
{
$queryParams = array_merge(Query::parse($uri->getQuery()), [$key => $value]);
$queryString = Query::build($queryParams);
return $uri->withQuery($queryString);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query;
/**
* @internal
*/
interface Sorter extends Modifier
{
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Sorter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Kreait\Firebase\Database\Query\Sorter;
use Psr\Http\Message\UriInterface;
use function is_array;
use function JmesPath\search;
use function sprintf;
use function str_replace;
use function uasort;
/**
* @internal
*/
final class OrderByChild implements Sorter
{
use ModifierTrait;
public function __construct(private readonly string $childKey)
{
}
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'orderBy', sprintf('"%s"', $this->childKey));
}
public function modifyValue(mixed $value): mixed
{
if (!is_array($value)) {
return $value;
}
$expression = str_replace('/', '.', $this->childKey);
uasort($value, static fn($a, $b): int => search($expression, $a) <=> search($expression, $b));
return $value;
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Sorter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Kreait\Firebase\Database\Query\Sorter;
use Psr\Http\Message\UriInterface;
use function is_array;
use function ksort;
/**
* @internal
*/
final class OrderByKey implements Sorter
{
use ModifierTrait;
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'orderBy', '"$key"');
}
public function modifyValue(mixed $value): mixed
{
if (!is_array($value)) {
return $value;
}
ksort($value);
return $value;
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database\Query\Sorter;
use Kreait\Firebase\Database\Query\ModifierTrait;
use Kreait\Firebase\Database\Query\Sorter;
use Psr\Http\Message\UriInterface;
use function asort;
use function is_array;
/**
* @internal
*/
final class OrderByValue implements Sorter
{
use ModifierTrait;
public function modifyUri(UriInterface $uri): UriInterface
{
return $this->appendQueryParam($uri, 'orderBy', '"$value"');
}
public function modifyValue(mixed $value): mixed
{
if (!is_array($value)) {
return $value;
}
asort($value);
return $value;
}
}

View File

@@ -0,0 +1,393 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database;
use Kreait\Firebase\Exception\DatabaseException;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Kreait\Firebase\Exception\OutOfRangeException;
use Psr\Http\Message\UriInterface;
use Stringable;
use function array_fill_keys;
use function array_keys;
use function array_map;
use function basename;
use function dirname;
use function is_array;
use function ltrim;
use function sprintf;
use function trim;
/**
* A Reference represents a specific location in your database and can be used
* for reading or writing data to that database location.
*/
class Reference implements Stringable
{
/**
* @internal
*/
public function __construct(
private readonly UriInterface $uri,
private readonly ApiClient $apiClient,
) {
}
/**
* Returns the absolute URL for this location.
*
* @see getUri()
*/
public function __toString(): string
{
return (string) $this->getUri();
}
/**
* The last part of the current path.
*
* For example, "ada" is the key for https://sample-app.firebaseio.example.com/users/ada.
*
* The key of the root Reference is null.
*/
public function getKey(): ?string
{
$key = basename($this->getPath());
return $key !== '' ? $key : null;
}
/**
* Returns the full path to a reference.
*/
public function getPath(): string
{
return trim($this->uri->getPath(), '/');
}
/**
* The parent location of a Reference.
*
* @throws OutOfRangeException if requested for the root Reference
*/
public function getParent(): self
{
$parentPath = dirname($this->getPath());
if ($parentPath === '.') {
throw new OutOfRangeException('Cannot get parent of root reference');
}
return new self($this->uri->withPath('/'.ltrim($parentPath, '/')), $this->apiClient);
}
/**
* The root location of a Reference.
*/
public function getRoot(): self
{
return new self($this->uri->withPath('/'), $this->apiClient);
}
/**
* 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").
*
* @throws InvalidArgumentException if the path is invalid
*/
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);
} 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()
*/
public function orderByChild(string $path): Query
{
return $this->query()->orderByChild($path);
}
/**
* Generates a new Query object ordered by key.
*
* @see Query::orderByKey()
*/
public function orderByKey(): Query
{
return $this->query()->orderByKey();
}
/**
* Generates a new Query object ordered by child values.
*
* @see Query::orderByValue()
*/
public function orderByValue(): Query
{
return $this->query()->orderByValue();
}
/**
* Generates a new Query limited to the first specific number of children.
*
* @see Query::limitToFirst()
*/
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()
*/
public function limitToLast(int $limit): Query
{
return $this->query()->limitToLast($limit);
}
/**
* Creates a Query with the specified starting point (inclusive).
*
* @see Query::startAt()
*/
public function startAt(bool|string|int|float $value): Query
{
return $this->query()->startAt($value);
}
/**
* Creates a Query with the specified starting point (exclusive).
*
* @see Query::startAfter()
*/
public function startAfter(bool|string|int|float $value): Query
{
return $this->query()->startAfter($value);
}
/**
* Creates a Query with the specified ending point (inclusive).
*
* @see Query::endAt()
*/
public function endAt(bool|string|int|float $value): Query
{
return $this->query()->endAt($value);
}
/**
* Creates a Query with the specified ending point (exclusive).
*
* @see Query::endBefore()
*/
public function endBefore(bool|string|int|float $value): Query
{
return $this->query()->endBefore($value);
}
/**
* Creates a Query which includes children which match the specified value.
*
* @see Query::equalTo()
*/
public function equalTo(bool|string|int|float $value): Query
{
return $this->query()->equalTo($value);
}
/**
* Creates a Query with shallow results.
*
* @see Query::shallow()
*/
public function shallow(): Query
{
return $this->query()->shallow();
}
/**
* Returns the keys of a reference's children.
*
* @throws DatabaseException if the API reported an error
* @throws OutOfRangeException if the reference has no children with keys
*
* @return string[]
*/
public function getChildKeys(): array
{
$snapshot = $this->shallow()->getSnapshot();
if (is_array($value = $snapshot->getValue())) {
return array_map('strval', array_keys($value));
}
throw new OutOfRangeException(sprintf('%s has no children with keys', $this));
}
/**
* Convenience method for {@see getSnapshot()}->getValue().
*
* @throws DatabaseException if the API reported an error
*/
public function getValue(): mixed
{
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.
*
* @throws DatabaseException if the API reported an error
*/
public function set(mixed $value): self
{
if ($value === null) {
$this->apiClient->remove($this->uri->getPath());
} else {
$this->apiClient->set($this->uri->getPath(), $value);
}
return $this;
}
/**
* Returns a data snapshot of the current location.
*
* @throws DatabaseException if the API reported an error
*/
public function getSnapshot(): Snapshot
{
$value = $this->apiClient->get($this->uri->getPath());
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).
*
* @param mixed|null $value
*
* @throws DatabaseException if the API reported an error
*/
public function push($value = null): self
{
$value ??= [];
$newKey = $this->apiClient->push($this->uri->getPath(), $value);
$newPath = sprintf('%s/%s', $this->uri->getPath(), $newKey);
return new self($this->uri->withPath($newPath), $this->apiClient);
}
/**
* Remove the data at this database location.
*
* Any data at child locations will also be deleted.
*
* @throws DatabaseException if the API reported an error
*/
public function remove(): self
{
$this->apiClient->remove($this->uri->getPath());
return $this;
}
/**
* Remove the data at the given locations.
*
* Each location 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 remove.
*
* Any data at child locations will also be deleted.
*
* @param string[] $keys Locations to remove
*
* @throws DatabaseException
*/
public function removeChildren(array $keys): self
{
$this->update(
array_fill_keys($keys, null),
);
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.
*
* @param array<mixed> $values
*
* @throws DatabaseException if the API reported an error
*/
public function update(array $values): self
{
$this->apiClient->update($this->uri->getPath(), $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.
*/
public function getUri(): UriInterface
{
return $this->uri;
}
/**
* Returns a new query for the current reference.
*/
private function query(): Query
{
return new Query($this, $this->apiClient);
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database;
use JsonSerializable;
use function array_key_exists;
class RuleSet implements JsonSerializable
{
/**
* @var array<string, array<mixed>>
*/
private readonly array $rules;
/**
* @param array<string, array<mixed>> $rules
*/
private function __construct(array $rules)
{
if (!array_key_exists('rules', $rules)) {
$rules = ['rules' => $rules];
}
$this->rules = $rules;
}
/**
* The default rules require Authentication. They allow full read and write access
* to authenticated users of your app. They are useful if you want data open to
* all users of your app but don't want it open to the world.
*
* @see https://firebase.google.com/docs/database/security/quickstart#sample-rules
*/
public static function default(): self
{
return new self([
'rules' => [
'.read' => 'auth != null',
'.write' => 'auth != null',
],
]);
}
/**
* During development, you can use the public rules in place of the default rules to set
* your files publicly readable and writable. This can be useful for prototyping,
* as you can get started without setting up Authentication.
*
* This level of access means anyone can read or write to your database. You should
* configure more secure rules before launching your app.
*
* @see https://firebase.google.com/docs/database/security/quickstart#sample-rules
*/
public static function public(): self
{
return new self([
'rules' => [
'.read' => true,
'.write' => true,
],
]);
}
/**
* Private rules disable read and write access to your database by users. With these rules,
* you can only access the database through the Firebase console and an Admin SDK.
*
* @see https://firebase.google.com/docs/database/security/quickstart#sample-rules
*/
public static function private(): self
{
return new self([
'rules' => [
'.read' => false,
'.write' => false,
],
]);
}
/**
* @param array<string, array<mixed>> $rules
*/
public static function fromArray(array $rules): self
{
return new self($rules);
}
/**
* @return array<string, array<mixed>>
*/
public function getRules(): array
{
return $this->rules;
}
public function jsonSerialize(): array
{
return $this->rules;
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database;
use Kreait\Firebase\Exception\InvalidArgumentException;
use function count;
use function is_array;
use function JmesPath\search;
use function str_replace;
use function trim;
/**
* A Snapshot contains data from a database location.
*
* It is an immutable copy of the data at a database location. It cannot be modified and will never
* change (to modify data, you always call the {@see Reference::set()}).
*
* You can extract the contents of the snapshot as a JavaScript object by calling
* the {@see getValue()} method.
*
* Alternatively, you can traverse into the snapshot by calling {@see getChild()}
* to return child snapshots (which you could then call {@see getValue()} on).
*/
class Snapshot
{
/**
* @internal
*/
public function __construct(private readonly Reference $reference, private readonly mixed $value)
{
}
/**
* Returns the key (last part of the path) of the location of this Snapshot.
*
* The last token in a database location is considered its key. For example, "ada" is the key for
* the /users/ada/ node. Accessing the key on any Snapshot will return the key for the
* location that generated it. However, accessing the key on the root URL of a database
* will return null.
*/
public function getKey(): ?string
{
return $this->reference->getKey();
}
/**
* Returns the Reference for the location that generated this Snapshot.
*/
public function getReference(): Reference
{
return $this->reference;
}
/**
* Returns another Snapshot for the location at the specified relative path.
*
* Passing a relative path to the child() method of a Snapshot returns another Snapshot for the location
* at the specified relative path. The relative path can either be a simple child name (e.g. "ada") or a
* deeper, slash-separated path (e.g. "ada/name/first"). If the child location has no data, an empty
* Snapshot (that is, a Snapshot whose value is null) is returned.
*
* @throws InvalidArgumentException if the given child path is invalid
*/
public function getChild(string $path): self
{
$path = trim($path, '/');
$expression = '"'.str_replace('/', '"."', $path).'"';
$childValue = search($expression, $this->value);
return new self($this->reference->getChild($path), $childValue);
}
/**
* Returns true if this Snapshot contains any data.
*
* It is a convenience method for `$snapshot->getValue() !== null`.
*/
public function exists(): bool
{
return $this->value !== null;
}
/**
* Returns true if the specified child path has (non-null) data.
*/
public function hasChild(string $path): bool
{
$path = trim($path, '/');
$expression = '"'.str_replace('/', '"."', $path).'"';
return search($expression, $this->value) !== null;
}
/**
* Returns true if the Snapshot has any child properties.
*
* You can use {@see hasChildren()} to determine if a Snapshot has any children. If it does,
* you can enumerate them using foreach(). If it does not, then either this snapshot
* contains a primitive value (which can be retrieved with {@see getValue()}) or
* it is empty (in which case {@see getValue()} will return null).
*/
public function hasChildren(): bool
{
return is_array($this->value) && $this->value !== [];
}
/**
* Returns the number of child properties of this Snapshot.
*/
public function numChildren(): int
{
return is_array($this->value) ? count($this->value) : 0;
}
/**
* Returns the data contained in this Snapshot.
*/
public function getValue(): mixed
{
return $this->value;
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database;
use Kreait\Firebase\Exception\Database\ReferenceHasNotBeenSnapshotted;
use Kreait\Firebase\Exception\Database\TransactionFailed;
use Kreait\Firebase\Exception\DatabaseException;
use function array_key_exists;
class Transaction
{
/**
* @var array<string, string>
*/
private array $etags;
/**
* @internal
*/
public function __construct(private readonly ApiClient $apiClient)
{
$this->etags = [];
}
/**
* @throws DatabaseException
*/
public function snapshot(Reference $reference): Snapshot
{
$path = $reference->getPath();
$result = $this->apiClient->getWithETag($path);
$this->etags[$path] = $result['etag'];
return new Snapshot($reference, $result['value']);
}
/**
* @throws ReferenceHasNotBeenSnapshotted
* @throws TransactionFailed
*/
public function set(Reference $reference, mixed $value): void
{
$etag = $this->getEtagForReference($reference);
try {
$this->apiClient->setWithEtag($reference->getPath(), $value, $etag);
} catch (DatabaseException $e) {
throw TransactionFailed::onReference($reference, $e);
}
}
/**
* @throws ReferenceHasNotBeenSnapshotted
* @throws TransactionFailed
*/
public function remove(Reference $reference): void
{
$etag = $this->getEtagForReference($reference);
try {
$this->apiClient->removeWithEtag($reference->getPath(), $etag);
} catch (DatabaseException $e) {
throw TransactionFailed::onReference($reference, $e);
}
}
/**
* @throws ReferenceHasNotBeenSnapshotted
*/
private function getEtagForReference(Reference $reference): string
{
$path = $reference->getPath();
if (array_key_exists($path, $this->etags)) {
return $this->etags[$path];
}
throw new ReferenceHasNotBeenSnapshotted($reference);
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Database;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Kreait\Firebase\Util;
use function http_build_query;
use function in_array;
use function preg_match;
use function rtrim;
use function strtr;
use function trim;
/**
* @internal
*/
final class UrlBuilder
{
private const EXPECTED_URL_FORMAT = '@^https://(?P<namespace>[^.]+)\.(?P<host>.+)$@';
/**
* @param 'http'|'https' $scheme
* @param non-empty-string $host
* @param array<string, string> $defaultQueryParams
*/
private function __construct(
private readonly string $scheme,
private readonly string $host,
private readonly array $defaultQueryParams,
) {
}
/**
* @param non-empty-string $databaseUrl
*/
public static function create(string $databaseUrl): self
{
['scheme' => $scheme, 'host' => $host, 'query' => $query] = self::parseDatabaseUrl($databaseUrl);
return new self($scheme, $host, $query);
}
/**
* @param array<string, string> $queryParams
*/
public function getUrl(string $path, array $queryParams = []): string
{
$allQueryParams = $this->defaultQueryParams + $queryParams;
$path = '/'.trim($path, '/');
$url = strtr('{scheme}://{host}{path}?{queryParams}', [
'{scheme}' => $this->scheme,
'{host}' => $this->host,
'{path}' => $path,
'{queryParams}' => http_build_query($allQueryParams),
]);
// If no queryParams are present, remove the trailing '?'
return trim($url, '?');
}
/**
* @param non-empty-string $databaseUrl
*
* @return array{
* scheme: 'http'|'https',
* host: non-empty-string,
* query: array<non-empty-string, non-empty-string>
* }
*/
private static function parseDatabaseUrl(string $databaseUrl): array
{
$databaseUrl = rtrim($databaseUrl, '/');
if (preg_match(self::EXPECTED_URL_FORMAT, $databaseUrl, $matches) !== 1) {
throw new InvalidArgumentException('Unexpected database URL format "'.$databaseUrl.'"');
}
$namespace = $matches['namespace'];
$host = $matches['host'];
$emulatorHost = Util::rtdbEmulatorHost();
if (!in_array($emulatorHost, ['', '0', null], true)) {
return [
'scheme' => 'http',
'host' => $emulatorHost,
'query' => ['ns' => $namespace],
];
}
return [
'scheme' => 'https',
'host' => $namespace.'.'.$host,
'query' => [],
];
}
}