diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dc27681..f3bffdbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ ## [Unreleased] +### Added + +* Added support for Remote Config Personalization + ([#731](https://github.com/kreait/firebase-php/pull/731)/[#733](https://github.com/kreait/firebase-php/pull/733)) + * Note: Personalization (currently) can not be added programmatically. The values can only be read and removed from a + Remote Config Template. To add Personalization, use the Firebase Web Console. +* Added `Kreait\Firebase\RemoteConfig\Template::withRemovedParameter(string $name)` to remove an existing parameter + from a Remote Config Template +* Added method `Kreait\Firebase\RemoteConfig\Template::withRemovedParameterGroup(string $name)` to remove an existing + parameter group from a Remote Config Template + +### Changed + +* Added `Kreait\Firebase\RemoteConfig\DefaultValue::useInAppDefault()` and deprecated + `\Kreait\Firebase\RemoteConfig\DefaultValue::none()` + +### Deprecated + +* `Kreait\Firebase\RemoteConfig\DefaultValue::IN_APP_DEFAULT_VALUE` +* `Kreait\Firebase\RemoteConfig\DefaultValue::none()` +* `Kreait\Firebase\RemoteConfig\DefaultValue::value()` + ## [6.8.0] - 2022-08-20 ### Added diff --git a/docs/remote-config.rst b/docs/remote-config.rst index e613244a..6c0383c3 100644 --- a/docs/remote-config.rst +++ b/docs/remote-config.rst @@ -138,6 +138,21 @@ Parameter Groups $template = $template->withParameterGroup($parameterGroup); +******************************* +Removing Remote Config Elements +******************************* + +You can remove elements from a Remote Config template with the following methods: + +.. code-block:: php + + $template = Template::new() + ->withParameterGroup(ParameterGroup::named('group')) + ->withParameter(Parameter::named('parameter')) + + $template = $template + ->withRemovedParameter('parameter') + ->withRemovedParameterGroup('group'); ********** Validation diff --git a/src/Firebase/Contract/RemoteConfig.php b/src/Firebase/Contract/RemoteConfig.php index 065c94ea..9685b7a8 100644 --- a/src/Firebase/Contract/RemoteConfig.php +++ b/src/Firebase/Contract/RemoteConfig.php @@ -18,6 +18,8 @@ * * @see https://firebase.google.com/docs/remote-config/use-config-rest * @see https://firebase.google.com/docs/remote-config/rest-reference + * + * @phpstan-import-type RemoteConfigTemplateShape from Template */ interface RemoteConfig { @@ -29,7 +31,7 @@ public function get(): Template; /** * Validates the given template without publishing it. * - * @param Template|array $template + * @param Template|RemoteConfigTemplateShape $template * * @throws ValidationFailed if the validation failed * @throws RemoteConfigException @@ -37,11 +39,11 @@ public function get(): Template; public function validate($template): void; /** - * @param Template|array $template + * @param Template|RemoteConfigTemplateShape $template * * @throws RemoteConfigException * - * @return string The etag value of the published template that can be compared to in later calls + * @return non-empty-string The etag value of the published template that can be compared to in later calls */ public function publish($template): string; diff --git a/src/Firebase/RemoteConfig.php b/src/Firebase/RemoteConfig.php index 2117018d..7f02d607 100644 --- a/src/Firebase/RemoteConfig.php +++ b/src/Firebase/RemoteConfig.php @@ -15,9 +15,12 @@ use Traversable; use function array_shift; +use function is_string; /** * @internal + * + * @phpstan-import-type RemoteConfigTemplateShape from Template */ final class RemoteConfig implements Contract\RemoteConfig { @@ -44,7 +47,13 @@ public function publish($template): string ->publishTemplate($this->ensureTemplate($template)) ->getHeader('ETag'); - return array_shift($etag) ?: ''; + $etag = array_shift($etag); + + if (is_string($etag) && $etag !== '') { + return $etag; + } + + return '*'; } public function getVersion($versionNumber): Version @@ -93,7 +102,7 @@ public function listVersions($query = null): Traversable } /** - * @param Template|array $value + * @param Template|RemoteConfigTemplateShape $value */ private function ensureTemplate($value): Template { diff --git a/src/Firebase/RemoteConfig/Condition.php b/src/Firebase/RemoteConfig/Condition.php index 13f8312c..f4c0a89f 100644 --- a/src/Firebase/RemoteConfig/Condition.php +++ b/src/Firebase/RemoteConfig/Condition.php @@ -6,14 +6,30 @@ use JsonSerializable; -use function array_filter; - +/** + * @phpstan-type RemoteConfigConditionShape array{ + * name: non-empty-string, + * expression: non-empty-string, + * tagColor?: ?non-empty-string + * } + */ class Condition implements JsonSerializable { + /** + * @var non-empty-string + */ private string $name; + + /** + * @var non-empty-string + */ private string $expression; private ?TagColor $tagColor; + /** + * @param non-empty-string $name + * @param non-empty-string $expression + */ private function __construct(string $name, string $expression, ?TagColor $tagColor = null) { $this->name = $name; @@ -22,11 +38,7 @@ private function __construct(string $name, string $expression, ?TagColor $tagCol } /** - * @param array{ - * name: string, - * expression: string, - * tagColor?: ?string - * } $data + * @param RemoteConfigConditionShape $data */ public static function fromArray(array $data): self { @@ -37,21 +49,33 @@ public static function fromArray(array $data): self ); } + /** + * @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; @@ -61,7 +85,7 @@ public function withExpression(string $expression): self } /** - * @param TagColor|string $tagColor + * @param TagColor|non-empty-string $tagColor */ public function withTagColor($tagColor): self { @@ -74,14 +98,27 @@ public function withTagColor($tagColor): self } /** - * @return array + * @return RemoteConfigConditionShape */ - public function jsonSerialize(): array + public function toArray(): array { - return array_filter([ + $array = [ 'name' => $this->name, 'expression' => $this->expression, - 'tagColor' => $this->tagColor !== null ? $this->tagColor->value() : null, - ], static fn ($value) => $value !== null); + ]; + + if ($this->tagColor !== null) { + $array['tagColor'] = $this->tagColor->value(); + } + + return $array; + } + + /** + * @return RemoteConfigConditionShape + */ + public function jsonSerialize(): array + { + return $this->toArray(); } } diff --git a/src/Firebase/RemoteConfig/ConditionalValue.php b/src/Firebase/RemoteConfig/ConditionalValue.php index 3eefefcb..43686a14 100644 --- a/src/Firebase/RemoteConfig/ConditionalValue.php +++ b/src/Firebase/RemoteConfig/ConditionalValue.php @@ -6,53 +6,88 @@ use JsonSerializable; +use function is_string; + +/** + * @phpstan-import-type RemoteConfigPersonalizationValueShape from PersonalizationValue + * @phpstan-import-type RemoteConfigExplicitValueShape from ExplicitValue + * @phpstan-import-type RemoteConfigInAppDefaultValueShape from DefaultValue + */ class ConditionalValue implements JsonSerializable { + /** + * @var non-empty-string + */ private string $conditionName; - private string $value; + + /** + * @var RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape|string + */ + private $data; /** * @internal + * + * @param non-empty-string $conditionName + * @param RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape|string $data */ - public function __construct(string $conditionName, string $value) + public function __construct(string $conditionName, $data) { $this->conditionName = $conditionName; - $this->value = $value; + $this->data = $data; } + /** + * @return non-empty-string + */ public function conditionName(): string { return $this->conditionName; } /** - * @param string|Condition $condition + * @param non-empty-string|Condition $condition */ public static function basedOn($condition): self { $name = $condition instanceof Condition ? $condition->name() : $condition; - return new self($name, ''); + return new self($name, ['value' => '']); } - public function value(): string + /** + * @return RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape|string + */ + public function value() + { + return $this->data; + } + + /** + * @param RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape|string $value + */ + public function withValue($value): self { - return $this->value; + return new self($this->conditionName, $value); } - public function withValue(string $value): self + /** + * @return RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape + */ + public function toArray(): array { - $conditionalValue = clone $this; - $conditionalValue->value = $value; + if (is_string($this->data)) { + return ExplicitValue::fromString($this->data)->toArray(); + } - return $conditionalValue; + return $this->data; } /** - * @return array + * @return RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape */ public function jsonSerialize(): array { - return ['value' => $this->value]; + return $this->toArray(); } } diff --git a/src/Firebase/RemoteConfig/DefaultValue.php b/src/Firebase/RemoteConfig/DefaultValue.php index 4fbb61d0..138d1b89 100644 --- a/src/Firebase/RemoteConfig/DefaultValue.php +++ b/src/Firebase/RemoteConfig/DefaultValue.php @@ -6,60 +6,95 @@ use JsonSerializable; -use function is_string; +use function array_key_exists; +/** + * @phpstan-import-type RemoteConfigPersonalizationValueShape from PersonalizationValue + * @phpstan-import-type RemoteConfigExplicitValueShape from ExplicitValue + * + * @phpstan-type RemoteConfigInAppDefaultValueShape array{ + * useInAppDefault: bool + * } + */ class DefaultValue implements JsonSerializable { + /** @deprecated 6.9.0 */ public const IN_APP_DEFAULT_VALUE = true; - /** @var string|bool */ - private $value; + /** + * @var RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape + */ + private $data; /** - * @param string|bool $value + * @param RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape $data */ - private function __construct($value) + private function __construct(array $data) { - $this->value = is_string($value) ? $value : true; + $this->data = $data; } + /** + * @deprecated 6.9.0 Use {@see useInAppDefault()} instead + */ public static function none(): self { - return new self(self::IN_APP_DEFAULT_VALUE); + return self::useInAppDefault(); + } + + public static function useInAppDefault(): self + { + return new self(['useInAppDefault' => true]); } public static function with(string $value): self { - return new self($value); + return new self(['value' => $value]); } /** - * @return string|bool + * @deprecated 6.9.0 Use {@see toArray()} instead + * + * @return string|bool|null */ public function value() { - return $this->value; + if (array_key_exists('value', $this->data)) { + return $this->data['value']; + } + + if (array_key_exists('useInAppDefault', $this->data)) { + return $this->data['useInAppDefault']; + } + + if (array_key_exists('personalizationId', $this->data)) { + return $this->data['personalizationId']; + } + + return null; + } + + /** + * @return RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape + */ + public function toArray(): array + { + return $this->data; } /** - * @param array{ - * value: string|bool - * }|array{ - * useInAppDefault: bool - * } $data + * @param RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape $data */ public static function fromArray(array $data): self { - return new self($data['value'] ?? $data['useInAppDefault'] ?? true); + return new self($data); } /** - * @return array + * @return RemoteConfigExplicitValueShape|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape */ public function jsonSerialize(): array { - $key = $this->value === true ? 'useInAppDefault' : 'value'; - - return [$key => $this->value]; + return $this->data; } } diff --git a/src/Firebase/RemoteConfig/ExplicitValue.php b/src/Firebase/RemoteConfig/ExplicitValue.php new file mode 100644 index 00000000..44692778 --- /dev/null +++ b/src/Firebase/RemoteConfig/ExplicitValue.php @@ -0,0 +1,49 @@ +data = $data; + } + + public static function fromString(string $value): self + { + return new self(['value' => $value]); + } + + /** + * @return RemoteConfigExplicitValueShape + */ + public function toArray(): array + { + return $this->data; + } + + /** + * @return RemoteConfigExplicitValueShape + */ + public function jsonSerialize(): array + { + return $this->data; + } +} diff --git a/src/Firebase/RemoteConfig/Parameter.php b/src/Firebase/RemoteConfig/Parameter.php index f71184a5..a2f2a18a 100644 --- a/src/Firebase/RemoteConfig/Parameter.php +++ b/src/Firebase/RemoteConfig/Parameter.php @@ -5,42 +5,70 @@ namespace Kreait\Firebase\RemoteConfig; use JsonSerializable; -use Kreait\Firebase\Exception\InvalidArgumentException; -use function array_filter; +use function is_bool; use function is_string; +/** + * @phpstan-import-type RemoteConfigPersonalizationValueShape from PersonalizationValue + * @phpstan-import-type RemoteConfigExplicitValueShape from ExplicitValue + * @phpstan-import-type RemoteConfigInAppDefaultValueShape from DefaultValue + * + * @phpstan-type RemoteConfigParameterShape array{ + * defaultValue?: RemoteConfigInAppDefaultValueShape|RemoteConfigExplicitValueShape|RemoteConfigPersonalizationValueShape, + * conditionalValues?: array, + * description?: string + * } + */ class Parameter implements JsonSerializable { + /** + * @var non-empty-string + */ private string $name; - private string $description = ''; - private DefaultValue $defaultValue; + private ?string $description = ''; + private ?DefaultValue $defaultValue; - /** @var ConditionalValue[] */ + /** @var list */ private array $conditionalValues = []; - private function __construct(string $name, DefaultValue $defaultValue) + /** + * @param non-empty-string $name + */ + private function __construct(string $name, ?DefaultValue $defaultValue = null) { $this->name = $name; $this->defaultValue = $defaultValue; } /** - * @param DefaultValue|string|mixed $defaultValue + * @param non-empty-string $name + * @param DefaultValue|RemoteConfigInAppDefaultValueShape|RemoteConfigPersonalizationValueShape|RemoteConfigExplicitValueShape|string|bool|null $defaultValue */ public static function named(string $name, $defaultValue = null): self { if ($defaultValue === null) { - $defaultValue = DefaultValue::none(); - } elseif (is_string($defaultValue)) { - $defaultValue = DefaultValue::with($defaultValue); - } else { - throw new InvalidArgumentException('The default value for a remote config parameter must be a string or NULL to use the in-app default.'); + return new self($name, null); } - return new self($name, $defaultValue); + if ($defaultValue instanceof DefaultValue) { + return new self($name, $defaultValue); + } + + if (is_string($defaultValue)) { + return new self($name, DefaultValue::fromArray(['value' => $defaultValue])); + } + + if (is_bool($defaultValue)) { + return new self($name, DefaultValue::fromArray(['useInAppDefault' => $defaultValue])); + } + + return new self($name, DefaultValue::fromArray($defaultValue)); } + /** + * @return non-empty-string + */ public function name(): string { return $this->name; @@ -48,7 +76,7 @@ public function name(): string public function description(): string { - return $this->description; + return $this->description ?: ''; } public function withDescription(string $description): self @@ -72,7 +100,7 @@ public function withDefaultValue($defaultValue): self return $parameter; } - public function defaultValue(): DefaultValue + public function defaultValue(): ?DefaultValue { return $this->defaultValue; } @@ -86,7 +114,7 @@ public function withConditionalValue(ConditionalValue $conditionalValue): self } /** - * @return ConditionalValue[] + * @return list */ public function conditionalValues(): array { @@ -94,20 +122,38 @@ public function conditionalValues(): array } /** - * @return array + * @return RemoteConfigParameterShape */ - public function jsonSerialize(): array + public function toArray(): array { $conditionalValues = []; foreach ($this->conditionalValues() as $conditionalValue) { - $conditionalValues[$conditionalValue->conditionName()] = $conditionalValue->jsonSerialize(); + $conditionalValues[$conditionalValue->conditionName()] = $conditionalValue->toArray(); } - return array_filter([ - 'defaultValue' => $this->defaultValue, - 'conditionalValues' => $conditionalValues, - 'description' => $this->description, - ]); + $array = []; + + if ($this->defaultValue !== null) { + $array['defaultValue'] = $this->defaultValue->toArray(); + } + + if ($conditionalValues !== []) { + $array['conditionalValues'] = $conditionalValues; + } + + if ($this->description !== null && $this->description !== '') { + $array['description'] = $this->description; + } + + return $array; + } + + /** + * @return RemoteConfigParameterShape + */ + public function jsonSerialize(): array + { + return $this->toArray(); } } diff --git a/src/Firebase/RemoteConfig/ParameterGroup.php b/src/Firebase/RemoteConfig/ParameterGroup.php index 4100cc25..3c8a795e 100644 --- a/src/Firebase/RemoteConfig/ParameterGroup.php +++ b/src/Firebase/RemoteConfig/ParameterGroup.php @@ -6,24 +6,43 @@ use JsonSerializable; +/** + * @phpstan-import-type RemoteConfigParameterShape from Parameter + * + * @phpstan-type RemoteConfigParameterGroupShape array{ + * description?: string|null, + * parameters: array} + */ final class ParameterGroup implements JsonSerializable { + /** + * @var non-empty-string + */ private string $name; private string $description = ''; - /** @var Parameter[] */ + /** @var array */ private array $parameters = []; + /** + * @param non-empty-string $name + */ private function __construct(string $name) { $this->name = $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; @@ -35,7 +54,7 @@ public function description(): string } /** - * @return Parameter[] + * @return array */ public function parameters(): array { @@ -59,13 +78,27 @@ public function withParameter(Parameter $parameter): self } /** - * @return array{description: string, parameters: Parameter[]} + * @return RemoteConfigParameterGroupShape */ - public function jsonSerialize(): array + public function toArray(): array { + $parameters = []; + + foreach ($this->parameters as $parameter) { + $parameters[$parameter->name()] = $parameter->toArray(); + } + return [ 'description' => $this->description, - 'parameters' => $this->parameters, + 'parameters' => $parameters, ]; } + + /** + * @return RemoteConfigParameterGroupShape + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } } diff --git a/src/Firebase/RemoteConfig/PersonalizationValue.php b/src/Firebase/RemoteConfig/PersonalizationValue.php new file mode 100644 index 00000000..b8958579 --- /dev/null +++ b/src/Firebase/RemoteConfig/PersonalizationValue.php @@ -0,0 +1,44 @@ +data = $data; + } + + /** + * @param RemoteConfigPersonalizationValueShape $data + */ + public static function fromArray(array $data): self + { + return new self($data); + } + + /** + * @return RemoteConfigPersonalizationValueShape + */ + public function jsonSerialize(): array + { + return $this->data; + } +} diff --git a/src/Firebase/RemoteConfig/TagColor.php b/src/Firebase/RemoteConfig/TagColor.php index 0b0fa37d..c5f3cedb 100644 --- a/src/Firebase/RemoteConfig/TagColor.php +++ b/src/Firebase/RemoteConfig/TagColor.php @@ -28,8 +28,15 @@ class TagColor 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 string $value; + /** + * @param non-empty-string $value + */ public function __construct(string $value) { $value = mb_strtoupper($value); @@ -47,11 +54,17 @@ public function __construct(string $value) $this->value = $value; } + /** + * @return non-empty-string + */ public function __toString() { return $this->value; } + /** + * @return non-empty-string + */ public function value(): string { return $this->value; diff --git a/src/Firebase/RemoteConfig/Template.php b/src/Firebase/RemoteConfig/Template.php index 513d095f..c851b5be 100644 --- a/src/Firebase/RemoteConfig/Template.php +++ b/src/Firebase/RemoteConfig/Template.php @@ -8,21 +8,35 @@ use Kreait\Firebase\Exception\InvalidArgumentException; use function array_key_exists; +use function array_map; use function array_values; -use function is_array; +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, + * parameters?: array, + * version?: RemoteConfigVersionShape, + * parameterGroups?: array + * } + */ class Template implements JsonSerializable { private string $etag = '*'; - /** @var Parameter[] */ + /** @var array */ private array $parameters = []; - /** @var ParameterGroup[] */ + /** @var array */ private array $parameterGroups = []; - /** @var Condition[] */ + /** @var list */ private array $conditions = []; private ?Version $version = null; @@ -36,27 +50,29 @@ public static function new(): self } /** - * @param array $data + * @param RemoteConfigTemplateShape $data */ public static function fromArray(array $data, ?string $etag = null): self { $template = new self(); $template->etag = $etag ?? '*'; - foreach ((array) ($data['conditions'] ?? []) as $conditionData) { + foreach (($data['conditions'] ?? []) as $conditionData) { $template = $template->withCondition(self::buildCondition($conditionData['name'], $conditionData)); } - foreach ((array) ($data['parameters'] ?? []) as $name => $parameterData) { + foreach (($data['parameters'] ?? []) as $name => $parameterData) { $template = $template->withParameter(self::buildParameter($name, $parameterData)); } - foreach ((array) ($data['parameterGroups'] ?? []) as $name => $parameterGroupData) { + foreach (($data['parameterGroups'] ?? []) as $name => $parameterGroupData) { $template = $template->withParameterGroup(self::buildParameterGroup($name, $parameterGroupData)); } - if (is_array($data['version'] ?? null)) { - $template->version = Version::fromArray($data['version']); + $versionData = $data['version'] ?? null; + + if ($versionData !== null) { + $template->version = Version::fromArray($versionData); } return $template; @@ -79,7 +95,7 @@ public function conditions(): array } /** - * @return Parameter[] + * @return array */ public function parameters(): array { @@ -109,6 +125,20 @@ public function withParameter(Parameter $parameter): self 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; @@ -117,10 +147,21 @@ public function withParameterGroup(ParameterGroup $parameterGroup): self 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->name()] = $condition; + $template->conditions[] = $condition; return $template; } @@ -138,7 +179,8 @@ public function jsonSerialize(): array } /** - * @param array $data + * @param non-empty-string $name + * @param RemoteConfigConditionShape $data */ private static function buildCondition(string $name, array $data): Condition { @@ -152,30 +194,34 @@ private static function buildCondition(string $name, array $data): Condition } /** - * @param array $data + * @param non-empty-string $name + * @param RemoteConfigParameterShape $data */ private static function buildParameter(string $name, array $data): Parameter { - $parameter = Parameter::named($name) - ->withDescription((string) ($data['description'] ?? '')) - ->withDefaultValue(DefaultValue::fromArray($data['defaultValue'] ?? [])); + $parameter = Parameter::named($name)->withDescription((string) ($data['description'] ?? '')); + + if (array_key_exists('defaultValue', $data) && $data['defaultValue'] !== null) { + $parameter = $parameter->withDefaultValue(DefaultValue::fromArray($data['defaultValue'])); + } foreach ((array) ($data['conditionalValues'] ?? []) as $key => $conditionalValueData) { - $parameter = $parameter->withConditionalValue(new ConditionalValue($key, $conditionalValueData['value'])); + $parameter = $parameter->withConditionalValue(new ConditionalValue($key, $conditionalValueData)); } return $parameter; } /** - * @param array $parameterGroupData + * @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) { + foreach ($parameterGroupData['parameters'] as $parameterName => $parameterData) { $group = $group->withParameter(self::buildParameter($parameterName, $parameterData)); } @@ -184,8 +230,10 @@ private static function buildParameterGroup(string $name, array $parameterGroupD private function assertThatAllConditionalValuesAreValid(Parameter $parameter): void { + $conditionNames = array_map(static fn (Condition $c) => $c->name(), $this->conditions); + foreach ($parameter->conditionalValues() as $conditionalValue) { - if (!array_key_exists($conditionalValue->conditionName(), $this->conditions)) { + 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())); diff --git a/src/Firebase/RemoteConfig/User.php b/src/Firebase/RemoteConfig/User.php index 6ef27558..e5144895 100644 --- a/src/Firebase/RemoteConfig/User.php +++ b/src/Firebase/RemoteConfig/User.php @@ -7,36 +7,65 @@ 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 { - private ?string $name = null; - private ?string $email = null; - private ?UriInterface $imageUri = null; + /** + * @var non-empty-string|null + */ + private ?string $name; + + /** + * @var non-empty-string|null + */ + private ?string $email; + private ?UriInterface $imageUri; - private function __construct() + /** + * @param non-empty-string|null $name + * @param non-empty-string|null $email + */ + private function __construct(?string $name, ?string $email, ?UriInterface $imageUri) { + $this->name = $name; + $this->email = $email; + $this->imageUri = $imageUri; } /** * @internal * - * @param array $data + * @param RemoteConfigUserShape $data */ public static function fromArray(array $data): self { - $new = new self(); - $new->name = $data['name'] ?? null; - $new->email = $data['email'] ?? null; - $new->imageUri = ($data['imageUrl'] ?? null) ? new Uri($data['imageUrl']) : null; + $imageUrl = $data['imageUrl'] ?? null; + $imageUri = $imageUrl ? new Uri($imageUrl) : null; - return $new; + 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; diff --git a/src/Firebase/RemoteConfig/Version.php b/src/Firebase/RemoteConfig/Version.php index 9de7ec5f..c6eaeedb 100644 --- a/src/Firebase/RemoteConfig/Version.php +++ b/src/Firebase/RemoteConfig/Version.php @@ -7,6 +7,21 @@ 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 VersionNumber $versionNumber; @@ -38,7 +53,7 @@ private function __construct( /** * @internal * - * @param array $data + * @param RemoteConfigVersionShape $data */ public static function fromArray(array $data): self { @@ -46,16 +61,10 @@ public static function fromArray(array $data): self $user = User::fromArray($data['updateUser']); $updatedAt = DT::toUTCDateTimeImmutable($data['updateTime']); $description = $data['description'] ?? ''; + $updateOrigin = UpdateOrigin::fromValue($data['updateOrigin']); + $updateType = UpdateType::fromValue($data['updateType']); - $updateOrigin = ($data['updateOrigin'] ?? null) - ? UpdateOrigin::fromValue($data['updateOrigin']) - : UpdateOrigin::fromValue(UpdateOrigin::UNSPECIFIED); - - $updateType = ($data['updateType'] ?? null) - ? UpdateType::fromValue($data['updateType']) - : UpdateType::fromValue(UpdateType::UNSPECIFIED); - - $rollbackSource = ($data['rollbackSource'] ?? null) + $rollbackSource = array_key_exists('rollbackSource', $data) ? VersionNumber::fromValue($data['rollbackSource']) : null; diff --git a/tests/Integration/RemoteConfigTest.php b/tests/Integration/RemoteConfigTest.php index e10b18a8..3d957614 100644 --- a/tests/Integration/RemoteConfigTest.php +++ b/tests/Integration/RemoteConfigTest.php @@ -310,7 +310,7 @@ private function templateWithTooManyParameters(): Template $template = Template::new(); for ($i = 0; $i < 3001; ++$i) { - $template = $template->withParameter(Parameter::named('i_'.$i)); + $template = $template->withParameter(Parameter::named('i_'.$i, 'v_'.$i)); } return $template; diff --git a/tests/Unit/RemoteConfig/DefaultValueTest.php b/tests/Unit/RemoteConfig/DefaultValueTest.php index a4082328..e0a8246b 100644 --- a/tests/Unit/RemoteConfig/DefaultValueTest.php +++ b/tests/Unit/RemoteConfig/DefaultValueTest.php @@ -5,16 +5,22 @@ namespace Kreait\Firebase\Tests\Unit\RemoteConfig; use Kreait\Firebase\RemoteConfig\DefaultValue; +use Kreait\Firebase\RemoteConfig\ExplicitValue; +use Kreait\Firebase\RemoteConfig\PersonalizationValue; use PHPUnit\Framework\TestCase; /** * @internal + * + * @phpstan-import-type RemoteConfigPersonalizationValueShape from PersonalizationValue + * @phpstan-import-type RemoteConfigExplicitValueShape from ExplicitValue + * @phpstan-import-type RemoteConfigInAppDefaultValueShape from DefaultValue */ final class DefaultValueTest extends TestCase { public function testCreateInAppDefaultValue(): void { - $defaultValue = DefaultValue::none(); + $defaultValue = DefaultValue::useInAppDefault(); $this->assertTrue($defaultValue->value()); $this->assertEquals(['useInAppDefault' => true], $defaultValue->jsonSerialize()); @@ -31,29 +37,34 @@ public function testCreate(): void /** * @dataProvider arrayValueProvider * - * @param bool|string $expected - * @param array{ - * value: string|bool - * }|array{ - * useInAppDefault: bool - * } $data + * @param RemoteConfigInAppDefaultValueShape $expected + * @param RemoteConfigInAppDefaultValueShape $data */ - public function testCreateFromArray($expected, array $data): void + public function testCreateFromArray(array $expected, array $data): void { $defaultValue = DefaultValue::fromArray($data); - $this->assertSame($expected, $defaultValue->value()); + $this->assertSame($expected, $defaultValue->toArray()); } /** - * @return iterable + * @return iterable> */ - public function arrayValueProvider() + public function arrayValueProvider(): iterable { - yield 'inAppDefault' => [true, ['useInAppDefault' => true]]; + yield 'inAppDefault' => [ + ['useInAppDefault' => true], + ['useInAppDefault' => true], + ]; - yield 'bool' => [true, ['value' => true]]; + yield 'explicit' => [ + ['value' => '1'], + ['value' => '1'], + ]; - yield 'string' => ['foo', ['value' => 'foo']]; + yield 'personalization' => [ + ['personalizationId' => 'pid'], + ['personalizationId' => 'pid'], + ]; } } diff --git a/tests/Unit/RemoteConfig/ParameterTest.php b/tests/Unit/RemoteConfig/ParameterTest.php index 29388867..f5c3d1a8 100644 --- a/tests/Unit/RemoteConfig/ParameterTest.php +++ b/tests/Unit/RemoteConfig/ParameterTest.php @@ -4,7 +4,6 @@ namespace Kreait\Firebase\Tests\Unit\RemoteConfig; -use Kreait\Firebase\Exception\InvalidArgumentException; use Kreait\Firebase\RemoteConfig\DefaultValue; use Kreait\Firebase\RemoteConfig\Parameter; use Kreait\Firebase\Tests\UnitTestCase; @@ -18,7 +17,7 @@ public function testCreateWithImplicitDefaultValue(): void { $parameter = Parameter::named('empty'); - $this->assertEquals(DefaultValue::none(), $parameter->defaultValue()); + $this->assertNull($parameter->defaultValue()); } public function testCreateWithDefaultValue(): void @@ -28,12 +27,6 @@ public function testCreateWithDefaultValue(): void $this->assertEquals(DefaultValue::with('foo'), $parameter->defaultValue()); } - public function testCreateWithInvalidDefaultValue(): void - { - $this->expectException(InvalidArgumentException::class); - Parameter::named('invalid', 1); - } - public function testCreateWithDescription(): void { $parameter = Parameter::named('something')->withDescription('description'); diff --git a/tests/Unit/RemoteConfig/TemplateTest.php b/tests/Unit/RemoteConfig/TemplateTest.php index 38dd31b0..1b34bea4 100644 --- a/tests/Unit/RemoteConfig/TemplateTest.php +++ b/tests/Unit/RemoteConfig/TemplateTest.php @@ -13,6 +13,8 @@ use Kreait\Firebase\RemoteConfig\Template; use Kreait\Firebase\Tests\UnitTestCase; +use function array_map; + /** * @internal */ @@ -49,7 +51,7 @@ public function testConditionNamesAreImportedCorrectlyWhenUsingFromArray(): void $template = $template->withParameter($parameter); - $condition = $template->conditions()['foo']; + $condition = $template->conditions()[0]; $this->assertSame('foo', $condition->name()); $this->assertSame('"true"', $condition->expression()); @@ -86,9 +88,87 @@ public function testWithFluidConfiguration(): void ->withParameter($welcomeMessageParameter) ->withParameterGroup($uiColors); - $this->assertSame($german, $template->conditions()['lang_german']); - $this->assertSame($french, $template->conditions()['lang_french']); + $conditionNames = array_map(static fn (Condition $c) => $c->name(), $template->conditions()); + + $this->assertContains('lang_german', $conditionNames); + $this->assertContains('lang_french', $conditionNames); $this->assertSame($welcomeMessageParameter, $template->parameters()['welcome_message']); $this->assertSame($uiColors, $template->parameterGroups()['ui_colors']); } + + public function testParametersCanBeRemoved(): void + { + $template = Template::new() + ->withParameter(Parameter::named('foo')) + ->withRemovedParameter('foo'); + + $this->assertCount(0, $template->parameters()); + } + + public function testParameterGroupsCanBeRemoved(): void + { + $template = Template::new() + ->withParameterGroup(ParameterGroup::named('group')) + ->withRemovedParameterGroup('group'); + + $this->assertCount(0, $template->parameterGroups()); + } + + public function testPersonalizationValuesAreImportedInDefaultValues(): void + { + $data = [ + 'parameters' => [ + 'foo' => [ + 'defaultValue' => [ + 'personalizationValue' => [ + 'personalizationId' => 'id', + ], + ], + ], + ], + ]; + + $template = Template::fromArray($data); + $this->assertArrayHasKey('foo', $parameters = $template->parameters()); + $this->assertNotNull($parameter = $parameters['foo']); + $this->assertNotNull($defaultValue = $parameter->defaultValue()); + + $this->assertArrayHasKey('personalizationValue', $array = $defaultValue->toArray()); + $this->assertArrayHasKey('personalizationId', $personalizationIdArray = $array['personalizationValue']); + $this->assertSame('id', $personalizationIdArray['personalizationId']); + } + + public function testPersonalizationValuesAreImportedInConditionalValues(): void + { + $data = [ + 'conditions' => [ + [ + 'name' => 'condition', + 'expression' => "device.language in ['de', 'de_AT', 'de_CH']", + ], + ], + 'parameters' => [ + 'foo' => [ + 'conditionalValues' => [ + 'condition' => [ + 'personalizationValue' => [ + 'personalizationId' => 'id', + ], + ], + ], + ], + ], + ]; + + $template = Template::fromArray($data); + $this->assertArrayHasKey('foo', $parameters = $template->parameters()); + $this->assertNotNull($parameter = $parameters['foo']); + + $conditionalValues = $parameter->conditionalValues(); + $this->assertArrayHasKey(0, $conditionalValues); + + $this->assertArrayHasKey('personalizationValue', $array = $conditionalValues[0]->toArray()); + $this->assertArrayHasKey('personalizationId', $personalizationIdArray = $array['personalizationValue']); + $this->assertSame('id', $personalizationIdArray['personalizationId']); + } } diff --git a/tests/_fixtures/remote_config_template.json b/tests/_fixtures/remote_config_template.json new file mode 100644 index 00000000..f519358c --- /dev/null +++ b/tests/_fixtures/remote_config_template.json @@ -0,0 +1,73 @@ +{ + "conditions": [ + { + "name": "lang_german", + "expression": "device.language in ['de', 'de_AT', 'de_CH']", + "tagColor": "ORANGE" + }, + { + "name": "lang_french", + "expression": "device.language in ['fr', 'fr_CA', 'fr_CH']", + "tagColor": "GREEN" + } + ], + "parameters": { + "welcome_message": { + "defaultValue": { + "value": "Welcome!" + }, + "conditionalValues": { + "lang_german": { + "value": "Willkommen!" + }, + "lang_french": { + "value": "Bienvenu!" + } + }, + "description": "This is a welcome message" + }, + "level": { + "defaultValue": { + "useInAppDefault": true + }, + "personalizationValue": { + "personalizationId": "abc" + } + } + }, + "parameterGroups": { + "welcome_messages": { + "description": "A group of parameters", + "parameters": { + "welcome_message_new_users": { + "defaultValue": { + "value": "Welcome, new user!" + }, + "conditionalValues": { + "lang_german": { + "value": "Willkommen, neuer Benutzer!" + }, + "lang_french": { + "value": "Bienvenu, nouvel utilisateur!" + } + }, + "description": "This is a welcome message for new users" + }, + "welcome_message_existing_users": { + "defaultValue": { + "value": "Welcome, existing user!" + }, + "conditionalValues": { + "lang_german": { + "value": "Willkommen, bestehender Benutzer!" + }, + "lang_french": { + "value": "Bienvenu, utilisant existant!" + } + }, + "description": "This is a welcome message for existing users" + } + } + } + } +}