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,142 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use Beste\Json;
use GuzzleHttp\ClientInterface;
use Kreait\Firebase\Exception\RemoteConfigApiExceptionConverter;
use Kreait\Firebase\Exception\RemoteConfigException;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use function array_filter;
/**
* @internal
*/
class ApiClient
{
private readonly string $baseUri;
public function __construct(
string $projectId,
private readonly ClientInterface $client,
private readonly RemoteConfigApiExceptionConverter $errorHandler,
) {
$this->baseUri = "https://firebaseremoteconfig.googleapis.com/v1/projects/{$projectId}/remoteConfig";
}
/**
* @see https://firebase.google.com/docs/reference/remote-config/rest/v1/projects/getRemoteConfig
*
* @throws RemoteConfigException
*/
public function getTemplate(VersionNumber|int|string|null $versionNumber = null): ResponseInterface
{
if (in_array($versionNumber, [null, '', '0'], true)) {
$versionNumber = VersionNumber::fromValue(0);
}
return $this->requestApi('GET', 'remoteConfig', [
'query' => [
'version_number' => (string) $versionNumber,
],
]);
}
/**
* @throws RemoteConfigException
*/
public function validateTemplate(Template $template): ResponseInterface
{
return $this->requestApi('PUT', 'remoteConfig', [
'headers' => [
'Content-Type' => 'application/json; UTF-8',
'If-Match' => $template->etag(),
],
'query' => [
'validate_only' => 'true',
],
'body' => Json::encode($template),
]);
}
/**
* @throws RemoteConfigException
*/
public function publishTemplate(Template $template): ResponseInterface
{
return $this->requestApi('PUT', 'remoteConfig', [
'headers' => [
'Content-Type' => 'application/json; UTF-8',
'If-Match' => $template->etag(),
],
'body' => Json::encode($template),
]);
}
/**
* @see https://firebase.google.com/docs/reference/remote-config/rest/v1/projects.remoteConfig/listVersions
*
* @throws RemoteConfigException
*/
public function listVersions(FindVersions $query, ?string $nextPageToken = null): ResponseInterface
{
$uri = $this->baseUri.':listVersions';
$since = $query->since();
$until = $query->until();
$lastVersionNumber = $query->lastVersionNumber();
$pageSize = $query->pageSize();
$since = $since?->format('Y-m-d\TH:i:s.v\Z');
$until = $until?->format('Y-m-d\TH:i:s.v\Z');
$lastVersionNumber = $lastVersionNumber !== null ? (string) $lastVersionNumber : null;
$pageSize = $pageSize !== null ? (string) $pageSize : null;
return $this->requestApi('GET', $uri, [
'query' => array_filter([
'startTime' => $since,
'endTime' => $until,
'endVersionNumber' => $lastVersionNumber,
'pageSize' => $pageSize,
'pageToken' => $nextPageToken,
], fn(?string $value): bool => $value !== null),
]);
}
/**
* @throws RemoteConfigException
*/
public function rollbackToVersion(VersionNumber $versionNumber): ResponseInterface
{
$uri = $this->baseUri.':rollback';
return $this->requestApi('POST', $uri, [
'json' => [
'version_number' => (string) $versionNumber,
],
]);
}
/**
* @param non-empty-string $method
* @param non-empty-string $uri
* @param array<string, mixed>|null $options
*
* @throws RemoteConfigException
*/
private function requestApi(string $method, string $uri, ?array $options = null): ResponseInterface
{
$options ??= [];
$options['decode_content'] = 'gzip';
try {
return $this->client->request($method, $uri, $options);
} catch (Throwable $e) {
throw $this->errorHandler->convertException($e);
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
/**
* @phpstan-type RemoteConfigConditionShape array{
* name: non-empty-string,
* expression: non-empty-string,
* tagColor?: ?non-empty-string
* }
*/
class Condition implements JsonSerializable
{
/**
* @param non-empty-string $name
* @param non-empty-string $expression
*/
private function __construct(
private readonly string $name,
private string $expression,
private ?TagColor $tagColor = null,
) {
}
/**
* @param RemoteConfigConditionShape $data
*/
public static function fromArray(array $data): self
{
return new self(
$data['name'],
$data['expression'],
isset($data['tagColor']) ? new TagColor($data['tagColor']) : null,
);
}
/**
* @param non-empty-string $name
*/
public static function named(string $name): self
{
return new self($name, 'false', null);
}
/**
* @return non-empty-string
*/
public function name(): string
{
return $this->name;
}
/**
* @return non-empty-string
*/
public function expression(): string
{
return $this->expression;
}
/**
* @param non-empty-string $expression
*/
public function withExpression(string $expression): self
{
$condition = clone $this;
$condition->expression = $expression;
return $condition;
}
public function tagColor(): ?TagColor
{
return $this->tagColor;
}
/**
* @param TagColor|non-empty-string $tagColor
*/
public function withTagColor($tagColor): self
{
$tagColor = $tagColor instanceof TagColor ? $tagColor : new TagColor($tagColor);
$condition = clone $this;
$condition->tagColor = $tagColor;
return $condition;
}
/**
* @return RemoteConfigConditionShape
*/
public function toArray(): array
{
$array = [
'name' => $this->name,
'expression' => $this->expression,
];
if ($this->tagColor !== null) {
$array['tagColor'] = $this->tagColor->value();
}
return $array;
}
/**
* @return RemoteConfigConditionShape
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
use function is_array;
use function is_string;
/**
* @phpstan-import-type RemoteConfigParameterValueShape from ParameterValue
*/
class ConditionalValue implements JsonSerializable
{
/**
* @internal
*
* @param non-empty-string $conditionName
*/
public function __construct(private readonly string $conditionName, private readonly ParameterValue $value)
{
}
/**
* @return non-empty-string
*/
public function conditionName(): string
{
return $this->conditionName;
}
/**
* @param non-empty-string|Condition $condition
*/
public static function basedOn($condition): self
{
$name = $condition instanceof Condition ? $condition->name() : $condition;
return new self($name, ParameterValue::withValue(''));
}
/**
* @return RemoteConfigParameterValueShape|non-empty-string
*/
public function value()
{
$data = $this->value->toArray();
$valueString = $data['value'] ?? null;
if (is_string($valueString) && $valueString !== '') {
return $valueString;
}
return $data;
}
/**
* @param ParameterValue|RemoteConfigParameterValueShape|string $value
*/
public function withValue($value): self
{
if (is_string($value)) {
return new self($this->conditionName, ParameterValue::withValue($value));
}
if (is_array($value)) {
return new self($this->conditionName, ParameterValue::fromArray($value));
}
return new self($this->conditionName, $value);
}
/**
* @return RemoteConfigParameterValueShape
*/
public function toArray(): array
{
return $this->value->toArray();
}
/**
* @return RemoteConfigParameterValueShape
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
/**
* @phpstan-import-type RemoteConfigParameterValueShape from ParameterValue
*
* @todo Deprecate/Remove in 8.0
*
* @see ParameterValue
*/
class DefaultValue implements JsonSerializable
{
private function __construct(private readonly ParameterValue $value)
{
}
public static function useInAppDefault(): self
{
return new self(ParameterValue::inAppDefault());
}
public static function with(string $value): self
{
return new self(ParameterValue::withValue($value));
}
/**
* @param RemoteConfigParameterValueShape $data
*/
public static function fromArray(array $data): self
{
return new self(ParameterValue::fromArray($data));
}
/**
* @return RemoteConfigParameterValueShape
*/
public function toArray(): array
{
return $this->value->toArray();
}
/**
* @return RemoteConfigParameterValueShape
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
/**
* @deprecated 7.4.0
*
* @codeCoverageIgnore
*
* @phpstan-type RemoteConfigExplicitValueShape array{
* value: string
* }
*/
final class ExplicitValue implements JsonSerializable
{
/**
* @param RemoteConfigExplicitValueShape $data
*/
private function __construct(private readonly array $data)
{
}
public static function fromString(string $value): self
{
return new self(['value' => $value]);
}
/**
* @return RemoteConfigExplicitValueShape
*/
public function toArray(): array
{
return $this->data;
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use DateTimeImmutable;
use DateTimeInterface;
use Kreait\Firebase\Util\DT;
/**
* @phpstan-type FindVersionsShape array{
* startingAt?: non-empty-string,
* startTime?: non-empty-string,
* since?: non-empty-string,
* endingAt?: non-empty-string,
* endTime?: non-empty-string,
* until?: non-empty-string,
* lastVersionBeing?: VersionNumber|positive-int|non-empty-string,
* endVersionNumber?: VersionNumber|positive-int|non-empty-string,
* up_to_version?: VersionNumber|positive-int|non-empty-string,
* pageSize?: positive-int|non-empty-string,
* page_size?: positive-int|non-empty-string,
* limit?: positive-int|non-empty-string
* }
*/
class FindVersions
{
private ?DateTimeImmutable $since = null;
private ?DateTimeImmutable $until = null;
/**
* @var positive-int|null
*/
private ?int $limit = null;
/**
* @var positive-int|null
*/
private ?int $pageSize = null;
private ?VersionNumber $upToVersion = null;
private function __construct()
{
}
public static function all(): self
{
return new self();
}
/**
* @param FindVersionsShape $params
*/
public static function fromArray(array $params): self
{
$query = self::all();
$value = $params['startingAt'] ?? $params['startTime'] ?? $params['since'] ?? null;
if ($value !== null) {
$query = $query->startingAt(DT::toUTCDateTimeImmutable($value));
}
$value = $params['endingAt'] ?? $params['endTime'] ?? $params['until'] ?? null;
if ($value !== null) {
$query = $query->endingAt(DT::toUTCDateTimeImmutable($value));
}
$value = $params['lastVersionBeing'] ?? $params['endVersionNumber'] ?? $params['up_to_version'] ?? null;
if ($value !== null) {
$versionNumber = $value instanceof VersionNumber ? $value : VersionNumber::fromValue($value);
$query = $query->upToVersion($versionNumber);
}
$value = $params['pageSize'] ?? $params['page_size'] ?? null;
if ($value !== null) {
$value = (int) $value;
if ($value >= 1) {
// We can't throw an exception here, although we shouldn't because of backward compatibility
$query = $query->withPageSize($value);
}
}
$value = $params['limit'] ?? null;
if ($value !== null) {
$value = (int) $value;
if ($value >= 1) {
// We can't throw an exception here, although we shouldn't because of backward compatibility
$query = $query->withLimit($value);
}
}
return $query;
}
public function startingAt(DateTimeInterface $startTime): self
{
$query = clone $this;
$query->since = DT::toUTCDateTimeImmutable($startTime);
return $query;
}
public function since(): ?DateTimeImmutable
{
return $this->since;
}
public function endingAt(DateTimeInterface $endTime): self
{
$query = clone $this;
$query->until = DT::toUTCDateTimeImmutable($endTime);
return $query;
}
public function until(): ?DateTimeImmutable
{
return $this->until;
}
public function upToVersion(VersionNumber $versionNumber): self
{
$query = clone $this;
$query->upToVersion = $versionNumber;
return $query;
}
public function lastVersionNumber(): ?VersionNumber
{
return $this->upToVersion;
}
/**
* @param positive-int $pageSize
*/
public function withPageSize(int $pageSize): self
{
$query = clone $this;
$query->pageSize = $pageSize;
return $query;
}
/**
* @return positive-int|null $pageSize
*/
public function pageSize(): ?int
{
return $this->pageSize;
}
/**
* @param positive-int $limit
*/
public function withLimit(int $limit): self
{
$query = clone $this;
$query->limit = $limit;
return $query;
}
/**
* @return positive-int|null
*/
public function limit(): ?int
{
return $this->limit;
}
}

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
use function is_bool;
use function is_string;
/**
* @phpstan-import-type RemoteConfigParameterValueShape from ParameterValue
*
* @phpstan-type RemoteConfigParameterShape array{
* description?: string|null,
* defaultValue?: RemoteConfigParameterValueShape|null,
* conditionalValues?: array<non-empty-string, RemoteConfigParameterValueShape>|null,
* valueType?: non-empty-string|null
* }
*/
class Parameter implements JsonSerializable
{
/**
* @param non-empty-string $name
* @param list<ConditionalValue> $conditionalValues
*/
private function __construct(
private readonly string $name,
private readonly string $description,
private readonly ?ParameterValue $defaultValue,
private readonly array $conditionalValues,
private readonly ParameterValueType $valueType,
) {
}
/**
* @param non-empty-string $name
* @param DefaultValue|RemoteConfigParameterValueShape|string|bool|null $defaultValue
*/
public static function named(string $name, $defaultValue = null, ?ParameterValueType $valueType = null): self
{
$defaultValue = self::mapDefaultValue($defaultValue);
return new self(
name: $name,
description: '',
defaultValue: $defaultValue,
conditionalValues: [],
valueType: $valueType ?? ParameterValueType::UNSPECIFIED,
);
}
/**
* @param DefaultValue|RemoteConfigParameterValueShape|string|bool|null $defaultValue
*/
private static function mapDefaultValue($defaultValue): ?ParameterValue
{
if ($defaultValue === null) {
return null;
}
if ($defaultValue instanceof DefaultValue) {
return ParameterValue::fromArray($defaultValue->toArray());
}
if (is_string($defaultValue)) {
return ParameterValue::withValue($defaultValue);
}
if (is_bool($defaultValue)) {
return ParameterValue::inAppDefault();
}
return ParameterValue::fromArray($defaultValue);
}
/**
* @return non-empty-string
*/
public function name(): string
{
return $this->name;
}
public function description(): string
{
return $this->description;
}
public function withDescription(string $description): self
{
return new self(
name: $this->name,
description: $description,
defaultValue: $this->defaultValue,
conditionalValues: $this->conditionalValues,
valueType: $this->valueType,
);
}
/**
* @param DefaultValue|RemoteConfigParameterValueShape|string|bool|null $defaultValue
*/
public function withDefaultValue($defaultValue): self
{
$defaultValue = self::mapDefaultValue($defaultValue);
return new self(
name: $this->name,
description: $this->description,
defaultValue: $defaultValue,
conditionalValues: $this->conditionalValues,
valueType: $this->valueType,
);
}
/**
* @todo 8.0 Replace with `ParameterValue`
*/
public function defaultValue(): ?DefaultValue
{
if ($this->defaultValue === null) {
return null;
}
return DefaultValue::fromArray($this->defaultValue->toArray());
}
public function withConditionalValue(ConditionalValue $conditionalValue): self
{
$conditionalValues = $this->conditionalValues;
$conditionalValues[] = $conditionalValue;
return new self(
name: $this->name,
description: $this->description,
defaultValue: $this->defaultValue,
conditionalValues: $conditionalValues,
valueType: $this->valueType,
);
}
/**
* @return list<ConditionalValue>
*/
public function conditionalValues(): array
{
return $this->conditionalValues;
}
public function withValueType(ParameterValueType $valueType): self
{
return new self(
name: $this->name,
description: $this->description,
defaultValue: $this->defaultValue,
conditionalValues: $this->conditionalValues,
valueType: $valueType,
);
}
public function valueType(): ParameterValueType
{
return $this->valueType;
}
/**
* @return RemoteConfigParameterShape
*/
public function toArray(): array
{
$conditionalValues = [];
foreach ($this->conditionalValues() as $conditionalValue) {
$conditionalValues[$conditionalValue->conditionName()] = $conditionalValue->toArray();
}
$array = [];
if ($this->defaultValue !== null) {
$array['defaultValue'] = $this->defaultValue->toArray();
}
if ($conditionalValues !== []) {
$array['conditionalValues'] = $conditionalValues;
}
if ($this->description !== '') {
$array['description'] = $this->description;
}
$array['valueType'] = $this->valueType->value;
return $array;
}
/**
* @return RemoteConfigParameterShape
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
/**
* @phpstan-import-type RemoteConfigParameterShape from Parameter
*
* @phpstan-type RemoteConfigParameterGroupShape array{
* description?: string|null,
* parameters: array<non-empty-string, RemoteConfigParameterShape>}
*/
final class ParameterGroup implements JsonSerializable
{
private string $description = '';
/**
* @var array<non-empty-string, Parameter>
*/
private array $parameters = [];
/**
* @param non-empty-string $name
*/
private function __construct(private readonly string $name)
{
}
/**
* @param non-empty-string $name
*/
public static function named(string $name): self
{
return new self($name);
}
/**
* @return non-empty-string
*/
public function name(): string
{
return $this->name;
}
public function description(): string
{
return $this->description;
}
/**
* @return array<non-empty-string, Parameter>
*/
public function parameters(): array
{
return $this->parameters;
}
public function withDescription(string $description): self
{
$group = clone $this;
$group->description = $description;
return $group;
}
public function withParameter(Parameter $parameter): self
{
$group = clone $this;
$group->parameters[$parameter->name()] = $parameter;
return $group;
}
/**
* @return RemoteConfigParameterGroupShape
*/
public function toArray(): array
{
$parameters = [];
foreach ($this->parameters as $parameter) {
$parameters[$parameter->name()] = $parameter->toArray();
}
return [
'description' => $this->description,
'parameters' => $parameters,
];
}
/**
* @return RemoteConfigParameterGroupShape
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
use function array_key_exists;
/**
* @phpstan-import-type RemoteConfigPersonalizationValueShape from PersonalizationValue
* @phpstan-import-type RemoteConfigRolloutValueShape from RolloutValue
*
* @phpstan-type RemoteConfigParameterValueShape array{
* value?: string,
* useInAppDefault?: bool,
* personalizationValue?: RemoteConfigPersonalizationValueShape,
* rolloutValue?: RemoteConfigRolloutValueShape
* }
*
* @see https://firebase.google.com/docs/reference/remote-config/rest/v1/RemoteConfig#remoteconfigparametervalue
*/
final class ParameterValue implements JsonSerializable
{
private function __construct(
private readonly ?string $value = null,
private readonly ?bool $useInAppDefault = null,
private readonly ?PersonalizationValue $personalizationValue = null,
private readonly ?RolloutValue $rolloutValue = null,
) {
}
public static function withValue(string $value): self
{
return new self(value: $value);
}
public static function inAppDefault(): self
{
return new self(useInAppDefault: true);
}
public static function withPersonalizationValue(PersonalizationValue $value): self
{
return new self(personalizationValue: $value);
}
public static function withRolloutValue(RolloutValue $value): self
{
return new self(rolloutValue: $value);
}
/**
* @param RemoteConfigParameterValueShape $data
*/
public static function fromArray(array $data): self
{
if (array_key_exists('value', $data)) {
return self::withValue($data['value']);
}
if (array_key_exists('useInAppDefault', $data)) {
return self::inAppDefault();
}
if (array_key_exists('personalizationValue', $data)) {
return self::withPersonalizationValue(PersonalizationValue::fromArray($data['personalizationValue']));
}
if (array_key_exists('rolloutValue', $data)) {
return self::withRolloutValue(RolloutValue::fromArray($data['rolloutValue']));
}
return new self();
}
/**
* @return RemoteConfigParameterValueShape
*/
public function toArray(): array
{
if ($this->value !== null) {
return ['value' => $this->value];
}
if ($this->useInAppDefault !== null) {
return ['useInAppDefault' => $this->useInAppDefault];
}
if ($this->personalizationValue !== null) {
return ['personalizationValue' => $this->personalizationValue->toArray()];
}
if ($this->rolloutValue !== null) {
return ['rolloutValue' => $this->rolloutValue->toArray()];
}
return [];
}
/**
* @return RemoteConfigParameterValueShape
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
/**
* @see https://firebase.google.com/docs/reference/remote-config/rest/v1/RemoteConfig#ParameterValueType
*/
enum ParameterValueType: string
{
case UNSPECIFIED = 'PARAMETER_VALUE_TYPE_UNSPECIFIED';
case STRING = 'STRING';
case BOOL = 'BOOLEAN';
case NUMBER = 'NUMBER';
case JSON = 'JSON';
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
/**
* @phpstan-type RemoteConfigPersonalizationValueShape array{
* personalizationId: string
* }
*
* @see https://firebase.google.com/docs/reference/remote-config/rest/v1/RemoteConfig#personalizationvalue
*/
final class PersonalizationValue implements JsonSerializable
{
/**
* @param RemoteConfigPersonalizationValueShape $data
*/
public function __construct(private readonly array $data)
{
}
/**
* @param RemoteConfigPersonalizationValueShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
/**
* @return RemoteConfigPersonalizationValueShape
*/
public function toArray(): array
{
return $this->data;
}
/**
* @return RemoteConfigPersonalizationValueShape
*/
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
/**
* @phpstan-type RemoteConfigRolloutValueShape array{
* rolloutId: non-empty-string,
* value: string,
* percent: int<0, 100>
* }
*
* @see https://firebase.google.com/docs/reference/remote-config/rest/v1/RemoteConfig#rolloutvalue
*/
final class RolloutValue implements JsonSerializable
{
/**
* @param RemoteConfigRolloutValueShape $data
*/
private function __construct(private readonly array $data)
{
}
/**
* @param RemoteConfigRolloutValueShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
/**
* @return RemoteConfigRolloutValueShape
*/
public function toArray(): array
{
return $this->data;
}
/**
* @return RemoteConfigRolloutValueShape
*/
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Stringable;
use function implode;
use function in_array;
use function mb_strtoupper;
use function sprintf;
class TagColor implements Stringable
{
final public const BLUE = 'BLUE';
final public const BROWN = 'BROWN';
final public const CYAN = 'CYAN';
final public const DEEP_ORANGE = 'DEEP_ORANGE';
final public const GREEN = 'GREEN';
final public const INDIGO = 'INDIGO';
final public const LIME = 'LIME';
final public const ORANGE = 'ORANGE';
final public const PINK = 'PINK';
final public const PURPLE = 'PURPLE';
final public const TEAL = 'TEAL';
final public const VALID_COLORS = [
self::BLUE, self::BROWN, self::CYAN, self::DEEP_ORANGE, self::GREEN, self::INDIGO, self::LIME,
self::ORANGE, self::PINK, self::PURPLE, self::TEAL,
];
/**
* @var non-empty-string
*/
private readonly string $value;
/**
* @param non-empty-string $value
*/
public function __construct(string $value)
{
$value = mb_strtoupper($value);
if (!in_array($value, self::VALID_COLORS, true)) {
throw new InvalidArgumentException(
sprintf(
'Invalid tag color "%s". Supported colors are "%s".',
$value,
implode('", "', self::VALID_COLORS),
),
);
}
$this->value = $value;
}
/**
* @return non-empty-string
*/
public function __toString()
{
return $this->value;
}
/**
* @return non-empty-string
*/
public function value(): string
{
return $this->value;
}
}

View File

@@ -0,0 +1,288 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
use Kreait\Firebase\Exception\InvalidArgumentException;
use function array_filter;
use function array_map;
use function array_unique;
use function array_values;
use function in_array;
use function sprintf;
/**
* @phpstan-import-type RemoteConfigConditionShape from Condition
* @phpstan-import-type RemoteConfigParameterShape from Parameter
* @phpstan-import-type RemoteConfigParameterGroupShape from ParameterGroup
* @phpstan-import-type RemoteConfigVersionShape from Version
*
* @phpstan-type RemoteConfigTemplateShape array{
* conditions?: list<RemoteConfigConditionShape>,
* parameters?: array<non-empty-string, RemoteConfigParameterShape>,
* version?: RemoteConfigVersionShape,
* parameterGroups?: array<non-empty-string, RemoteConfigParameterGroupShape>
* }
*
* @see https://firebase.google.com/docs/reference/remote-config/rest/v1/RemoteConfig
*/
class Template implements JsonSerializable
{
private string $etag = '*';
/**
* @var array<non-empty-string, Parameter>
*/
private array $parameters = [];
/**
* @var array<non-empty-string, ParameterGroup>
*/
private array $parameterGroups = [];
/**
* @var list<Condition>
*/
private array $conditions = [];
private ?Version $version = null;
private function __construct()
{
}
public static function new(): self
{
return new self();
}
/**
* @param RemoteConfigTemplateShape $data
*/
public static function fromArray(array $data, ?string $etag = null): self
{
$template = new self();
$template->etag = $etag ?? '*';
foreach (($data['conditions'] ?? []) as $conditionData) {
$template = $template->withCondition(self::buildCondition($conditionData['name'], $conditionData));
}
foreach (($data['parameters'] ?? []) as $name => $parameterData) {
$template = $template->withParameter(self::buildParameter($name, $parameterData));
}
foreach (($data['parameterGroups'] ?? []) as $name => $parameterGroupData) {
$template = $template->withParameterGroup(self::buildParameterGroup($name, $parameterGroupData));
}
$versionData = $data['version'] ?? null;
if ($versionData !== null) {
$template->version = Version::fromArray($versionData);
}
return $template;
}
/**
* @internal
*/
public function etag(): string
{
return $this->etag;
}
/**
* @return Condition[]
*/
public function conditions(): array
{
return $this->conditions;
}
/**
* @return array<non-empty-string, Parameter>
*/
public function parameters(): array
{
return $this->parameters;
}
/**
* @return ParameterGroup[]
*/
public function parameterGroups(): array
{
return $this->parameterGroups;
}
public function version(): ?Version
{
return $this->version;
}
public function withParameter(Parameter $parameter): self
{
$this->assertThatAllConditionalValuesAreValid($parameter);
$template = clone $this;
$template->parameters[$parameter->name()] = $parameter;
return $template;
}
/**
* @param non-empty-string $name
*/
public function withRemovedParameter(string $name): self
{
$parameters = $this->parameters;
unset($parameters[$name]);
$template = clone $this;
$template->parameters = $parameters;
return $template;
}
public function withParameterGroup(ParameterGroup $parameterGroup): self
{
$template = clone $this;
$template->parameterGroups[$parameterGroup->name()] = $parameterGroup;
return $template;
}
public function withRemovedParameterGroup(string $name): self
{
$groups = $this->parameterGroups;
unset($groups[$name]);
$template = clone $this;
$template->parameterGroups = $groups;
return $template;
}
public function withCondition(Condition $condition): self
{
$template = clone $this;
$template->conditions[] = $condition;
return $template;
}
/**
* @return list<non-empty-string>
*/
public function conditionNames(): array
{
return array_values(array_unique(
array_map(static fn(Condition $c): string => $c->name(), $this->conditions),
));
}
/**
* @param non-empty-string $name
*/
public function withRemovedCondition(string $name): self
{
$template = clone $this;
$template->conditions = array_values(
array_filter($this->conditions, static fn(Condition $c): bool => $c->name() !== $name),
);
return $template;
}
/**
* @return array{
* conditions: list<RemoteConfigConditionShape>|null,
* parameters: array<non-empty-string, RemoteConfigParameterShape>|null,
* parameterGroups: array<non-empty-string, RemoteConfigParameterGroupShape>|null
* }
*/
public function jsonSerialize(): array
{
$conditions = array_map(fn(Condition $c): array => $c->jsonSerialize(), $this->conditions);
$parameters = array_map(fn(Parameter $p): array => $p->jsonSerialize(), $this->parameters);
$parameterGroups = array_map(fn(ParameterGroup $p): array => $p->jsonSerialize(), $this->parameterGroups);
return [
'conditions' => $conditions !== [] ? $conditions : null,
'parameters' => $parameters !== [] ? $parameters : null,
'parameterGroups' => $parameterGroups !== [] ? $parameterGroups : null,
];
}
/**
* @param non-empty-string $name
* @param RemoteConfigConditionShape $data
*/
private static function buildCondition(string $name, array $data): Condition
{
$condition = Condition::named($name)->withExpression($data['expression']);
$tagColor = $data['tagColor'] ?? null;
if ($tagColor !== null) {
return $condition->withTagColor(new TagColor($tagColor));
}
return $condition;
}
/**
* @param non-empty-string $name
* @param RemoteConfigParameterShape $data
*/
private static function buildParameter(string $name, array $data): Parameter
{
$valueType = ParameterValueType::tryFrom($data['valueType'] ?? '') ?? ParameterValueType::UNSPECIFIED;
$parameter = Parameter::named($name)
->withDescription((string) ($data['description'] ?? ''))
->withDefaultValue($data['defaultValue'] ?? null)
->withValueType($valueType)
;
foreach ((array) ($data['conditionalValues'] ?? []) as $key => $conditionalValueData) {
$parameter = $parameter->withConditionalValue(new ConditionalValue($key, ParameterValue::fromArray($conditionalValueData)));
}
return $parameter;
}
/**
* @param non-empty-string $name
* @param RemoteConfigParameterGroupShape $parameterGroupData
*/
private static function buildParameterGroup(string $name, array $parameterGroupData): ParameterGroup
{
$group = ParameterGroup::named($name)
->withDescription((string) ($parameterGroupData['description'] ?? ''))
;
foreach ($parameterGroupData['parameters'] as $parameterName => $parameterData) {
$group = $group->withParameter(self::buildParameter($parameterName, $parameterData));
}
return $group;
}
private function assertThatAllConditionalValuesAreValid(Parameter $parameter): void
{
$conditionNames = array_map(static fn(Condition $c): string => $c->name(), $this->conditions);
foreach ($parameter->conditionalValues() as $conditionalValue) {
if (!in_array($conditionalValue->conditionName(), $conditionNames, true)) {
$message = 'The conditional value of the parameter named "%s" refers to a condition "%s" which does not exist.';
throw new InvalidArgumentException(sprintf($message, $parameter->name(), $conditionalValue->conditionName()));
}
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
use Stringable;
final class UpdateOrigin implements JsonSerializable, Stringable
{
public const UNSPECIFIED = 'REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED';
public const CONSOLE = 'CONSOLE';
public const REST_API = 'REST_API';
private function __construct(private readonly string $value)
{
}
public function __toString(): string
{
return $this->value;
}
public static function fromValue(string $value): self
{
return new self($value);
}
public function jsonSerialize(): string
{
return $this->value;
}
/**
* @param self|string $other
*/
public function equalsTo($other): bool
{
return $this->value === (string) $other;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
use Stringable;
final class UpdateType implements JsonSerializable, Stringable
{
public const UNSPECIFIED = 'REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED';
public const INCREMENTAL_UPDATE = 'INCREMENTAL_UPDATE';
public const FORCED_UPDATE = 'FORCED_UPDATE';
public const ROLLBACK = 'ROLLBACK';
private function __construct(private readonly string $value)
{
}
public function __toString(): string
{
return $this->value;
}
public static function fromValue(string $value): self
{
return new self($value);
}
public function jsonSerialize(): string
{
return $this->value;
}
/**
* @param self|string $other
*/
public function equalsTo($other): bool
{
return $this->value === (string) $other;
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\UriInterface;
/**
* @phpstan-type RemoteConfigUserShape array{
* name?: non-empty-string,
* email?: non-empty-string,
* imageUrl?: non-empty-string
* }
*/
final class User
{
/**
* @param non-empty-string|null $name
* @param non-empty-string|null $email
*/
private function __construct(
private readonly ?string $name,
private readonly ?string $email,
private readonly ?UriInterface $imageUri,
) {
}
/**
* @internal
*
* @param RemoteConfigUserShape $data
*/
public static function fromArray(array $data): self
{
$imageUrl = $data['imageUrl'] ?? null;
$imageUri = $imageUrl !== null ? new Uri($imageUrl) : null;
return new self(
$data['name'] ?? null,
$data['email'] ?? null,
$imageUri,
);
}
/**
* @return non-empty-string|null
*/
public function name(): ?string
{
return $this->name;
}
/**
* @return non-empty-string|null
*/
public function email(): ?string
{
return $this->email;
}
public function imageUri(): ?UriInterface
{
return $this->imageUri;
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use DateTimeImmutable;
use Kreait\Firebase\Util\DT;
use function array_key_exists;
/**
* @phpstan-import-type RemoteConfigUserShape from User
*
* @phpstan-type RemoteConfigVersionShape array{
* versionNumber: non-empty-string,
* updateTime: non-empty-string,
* updateUser: RemoteConfigUserShape,
* description?: string|null,
* updateOrigin: non-empty-string,
* updateType: non-empty-string,
* rollbackSource?: non-empty-string
* }
*/
final class Version
{
private function __construct(
private readonly VersionNumber $versionNumber,
private readonly User $user,
private readonly string $description,
private readonly DateTimeImmutable $updatedAt,
private readonly UpdateOrigin $updateOrigin,
private readonly UpdateType $updateType,
private readonly ?VersionNumber $rollbackSource,
) {
}
/**
* @internal
*
* @param RemoteConfigVersionShape $data
*/
public static function fromArray(array $data): self
{
$versionNumber = VersionNumber::fromValue($data['versionNumber']);
$user = User::fromArray($data['updateUser']);
$updatedAt = DT::toUTCDateTimeImmutable($data['updateTime']);
$description = $data['description'] ?? '';
$updateOrigin = UpdateOrigin::fromValue($data['updateOrigin']);
$updateType = UpdateType::fromValue($data['updateType']);
$rollbackSource = array_key_exists('rollbackSource', $data)
? VersionNumber::fromValue($data['rollbackSource'])
: null;
return new self(
$versionNumber,
$user,
$description,
$updatedAt,
$updateOrigin,
$updateType,
$rollbackSource,
);
}
public function versionNumber(): VersionNumber
{
return $this->versionNumber;
}
public function user(): User
{
return $this->user;
}
public function updatedAt(): DateTimeImmutable
{
return $this->updatedAt;
}
public function description(): string
{
return $this->description;
}
public function updateOrigin(): UpdateOrigin
{
return $this->updateOrigin;
}
public function updateType(): UpdateType
{
return $this->updateType;
}
public function rollbackSource(): ?VersionNumber
{
return $this->rollbackSource;
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\RemoteConfig;
use JsonSerializable;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Stringable;
use function ctype_digit;
final class VersionNumber implements JsonSerializable, Stringable
{
/**
* @param non-empty-string $value
*/
private function __construct(private readonly string $value)
{
}
public function __toString(): string
{
return $this->value;
}
public static function fromValue($value): self
{
$valueString = (string) $value;
if (!ctype_digit($valueString)) {
throw new InvalidArgumentException('A version number should only consist of digits');
}
return new self($valueString);
}
/**
* @return non-empty-string
*/
public function jsonSerialize(): string
{
return $this->value;
}
/**
* @param self|non-empty-string $other
*/
public function equalsTo($other): bool
{
return $this->value === (string) $other;
}
}