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

455
vendor/kreait/firebase-php/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,455 @@
# CHANGELOG
**Support the project:** This SDK is downloaded 1M+ times monthly and powers thousands of applications.
If it saves you or your team time, please consider [sponsoring its development](https://github.com/sponsors/jeromegamez).
## [Unreleased]
## [7.24.0] - 2025-11-27
### Changed
* Realtime Database references are now validated by the API instead of locally. Validation rules can change at any time,
and the SDK can only adapt to changes in the API. While local checks could prevent obviously invalid paths, theyd
also require an SDK update whenever Firebase loosens a rule. Developers can be trusted not to use invalid paths 😅.
* Removed the `#[SensitiveParameter]` attribute again, because it's supported by PHP 8.1 itself, but not in combination
with Valinor.
([#1034](https://github.com/kreait/firebase-php/pull/1034))
## [7.23.0] - 2025-10-13
* Require `cuyz/valinor:^2.2.1` for better mapping.
## [7.22.0] - 2025-09-21
### Added
* Added support for PHP 8.5
### Changed
* The project now features a custom logo (I came up with it myself, and took the wise decision to not look up if there's something similar already)
* Refined README for improved clarity, removed outdated documentation sections, and streamlined project support messaging with a more positive call to action
* Documentation now uses the modern Furo theme, providing a cleaner and more pleasant reading experience
## [7.21.2] - 2025-08-15
### Fixed
* Re-added the `#[SensitiveParameter]` attribute because, while it's not supported in PHP 8.1, it can still be used
if placed in a standalone line above the variable or property.
* Re-added support for JSON files with any file extension
* With the introduction of Valinor, Service Account credentials were required to have more fields than necessary to
work with the SDK, although it only needs the client email, private key, and project ID.
## [7.21.1] - 2025-07-24
### Fixed
* Removed the `#[SensitiveParameter]` attribute because it's not supported in PHP 8.1.
## [7.21.0] - 2025-07-23
### Changed
* This release introduces [Valinor](https://valinor.cuyz.io/) for type-safe object mapping. The first application is
mapping a given service account file, JSON, or array to the newly added internal `ServiceAccount` class, with more
to follow in future releases.
## [7.20.0] - 2025-07-18
### Added
* You can now get a user by their federated identity provider (e.g. Google, Facebook, etc.) UID with
`Kreait\Firebase\Auth::getUserByProviderUid()`. ([#1000](https://github.com/kreait/firebase-php/pull/1000))
Since this method couldn't be added to the `Kreait\Firebase\Contract\Auth` interface without causing a breaking
change, a new transitional interface/contract named `Kreait\Firebase\Contract\Transitional\FederatedUserFetcher`
was added. This interface will be removed in the next major version of the SDK.
There are several ways to check if you can use the `getUserByProviderUid()` method:
```php
use Kreait\Firebase\Contract\Transitional\FederatedUserFetcher;
use Kreait\Firebase\Factory;
$auth = (new Factory())->createAuth();
// The return type is Kreait\Firebase\Contract\Auth, which doesn't have the method
if (method_exists($auth, 'getUserByProviderUid')) {
$user = $auth->getUserByProviderUid('google.com', 'google-uid');
}
if ($auth instanceof \Kreait\Firebase\Auth) { // This is the implementation, not the interface
$user = $auth->getUserByProviderUid('google.com', 'google-uid');
}
if ($auth instanceof FederatedUserFetcher) {
$user = $auth->getUserByProviderUid('google.com', 'google-uid');
}
```
* The new method `Kreait\Firebase\Factory::withDefaultCache()` allows you to set a default cache
implementation for the SDK. This is useful if you want to use one cache implementation for all components
that support caching.
([Documentation](https://firebase-php.readthedocs.io/en/latest/setup.html#caching))
### Deprecated
* `Kreait\Firebase\Factory::getDebugInfo`
## [7.19.0] - 2025-06-14
### Added
* You can now save on method call by passing a custom Firestore database name to
`Kreait\Firebase\Factory::createFirestore($databaseName)` instead of having to chain
``::withFirestoreDatabase($databaseName)->createFirestore()``
* It is now possible to set [live activity tokens](https://firebase.google.com/docs/cloud-messaging/customize-messages/live-activity?hl=en)
in Apns configs.
* `Kreait\Firebase\Http\HttpClientOptions::withGuzzleMiddleware()` and
`Kreait\Firebase\Http\HttpClientOptions::withGuzzleMiddlewares()` now accept callable strings, in addition
to callables. ([#1004](https://github.com/kreait/firebase-php/pull/1004))
### Deprecated
* `Kreait\Firebase\Factory::withFirestoreDatabase()`
## [7.18.0] - 2025-03-08
### Added
* It is now possible to configure multi factor authentication for a user.
## [7.17.0] - 2025-02-22
### Added
* FCM Error responses with status code `502` are now caught and converted to `ServerUnavailable` exceptions.
## [7.16.1] - 2025-01-20
### Fixed
* It wasn't possible to upgrade the SDK to a newer version because it required a `lcobucci/jwt` release that doesn't
support PHP 8.1 anymore. This was fixed by changing the version requirement from `^5.4.2` to `^5.3`.
## [7.16.0] - 2024-11-17
### Added
* It is now possible to override the Guzzle HTTP handler by using the `HttpClientOptions::withGuzzleHandler()` method.
([#956](https://github.com/kreait/firebase-php/pull/956))
### Changed
* The Messaging component doesn't rely on the `CloudMessage` class for message handling anymore. If you provide a
message as an array and it has an error, the Firebase API will report it. You can still use the `CloudMessage`
class as a message builder
* Deprecated the `CloudMessage::withTarget()` method, use the new `toToken()`, `toTopic()` or `toCondition()` methods instead
### Deprecated
* `Kreait\Firebase\Messaging\CloudMessage::withTarget()`
* `Kreait\Firebase\Messaging\CloudMessage::withChangedTarget()`
* `Kreait\Firebase\Messaging\CloudMessage::target()`
* `Kreait\Firebase\Messaging\CloudMessage::hasTarget()`
## [7.15.0] - 2024-09-11
### Added
* Added support for [rollout parameter values](https://firebase.google.com/docs/reference/remote-config/rest/v1/RemoteConfig#RolloutValue)
in Remote Config Templates.
([#923](https://github.com/kreait/firebase-php/pull/923)), ([#927](https://github.com/kreait/firebase-php/pull/927))
* Please note that it's not (yet?) possible to create rollouts programmatically via the Firebase API. This means that
you have to manually create a rollout in the Firebase console to be able to reference it in the Remote Config
template. Rollout IDs are named `rollout_<number>`, and you can find the ID in the URL after clicking on a rollout in the list.
## [7.14.0] - 2024-08-21
### Added
* Added support for PHP 8.4.
* Please note: While the SDK supports PHP 8.4, not all dependencies support it. If you want to use the SDK with
PHP 8.4, you probably will need to ignore platform requirements when working with Composer, by setting the
[appropriate environment variables](https://getcomposer.org/doc/03-cli.md#composer-ignore-platform-req-or-composer-ignore-platform-reqs)
or [`composer` CLI options]() when running `composer install/update/require`.
### Deprecated
* Firebase Dynamic Links is deprecated and should not be used in new projects. The service will shut down on
August 25, 2025. The component will remain in the SDK until then, but as the Firebase service is deprecated,
this component is also deprecated.
([Dynamic Links Deprecation FAQ](https://firebase.google.com/support/dynamic-links-faq))
## [7.13.1] - 2024-07-02
### Fixed
* Requests to the FCM APIs will not use HTTP/2 if the environment doesn't support them
([#888](https://github.com/kreait/firebase-php/pull/888), [#908](https://github.com/kreait/firebase-php/pull/908))
## [7.13.0] - 2024-06-23
### Changed
* Service Account auto-discovery was done on instantiation of the Factory, causing it to fail when credentials weren't
ready yet. It will now be done the first time a component is to be instantiated.
## [7.12.0] - 2024-05-26
### Fixed
* Fix `WebPushNotification` Shape
([#895](https://github.com/kreait/firebase-php/pull/895))
* Catch `Throwable` and let the exception converter handle details
([#896](https://github.com/kreait/firebase-php/pull/896))
## [7.11.0] - 2024-05-16
### Added
* It is now possible to get a Remote Config template by its version number.
([#890](https://github.com/kreait/firebase-php/pull/890))
## [7.10.0] - 2024-04-25
### Changed
* FCM Messages are now sent asynchronously using HTTP connection pooling with HTTP/2. This should improve performance
when sending messages to many devices.
([#874](https://github.com/kreait/firebase-php/pull/874))
## [7.9.1] - 2023-12-04
### Changed
* Re-enabled the use of `psr/http-message` v1.0
([#850](https://github.com/kreait/firebase-php/issues/850))
## [7.9.0] - 2023-11-30
### Added
* Added support for PHP 8.3
## [7.8.0] - 2023-11-25
### Added
* Added `Kreait\Firebase\Factory::withFirestoreClientConfig()` to support setting additional options when
creating the Firestore component.
([Documentation](https://firebase-php.readthedocs.io/en/latest/cloud-firestore.html#add-firestore-configuration-options))
* Added `Kreait\Firebase\Factory::withFirestoreDatabase()` to specify the database used when creating the Firestore
component.
([Documentation](https://firebase-php.readthedocs.io/en/latest/cloud-firestore.html#use-another-firestore-database))
## [7.7.0] - 2023-11-25
### Changed
* Required transitive dependencies directly ([#842](https://github.com/kreait/firebase-php/issues/842))
```json5
{
"require": {
// ...
"ext-filter": "*",
"guzzlehttp/promises": "^2.0",
"guzzlehttp/psr7": "^2.6",
"psr/clock": "^1.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^2.0",
}
}
```
## [7.6.0] - 2023-09-07
### Added
* The `Kreait\Firebase\Exception\Messaging\NotFound` exception now exposes the token that hasn't been found
with the `token()` method.
([#825](https://github.com/kreait/firebase-php/issues/825))
## [7.5.2] - 2023-06-29
### Added
* Added FCM error handling to the documentation
## [7.5.1] - 2023-06-29
### Fixed
* The cached KeySet used by the AppCheck component didn't use the same Guzzle Config Options as the other clients
([#812](https://github.com/kreait/firebase-php/issues/812))
## [7.5.0] - 2023-06-27
### Changed
* Replaced calls to deprecated FCM batch endpoints with asynchronous requests
to the HTTP V1 API
([#804](https://github.com/kreait/firebase-php/pull/804)/[#805](https://github.com/kreait/firebase-php/pull/805))
* Removed message limit when sending multiple FCM messages
* The message limit was needed when using the FCM batch endpoints because they used multipart requests and responses.
The limit prevented these messages to become too large. Since we're now using asynchronous calls to send one
request per message, this limitation is not needed anymore.
* Simplified convoluted Dynamic Link operations
([#810](https://github.com/kreait/firebase-php/pull/810))
### Removed
* Removed obsolete internal classes
* `Kreait\Firebase\Http\HasSubRequests`
* `Kreait\Firebase\Http\HasSubResponses`
* `Kreait\Firebase\Http\Requests`
* `Kreait\Firebase\Http\RequestWithSubRequests`
* `Kreait\Firebase\Http\Responses`
* `Kreait\Firebase\Http\ResponseWithSubResponses`
* `Kreait\Firebase\Http\WrappedPsr7Response`
* `Kreait\Firebase\Http\WrappedPsr7Request`
* `Kreait\Firebase\Messaging\Http\Request\MessageRequest`
* `Kreait\Firebase\Messaging\Http\Request\SendMessage`
* `Kreait\Firebase\Messaging\Http\Request\SendMessageToTokens`
* `Kreait\Firebase\Messaging\Http\Request\SendMessages`
* Removed obsolete internal methods
* `Kreait\Firebase\Http\Middleware::responseWithSubResponses()`
* Removed obsolete Composer dependency `riverline/multipart-parser`
## [7.4.0] - 2023-06-18
### Added
* Added support for [Parameter Value Types](https://firebase.google.com/docs/reference/remote-config/rest/v1/RemoteConfig#parametervaluetype)
when getting and setting a RemoteConfig template.
([Documentation](https://firebase-php.readthedocs.io/en/latest/remote-config.html#parameter-value-types))
### Deprecated
* `Kreait\Firebase\RemoteConfig\ExplicitValue` is deprecated
* `Kreait\Firebase\RemoteConfig\DefaultValue` should be regarded as deprecated, it is kept to not create a breaking changes
## [7.3.1] - 2023-06-10
### Changed
* Removed direct dependency to `psr/http-message`
## [7.3.0] - 2023-06-03
### Added
* It is now possible to add config options and middlewares to the Guzzle HTTP Client performing the HTTP Requests
to the Firebase APIs through the `HttpClientOptions` class.
([Documentation](https://firebase-php.readthedocs.io/en/latest/setup.html#http-client-options))
## [7.2.1] - 2023-04-04
### Fixed
* Fixed a user's MFA information not being correctly parsed
([#783](https://github.com/kreait/firebase-php/pull/783))
## [7.2.0] - 2023-03-24
### Added
* Added support for the Firebase Auth Emulator when using `lcobucci/jwt` 5.*
## [7.1.0] - 2023-03-01
### Added
* Added support for `lcobucci/jwt` 5.*
## [7.0.3] - 2023-02-13
### Fixed
* Restored support for using a JSON string in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable.
([#767](https://github.com/kreait/firebase-php/pull/767))
## [7.0.2] - 2023-01-27
### Fixed
* Cloud Messaging: The APNS `content-available` payload field was not set correctly when a message contained
message data at the root level, but not at the APNS config level.
([#762](https://github.com/kreait/firebase-php/pull/762))
## [7.0.1] - 2023-01-24
### Fixed
* When trying to work with unknown FCM tokens, errors returned from the Messaging REST API were not passed to
the `NotFound` exception, which prevented the inspection of further details.
([#760](https://github.com/kreait/firebase-php/pull/760))
## [7.0.0] - 2022-12-20
The most notable change is that you need PHP 8.1/8.2 to use the new version. The language migration of
the SDK introduces breaking changes concerning the strictness of parameter types almost everywhere in
the SDK - however, this should not affect your project in most cases (unless you have used internal classes
directly or by extension).
This release adds many more PHPDoc annotations to support the usage of Static Analysis Tools like PHPStan
and Psalm and moves away from doing runtime checks. It is strongly recommended to use a Static Analysis
Tool and ensure that input values are validated before handing them over to the SDK.
### Added features
* Added support for verifying Firebase App Check Tokens. ([#747](https://github.com/kreait/firebase-php/pull/747))
### Notable changes
* The ability to disable credentials auto-discovery has been removed. If you don't want a service account to be
auto-discovered, provide it by using the `withServiceAccount()` method of the Factory or by setting the
`GOOGLE_APPLICATION_CREDENTIALS` environment variable. Depending on the environment in which the SDK is running,
credentials could be auto-discovered otherwise, for example on GCP or GCE.
See **[UPGRADE-7.0](UPGRADE-7.0.md) for more details on the changes between 6.x and 7.0.**
## 6.x Changelog
https://github.com/kreait/firebase-php/blob/6.9.6/CHANGELOG.md
[Unreleased]: https://github.com/kreait/firebase-php/compare/7.24.0...7.x
[7.24.0]: https://github.com/kreait/firebase-php/compare/7.23.0...7.24.0
[7.23.0]: https://github.com/kreait/firebase-php/compare/7.22.0...7.23.0
[7.22.0]: https://github.com/kreait/firebase-php/compare/7.21.2...7.22.0
[7.21.2]: https://github.com/kreait/firebase-php/compare/7.21.1...7.21.2
[7.21.1]: https://github.com/kreait/firebase-php/compare/7.21.0...7.21.1
[7.21.0]: https://github.com/kreait/firebase-php/compare/7.20.0...7.21.0
[7.20.0]: https://github.com/kreait/firebase-php/compare/7.19.0...7.20.0
[7.19.0]: https://github.com/kreait/firebase-php/compare/7.18.0...7.19.0
[7.18.0]: https://github.com/kreait/firebase-php/compare/7.17.0...7.18.0
[7.17.0]: https://github.com/kreait/firebase-php/compare/7.16.1...7.17.0
[7.16.1]: https://github.com/kreait/firebase-php/compare/7.16.0...7.16.1
[7.16.0]: https://github.com/kreait/firebase-php/compare/7.15.0...7.16.0
[7.15.0]: https://github.com/kreait/firebase-php/compare/7.14.0...7.15.0
[7.14.0]: https://github.com/kreait/firebase-php/compare/7.13.1...7.14.0
[7.13.1]: https://github.com/kreait/firebase-php/compare/7.13.0...7.13.1
[7.13.0]: https://github.com/kreait/firebase-php/compare/7.12.0...7.13.0
[7.12.0]: https://github.com/kreait/firebase-php/compare/7.11.0...7.12.0
[7.11.0]: https://github.com/kreait/firebase-php/compare/7.10.0...7.11.0
[7.10.0]: https://github.com/kreait/firebase-php/compare/7.9.1...7.10.0
[7.9.1]: https://github.com/kreait/firebase-php/compare/7.9.0...7.9.1
[7.9.0]: https://github.com/kreait/firebase-php/compare/7.8.0...7.9.0
[7.8.0]: https://github.com/kreait/firebase-php/compare/7.7.0...7.8.0
[7.7.0]: https://github.com/kreait/firebase-php/compare/7.6.0...7.7.0
[7.6.0]: https://github.com/kreait/firebase-php/compare/7.5.2...7.6.0
[7.5.2]: https://github.com/kreait/firebase-php/compare/7.5.1...7.5.2
[7.5.1]: https://github.com/kreait/firebase-php/compare/7.5.0...7.5.1
[7.5.0]: https://github.com/kreait/firebase-php/compare/7.3.1...7.5.0
[7.4.0]: https://github.com/kreait/firebase-php/compare/7.3.1...7.4.0
[7.3.1]: https://github.com/kreait/firebase-php/compare/7.3.0...7.3.1
[7.3.0]: https://github.com/kreait/firebase-php/compare/7.2.1...7.3.0
[7.2.1]: https://github.com/kreait/firebase-php/compare/7.2.0...7.2.1
[7.2.0]: https://github.com/kreait/firebase-php/compare/7.1.0...7.2.0
[7.1.0]: https://github.com/kreait/firebase-php/compare/7.0.3...7.1.0
[7.0.3]: https://github.com/kreait/firebase-php/compare/7.0.2...7.0.3
[7.0.2]: https://github.com/kreait/firebase-php/compare/7.0.1...7.0.2
[7.0.1]: https://github.com/kreait/firebase-php/compare/7.0.0...7.0.1
[7.0.0]: https://github.com/kreait/firebase-php/releases/tag/7.0.0

21
vendor/kreait/firebase-php/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Jérôme Gamez, https://github.com/jeromegamez <jerome@gamez.name>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

79
vendor/kreait/firebase-php/README.md vendored Normal file
View File

@@ -0,0 +1,79 @@
<div align="center">
# Firebase for PHP
<img src="docs/_static/logo.svg" alt="Firebase Admin PHP SDK Logo" width="120">
<p><em>Firebase Admin SDK for PHP</em></p>
[![Current version](https://img.shields.io/packagist/v/kreait/firebase-php.svg?logo=composer)](https://packagist.org/packages/kreait/firebase-php)
[![Monthly Downloads](https://img.shields.io/packagist/dm/kreait/firebase-php.svg)](https://packagist.org/packages/kreait/firebase-php/stats)
[![Total Downloads](https://img.shields.io/packagist/dt/kreait/firebase-php.svg)](https://packagist.org/packages/kreait/firebase-php/stats)<br/>
[![Tests](https://github.com/kreait/firebase-php/actions/workflows/tests.yml/badge.svg)](https://github.com/kreait/firebase-php/actions/workflows/tests.yml)
[![Integration Tests](https://github.com/kreait/firebase-php/actions/workflows/integration-tests.yml/badge.svg)](https://github.com/kreait/firebase-php/actions/workflows/integration-tests.yml)
[![Emulator Tests](https://github.com/kreait/firebase-php/actions/workflows/emulator-tests.yml/badge.svg)](https://github.com/kreait/firebase-php/actions/workflows/emulator-tests.yml)
[![Sponsor](https://img.shields.io/static/v1?logo=GitHub&label=Sponsor&message=%E2%9D%A4&color=ff69b4)](https://github.com/sponsors/jeromegamez)
</div>
> [!IMPORTANT]
> **Support the project:** This SDK is downloaded 1M+ times monthly and powers thousands of applications.
> If it saves you or your team time, please consider
> [sponsoring its development](https://github.com/sponsors/jeromegamez).
> [!NOTE]
> If you are interested in using the PHP Admin SDK as a client for end-user access (for example, in a web application),
> as opposed to admin access from a privileged environment (like a server), you should instead follow the
> [instructions for setting up the client JavaScript SDK](https://firebase.google.com/docs/web/setup).
## Overview
[Firebase](https://firebase.google.com/) provides the tools and infrastructure you need to develop your app, grow your user base, and earn money. The Firebase Admin PHP SDK enables access to Firebase services from privileged environments (such as servers or cloud) in PHP.
For more information, visit the [Firebase Admin PHP SDK documentation](https://firebase-php.readthedocs.io/).
## Installation
The recommended way to install the Firebase Admin SDK is with [Composer](https://getcomposer.org).
Composer is a dependency management tool for PHP that allows you to declare the dependencies
your project needs and installs them into your project.
```bash
composer require "kreait/firebase-php:^7.0"
```
Please continue to the [Setup section](docs/setup.rst) to learn more about connecting your application to Firebase.
If you want to use the SDK within a Framework, please follow the installation instructions here:
- **Laravel**: https://github.com/kreait/laravel-firebase
- **Symfony**: https://github.com/kreait/firebase-bundle
## Quickstart
```php
use Kreait\Firebase\Factory;
$factory = (new Factory)
->withServiceAccount('/path/to/firebase_credentials.json')
->withDatabaseUri('https://my-project-default-rtdb.firebaseio.com');
$auth = $factory->createAuth();
$realtimeDatabase = $factory->createDatabase();
$cloudMessaging = $factory->createMessaging();
$remoteConfig = $factory->createRemoteConfig();
$cloudStorage = $factory->createStorage();
$firestore = $factory->createFirestore();
```
## Powered by
[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport)
Thanks to [JetBrains](https://www.jetbrains.com/) credits for providing [a free PhpStorm license](https://jb.gg/OpenSourceSupport) for the development of this open-source package.
## License
Firebase Admin PHP SDK is licensed under the [MIT License](LICENSE).
Your use of Firebase is governed by the [Terms of Service for Firebase Services](https://firebase.google.com/terms/).

View File

@@ -0,0 +1,338 @@
# Upgrade from 6.x to 7.0
## Introduction
This is a major release, but its aim is to provide as much backward compatibility as possible to ease upgrades
from 6.x to 7.0.
The minimum required PHP version has been changed from `^7.4` to `~8.1.0 || ~8.2.0`, the two PHP versions that
are [actively supported since 2022-11-26](https://www.php.net/supported-versions.php).
If you're using version 6.x of the SDK as documented, an upgrade to 7.x should hopefully be as simple as updating the
version constraint in your project's `composer.json` file.
"As documented" means:
* When injecting SDK dependencies in your project's controllers and services, you use the interfaces in the
`Kreait\Firebase\Contract` namespace, not the (internal) implementations.
* You're not using classes marked as `@internal`
* You don't have created your own implementations based on the Interfaces in the `Kreait\Firebase\Contract` namespace.
* Updating your custom implementations will take some work but should™ be straightforward.
## Notable changes
* The ability to disable credentials auto-discovery has been removed. If you don't want a service account to be
auto-discovered, provide it by using the `withServiceAccount()` method of the Factory or by setting the
`GOOGLE_APPLICATION_CREDENTIALS` environment variable. Depending on the environment in which the SDK is running,
credentials could be auto-discovered otherwise, for example on GCP or GCE.
* Classes, Methods and Constants marked as `@deprecated` have been removed.
* Classes, Methods and Constants marked as `@internal` have been removed or refactored.
* `@param` annotations in PHPDoc blocks have been moved to typed parameters, for example:
```php
/**
* @param scalar $value
*/
public function method($value);
```
would change to
```php
public function method(bool|string|int|float $value);
```
* Methods that accepted strings and objects with a `__toString()` method have been replaced with `Stringable|string`,
for example:
```php
/**
* @param Uid|string $uid
*/
public function method($uid);
```
have been changed to
```php
public function method(Stringable|string $value);
```
## Complete list of breaking changes
The following list has been generated with [roave/backward-compatibility-check](https://github.com/Roave/BackwardCompatibilityCheck).
### Removals
```
[BC] CHANGED: Class Kreait\Firebase\Auth\UserInfo became final
[BC] CHANGED: Class Kreait\Firebase\Auth\UserMetaData became final
[BC] CHANGED: Class Kreait\Firebase\Auth\UserRecord became final
[BC] CHANGED: Kreait\Firebase\Auth\CreateActionLink was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\CreateActionLink\GuzzleApiClientHandler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\CreateActionLink\Handler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\CreateSessionCookie was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\CreateSessionCookie\GuzzleApiClientHandler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\CreateSessionCookie\Handler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\DeleteUsersRequest was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\DeleteUsersResult::fromRequestAndResponse() was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SendActionLink was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SendActionLink\GuzzleApiClientHandler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SendActionLink\Handler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SignIn was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SignInAnonymously was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SignInWithCustomToken was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SignInWithEmailAndOobCode was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SignInWithEmailAndPassword was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SignInWithIdpCredentials was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\SignInWithRefreshToken was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\TenantAwareAuthResourceUrlBuilder was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\UserInfo::fromResponseData() was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\UserMetaData::fromResponseData() was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Auth\UserRecord::fromResponseData() was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Filter was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Filter\EndAt was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Filter\EndBefore was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Filter\EqualTo was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Filter\LimitToFirst was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Filter\LimitToLast was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Filter\Shallow was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Filter\StartAfter was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Filter\StartAt was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Modifier was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\ModifierTrait was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Sorter was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Sorter\OrderByChild was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Sorter\OrderByKey was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Query\Sorter\OrderByValue was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\Reference\Validator was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Database\UrlBuilder was marked "@internal"
[BC] CHANGED: Kreait\Firebase\DynamicLink\CreateDynamicLink\ApiRequest was marked "@internal"
[BC] CHANGED: Kreait\Firebase\DynamicLink\CreateDynamicLink\GuzzleApiClientHandler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\DynamicLink\CreateDynamicLink\Handler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink\ApiRequest was marked "@internal"
[BC] CHANGED: Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink\GuzzleApiClientHandler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink\Handler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\DynamicLink\ShortenLongDynamicLink\ApiRequest was marked "@internal"
[BC] CHANGED: Kreait\Firebase\DynamicLink\ShortenLongDynamicLink\GuzzleApiClientHandler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\DynamicLink\ShortenLongDynamicLink\Handler was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Exception\HasErrors was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Messaging\Processor\SetApnsContentAvailableIfNeeded was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Messaging\Processor\SetApnsPushTypeIfNeeded was marked "@internal"
[BC] CHANGED: Kreait\Firebase\Messaging\RegistrationTokens#__construct() was marked "@internal"
[BC] CHANGED: Property Kreait\Firebase\Auth\UserRecord#$customClaims changed default value from array () to NULL
[BC] CHANGED: Property Kreait\Firebase\Auth\UserRecord#$disabled changed default value from false to NULL
[BC] CHANGED: Property Kreait\Firebase\Auth\UserRecord#$emailVerified changed default value from false to NULL
[BC] CHANGED: Property Kreait\Firebase\Auth\UserRecord#$providerData changed default value from array () to NULL
[BC] CHANGED: Property Kreait\Firebase\Auth\UserRecord#$uid changed default value from '' to NULL
[BC] CHANGED: The number of required arguments for Kreait\Firebase\Auth\UserRecord#__construct() increased from 0 to 15
[BC] CHANGED: The parameter $actionOrParametersOrUrl of Kreait\Firebase\Contract\DynamicLinks#createDynamicLink() changed from no type to Stringable|string|Kreait\Firebase\DynamicLink\CreateDynamicLink|array
[BC] CHANGED: The parameter $actionOrParametersOrUrl of Kreait\Firebase\Contract\DynamicLinks#createDynamicLink() changed from no type to a non-contravariant Stringable|string|Kreait\Firebase\DynamicLink\CreateDynamicLink|array
[BC] CHANGED: The parameter $clearTextPassword of Kreait\Firebase\Contract\Auth#signInWithEmailAndPassword() changed from no type to Stringable|string
[BC] CHANGED: The parameter $clearTextPassword of Kreait\Firebase\Contract\Auth#signInWithEmailAndPassword() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $clearTextPassword of Kreait\Firebase\Request\EditUserTrait#withClearTextPassword() changed from no type to Stringable|string
[BC] CHANGED: The parameter $clearTextPassword of Kreait\Firebase\Request\EditUserTrait#withClearTextPassword() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $clearTextPassword of Kreait\Firebase\Request\EditUserTrait#withClearTextPassword() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $clearTextPassword of Kreait\Firebase\Request\EditUserTrait#withClearTextPassword() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $data of Kreait\Firebase\DynamicLink\CreateDynamicLink#withAnalyticsInfo() changed from no type to a non-contravariant Kreait\Firebase\DynamicLink\AnalyticsInfo|array
[BC] CHANGED: The parameter $data of Kreait\Firebase\DynamicLink\CreateDynamicLink#withAndroidInfo() changed from no type to a non-contravariant Kreait\Firebase\DynamicLink\AndroidInfo|array
[BC] CHANGED: The parameter $data of Kreait\Firebase\DynamicLink\CreateDynamicLink#withIOSInfo() changed from no type to a non-contravariant Kreait\Firebase\DynamicLink\IOSInfo|array
[BC] CHANGED: The parameter $data of Kreait\Firebase\DynamicLink\CreateDynamicLink#withNavigationInfo() changed from no type to a non-contravariant Kreait\Firebase\DynamicLink\NavigationInfo|array
[BC] CHANGED: The parameter $data of Kreait\Firebase\DynamicLink\CreateDynamicLink#withSocialMetaTagInfo() changed from no type to a non-contravariant Kreait\Firebase\DynamicLink\SocialMetaTagInfo|array
[BC] CHANGED: The parameter $data of Kreait\Firebase\Messaging\CloudMessage#withData() changed from no type to a non-contravariant Kreait\Firebase\Messaging\MessageData|array
[BC] CHANGED: The parameter $dynamicLinkDomain of Kreait\Firebase\DynamicLink\CreateDynamicLink#withDynamicLinkDomain() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $dynamicLinkOrAction of Kreait\Firebase\Contract\DynamicLinks#getStatistics() changed from no type to Stringable|string|Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink
[BC] CHANGED: The parameter $dynamicLinkOrAction of Kreait\Firebase\Contract\DynamicLinks#getStatistics() changed from no type to a non-contravariant Stringable|string|Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink
[BC] CHANGED: The parameter $email of Kreait\Firebase\Auth\CreateActionLink::new() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#createUserWithEmailAndPassword() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#createUserWithEmailAndPassword() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getEmailActionLink() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getEmailActionLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getEmailVerificationLink() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getEmailVerificationLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getPasswordResetLink() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getPasswordResetLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getSignInWithEmailLink() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getSignInWithEmailLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getUserByEmail() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#getUserByEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#sendEmailActionLink() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#sendEmailActionLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#sendEmailVerificationLink() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#sendEmailVerificationLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#sendPasswordResetLink() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#sendPasswordResetLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#sendSignInWithEmailLink() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#sendSignInWithEmailLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#signInWithEmailAndOobCode() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#signInWithEmailAndOobCode() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#signInWithEmailAndPassword() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Contract\Auth#signInWithEmailAndPassword() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withEmail() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withUnverifiedEmail() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withUnverifiedEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withUnverifiedEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withUnverifiedEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withVerifiedEmail() changed from no type to Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withVerifiedEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withVerifiedEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $email of Kreait\Firebase\Request\EditUserTrait#withVerifiedEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $idToken of Kreait\Firebase\Contract\Auth#createSessionCookie() changed from no type to Lcobucci\JWT\Token|string
[BC] CHANGED: The parameter $idToken of Kreait\Firebase\Contract\Auth#createSessionCookie() changed from no type to a non-contravariant Lcobucci\JWT\Token|string
[BC] CHANGED: The parameter $idToken of Kreait\Firebase\Contract\Auth#signInWithIdpIdToken() changed from no type to Lcobucci\JWT\Token|string
[BC] CHANGED: The parameter $idToken of Kreait\Firebase\Contract\Auth#signInWithIdpIdToken() changed from no type to a non-contravariant Lcobucci\JWT\Token|string
[BC] CHANGED: The parameter $idToken of Kreait\Firebase\Contract\Auth#verifyIdToken() changed from no type to Lcobucci\JWT\Token|string
[BC] CHANGED: The parameter $idToken of Kreait\Firebase\Contract\Auth#verifyIdToken() changed from no type to a non-contravariant Lcobucci\JWT\Token|string
[BC] CHANGED: The parameter $link of Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink::forLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $longDynamicLinkOrAction of Kreait\Firebase\Contract\DynamicLinks#shortenLongDynamicLink() changed from no type to Stringable|string|Kreait\Firebase\DynamicLink\ShortenLongDynamicLink|array
[BC] CHANGED: The parameter $longDynamicLinkOrAction of Kreait\Firebase\Contract\DynamicLinks#shortenLongDynamicLink() changed from no type to a non-contravariant Stringable|string|Kreait\Firebase\DynamicLink\ShortenLongDynamicLink|array
[BC] CHANGED: The parameter $message of Kreait\Firebase\Contract\Messaging#send() changed from no type to Kreait\Firebase\Messaging\Message|array
[BC] CHANGED: The parameter $message of Kreait\Firebase\Contract\Messaging#send() changed from no type to a non-contravariant Kreait\Firebase\Messaging\Message|array
[BC] CHANGED: The parameter $message of Kreait\Firebase\Contract\Messaging#sendMulticast() changed from no type to Kreait\Firebase\Messaging\Message|array
[BC] CHANGED: The parameter $message of Kreait\Firebase\Contract\Messaging#sendMulticast() changed from no type to a non-contravariant Kreait\Firebase\Messaging\Message|array
[BC] CHANGED: The parameter $message of Kreait\Firebase\Contract\Messaging#validate() changed from no type to Kreait\Firebase\Messaging\Message|array
[BC] CHANGED: The parameter $message of Kreait\Firebase\Contract\Messaging#validate() changed from no type to a non-contravariant Kreait\Firebase\Messaging\Message|array
[BC] CHANGED: The parameter $messages of Kreait\Firebase\Contract\Messaging#sendAll() changed from no type to a non-contravariant array|Kreait\Firebase\Messaging\Messages
[BC] CHANGED: The parameter $messages of Kreait\Firebase\Contract\Messaging#sendAll() changed from no type to array|Kreait\Firebase\Messaging\Messages
[BC] CHANGED: The parameter $newEmail of Kreait\Firebase\Contract\Auth#changeUserEmail() changed from no type to Stringable|string
[BC] CHANGED: The parameter $newEmail of Kreait\Firebase\Contract\Auth#changeUserEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $newPassword of Kreait\Firebase\Contract\Auth#changeUserPassword() changed from no type to Stringable|string
[BC] CHANGED: The parameter $newPassword of Kreait\Firebase\Contract\Auth#changeUserPassword() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $newPassword of Kreait\Firebase\Contract\Auth#confirmPasswordReset() changed from no type to Stringable|string
[BC] CHANGED: The parameter $newPassword of Kreait\Firebase\Contract\Auth#confirmPasswordReset() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $notification of Kreait\Firebase\Messaging\CloudMessage#withNotification() changed from no type to a non-contravariant Kreait\Firebase\Messaging\Notification|array
[BC] CHANGED: The parameter $password of Kreait\Firebase\Contract\Auth#createUserWithEmailAndPassword() changed from no type to Stringable|string
[BC] CHANGED: The parameter $password of Kreait\Firebase\Contract\Auth#createUserWithEmailAndPassword() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $phoneNumber of Kreait\Firebase\Contract\Auth#getUserByPhoneNumber() changed from no type to Stringable|string
[BC] CHANGED: The parameter $phoneNumber of Kreait\Firebase\Contract\Auth#getUserByPhoneNumber() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $properties of Kreait\Firebase\Contract\Auth#createUser() changed from no type to a non-contravariant array|Kreait\Firebase\Request\CreateUser
[BC] CHANGED: The parameter $properties of Kreait\Firebase\Contract\Auth#createUser() changed from no type to array|Kreait\Firebase\Request\CreateUser
[BC] CHANGED: The parameter $properties of Kreait\Firebase\Contract\Auth#updateUser() changed from no type to a non-contravariant array|Kreait\Firebase\Request\UpdateUser
[BC] CHANGED: The parameter $properties of Kreait\Firebase\Contract\Auth#updateUser() changed from no type to array|Kreait\Firebase\Request\UpdateUser
[BC] CHANGED: The parameter $provider of Kreait\Firebase\Contract\Auth#signInWithIdpAccessToken() changed from no type to Stringable|string
[BC] CHANGED: The parameter $provider of Kreait\Firebase\Contract\Auth#signInWithIdpAccessToken() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $provider of Kreait\Firebase\Contract\Auth#signInWithIdpIdToken() changed from no type to Stringable|string
[BC] CHANGED: The parameter $provider of Kreait\Firebase\Contract\Auth#signInWithIdpIdToken() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $provider of Kreait\Firebase\Contract\Auth#unlinkProvider() changed from no type to a non-contravariant array|Stringable|string
[BC] CHANGED: The parameter $provider of Kreait\Firebase\Contract\Auth#unlinkProvider() changed from no type to array|Stringable|string
[BC] CHANGED: The parameter $query of Kreait\Firebase\Contract\Auth#queryUsers() changed from no type to Kreait\Firebase\Auth\UserQuery|array
[BC] CHANGED: The parameter $query of Kreait\Firebase\Contract\Auth#queryUsers() changed from no type to a non-contravariant Kreait\Firebase\Auth\UserQuery|array
[BC] CHANGED: The parameter $registrationToken of Kreait\Firebase\Contract\Messaging#getAppInstance() changed from no type to Kreait\Firebase\Messaging\RegistrationToken|string
[BC] CHANGED: The parameter $registrationToken of Kreait\Firebase\Contract\Messaging#getAppInstance() changed from no type to a non-contravariant Kreait\Firebase\Messaging\RegistrationToken|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#subscribeToTopic() changed from no type to Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#subscribeToTopic() changed from no type to a non-contravariant Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#subscribeToTopics() changed from no type to Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#subscribeToTopics() changed from no type to a non-contravariant Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#unsubscribeFromAllTopics() changed from no type to Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#unsubscribeFromAllTopics() changed from no type to a non-contravariant Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#unsubscribeFromTopic() changed from no type to Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#unsubscribeFromTopic() changed from no type to a non-contravariant Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#unsubscribeFromTopics() changed from no type to Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#unsubscribeFromTopics() changed from no type to a non-contravariant Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#validateRegistrationTokens() changed from no type to Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokenOrTokens of Kreait\Firebase\Contract\Messaging#validateRegistrationTokens() changed from no type to a non-contravariant Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokens of Kreait\Firebase\Contract\Messaging#sendMulticast() changed from no type to Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $registrationTokens of Kreait\Firebase\Contract\Messaging#sendMulticast() changed from no type to a non-contravariant Kreait\Firebase\Messaging\RegistrationTokens|Kreait\Firebase\Messaging\RegistrationToken|array|string
[BC] CHANGED: The parameter $token of Kreait\Firebase\Contract\Auth#signInWithCustomToken() changed from no type to Lcobucci\JWT\Token|string
[BC] CHANGED: The parameter $token of Kreait\Firebase\Contract\Auth#signInWithCustomToken() changed from no type to a non-contravariant Lcobucci\JWT\Token|string
[BC] CHANGED: The parameter $topic of Kreait\Firebase\Contract\Messaging#subscribeToTopic() changed from no type to a non-contravariant string|Kreait\Firebase\Messaging\Topic
[BC] CHANGED: The parameter $topic of Kreait\Firebase\Contract\Messaging#subscribeToTopic() changed from no type to string|Kreait\Firebase\Messaging\Topic
[BC] CHANGED: The parameter $topic of Kreait\Firebase\Contract\Messaging#unsubscribeFromTopic() changed from no type to a non-contravariant string|Kreait\Firebase\Messaging\Topic
[BC] CHANGED: The parameter $topic of Kreait\Firebase\Contract\Messaging#unsubscribeFromTopic() changed from no type to string|Kreait\Firebase\Messaging\Topic
[BC] CHANGED: The parameter $topic of Kreait\Firebase\Messaging\AppInstance#isSubscribedToTopic() changed from no type to a non-contravariant Kreait\Firebase\Messaging\Topic|string
[BC] CHANGED: The parameter $ttl of Kreait\Firebase\Contract\Auth#createCustomToken() changed from no type to a non-contravariant int|DateInterval|string
[BC] CHANGED: The parameter $ttl of Kreait\Firebase\Contract\Auth#createCustomToken() changed from no type to int|DateInterval|string
[BC] CHANGED: The parameter $ttl of Kreait\Firebase\Contract\Auth#createSessionCookie() changed from no type to DateInterval|int
[BC] CHANGED: The parameter $ttl of Kreait\Firebase\Contract\Auth#createSessionCookie() changed from no type to a non-contravariant DateInterval|int
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#changeUserEmail() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#changeUserEmail() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#changeUserPassword() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#changeUserPassword() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#createCustomToken() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#createCustomToken() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#deleteUser() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#deleteUser() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#disableUser() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#disableUser() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#enableUser() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#enableUser() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#getUser() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#getUser() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#revokeRefreshTokens() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#revokeRefreshTokens() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#setCustomUserClaims() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#setCustomUserClaims() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#unlinkProvider() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#unlinkProvider() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#updateUser() changed from no type to Stringable|string
[BC] CHANGED: The parameter $uid of Kreait\Firebase\Contract\Auth#updateUser() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $uri of Kreait\Firebase\Factory#withDatabaseUri() changed from no type to a non-contravariant Psr\Http\Message\UriInterface|string
[BC] CHANGED: The parameter $url of Kreait\Firebase\Contract\DynamicLinks#createShortLink() changed from no type to Stringable|string|Kreait\Firebase\DynamicLink\CreateDynamicLink|array
[BC] CHANGED: The parameter $url of Kreait\Firebase\Contract\DynamicLinks#createShortLink() changed from no type to a non-contravariant Stringable|string|Kreait\Firebase\DynamicLink\CreateDynamicLink|array
[BC] CHANGED: The parameter $url of Kreait\Firebase\Contract\DynamicLinks#createUnguessableLink() changed from no type to Stringable|string|Kreait\Firebase\DynamicLink\CreateDynamicLink|array
[BC] CHANGED: The parameter $url of Kreait\Firebase\Contract\DynamicLinks#createUnguessableLink() changed from no type to a non-contravariant Stringable|string|Kreait\Firebase\DynamicLink\CreateDynamicLink|array
[BC] CHANGED: The parameter $url of Kreait\Firebase\DynamicLink\CreateDynamicLink::forUrl() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $url of Kreait\Firebase\DynamicLink\ShortenLongDynamicLink::forLongDynamicLink() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $url of Kreait\Firebase\Request\EditUserTrait#withPhotoUrl() changed from no type to Stringable|string
[BC] CHANGED: The parameter $url of Kreait\Firebase\Request\EditUserTrait#withPhotoUrl() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $url of Kreait\Firebase\Request\EditUserTrait#withPhotoUrl() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $url of Kreait\Firebase\Request\EditUserTrait#withPhotoUrl() changed from no type to a non-contravariant Stringable|string
[BC] CHANGED: The parameter $user of Kreait\Firebase\Contract\Auth#signInAsUser() changed from no type to Kreait\Firebase\Auth\UserRecord|Stringable|string
[BC] CHANGED: The parameter $user of Kreait\Firebase\Contract\Auth#signInAsUser() changed from no type to a non-contravariant Kreait\Firebase\Auth\UserRecord|Stringable|string
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Query\Filter\EndAt#__construct() changed from no type to a non-contravariant bool|float|int|string
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Query\Filter\EndBefore#__construct() changed from no type to a non-contravariant int|float|string|bool
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Query\Filter\EqualTo#__construct() changed from no type to a non-contravariant bool|float|int|string
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Query\Filter\StartAfter#__construct() changed from no type to a non-contravariant int|float|string|bool
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Query\Filter\StartAt#__construct() changed from no type to a non-contravariant int|float|string|bool
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Query\ModifierTrait#appendQueryParam() changed from no type to mixed
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#endAt() changed from no type to a non-contravariant bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#endAt() changed from no type to bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#endBefore() changed from no type to a non-contravariant bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#endBefore() changed from no type to bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#equalTo() changed from no type to a non-contravariant bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#equalTo() changed from no type to bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#set() changed from no type to mixed
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#startAfter() changed from no type to a non-contravariant bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#startAfter() changed from no type to bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#startAt() changed from no type to a non-contravariant bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Reference#startAt() changed from no type to bool|string|int|float
[BC] CHANGED: The parameter $value of Kreait\Firebase\Database\Transaction#set() changed from no type to mixed
[BC] CHANGED: The parameter $value of Kreait\Firebase\Factory#withServiceAccount() changed from no type to a non-contravariant string|array
[BC] CHANGED: The parameter $versionNumber of Kreait\Firebase\Contract\RemoteConfig#getVersion() changed from no type to Kreait\Firebase\RemoteConfig\VersionNumber|int|string
[BC] CHANGED: The parameter $versionNumber of Kreait\Firebase\Contract\RemoteConfig#getVersion() changed from no type to a non-contravariant Kreait\Firebase\RemoteConfig\VersionNumber|int|string
[BC] CHANGED: The parameter $versionNumber of Kreait\Firebase\Contract\RemoteConfig#rollbackToVersion() changed from no type to Kreait\Firebase\RemoteConfig\VersionNumber|int|string
[BC] CHANGED: The parameter $versionNumber of Kreait\Firebase\Contract\RemoteConfig#rollbackToVersion() changed from no type to a non-contravariant Kreait\Firebase\RemoteConfig\VersionNumber|int|string
[BC] CHANGED: The return type of Kreait\Firebase\Contract\Database#runTransaction() changed from no type to mixed
[BC] CHANGED: The return type of Kreait\Firebase\Database\Query#getValue() changed from no type to mixed
[BC] CHANGED: The return type of Kreait\Firebase\Database\Query\Modifier#modifyValue() changed from no type to mixed
[BC] CHANGED: The return type of Kreait\Firebase\Database\Query\Modifier#modifyValue() changed from no type to mixed
[BC] CHANGED: The return type of Kreait\Firebase\Database\Query\Modifier#modifyValue() changed from no type to mixed
[BC] CHANGED: The return type of Kreait\Firebase\Database\Query\ModifierTrait#modifyValue() changed from no type to mixed
[BC] CHANGED: The return type of Kreait\Firebase\Database\Reference#getValue() changed from no type to mixed
[BC] CHANGED: The return type of Kreait\Firebase\Database\Snapshot#getValue() changed from no type to mixed
[BC] CHANGED: The return type of Kreait\Firebase\DynamicLink#previewUri() changed from Psr\Http\Message\UriInterface to the non-covariant Psr\Http\Message\UriInterface|null
[BC] CHANGED: Type of property Kreait\Firebase\Auth\UserInfo#$providerId changed from string|null to string
[BC] CHANGED: Type of property Kreait\Firebase\Auth\UserInfo#$uid changed from string|null to string
[BC] CHANGED: Type of property Kreait\Firebase\Auth\UserMetaData#$createdAt changed from DateTimeImmutable|null to DateTimeImmutable
[BC] REMOVED: Class Kreait\Firebase\Auth\CreateActionLink\ApiRequest has been deleted
[BC] REMOVED: Class Kreait\Firebase\Auth\CreateSessionCookie\ApiRequest has been deleted
[BC] REMOVED: Class Kreait\Firebase\Auth\SendActionLink\ApiRequest has been deleted
[BC] REMOVED: Class Kreait\Firebase\Exception\ServiceAccountDiscoveryFailed has been deleted
[BC] REMOVED: Constant Kreait\Firebase\RemoteConfig\DefaultValue::IN_APP_DEFAULT_VALUE was removed
[BC] REMOVED: Method Kreait\Firebase\Auth\DeleteUsersResult::fromRequestAndResponse() was removed
[BC] REMOVED: Method Kreait\Firebase\Auth\UserInfo#jsonSerialize() was removed
[BC] REMOVED: Method Kreait\Firebase\Auth\UserInfo::fromResponseData() was removed
[BC] REMOVED: Method Kreait\Firebase\Auth\UserMetaData#jsonSerialize() was removed
[BC] REMOVED: Method Kreait\Firebase\Auth\UserMetaData::fromResponseData() was removed
[BC] REMOVED: Method Kreait\Firebase\Auth\UserRecord#__get() was removed
[BC] REMOVED: Method Kreait\Firebase\Auth\UserRecord#jsonSerialize() was removed
[BC] REMOVED: Method Kreait\Firebase\Auth\UserRecord::fromResponseData() was removed
[BC] REMOVED: Method Kreait\Firebase\Factory#withClientEmail() was removed
[BC] REMOVED: Method Kreait\Firebase\Factory#withDisabledAutoDiscovery() was removed
[BC] REMOVED: Method Kreait\Firebase\Messaging\AndroidConfig#withHighPriority() was removed
[BC] REMOVED: Method Kreait\Firebase\Messaging\AndroidConfig#withNormalPriority() was removed
[BC] REMOVED: Method Kreait\Firebase\Messaging\AndroidConfig#withPriority() was removed
[BC] REMOVED: Method Kreait\Firebase\Messaging\RegistrationTokens#__construct() was removed
[BC] REMOVED: Method Kreait\Firebase\RemoteConfig\DefaultValue#value() was removed
[BC] REMOVED: Method Kreait\Firebase\RemoteConfig\DefaultValue::none() was removed
[BC] REMOVED: Property Kreait\Firebase\Auth\UserInfo#$screenName was removed
[BC] REMOVED: These ancestors of Kreait\Firebase\Auth\UserInfo have been removed: ["JsonSerializable"]
[BC] REMOVED: These ancestors of Kreait\Firebase\Auth\UserMetaData have been removed: ["JsonSerializable"]
[BC] REMOVED: These ancestors of Kreait\Firebase\Auth\UserRecord have been removed: ["JsonSerializable"]
```

159
vendor/kreait/firebase-php/composer.json vendored Normal file
View File

@@ -0,0 +1,159 @@
{
"name": "kreait/firebase-php",
"description": "Firebase Admin SDK",
"keywords": ["firebase", "google", "sdk", "api", "database"],
"type": "library",
"homepage": "https://github.com/kreait/firebase-php",
"license": "MIT",
"authors": [
{
"name": "Jérôme Gamez",
"homepage": "https://github.com/jeromegamez"
}
],
"support": {
"docs": "https://firebase-php.readthedocs.io",
"issues": "https://github.com/kreait/firebase-php/issues",
"source": "https://github.com/kreait/firebase-php"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jeromegamez"
}
],
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"ext-ctype": "*",
"ext-filter": "*",
"ext-json": "*",
"ext-mbstring": "*",
"beste/clock": "^3.0",
"beste/in-memory-cache": "^1.3.1",
"beste/json": "^1.5.1",
"cuyz/valinor": "^2.2.1",
"fig/http-message-util": "^1.1.5",
"firebase/php-jwt": "^6.10.2",
"google/auth": "^1.45",
"google/cloud-storage": "^1.48.7",
"guzzlehttp/guzzle": "^7.9.2",
"guzzlehttp/promises": "^2.0.4",
"guzzlehttp/psr7": "^2.7",
"kreait/firebase-tokens": "^5.2",
"lcobucci/jwt": "^5.3",
"mtdowling/jmespath.php": "^2.8.0",
"psr/cache": "^2.0 || ^3.0",
"psr/clock": "^1.0",
"psr/http-client": "^1.0.3",
"psr/http-factory": "^1.1",
"psr/http-message": "^1.1 || ^2.0",
"psr/log": "^3.0.2"
},
"require-dev": {
"php-cs-fixer/shim": "^3.88.2",
"google/cloud-firestore": "^1.54.2",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.31",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-strict-rules": "^2.0.7",
"phpstan/phpstan-phpunit": "^2.0.7",
"phpunit/phpunit": "^10.5.58",
"rector/rector": "^2.2.2",
"shipmonk/composer-dependency-analyser": "^1.8.3",
"symfony/var-dumper": "^6.4.15 || ^7.3.4",
"vlucas/phpdotenv": "^5.6.2"
},
"suggest": {
"google/cloud-firestore": "^1.0 to use the Firestore component"
},
"autoload": {
"psr-4": {
"Kreait\\Firebase\\": "src/Firebase"
}
},
"autoload-dev": {
"psr-4": {
"Kreait\\Firebase\\Tests\\": "tests"
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
"phpstan/extension-installer": true
}
},
"scripts": {
"analyse": [
"XDEBUG_MODE=off vendor/bin/phpstan"
],
"analyze": "@analyse",
"cs": [
"vendor/bin/php-cs-fixer fix --diff --verbose"
],
"docs": [
"@docs:clean",
"@docs:setup",
"@docs:build"
],
"docs:build": [
"cd docs && uv run sphinx-build -b html . _build/html"
],
"docs:clean": [
"cd docs && rm -rf _build/*"
],
"docs:linkcheck": [
"cd docs && uv run sphinx-build -b linkcheck . _build/linkcheck"
],
"docs:serve": [
"cd docs && uv run sphinx-autobuild . _build/html --host 0.0.0.0 --port 8000"
],
"docs:setup": [
"cd docs && uv sync"
],
"pre-push": [
"@rector-fix",
"@test",
"@test-bc"
],
"rector": [
"vendor/bin/rector --dry-run"
],
"rector-fix": [
"vendor/bin/rector",
"@cs"
],
"reset-project": [
"tests/bin/reset-project"
],
"test": [
"Composer\\Config::disableProcessTimeout",
"@analyze",
"@test-dependencies",
"vendor/bin/phpunit --stop-on-error --stop-on-failure"
],
"test-bc": [
"docker run -it --rm -v `pwd`:/app nyholm/roave-bc-check"
],
"test-coverage": [
"Composer\\Config::disableProcessTimeout",
"XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=.build/coverage"
],
"test-dependencies": [
"vendor/bin/composer-dependency-analyser"
],
"test-emulator": [
"FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 FIREBASE_DATABASE_EMULATOR_HOST=localhost:9100 firebase emulators:exec --only auth,database --project beste-firebase 'vendor/bin/phpunit --group=emulator'"
],
"test-integration": [
"vendor/bin/phpunit --testsuite=integration"
],
"test-units": [
"vendor/bin/phpunit --testsuite=unit"
]
},
"extra": {
"branch-alias": {
"dev-7.x": "7.x-dev"
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase;
use Kreait\Firebase\AppCheck\ApiClient;
use Kreait\Firebase\AppCheck\AppCheckToken;
use Kreait\Firebase\AppCheck\AppCheckTokenGenerator;
use Kreait\Firebase\AppCheck\AppCheckTokenOptions;
use Kreait\Firebase\AppCheck\AppCheckTokenVerifier;
use Kreait\Firebase\AppCheck\VerifyAppCheckTokenResponse;
use function is_array;
/**
* @internal
*/
final class AppCheck implements Contract\AppCheck
{
public function __construct(
private readonly ApiClient $client,
private readonly AppCheckTokenGenerator $tokenGenerator,
private readonly AppCheckTokenVerifier $tokenVerifier,
) {
}
public function createToken(string $appId, $options = null): AppCheckToken
{
if (is_array($options)) {
$options = AppCheckTokenOptions::fromArray($options);
}
$customToken = $this->tokenGenerator->createCustomToken($appId, $options);
$result = $this->client->exchangeCustomToken($appId, $customToken);
return AppCheckToken::fromArray($result);
}
public function verifyToken(string $appCheckToken): VerifyAppCheckTokenResponse
{
$decodedToken = $this->tokenVerifier->verifyToken($appCheckToken);
return new VerifyAppCheckTokenResponse($decodedToken->app_id, $decodedToken);
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\AppCheck;
use Beste\Json;
use GuzzleHttp\ClientInterface;
use Kreait\Firebase\Exception\AppCheckApiExceptionConverter;
use Kreait\Firebase\Exception\AppCheckException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Throwable;
/**
* @internal
*
* @phpstan-import-type AppCheckTokenShape from AppCheckToken
*/
final class ApiClient
{
public function __construct(
private readonly ClientInterface $client,
private readonly AppCheckApiExceptionConverter $errorHandler,
) {
}
/**
* @throws AppCheckException
*
* @return AppCheckTokenShape
*/
public function exchangeCustomToken(string $appId, string $customToken): array
{
$response = $this->requestApi('POST', 'apps/'.$appId.':exchangeCustomToken', [
'headers' => [
'Content-Type' => 'application/json; UTF-8',
],
'body' => Json::encode([
'customToken' => $customToken,
]),
]);
/** @var AppCheckTokenShape $decoded */
$decoded = Json::decode((string) $response->getBody(), true);
return $decoded;
}
/**
* @param non-empty-string $method
* @param array<string, mixed>|null $options
* @throws AppCheckException
*/
private function requestApi(string $method, string|UriInterface $uri, ?array $options = null): ResponseInterface
{
$options ??= [];
try {
return $this->client->request($method, $uri, $options);
} catch (Throwable $e) {
throw $this->errorHandler->convertException($e);
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\AppCheck;
/**
* @phpstan-type AppCheckTokenShape array{
* token: string,
* ttl: string
* }
*/
final class AppCheckToken
{
private function __construct(
public readonly string $token,
public readonly string $ttl,
) {
}
/**
* @param AppCheckTokenShape $data
*/
public static function fromArray(array $data): self
{
return new self($data['token'], $data['ttl']);
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\AppCheck;
use Beste\Clock\SystemClock;
use Firebase\JWT\JWT;
use Kreait\Firebase\Exception\AppCheck\InvalidAppCheckTokenOptions;
use Psr\Clock\ClockInterface;
/**
* @internal
*
* @todo Add #[SensitiveParameter] attribute to the private key once the minimum required PHP version is >=8.2
*/
final class AppCheckTokenGenerator
{
private const APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService';
private readonly ClockInterface $clock;
/**
* @param non-empty-string $clientEmail
* @param non-empty-string $privateKey
*/
public function __construct(
private readonly string $clientEmail,
private readonly string $privateKey,
?ClockInterface $clock = null,
) {
$this->clock = $clock ?? SystemClock::create();
}
/**
* @param non-empty-string $appId the Application ID to use for the generated token
*
* @throws InvalidAppCheckTokenOptions
*
* @return string the generated token
*/
public function createCustomToken(string $appId, ?AppCheckTokenOptions $options = null): string
{
$now = $this->clock->now()->getTimestamp();
$payload = [
'iss' => $this->clientEmail,
'sub' => $this->clientEmail,
'app_id' => $appId,
'aud' => self::APP_CHECK_AUDIENCE,
'iat' => $now,
'exp' => $now + 300,
];
if ($options?->ttl !== null) {
$payload['ttl'] = $options->ttl.'s';
}
return JWT::encode($payload, $this->privateKey, 'RS256');
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\AppCheck;
use Kreait\Firebase\Exception\AppCheck\InvalidAppCheckTokenOptions;
/**
* @phpstan-type AppCheckTokenOptionsShape array{
* ttl?: int|null,
* }
*/
final class AppCheckTokenOptions
{
private function __construct(
public readonly ?int $ttl = null,
) {
}
/**
* @param AppCheckTokenOptionsShape $data
*
* @throws InvalidAppCheckTokenOptions
*/
public static function fromArray(array $data): self
{
$ttl = $data['ttl'] ?? null;
if (null === $ttl) {
return new self();
}
if ($ttl < 1800 || $ttl > 604800) {
throw new InvalidAppCheckTokenOptions('The ttl must be a duration between 30 minutes and 7 days.');
}
return new self($ttl);
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\AppCheck;
use Firebase\JWT\CachedKeySet;
use Firebase\JWT\JWT;
use Kreait\Firebase\Exception\AppCheck\FailedToVerifyAppCheckToken;
use Kreait\Firebase\Exception\AppCheck\InvalidAppCheckToken;
use LogicException;
use Throwable;
use function in_array;
use function str_starts_with;
/**
* @internal
*
* @phpstan-import-type DecodedAppCheckTokenShape from DecodedAppCheckToken
*/
final class AppCheckTokenVerifier
{
private const APP_CHECK_ISSUER_PREFIX = 'https://firebaseappcheck.googleapis.com/';
/**
* @param non-empty-string $projectId
*/
public function __construct(
private readonly string $projectId,
private readonly CachedKeySet $keySet,
) {
}
/**
* Verifies the format and signature of a Firebase App Check token.
*
* @param string $token the Firebase Auth JWT token to verify
*
* @throws FailedToVerifyAppCheckToken if the token could not be verified
* @throws InvalidAppCheckToken if the token is invalid
*/
public function verifyToken(string $token): DecodedAppCheckToken
{
$decodedToken = $this->decodeJwt($token);
$this->verifyContent($decodedToken);
return $decodedToken;
}
/**
* @param string $token the Firebase App Check JWT token to decode
*
* @throws FailedToVerifyAppCheckToken if the token could not be verified
* @throws InvalidAppCheckToken if the token is invalid
*/
private function decodeJwt(string $token): DecodedAppCheckToken
{
try {
/** @var DecodedAppCheckTokenShape $payload */
$payload = (array) JWT::decode($token, $this->keySet);
} catch (LogicException $e) {
throw new InvalidAppCheckToken($e->getMessage(), $e->getCode(), $e);
} catch (Throwable $e) {
throw new FailedToVerifyAppCheckToken($e->getMessage(), $e->getCode(), $e);
}
return DecodedAppCheckToken::fromArray($payload);
}
/**
* Verifies the content of a Firebase App Check JWT.
*
* @param DecodedAppCheckToken $token the decoded Firebase App Check token to verify
*
* @throws FailedToVerifyAppCheckToken if the token could not be verified
*/
private function verifyContent(DecodedAppCheckToken $token): void
{
if (!in_array('projects/'.$this->projectId, $token->aud, true)) {
throw new FailedToVerifyAppCheckToken('The "aud" claim must include the project ID.');
}
if (!str_starts_with($token->iss, self::APP_CHECK_ISSUER_PREFIX)) {
throw new FailedToVerifyAppCheckToken('The provided App Check token has incorrect "iss" (issuer) claim.');
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\AppCheck;
/**
* @phpstan-type DecodedAppCheckTokenShape array{
* app_id?: non-empty-string,
* aud: array<string>,
* exp: int,
* iat: int,
* iss: non-empty-string,
* sub: non-empty-string,
* }
*/
final class DecodedAppCheckToken
{
/**
* @param non-empty-string $app_id
* @param array<string> $aud
* @param non-empty-string $sub
*/
private function __construct(
public readonly string $app_id,
public readonly array $aud,
public readonly int $exp,
public readonly int $iat,
public readonly string $iss,
public readonly string $sub,
) {
}
/**
* @param DecodedAppCheckTokenShape $data
*/
public static function fromArray(array $data): self
{
return new self(
$data['app_id'] ?? $data['sub'],
$data['aud'],
$data['exp'],
$data['iat'],
$data['iss'],
$data['sub'],
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\AppCheck;
/**
* @phpstan-import-type DecodedAppCheckTokenShape from DecodedAppCheckToken
*
* @phpstan-type VerifyAppCheckTokenResponseShape array{
* appId: non-empty-string,
* token: DecodedAppCheckTokenShape,
* }
*/
final class VerifyAppCheckTokenResponse
{
/**
* @param non-empty-string $appId
*/
public function __construct(
public readonly string $appId,
public readonly DecodedAppCheckToken $token,
) {
}
}

View File

@@ -0,0 +1,709 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase;
use Beste\Json;
use DateInterval;
use DateTimeImmutable;
use Kreait\Firebase\Auth\ActionCodeSettings;
use Kreait\Firebase\Auth\ActionCodeSettings\ValidatedActionCodeSettings;
use Kreait\Firebase\Auth\ApiClient;
use Kreait\Firebase\Auth\CustomTokenViaGoogleCredentials;
use Kreait\Firebase\Auth\DeleteUsersRequest;
use Kreait\Firebase\Auth\DeleteUsersResult;
use Kreait\Firebase\Auth\SendActionLink\FailedToSendActionLink;
use Kreait\Firebase\Auth\SignIn\FailedToSignIn;
use Kreait\Firebase\Auth\SignInAnonymously;
use Kreait\Firebase\Auth\SignInResult;
use Kreait\Firebase\Auth\SignInWithCustomToken;
use Kreait\Firebase\Auth\SignInWithEmailAndOobCode;
use Kreait\Firebase\Auth\SignInWithEmailAndPassword;
use Kreait\Firebase\Auth\SignInWithIdpCredentials;
use Kreait\Firebase\Auth\SignInWithRefreshToken;
use Kreait\Firebase\Auth\UserQuery;
use Kreait\Firebase\Auth\UserRecord;
use Kreait\Firebase\Contract\Transitional\FederatedUserFetcher;
use Kreait\Firebase\Exception\Auth\AuthError;
use Kreait\Firebase\Exception\Auth\FailedToVerifySessionCookie;
use Kreait\Firebase\Exception\Auth\FailedToVerifyToken;
use Kreait\Firebase\Exception\Auth\RevokedIdToken;
use Kreait\Firebase\Exception\Auth\RevokedSessionCookie;
use Kreait\Firebase\Exception\Auth\UserNotFound;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Kreait\Firebase\JWT\IdTokenVerifier;
use Kreait\Firebase\JWT\SessionCookieVerifier;
use Kreait\Firebase\JWT\Token\Parser;
use Kreait\Firebase\Request\CreateUser;
use Kreait\Firebase\Request\UpdateUser;
use Kreait\Firebase\Util\DT;
use Kreait\Firebase\Value\ClearTextPassword;
use Kreait\Firebase\Value\Email;
use Kreait\Firebase\Value\Uid;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\UnencryptedToken;
use Psr\Clock\ClockInterface;
use Psr\Http\Message\ResponseInterface;
use Stringable;
use Throwable;
use Traversable;
use function array_fill_keys;
use function array_map;
use function assert;
use function is_string;
use function mb_strtolower;
use function trim;
/**
* @internal
*
* @phpstan-import-type UserRecordResponseShape from UserRecord
*/
final class Auth implements Contract\Auth, FederatedUserFetcher
{
private readonly Parser $jwtParser;
public function __construct(
private readonly ApiClient $client,
private readonly ?CustomTokenViaGoogleCredentials $tokenGenerator,
private readonly IdTokenVerifier $idTokenVerifier,
private readonly SessionCookieVerifier $sessionCookieVerifier,
private readonly ClockInterface $clock,
) {
$this->jwtParser = new Parser(new JoseEncoder());
}
public function getUser(Stringable|string $uid): UserRecord
{
$uid = Uid::fromString($uid)->value;
$userRecord = $this->getUsers([$uid])[$uid] ?? null;
if ($userRecord !== null) {
return $userRecord;
}
throw new UserNotFound("No user with uid '{$uid}' found.");
}
public function getUsers(array $uids): array
{
$uids = array_map(static fn($uid): string => Uid::fromString($uid)->value, $uids);
$users = array_fill_keys($uids, null);
$response = $this->client->getAccountInfo($uids);
$data = Json::decode((string) $response->getBody(), true);
foreach ($data['users'] ?? [] as $userData) {
$userRecord = UserRecord::fromResponseData($userData);
$users[$userRecord->uid] = $userRecord;
}
return $users;
}
public function queryUsers(UserQuery|array $query): array
{
$userQuery = $query instanceof UserQuery ? $query : UserQuery::fromArray($query);
$response = $this->client->queryUsers($userQuery);
$data = Json::decode((string) $response->getBody(), true);
$users = [];
foreach ($data['userInfo'] ?? [] as $userData) {
$userRecord = UserRecord::fromResponseData($userData);
$users[$userRecord->uid] = $userRecord;
}
return $users;
}
public function listUsers(int $maxResults = 1000, int $batchSize = 1000): Traversable
{
$pageToken = null;
$count = 0;
if ($batchSize > $maxResults) {
$batchSize = $maxResults;
}
do {
$response = $this->client->downloadAccount($batchSize, $pageToken);
$result = Json::decode((string) $response->getBody(), true);
foreach ((array) ($result['users'] ?? []) as $userData) {
yield UserRecord::fromResponseData($userData);
if (++$count === $maxResults) {
return;
}
}
$pageToken = $result['nextPageToken'] ?? null;
} while ($pageToken !== null);
}
public function createUser(array|CreateUser $properties): UserRecord
{
$request = $properties instanceof CreateUser
? $properties
: CreateUser::withProperties($properties);
$response = $this->client->createUser($request);
return $this->getUserRecordFromResponseAfterUserUpdate($response);
}
public function updateUser(Stringable|string $uid, array|UpdateUser $properties): UserRecord
{
$request = $properties instanceof UpdateUser
? $properties
: UpdateUser::withProperties($properties);
$request = $request->withUid($uid);
$response = $this->client->updateUser($request);
return $this->getUserRecordFromResponseAfterUserUpdate($response);
}
public function createUserWithEmailAndPassword(Stringable|string $email, Stringable|string $password): UserRecord
{
return $this->createUser(
CreateUser::new()
->withUnverifiedEmail($email)
->withClearTextPassword($password),
);
}
public function getUserByEmail(Stringable|string $email): UserRecord
{
$email = Email::fromString((string) $email)->value;
$response = $this->client->getUserByEmail($email);
$userRecord = self::getFirstUserRecordFromUserListResponse($response);
if ($userRecord === null) {
throw new UserNotFound("No user with email '{$email}' found.");
}
return $userRecord;
}
public function getUserByPhoneNumber(Stringable|string $phoneNumber): UserRecord
{
$phoneNumber = (string) $phoneNumber;
$response = $this->client->getUserByPhoneNumber($phoneNumber);
$userRecord = self::getFirstUserRecordFromUserListResponse($response);
if ($userRecord === null) {
throw new UserNotFound("No user with phone number '{$phoneNumber}' found.");
}
return $userRecord;
}
public function getUserByProviderUid(Stringable|string $providerId, Stringable|string $providerUid): UserRecord
{
$providerId = (string) $providerId;
$providerUid = (string) $providerUid;
$response = $this->client->getUserByProviderUid($providerId, $providerUid);
$userRecord = self::getFirstUserRecordFromUserListResponse($response);
if ($userRecord === null) {
throw new UserNotFound("No user with federated account ID '{$providerId}:{$providerUid}' found.");
}
return $userRecord;
}
public function createAnonymousUser(): UserRecord
{
return $this->createUser(CreateUser::new());
}
public function changeUserPassword(Stringable|string $uid, Stringable|string $newPassword): UserRecord
{
return $this->updateUser($uid, UpdateUser::new()->withClearTextPassword($newPassword));
}
public function changeUserEmail(Stringable|string $uid, Stringable|string $newEmail): UserRecord
{
return $this->updateUser($uid, UpdateUser::new()->withEmail($newEmail));
}
public function enableUser(Stringable|string $uid): UserRecord
{
return $this->updateUser($uid, UpdateUser::new()->markAsEnabled());
}
public function disableUser(Stringable|string $uid): UserRecord
{
return $this->updateUser($uid, UpdateUser::new()->markAsDisabled());
}
public function deleteUser(Stringable|string $uid): void
{
$uid = Uid::fromString($uid)->value;
try {
$this->client->deleteUser($uid);
} catch (UserNotFound) {
throw new UserNotFound("No user with uid '{$uid}' found.");
}
}
public function deleteUsers(iterable $uids, bool $forceDeleteEnabledUsers = false): DeleteUsersResult
{
$request = DeleteUsersRequest::withUids($uids, $forceDeleteEnabledUsers);
$response = $this->client->deleteUsers(
$request->uids(),
$request->enabledUsersShouldBeForceDeleted(),
);
return DeleteUsersResult::fromRequestAndResponse($request, $response);
}
public function getEmailActionLink(string $type, Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): string
{
$email = Email::fromString((string) $email)->value;
if ($actionCodeSettings === null) {
$actionCodeSettings = ValidatedActionCodeSettings::empty();
} else {
$actionCodeSettings = $actionCodeSettings instanceof ActionCodeSettings
? $actionCodeSettings
: ValidatedActionCodeSettings::fromArray($actionCodeSettings);
}
return $this->client->getEmailActionLink($type, $email, $actionCodeSettings, $locale);
}
public function sendEmailActionLink(string $type, Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): void
{
$email = Email::fromString((string) $email)->value;
if ($actionCodeSettings === null) {
$actionCodeSettings = ValidatedActionCodeSettings::empty();
} else {
$actionCodeSettings = $actionCodeSettings instanceof ActionCodeSettings
? $actionCodeSettings
: ValidatedActionCodeSettings::fromArray($actionCodeSettings);
}
$idToken = null;
if (mb_strtolower($type) === 'verify_email') {
// The Firebase API expects an ID token for the user belonging to this email address
// see https://github.com/firebase/firebase-js-sdk/issues/1958
try {
$user = $this->getUserByEmail($email);
} catch (Throwable $e) {
throw new FailedToSendActionLink($e->getMessage(), $e->getCode(), $e);
}
try {
$signInResult = $this->signInAsUser($user);
} catch (Throwable $e) {
throw new FailedToSendActionLink($e->getMessage(), $e->getCode(), $e);
}
$idToken = $signInResult->idToken();
if ($idToken === null) {
throw new FailedToSendActionLink("Failed to send action link: Unable to retrieve ID token for user assigned to email {$email}");
}
}
$this->client->sendEmailActionLink($type, $email, $actionCodeSettings, $locale, $idToken);
}
public function getEmailVerificationLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): string
{
return $this->getEmailActionLink('VERIFY_EMAIL', $email, $actionCodeSettings, $locale);
}
public function sendEmailVerificationLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): void
{
$this->sendEmailActionLink('VERIFY_EMAIL', $email, $actionCodeSettings, $locale);
}
public function getPasswordResetLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): string
{
return $this->getEmailActionLink('PASSWORD_RESET', $email, $actionCodeSettings, $locale);
}
public function sendPasswordResetLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): void
{
$this->sendEmailActionLink('PASSWORD_RESET', $email, $actionCodeSettings, $locale);
}
public function getSignInWithEmailLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): string
{
return $this->getEmailActionLink('EMAIL_SIGNIN', $email, $actionCodeSettings, $locale);
}
public function sendSignInWithEmailLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): void
{
$this->sendEmailActionLink('EMAIL_SIGNIN', $email, $actionCodeSettings, $locale);
}
public function setCustomUserClaims(Stringable|string $uid, ?array $claims): void
{
$uid = Uid::fromString($uid)->value;
$claims ??= [];
$this->client->setCustomUserClaims($uid, $claims);
}
public function createCustomToken(Stringable|string $uid, array $claims = [], $ttl = 3600): UnencryptedToken
{
if ($this->tokenGenerator === null) {
throw new AuthError('Custom Token Generation is disabled because the current credentials do not permit it');
}
$uid = Uid::fromString($uid)->value;
if (!$ttl instanceof DateInterval) {
$ttl = new DateInterval(sprintf('PT%sS', $ttl));
}
$expiresAt = $this->clock->now()->add($ttl);
$token = $this->tokenGenerator->createCustomToken($uid, $claims, $expiresAt);
assert($token instanceof UnencryptedToken);
return $token;
}
public function parseToken(string $tokenString): UnencryptedToken
{
try {
$parsedToken = $this->jwtParser->parse($tokenString);
assert($parsedToken instanceof UnencryptedToken);
} catch (Throwable $e) {
throw new InvalidArgumentException('The given token could not be parsed: '.$e->getMessage());
}
return $parsedToken;
}
public function verifyIdToken($idToken, bool $checkIfRevoked = false, ?int $leewayInSeconds = null): UnencryptedToken
{
$verifier = $this->idTokenVerifier;
$idTokenString = is_string($idToken) ? $idToken : $idToken->toString();
try {
if ($leewayInSeconds !== null) {
$verifier->verifyIdTokenWithLeeway($idTokenString, $leewayInSeconds);
} else {
$verifier->verifyIdToken($idTokenString);
}
} catch (Throwable $e) {
throw new FailedToVerifyToken($e->getMessage());
}
$verifiedToken = $this->parseToken($idTokenString);
if (!$checkIfRevoked) {
return $verifiedToken;
}
$userId = $verifiedToken->claims()->get('sub');
assert(is_string($userId) && $userId !== ''); // It's safe to assume that the 'sub' claim is always a string
try {
$user = $this->getUser($userId);
} catch (Throwable $e) {
throw new FailedToVerifyToken("Error while getting the token's user: {$e->getMessage()}", 0, $e);
}
if ($this->userSessionHasBeenRevoked($verifiedToken, $user, $leewayInSeconds)) {
throw new RevokedIdToken($verifiedToken);
}
return $verifiedToken;
}
public function verifySessionCookie(string $sessionCookie, bool $checkIfRevoked = false, ?int $leewayInSeconds = null): UnencryptedToken
{
$verifier = $this->sessionCookieVerifier;
try {
if ($leewayInSeconds !== null) {
$verifier->verifySessionCookieWithLeeway($sessionCookie, $leewayInSeconds);
} else {
$verifier->verifySessionCookie($sessionCookie);
}
} catch (Throwable $e) {
throw new FailedToVerifySessionCookie($e->getMessage());
}
$verifiedSessionCookie = $this->parseToken($sessionCookie);
if (!$checkIfRevoked) {
return $verifiedSessionCookie;
}
$userId = $verifiedSessionCookie->claims()->get('sub');
assert(is_string($userId) && $userId !== ''); // It's safe to assume that the 'sub' claim is always a string
try {
$user = $this->getUser($userId);
} catch (Throwable $e) {
throw new FailedToVerifySessionCookie("Error while getting the session cookie's user: {$e->getMessage()}", 0, $e);
}
if ($this->userSessionHasBeenRevoked($verifiedSessionCookie, $user, $leewayInSeconds)) {
throw new RevokedSessionCookie($verifiedSessionCookie);
}
return $verifiedSessionCookie;
}
public function verifyPasswordResetCode(string $oobCode): string
{
$response = $this->client->verifyPasswordResetCode($oobCode);
$responseData = Json::decode((string) $response->getBody(), true);
if (!array_key_exists('email', $responseData) || $responseData['email'] === '') {
throw new AuthError('Expected API response to contain a field "email" being a non-empty string, got: '.gettype($responseData));
}
return $responseData['email'];
}
public function confirmPasswordReset(string $oobCode, $newPassword, bool $invalidatePreviousSessions = true): string
{
$newPassword = ClearTextPassword::fromString($newPassword)->value;
$response = $this->client->confirmPasswordReset($oobCode, $newPassword);
$responseData = Json::decode((string) $response->getBody(), true);
if (!array_key_exists('email', $responseData) || $responseData['email'] === '') {
throw new AuthError('Expected API response to contain a field "email" being a non-empty string, got: '.gettype($responseData));
}
$email = $responseData['email'];
if ($invalidatePreviousSessions) {
$this->revokeRefreshTokens($this->getUserByEmail($email)->uid);
}
return $email;
}
public function revokeRefreshTokens(Stringable|string $uid): void
{
$uid = Uid::fromString($uid)->value;
$this->client->revokeRefreshTokens($uid);
}
public function unlinkProvider($uid, $provider): UserRecord
{
$uid = Uid::fromString($uid)->value;
$provider = array_values(
array_filter(
array_map('strval', (array) $provider),
static fn(string $value): bool => $value !== '',
),
);
$response = $this->client->unlinkProvider($uid, $provider);
return $this->getUserRecordFromResponseAfterUserUpdate($response);
}
public function signInAsUser($user, ?array $claims = null): SignInResult
{
$claims ??= [];
$uid = $user instanceof UserRecord ? $user->uid : (string) $user;
try {
$customToken = $this->createCustomToken($uid, $claims);
} catch (Throwable $e) {
throw FailedToSignIn::fromPrevious($e);
}
return $this->client->handleSignIn(SignInWithCustomToken::fromValue($customToken->toString()));
}
public function signInWithCustomToken($token): SignInResult
{
$token = $token instanceof Token ? $token->toString() : $token;
$action = SignInWithCustomToken::fromValue($token);
return $this->client->handleSignIn($action);
}
public function signInWithRefreshToken(string $refreshToken): SignInResult
{
return $this->client->handleSignIn(SignInWithRefreshToken::fromValue($refreshToken));
}
public function signInWithEmailAndPassword($email, $clearTextPassword): SignInResult
{
$email = Email::fromString((string) $email)->value;
$clearTextPassword = ClearTextPassword::fromString($clearTextPassword)->value;
return $this->client->handleSignIn(SignInWithEmailAndPassword::fromValues($email, $clearTextPassword));
}
public function signInWithEmailAndOobCode($email, string $oobCode): SignInResult
{
$email = Email::fromString((string) $email)->value;
return $this->client->handleSignIn(SignInWithEmailAndOobCode::fromValues($email, $oobCode));
}
public function signInAnonymously(): SignInResult
{
$result = $this->client->handleSignIn(SignInAnonymously::new());
if ($result->idToken() !== null) {
return $result;
}
$uid = $result->firebaseUserId();
if ($uid !== null) {
return $this->signInAsUser($uid);
}
throw new FailedToSignIn('Failed to sign in anonymously: No ID token or UID available');
}
public function signInWithIdpAccessToken($provider, string $accessToken, $redirectUrl = null, ?string $oauthTokenSecret = null, ?string $linkingIdToken = null, ?string $rawNonce = null): SignInResult
{
$provider = (string) $provider;
$redirectUrl = trim((string) ($redirectUrl ?? 'http://localhost'));
$linkingIdToken = trim((string) $linkingIdToken);
$oauthTokenSecret = trim((string) $oauthTokenSecret);
$rawNonce = trim((string) $rawNonce);
if ($oauthTokenSecret !== '') {
$action = SignInWithIdpCredentials::withAccessTokenAndOauthTokenSecret($provider, $accessToken, $oauthTokenSecret);
} else {
$action = SignInWithIdpCredentials::withAccessToken($provider, $accessToken);
}
if ($linkingIdToken !== '') {
$action = $action->withLinkingIdToken($linkingIdToken);
}
if ($rawNonce !== '') {
$action = $action->withRawNonce($rawNonce);
}
if ($redirectUrl !== '') {
$action = $action->withRequestUri($redirectUrl);
}
return $this->client->handleSignIn($action);
}
public function signInWithIdpIdToken($provider, $idToken, $redirectUrl = null, ?string $linkingIdToken = null, ?string $rawNonce = null): SignInResult
{
$provider = trim((string) $provider);
$redirectUrl = trim((string) ($redirectUrl ?? 'http://localhost'));
$linkingIdToken = trim((string) $linkingIdToken);
$rawNonce = trim((string) $rawNonce);
if ($idToken instanceof Token) {
$idToken = $idToken->toString();
}
$action = SignInWithIdpCredentials::withIdToken($provider, $idToken);
if ($rawNonce !== '') {
$action = $action->withRawNonce($rawNonce);
}
if ($linkingIdToken !== '') {
$action = $action->withLinkingIdToken($linkingIdToken);
}
if ($redirectUrl !== '') {
$action = $action->withRequestUri($redirectUrl);
}
return $this->client->handleSignIn($action);
}
public function createSessionCookie($idToken, $ttl): string
{
if ($idToken instanceof Token) {
$idToken = $idToken->toString();
}
return $this->client->createSessionCookie($idToken, $ttl);
}
/**
* Gets the user ID from the response and queries a full UserRecord object for it.
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
private function getUserRecordFromResponseAfterUserUpdate(ResponseInterface $response): UserRecord
{
$responseData = Json::decode((string) $response->getBody(), true);
if (!array_key_exists('localId', $responseData) || $responseData['localId'] === '') {
throw new AuthError('Expected API response to contain a field "localId" being a non-empty string, got: '.gettype($responseData));
}
return $this->getUser($responseData['localId']);
}
private function userSessionHasBeenRevoked(UnencryptedToken $verifiedToken, UserRecord $user, ?int $leewayInSeconds = null): bool
{
// The timestamp, in seconds, which marks a boundary, before which Firebase ID token are considered revoked.
$validSince = $user->tokensValidAfterTime ?? null;
if (!$validSince instanceof DateTimeImmutable) {
// The user hasn't logged in yet, so there's nothing to revoke
return false;
}
$tokenAuthenticatedAt = DT::toUTCDateTimeImmutable($verifiedToken->claims()->get('auth_time'));
if ($leewayInSeconds !== null) {
$tokenAuthenticatedAt = $tokenAuthenticatedAt->modify('-'.$leewayInSeconds.' seconds');
}
return $tokenAuthenticatedAt->getTimestamp() < $validSince->getTimestamp();
}
private static function getFirstUserRecordFromUserListResponse(ResponseInterface $response): ?UserRecord
{
/** @var array{users?: list<UserRecordResponseShape>} $data */
$data = Json::decode((string) $response->getBody(), true);
if (!array_key_exists('users', $data)) {
return null;
}
$userData = array_shift($data['users']);
return $userData !== null
? UserRecord::fromResponseData($userData)
: null;
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
interface ActionCodeSettings
{
/**
* @return array<non-empty-string, bool|string>
*/
public function toArray(): array;
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth\ActionCodeSettings;
use GuzzleHttp\Psr7\Utils;
use Kreait\Firebase\Auth\ActionCodeSettings;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Psr\Http\Message\UriInterface;
use Stringable;
use function array_filter;
use function is_bool;
use function is_string;
use function mb_strtolower;
final class ValidatedActionCodeSettings implements ActionCodeSettings
{
private ?UriInterface $continueUrl = null;
private ?bool $canHandleCodeInApp = null;
private ?UriInterface $dynamicLinkDomain = null;
/**
* @var non-empty-string|null
*/
private ?string $androidPackageName = null;
/**
* @var non-empty-string|null
*/
private ?string $androidMinimumVersion = null;
private ?bool $androidInstallApp = null;
/**
* @var non-empty-string|null
*/
private ?string $iOSBundleId = null;
private function __construct()
{
}
public static function empty(): self
{
return new self();
}
/**
* @param array<non-empty-string, mixed> $settings
*/
public static function fromArray(array $settings): self
{
$instance = new self();
$settings = array_filter($settings, static fn($value): bool => $value !== null);
foreach ($settings as $key => $value) {
switch (mb_strtolower($key)) {
case 'continueurl':
case 'url':
$instance->continueUrl = ($value !== null)
? Utils::uriFor(self::ensureNonEmptyString($value))
: null;
break;
case 'handlecodeinapp':
$instance->canHandleCodeInApp = (bool) $value;
break;
case 'dynamiclinkdomain':
$instance->dynamicLinkDomain = ($value !== null)
? Utils::uriFor(self::ensureNonEmptyString($value))
: null;
break;
case 'androidpackagename':
$instance->androidPackageName = self::ensureNonEmptyString($value);
break;
case 'androidminimumversion':
$instance->androidMinimumVersion = self::ensureNonEmptyString($value);
break;
case 'androidinstallapp':
$instance->androidInstallApp = (bool) $value;
break;
case 'iosbundleid':
$instance->iOSBundleId = self::ensureNonEmptyString($value);
break;
default:
throw new InvalidArgumentException("Unsupported action code setting '{$key}'");
}
}
return $instance;
}
/**
* @return array<non-empty-string, bool|non-empty-string>
*/
public function toArray(): array
{
$continueUrl = $this->continueUrl !== null ? (string) $this->continueUrl : null;
$dynamicLinkDomain = $this->dynamicLinkDomain !== null ? (string) $this->dynamicLinkDomain : null;
return array_filter([
'continueUrl' => $continueUrl,
'canHandleCodeInApp' => $this->canHandleCodeInApp,
'dynamicLinkDomain' => $dynamicLinkDomain,
'androidPackageName' => $this->androidPackageName,
'androidMinimumVersion' => $this->androidMinimumVersion,
'androidInstallApp' => $this->androidInstallApp,
'iOSBundleId' => $this->iOSBundleId,
], static fn(string|bool|null $value): bool => is_bool($value) || (is_string($value) && $value !== ''));
}
/**
* @return non-empty-string
*/
private static function ensureNonEmptyString(mixed $value): string
{
if ($value instanceof Stringable) {
$value = (string) $value;
}
if (!is_string($value) || $value === '') {
throw new InvalidArgumentException('A non-empty string is required');
}
return $value;
}
}

View File

@@ -0,0 +1,327 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Beste\Json;
use DateInterval;
use GuzzleHttp\ClientInterface;
use Kreait\Firebase\Auth\CreateSessionCookie\GuzzleApiClientHandler;
use Kreait\Firebase\Auth\SignIn\GuzzleHandler;
use Kreait\Firebase\Exception\Auth\EmailNotFound;
use Kreait\Firebase\Exception\Auth\ExpiredOobCode;
use Kreait\Firebase\Exception\Auth\InvalidOobCode;
use Kreait\Firebase\Exception\Auth\OperationNotAllowed;
use Kreait\Firebase\Exception\Auth\UserDisabled;
use Kreait\Firebase\Exception\AuthApiExceptionConverter;
use Kreait\Firebase\Exception\AuthException;
use Kreait\Firebase\Request\CreateUser;
use Kreait\Firebase\Request\UpdateUser;
use Psr\Clock\ClockInterface;
use Psr\Http\Message\ResponseInterface;
use Stringable;
use Throwable;
use function array_filter;
use function array_map;
use function is_array;
use function str_contains;
use function time;
/**
* @internal
*/
class ApiClient
{
private readonly ProjectAwareAuthResourceUrlBuilder|TenantAwareAuthResourceUrlBuilder $awareAuthResourceUrlBuilder;
private readonly AuthResourceUrlBuilder $authResourceUrlBuilder;
/**
* @param non-empty-string $projectId
* @param non-empty-string|null $tenantId
*/
public function __construct(
private readonly string $projectId,
private readonly ?string $tenantId,
private readonly ClientInterface $client,
private readonly GuzzleHandler $signInHandler,
private readonly ClockInterface $clock,
private readonly AuthApiExceptionConverter $errorHandler,
) {
$this->awareAuthResourceUrlBuilder = $tenantId !== null
? TenantAwareAuthResourceUrlBuilder::forProjectAndTenant($projectId, $tenantId)
: ProjectAwareAuthResourceUrlBuilder::forProject($projectId);
$this->authResourceUrlBuilder = AuthResourceUrlBuilder::create();
}
/**
* @throws AuthException
*/
public function createUser(CreateUser $request): ResponseInterface
{
$url = $this->authResourceUrlBuilder->getUrl('/accounts:signUp');
return $this->requestApi($url, Json::decode(Json::encode($request), true));
}
/**
* @throws AuthException
*/
public function updateUser(UpdateUser $request): ResponseInterface
{
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:update');
return $this->requestApi($url, Json::decode(Json::encode($request), true));
}
/**
* @param array<non-empty-string, mixed> $claims
*
* @throws AuthException
*/
public function setCustomUserClaims(string $uid, array $claims): ResponseInterface
{
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:update');
return $this->requestApi($url, [
'localId' => $uid,
'customAttributes' => Json::encode((object) $claims),
]);
}
/**
* Returns a user for the given email address.
*
* @throws AuthException
* @throws EmailNotFound
*/
public function getUserByEmail(string $email): ResponseInterface
{
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:lookup');
return $this->requestApi($url, ['email' => [$email]]);
}
/**
* @throws AuthException
*/
public function getUserByPhoneNumber(string $phoneNumber): ResponseInterface
{
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:lookup');
return $this->requestApi($url, ['phoneNumber' => [$phoneNumber]]);
}
/**
* @throws AuthException
*/
public function getUserByProviderUid(string $providerId, string $uid): ResponseInterface
{
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:lookup');
return $this->requestApi($url, ['federatedUserId' => [['providerId' => $providerId, 'rawId' => $uid]]]);
}
/**
* @throws AuthException
*/
public function downloadAccount(?int $batchSize = null, ?string $nextPageToken = null): ResponseInterface
{
$batchSize ??= 1000;
$urlParams = array_filter([
'maxResults' => (string) $batchSize,
'nextPageToken' => (string) $nextPageToken,
], fn(string $value): bool => $value !== '');
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:batchGet', $urlParams);
return $this->requestApi($url);
}
/**
* @throws AuthException
*/
public function deleteUser(string $uid): ResponseInterface
{
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:delete');
return $this->requestApi($url, ['localId' => $uid]);
}
/**
* @param string[] $uids
*
* @throws AuthException
*/
public function deleteUsers(array $uids, bool $forceDeleteEnabledUsers): ResponseInterface
{
$data = [
'localIds' => $uids,
'force' => $forceDeleteEnabledUsers,
];
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:batchDelete');
return $this->requestApi($url, $data);
}
/**
* @param string|list<non-empty-string> $uids
*
* @throws AuthException
*/
public function getAccountInfo(string|array $uids): ResponseInterface
{
if (!is_array($uids)) {
$uids = [$uids];
}
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:lookup');
return $this->requestApi($url, ['localId' => $uids]);
}
/**
* @throws AuthException
*/
public function queryUsers(UserQuery $query): ResponseInterface
{
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:query');
return $this->requestApi($url, Json::decode(Json::encode($query), true));
}
/**
* @throws AuthException
* @throws ExpiredOobCode
* @throws InvalidOobCode
* @throws OperationNotAllowed
*/
public function verifyPasswordResetCode(string $oobCode): ResponseInterface
{
$url = $this->authResourceUrlBuilder->getUrl('/accounts:resetPassword');
return $this->requestApi($url, ['oobCode' => $oobCode]);
}
/**
* @throws AuthException
* @throws ExpiredOobCode
* @throws InvalidOobCode
* @throws OperationNotAllowed
* @throws UserDisabled
*/
public function confirmPasswordReset(string $oobCode, string $newPassword): ResponseInterface
{
$url = $this->authResourceUrlBuilder->getUrl('/accounts:resetPassword');
return $this->requestApi($url, [
'oobCode' => $oobCode,
'newPassword' => $newPassword,
]);
}
/**
* @throws AuthException
*/
public function revokeRefreshTokens(string $uid): ResponseInterface
{
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:update');
return $this->requestApi($url, [
'localId' => $uid,
'validSince' => (string) time(),
]);
}
/**
* @param list<Stringable|non-empty-string> $providers
*
* @throws AuthException
*/
public function unlinkProvider(string $uid, array $providers): ResponseInterface
{
$url = $this->awareAuthResourceUrlBuilder->getUrl('/accounts:update');
$providers = array_map('strval', $providers);
return $this->requestApi($url, [
'localId' => $uid,
'deleteProvider' => $providers,
]);
}
public function createSessionCookie(string $idToken, int|DateInterval $ttl): string
{
return (new GuzzleApiClientHandler($this->client, $this->projectId))
->handle(CreateSessionCookie::forIdToken($idToken, $this->tenantId, $ttl, $this->clock))
;
}
public function getEmailActionLink(string $type, string $email, ActionCodeSettings $actionCodeSettings, ?string $locale = null): string
{
return (new CreateActionLink\GuzzleApiClientHandler($this->client, $this->projectId))
->handle(CreateActionLink::new($type, $email, $actionCodeSettings, $this->tenantId, $locale))
;
}
/**
* TODO: Make that this method can be emulated.
*/
public function sendEmailActionLink(string $type, string $email, ActionCodeSettings $actionCodeSettings, ?string $locale = null, ?string $idToken = null): void
{
$createAction = CreateActionLink::new($type, $email, $actionCodeSettings, $this->tenantId, $locale);
$sendAction = new SendActionLink($createAction, $locale);
if ($idToken !== null) {
$sendAction = $sendAction->withIdTokenString($idToken);
}
(new SendActionLink\GuzzleApiClientHandler($this->client, $this->projectId))->handle($sendAction);
}
/**
* TODO: Make that this method can be emulated.
*/
public function handleSignIn(SignIn $action): SignInResult
{
if ($this->tenantId !== null) {
$action = $action->withTenantId($this->tenantId);
}
return $this->signInHandler->handle($action);
}
/**
* @param array<string, mixed> $data
*
* @throws AuthException
*/
private function requestApi(string $uri, ?array $data = null): ResponseInterface
{
$options = [];
$method = 'GET';
if (!str_contains($uri, 'projects')) {
$data['targetProjectId'] = $this->projectId;
}
if ($this->tenantId !== null && !str_contains($uri, 'tenants')) {
$data['tenantId'] = $this->tenantId;
}
if (is_array($data) && $data !== []) {
$method = 'POST';
$options['json'] = $data;
}
try {
return $this->client->request($method, $uri, $options);
} catch (Throwable $e) {
throw $this->errorHandler->convertException($e);
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Kreait\Firebase\Util;
use function assert;
use function http_build_query;
use function str_replace;
use function strtr;
/**
* @internal
*/
final class AuthResourceUrlBuilder
{
private const URL_FORMAT = 'https://identitytoolkit.googleapis.com/{version}{api}';
private const EMULATOR_URL_FORMAT = 'http://{host}/identitytoolkit.googleapis.com/{version}{api}';
private const DEFAULT_API_VERSION = 'v1';
/**
* @param non-empty-string $apiVersion
* @param non-empty-string $urlFormat
*/
private function __construct(
private readonly string $apiVersion,
private readonly string $urlFormat,
) {
}
/**
* @param non-empty-string|null $version
*/
public static function create(?string $version = null): self
{
$version ??= self::DEFAULT_API_VERSION;
$emulatorHost = Util::authEmulatorHost();
$urlFormat = $emulatorHost !== null
? str_replace('{host}', $emulatorHost, self::EMULATOR_URL_FORMAT)
: self::URL_FORMAT;
return new self($version, $urlFormat);
}
/**
* @param non-empty-string|null $api
* @param array<non-empty-string, scalar>|null $params
*
* @return non-empty-string
*/
public function getUrl(?string $api = null, ?array $params = null): string
{
$api ??= '';
$url = strtr($this->urlFormat, [
'{version}' => $this->apiVersion,
'{api}' => $api,
]);
assert($url !== '');
if ($params !== null) {
$url .= '?'.http_build_query($params);
}
return $url;
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Kreait\Firebase\Value\Email;
use Stringable;
/**
* @internal
*/
final class CreateActionLink
{
private function __construct(
private readonly ?string $tenantId,
private readonly ?string $locale,
private readonly string $type,
private readonly string $email,
private readonly ActionCodeSettings $settings,
) {
}
public static function new(string $type, Stringable|string $email, ActionCodeSettings $settings, ?string $tenantId = null, ?string $locale = null): self
{
$email = Email::fromString((string) $email)->value;
return new self($tenantId, $locale, $type, $email, $settings);
}
public function type(): string
{
return $this->type;
}
public function email(): string
{
return $this->email;
}
public function settings(): ActionCodeSettings
{
return $this->settings;
}
public function tenantId(): ?string
{
return $this->tenantId;
}
public function locale(): ?string
{
return $this->locale;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth\CreateActionLink;
use Beste\Json;
use InvalidArgumentException;
use Kreait\Firebase\Auth\CreateActionLink;
use Kreait\Firebase\Exception\AuthException;
use Kreait\Firebase\Exception\RuntimeException;
use Psr\Http\Message\ResponseInterface;
final class FailedToCreateActionLink extends RuntimeException implements AuthException
{
private ?CreateActionLink $action = null;
private ?ResponseInterface $response = null;
public static function withActionAndResponse(CreateActionLink $action, ResponseInterface $response): self
{
$fallbackMessage = 'Failed to create action link';
try {
$message = Json::decode((string) $response->getBody(), true)['error']['message'] ?? $fallbackMessage;
} catch (InvalidArgumentException) {
$message = $fallbackMessage;
}
$error = new self($message);
$error->action = $action;
$error->response = $response;
return $error;
}
public function action(): ?CreateActionLink
{
return $this->action;
}
public function response(): ?ResponseInterface
{
return $this->response;
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth\CreateActionLink;
use Beste\Json;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use InvalidArgumentException;
use Kreait\Firebase\Auth\CreateActionLink;
use Kreait\Firebase\Auth\ProjectAwareAuthResourceUrlBuilder;
use Kreait\Firebase\Auth\TenantAwareAuthResourceUrlBuilder;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestInterface;
use function array_filter;
use const JSON_FORCE_OBJECT;
/**
* @internal
*/
final class GuzzleApiClientHandler
{
/**
* @param non-empty-string $projectId
*/
public function __construct(
private readonly ClientInterface $client,
private readonly string $projectId,
) {
}
public function handle(CreateActionLink $action): string
{
$request = $this->createRequest($action);
try {
$response = $this->client->send($request, ['http_errors' => false]);
} catch (ClientExceptionInterface $e) {
throw new FailedToCreateActionLink('Failed to create action link: '.$e->getMessage(), $e->getCode(), $e);
}
if ($response->getStatusCode() !== 200) {
throw FailedToCreateActionLink::withActionAndResponse($action, $response);
}
try {
$data = Json::decode((string) $response->getBody(), true);
} catch (InvalidArgumentException $e) {
throw new FailedToCreateActionLink('Unable to parse the response data: '.$e->getMessage(), $e->getCode(), $e);
}
$actionCode = $data['oobLink'] ?? null;
if (!is_scalar($actionCode)) {
throw new FailedToCreateActionLink('The response did not contain an action link');
}
return (string) $actionCode;
}
private function createRequest(CreateActionLink $action): RequestInterface
{
$data = [
'requestType' => $action->type(),
'email' => $action->email(),
'returnOobLink' => true,
...$action->settings()->toArray(),
];
$tenantId = $action->tenantId();
if (is_string($tenantId) && $tenantId !== '') {
$urlBuilder = TenantAwareAuthResourceUrlBuilder::forProjectAndTenant($this->projectId, $tenantId);
} else {
$urlBuilder = ProjectAwareAuthResourceUrlBuilder::forProject($this->projectId);
}
$url = $urlBuilder->getUrl('/accounts:sendOobCode');
$body = Utils::streamFor(Json::encode($data, JSON_FORCE_OBJECT));
$headers = array_filter([
'Content-Type' => 'application/json; charset=UTF-8',
'Content-Length' => (string) $body->getSize(),
'X-Firebase-Locale' => $action->locale(),
], fn(?string $value): bool => $value !== null);
return new Request('POST', $url, $headers, $body);
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Beste\Clock\SystemClock;
use Beste\Clock\WrappingClock;
use DateInterval;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Lcobucci\JWT\Token;
use Psr\Clock\ClockInterface;
use function is_int;
/**
* @internal
*/
final class CreateSessionCookie
{
private const FIVE_MINUTES = 'PT5M';
private const TWO_WEEKS = 'P14D';
private function __construct(
private readonly string $idToken,
private readonly ?string $tenantId,
private readonly DateInterval $ttl,
private readonly ClockInterface $clock,
) {
}
/**
* @param Token|string $idToken
* @param int|DateInterval $ttl
*/
public static function forIdToken($idToken, ?string $tenantId, $ttl, ?object $clock = null): self
{
$clock ??= SystemClock::create();
if (!$clock instanceof ClockInterface) {
$clock = WrappingClock::wrapping($clock);
}
if ($idToken instanceof Token) {
$idToken = $idToken->toString();
}
$ttl = self::assertValidDuration($ttl, $clock);
return new self($idToken, $tenantId, $ttl, $clock);
}
public function idToken(): string
{
return $this->idToken;
}
public function tenantId(): ?string
{
return $this->tenantId;
}
public function ttl(): DateInterval
{
return $this->ttl;
}
public function ttlInSeconds(): int
{
$now = $this->clock->now();
return $now->add($this->ttl)->getTimestamp() - $now->getTimestamp();
}
/**
* @param int|DateInterval $ttl
*
* @throws InvalidArgumentException
*/
private static function assertValidDuration($ttl, ClockInterface $clock): DateInterval
{
if (is_int($ttl)) {
if ($ttl < 0) {
throw new InvalidArgumentException('A session cookie cannot be valid for a negative amount of time');
}
$ttl = new DateInterval('PT'.$ttl.'S');
}
$now = $clock->now();
$expiresAt = $now->add($ttl);
$min = $now->add(new DateInterval(self::FIVE_MINUTES));
$max = $now->add(new DateInterval(self::TWO_WEEKS));
if ($expiresAt >= $min && $expiresAt <= $max) {
return $ttl;
}
throw new InvalidArgumentException('The TTL of a session must be between 5 minutes and 14 days');
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth\CreateSessionCookie;
use Beste\Json;
use InvalidArgumentException;
use Kreait\Firebase\Auth\CreateSessionCookie;
use Kreait\Firebase\Exception\AuthException;
use Kreait\Firebase\Exception\RuntimeException;
use Psr\Http\Message\ResponseInterface;
use Throwable;
final class FailedToCreateSessionCookie extends RuntimeException implements AuthException
{
public function __construct(
private readonly CreateSessionCookie $action,
private readonly ?ResponseInterface $response,
?string $message = null,
?int $code = null,
?Throwable $previous = null,
) {
$message ??= '';
$code ??= 0;
parent::__construct($message, $code, $previous);
}
public static function withActionAndResponse(CreateSessionCookie $action, ResponseInterface $response): self
{
$fallbackMessage = 'Failed to create session cookie';
try {
$message = Json::decode((string) $response->getBody(), true)['error']['message'] ?? $fallbackMessage;
} catch (InvalidArgumentException) {
$message = $fallbackMessage;
}
return new self($action, $response, $message);
}
public function action(): CreateSessionCookie
{
return $this->action;
}
public function response(): ?ResponseInterface
{
return $this->response;
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth\CreateSessionCookie;
use Beste\Json;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use InvalidArgumentException;
use Kreait\Firebase\Auth\CreateSessionCookie;
use Kreait\Firebase\Auth\ProjectAwareAuthResourceUrlBuilder;
use Kreait\Firebase\Auth\TenantAwareAuthResourceUrlBuilder;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestInterface;
use function array_filter;
use const JSON_FORCE_OBJECT;
/**
* @internal
*/
final class GuzzleApiClientHandler
{
/**
* @param non-empty-string $projectId
*/
public function __construct(
private readonly ClientInterface $client,
private readonly string $projectId,
) {
}
public function handle(CreateSessionCookie $action): string
{
$request = $this->createRequest($action);
try {
$response = $this->client->send($request, ['http_errors' => false]);
} catch (ClientExceptionInterface $e) {
throw new FailedToCreateSessionCookie($action, null, 'Connection error', 0, $e);
}
if ($response->getStatusCode() !== 200) {
throw FailedToCreateSessionCookie::withActionAndResponse($action, $response);
}
try {
/** @var array{sessionCookie?: string|null} $data */
$data = Json::decode((string) $response->getBody(), true);
} catch (InvalidArgumentException $e) {
throw new FailedToCreateSessionCookie($action, $response, 'Unable to parse the response data: '.$e->getMessage(), 0, $e);
}
$sessionCookie = $data['sessionCookie'] ?? null;
if ($sessionCookie !== null) {
return $sessionCookie;
}
throw new FailedToCreateSessionCookie($action, $response, 'The response did not contain a session cookie');
}
private function createRequest(CreateSessionCookie $action): RequestInterface
{
$data = [
'idToken' => $action->idToken(),
'validDuration' => $action->ttlInSeconds(),
];
$tenantId = $action->tenantId();
if (is_string($tenantId) && $tenantId !== '') {
$urlBuilder = TenantAwareAuthResourceUrlBuilder::forProjectAndTenant($this->projectId, $tenantId);
} else {
$urlBuilder = ProjectAwareAuthResourceUrlBuilder::forProject($this->projectId);
}
$url = $urlBuilder->getUrl(':createSessionCookie');
$body = Utils::streamFor(Json::encode($data, JSON_FORCE_OBJECT));
$headers = array_filter([
'Content-Type' => 'application/json; charset=UTF-8',
'Content-Length' => (string) ($body->getSize() ?? ''),
], fn($value): bool => $value !== '' && $value !== '0');
return new Request('POST', $url, $headers, $body);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use DateInterval;
use DateTimeImmutable;
use DateTimeInterface;
use Google\Auth\SignBlobInterface;
use Kreait\Firebase\Exception\Auth\AuthError;
use Kreait\Firebase\Util\DT;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\Parser;
use Stringable;
/**
* @internal
*/
final class CustomTokenViaGoogleCredentials
{
private readonly JoseEncoder $encoder;
private readonly Parser $parser;
public function __construct(private readonly SignBlobInterface $signer, private readonly ?string $tenantId = null)
{
$this->encoder = new JoseEncoder();
$this->parser = new Parser($this->encoder);
}
/**
* @param Stringable|string $uid
* @param array<non-empty-string, mixed> $claims
*
* @throws AuthError
*/
public function createCustomToken($uid, array $claims = [], ?DateTimeInterface $expiresAt = null): Token
{
$now = new DateTimeImmutable();
$expiresAt = ($expiresAt !== null)
? DT::toUTCDateTimeImmutable($expiresAt)
: $now->add(new DateInterval('PT1H'));
$header = ['typ' => 'JWT', 'alg' => 'RS256'];
$payload = [
'iss' => $this->signer->getClientName(),
'sub' => $this->signer->getClientName(),
'aud' => 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit',
'iat' => $now->getTimestamp(),
'exp' => $expiresAt->getTimestamp(),
'uid' => (string) $uid,
];
if ($this->tenantId !== null) {
$payload['tenant_id'] = $this->tenantId;
}
if ($claims !== []) {
$payload['claims'] = $claims;
}
$base64UrlHeader = $this->base64EncodeArray($header);
$base64UrlPayload = $this->base64EncodeArray($payload);
$signature = $this->signer->signBlob($base64UrlHeader.'.'.$base64UrlPayload);
$signature = str_replace(['=', '+', '/'], ['', '-', '_'], $signature);
return $this->parser->parse(sprintf('%s.%s.%s', $base64UrlHeader, $base64UrlPayload, $signature));
}
/**
* @param array<mixed> $array
*/
private function base64EncodeArray(array $array): string
{
return $this->encoder->base64UrlEncode($this->encoder->jsonEncode($array));
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Kreait\Firebase\Value\Uid;
use Stringable;
/**
* @internal
*/
final class DeleteUsersRequest
{
private const MAX_BATCH_SIZE = 1000;
private function __construct(
/** @var list<string> $uids */
private readonly array $uids,
private readonly bool $enabledUsersShouldBeForceDeleted,
) {
}
/**
* @param iterable<Stringable|string> $uids
*/
public static function withUids(iterable $uids, bool $forceDeleteEnabledUsers = false): self
{
$validatedUids = [];
$count = 0;
foreach ($uids as $uid) {
$validatedUids[] = Uid::fromString($uid)->value;
++$count;
if ($count > self::MAX_BATCH_SIZE) {
throw new InvalidArgumentException('Only '.self::MAX_BATCH_SIZE.' users can be deleted at a time');
}
}
return new self($validatedUids, $forceDeleteEnabledUsers);
}
/**
* @return string[]
*/
public function uids(): array
{
return $this->uids;
}
public function enabledUsersShouldBeForceDeleted(): bool
{
return $this->enabledUsersShouldBeForceDeleted;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Beste\Json;
use Psr\Http\Message\ResponseInterface;
use function count;
use function is_countable;
final class DeleteUsersResult
{
/**
* @param list<array{
* index: int,
* localId: string,
* message: string
* }> $rawErrors
*/
private function __construct(
private readonly int $successCount,
private readonly int $failureCount,
private readonly array $rawErrors,
) {
}
/**
* @internal
*/
public static function fromRequestAndResponse(DeleteUsersRequest $request, ResponseInterface $response): self
{
$data = Json::decode((string) $response->getBody(), true);
$errors = $data['errors'] ?? [];
$failureCount = is_countable($errors) ? count($errors) : 0;
$successCount = count($request->uids()) - $failureCount;
return new self($successCount, $failureCount, $errors);
}
public function failureCount(): int
{
return $this->failureCount;
}
public function successCount(): int
{
return $this->successCount;
}
/**
* @return list<array{
* index: int,
* localId: string,
* message: string
* }>
*/
public function rawErrors(): array
{
return $this->rawErrors;
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* @internal
*/
interface IsTenantAware
{
public function tenantId(): ?string;
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use DateTimeImmutable;
use Kreait\Firebase\Util\DT;
use function array_key_exists;
/**
* @phpstan-type MfaInfoResponseShape array{
* mfaEnrollmentId: non-empty-string,
* displayName?: non-empty-string,
* phoneInfo?: non-empty-string,
* enrolledAt?: non-empty-string
* }
*/
final class MfaInfo
{
private function __construct(
public readonly string $mfaEnrollmentId,
public readonly ?string $displayName,
public readonly ?string $phoneInfo,
public readonly ?DateTimeImmutable $enrolledAt,
) {
}
/**
* @internal
*
* @param MfaInfoResponseShape $data
*/
public static function fromResponseData(array $data): self
{
$enrolledAt = array_key_exists('enrolledAt', $data)
? DT::toUTCDateTimeImmutable($data['enrolledAt'])
: null;
return new self(
$data['mfaEnrollmentId'],
$data['displayName'] ?? null,
$data['phoneInfo'] ?? null,
$enrolledAt,
);
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Kreait\Firebase\Util;
use function http_build_query;
use function str_replace;
use function strtr;
/**
* @internal
*/
final class ProjectAwareAuthResourceUrlBuilder
{
private const URL_FORMAT = 'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';
private const EMULATOR_URL_FORMAT = 'http://{host}/identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';
private const DEFAULT_API_VERSION = 'v1';
/**
* @param non-empty-string $projectId
* @param non-empty-string $apiVersion
* @param non-empty-string $urlFormat
*/
private function __construct(
private readonly string $projectId,
private readonly string $apiVersion,
private readonly string $urlFormat,
) {
}
/**
* @param non-empty-string $projectId
* @param non-empty-string|null $version
*/
public static function forProject(string $projectId, ?string $version = null): self
{
$version ??= self::DEFAULT_API_VERSION;
$emulatorHost = Util::authEmulatorHost();
$urlFormat = $emulatorHost !== null
? str_replace('{host}', $emulatorHost, self::EMULATOR_URL_FORMAT)
: self::URL_FORMAT;
return new self($projectId, $version, $urlFormat);
}
/**
* @param non-empty-string|null $api
* @param array<non-empty-string, scalar>|null $params
*/
public function getUrl(?string $api = null, ?array $params = null): string
{
$api ??= '';
$url = strtr($this->urlFormat, [
'{version}' => $this->apiVersion,
'{projectId}' => $this->projectId,
'{api}' => $api,
]);
if ($params !== null) {
$url .= '?'.http_build_query($params);
}
return $url;
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* @internal
*/
final class SendActionLink
{
private ?string $idTokenString = null;
public function __construct(private CreateActionLink $action, private readonly ?string $locale = null)
{
}
public function type(): string
{
return $this->action->type();
}
public function email(): string
{
return $this->action->email();
}
public function settings(): ActionCodeSettings
{
return $this->action->settings();
}
public function tenantId(): ?string
{
return $this->action->tenantId();
}
public function locale(): ?string
{
return $this->locale;
}
/**
* @internal
*
* Only to be used when the API endpoint expects the ID Token of the given user.
*
* Currently, this seems only to be the case on VERIFY_EMAIL actions.
*
* @see https://github.com/firebase/firebase-js-sdk/issues/1958
*/
public function withIdTokenString(string $idTokenString): self
{
$instance = clone $this;
$instance->action = clone $this->action;
$instance->idTokenString = $idTokenString;
return $instance;
}
/**
* @internal
*
* Only to be used when the API endpoint expects the ID Token of the given user.
*
* Currently seems only to be the case on VERIFY_EMAIL actions.
*
* @see https://github.com/firebase/firebase-js-sdk/issues/1958
*/
public function idTokenString(): ?string
{
return $this->idTokenString;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth\SendActionLink;
use Beste\Json;
use InvalidArgumentException;
use Kreait\Firebase\Auth\SendActionLink;
use Kreait\Firebase\Exception\AuthException;
use Kreait\Firebase\Exception\RuntimeException;
use Psr\Http\Message\ResponseInterface;
final class FailedToSendActionLink extends RuntimeException implements AuthException
{
private ?SendActionLink $action = null;
private ?ResponseInterface $response = null;
public static function withActionAndResponse(SendActionLink $action, ResponseInterface $response): self
{
$fallbackMessage = 'Failed to send action link';
try {
$message = Json::decode((string) $response->getBody(), true)['error']['message'] ?? $fallbackMessage;
} catch (InvalidArgumentException) {
$message = $fallbackMessage;
}
$error = new self($message);
$error->action = $action;
$error->response = $response;
return $error;
}
public function action(): ?SendActionLink
{
return $this->action;
}
public function response(): ?ResponseInterface
{
return $this->response;
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth\SendActionLink;
use Beste\Json;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use Kreait\Firebase\Auth\ProjectAwareAuthResourceUrlBuilder;
use Kreait\Firebase\Auth\SendActionLink;
use Kreait\Firebase\Auth\TenantAwareAuthResourceUrlBuilder;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestInterface;
use function array_filter;
use const JSON_FORCE_OBJECT;
/**
* @internal
*/
final class GuzzleApiClientHandler
{
/**
* @param non-empty-string $projectId
*/
public function __construct(
private readonly ClientInterface $client,
private readonly string $projectId,
) {
}
public function handle(SendActionLink $action): void
{
$request = $this->createRequest($action);
try {
$response = $this->client->send($request, ['http_errors' => false]);
} catch (ClientExceptionInterface $e) {
throw new FailedToSendActionLink('Failed to send action link: '.$e->getMessage(), $e->getCode(), $e);
}
if ($response->getStatusCode() !== 200) {
throw FailedToSendActionLink::withActionAndResponse($action, $response);
}
}
private function createRequest(SendActionLink $action): RequestInterface
{
$data = [
'requestType' => $action->type(),
'email' => $action->email(),
...$action->settings()->toArray(),
];
$tenantId = $action->tenantId();
if (is_string($tenantId) && $tenantId !== '') {
$urlBuilder = TenantAwareAuthResourceUrlBuilder::forProjectAndTenant($this->projectId, $tenantId);
$data['tenantId'] = $tenantId;
} else {
$urlBuilder = ProjectAwareAuthResourceUrlBuilder::forProject($this->projectId);
}
$url = $urlBuilder->getUrl('/accounts:sendOobCode');
$idTokenString = $action->idTokenString();
if ($idTokenString !== null) {
$data['idToken'] = $idTokenString;
}
$body = Utils::streamFor(Json::encode($data, JSON_FORCE_OBJECT));
$headers = array_filter([
'Content-Type' => 'application/json; charset=UTF-8',
'Content-Length' => (string) $body->getSize(),
'X-Firebase-Locale' => $action->locale(),
], fn(?string $value): bool => !in_array($value, ['', null, '0'], true));
return new Request('POST', $url, $headers, $body);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* @internal
*/
interface SignIn
{
public function withTenantId(string $tenantId): self;
public function tenantId(): ?string;
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth\SignIn;
use Beste\Json;
use InvalidArgumentException;
use Kreait\Firebase\Auth\SignIn;
use Kreait\Firebase\Exception\AuthException;
use Kreait\Firebase\Exception\RuntimeException;
use Psr\Http\Message\ResponseInterface;
use Throwable;
final class FailedToSignIn extends RuntimeException implements AuthException
{
private ?SignIn $action = null;
private ?ResponseInterface $response = null;
public static function withActionAndResponse(SignIn $action, ResponseInterface $response): self
{
$fallbackMessage = 'Failed to sign in';
try {
$message = Json::decode((string) $response->getBody(), true)['error']['message'] ?? $fallbackMessage;
} catch (InvalidArgumentException) {
$message = $fallbackMessage;
}
$error = new self($message);
$error->action = $action;
$error->response = $response;
return $error;
}
public static function fromPrevious(Throwable $e): self
{
return new self('Sign in failed: '.$e->getMessage(), $e->getCode(), $e);
}
public function action(): ?SignIn
{
return $this->action;
}
public function response(): ?ResponseInterface
{
return $this->response;
}
}

View File

@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth\SignIn;
use Beste\Json;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Query;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use Kreait\Firebase\Auth\AuthResourceUrlBuilder;
use Kreait\Firebase\Auth\IsTenantAware;
use Kreait\Firebase\Auth\SignIn;
use Kreait\Firebase\Auth\SignInAnonymously;
use Kreait\Firebase\Auth\SignInResult;
use Kreait\Firebase\Auth\SignInWithCustomToken;
use Kreait\Firebase\Auth\SignInWithEmailAndOobCode;
use Kreait\Firebase\Auth\SignInWithEmailAndPassword;
use Kreait\Firebase\Auth\SignInWithIdpCredentials;
use Kreait\Firebase\Auth\SignInWithRefreshToken;
use Kreait\Firebase\Util;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestInterface;
use UnexpectedValueException;
use function http_build_query;
use function str_replace;
use const JSON_FORCE_OBJECT;
/**
* @internal
*/
final class GuzzleHandler
{
/**
* @var array<non-empty-string, mixed>
*/
private static array $defaultBody = [
'returnSecureToken' => true,
];
/**
* @var array<non-empty-string, mixed>
*/
private static array $defaultHeaders = [
'Content-Type' => 'application/json; charset=UTF-8',
];
public function __construct(
private readonly string $projectId,
private readonly ClientInterface $client,
) {
}
public function handle(SignIn $action): SignInResult
{
$request = $this->createApiRequest($action);
try {
$response = $this->client->send($request, ['http_errors' => false]);
} catch (ClientExceptionInterface $e) {
throw FailedToSignIn::fromPrevious($e);
}
if ($response->getStatusCode() !== 200) {
throw FailedToSignIn::withActionAndResponse($action, $response);
}
try {
$data = Json::decode((string) $response->getBody(), true);
} catch (UnexpectedValueException $e) {
throw FailedToSignIn::fromPrevious($e);
}
return SignInResult::fromData($data);
}
private function createApiRequest(SignIn $action): RequestInterface
{
return match (true) {
$action instanceof SignInAnonymously => $this->anonymous($action),
$action instanceof SignInWithCustomToken => $this->customToken($action),
$action instanceof SignInWithEmailAndPassword => $this->emailAndPassword($action),
$action instanceof SignInWithEmailAndOobCode => $this->emailAndOobCode($action),
$action instanceof SignInWithIdpCredentials => $this->idpCredentials($action),
$action instanceof SignInWithRefreshToken => $this->refreshToken($action),
default => throw new FailedToSignIn(self::class.' does not support '.$action::class),
};
}
private function anonymous(SignInAnonymously $action): Request
{
$url = AuthResourceUrlBuilder::create()->getUrl('/accounts:signUp');
$body = Utils::streamFor(Json::encode($this->prepareBody($action), JSON_FORCE_OBJECT));
$headers = self::$defaultHeaders;
return new Request('POST', $url, $headers, $body);
}
private function customToken(SignInWithCustomToken $action): Request
{
$url = AuthResourceUrlBuilder::create()->getUrl('/accounts:signInWithCustomToken');
$body = Utils::streamFor(
Json::encode([...$this->prepareBody($action), 'token' => $action->customToken()], JSON_FORCE_OBJECT),
);
$headers = self::$defaultHeaders;
return new Request('POST', $url, $headers, $body);
}
private function emailAndPassword(SignInWithEmailAndPassword $action): Request
{
$url = AuthResourceUrlBuilder::create()->getUrl('/accounts:signInWithPassword');
$body = Utils::streamFor(
Json::encode([
...$this->prepareBody($action),
'email' => $action->email(),
'password' => $action->clearTextPassword(),
'returnSecureToken' => true,
], JSON_FORCE_OBJECT),
);
$headers = self::$defaultHeaders;
return new Request('POST', $url, $headers, $body);
}
private function emailAndOobCode(SignInWithEmailAndOobCode $action): Request
{
$url = AuthResourceUrlBuilder::create()->getUrl('/accounts:signInWithEmailLink');
$body = Utils::streamFor(
Json::encode([
...$this->prepareBody($action),
'email' => $action->email(),
'oobCode' => $action->oobCode(),
'returnSecureToken' => true,
], JSON_FORCE_OBJECT),
);
$headers = self::$defaultHeaders;
return new Request('POST', $url, $headers, $body);
}
private function idpCredentials(SignInWithIdpCredentials $action): Request
{
$url = AuthResourceUrlBuilder::create()->getUrl('/accounts:signInWithIdp');
$postBody = array_filter([
'access_token' => $action->accessToken(),
'id_token' => $action->idToken(),
'providerId' => $action->provider(),
'oauth_token_secret' => $action->oauthTokenSecret(),
'nonce' => $action->rawNonce(),
], fn(?string $value): bool => $value !== null);
$rawBody = array_filter([
...$this->prepareBody($action),
'postBody' => http_build_query($postBody),
'returnIdpCredential' => true,
'requestUri' => $action->requestUri(),
'idToken' => $action->linkingIdToken(),
], fn($value): bool => $value !== null);
$body = Utils::streamFor(Json::encode($rawBody, JSON_FORCE_OBJECT));
$headers = self::$defaultHeaders;
return new Request('POST', $url, $headers, $body);
}
private function refreshToken(SignInWithRefreshToken $action): Request
{
$body = Query::build([
'grant_type' => 'refresh_token',
'refresh_token' => $action->refreshToken(),
]);
$headers = [
'Content-Type' => 'application/x-www-form-urlencoded',
'Accept' => 'application/json',
];
$emulatorHost = Util::authEmulatorHost();
if ($emulatorHost !== null) {
// The emulator host requires an api key query parameter.
$url = str_replace('{host}', $emulatorHost, 'http://{host}/securetoken.googleapis.com/v1/token?key=any');
} else {
$url = 'https://securetoken.googleapis.com/v1/token';
}
return new Request('POST', $url, $headers, $body);
}
/**
* @return array<non-empty-string, mixed>
*/
private function prepareBody(SignIn $action): array
{
$body = self::$defaultBody;
$body['targetProjectId'] = $this->projectId;
$tenantId = $action->tenantId();
if ($action instanceof IsTenantAware && $tenantId !== null) {
$body['tenantId'] = $tenantId;
}
return $body;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* @internal
*/
final class SignInAnonymously implements SignIn
{
private ?string $tenantId = null;
private function __construct()
{
}
public static function new(): self
{
return new self();
}
public function withTenantId(string $tenantId): self
{
$action = clone $this;
$action->tenantId = $tenantId;
return $action;
}
public function tenantId(): ?string
{
return $this->tenantId;
}
}

View File

@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Kreait\Firebase\JWT\Token\Parser;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\UnencryptedToken;
use stdClass;
use function array_key_exists;
use function assert;
use function is_array;
use function is_object;
use function property_exists;
final class SignInResult
{
/**
* @var non-empty-string|null
*/
private ?string $idToken = null;
/**
* @var non-empty-string|null
*/
private ?string $accessToken = null;
/**
* @var non-empty-string|null
*/
private ?string $refreshToken = null;
/**
* @var positive-int|null
*/
private ?int $ttl = null;
/**
* @var array<non-empty-string, mixed>
*/
private array $data = [];
/**
* @var non-empty-string|null
*/
private ?string $firebaseUserId = null;
/**
* @var non-empty-string|null
*/
private ?string $tenantId = null;
private readonly Parser $parser;
private function __construct()
{
$this->parser = new Parser(new JoseEncoder());
}
/**
* @param array<non-empty-string, mixed> $data
*/
public static function fromData(array $data): self
{
$instance = new self();
$expiresIn = (int) ($data['expiresIn'] ?? $data['expires_in'] ?? null);
if ($expiresIn > 0) {
$instance->ttl = $expiresIn;
}
$instance->idToken = $data['idToken'] ?? $data['id_token'] ?? null;
$instance->accessToken = $data['accessToken'] ?? $data['access_token'] ?? null;
$instance->refreshToken = $data['refreshToken'] ?? $data['refresh_token'] ?? null;
$instance->data = $data;
return $instance;
}
/**
* @return non-empty-string|null
*/
public function idToken(): ?string
{
return $this->idToken;
}
/**
* @return non-empty-string|null
*/
public function firebaseUserId(): ?string
{
if ($this->firebaseUserId !== null) {
return $this->firebaseUserId;
}
if ($this->idToken !== null) {
$idToken = $this->parser->parse($this->idToken);
assert($idToken instanceof UnencryptedToken);
foreach (['sub', 'localId', 'user_id'] as $claim) {
$uid = $idToken->claims()->get($claim, false);
if (is_string($uid) && $uid !== '') {
return $this->firebaseUserId = $uid;
}
}
}
$localId = $this->data['localId'] ?? null;
if (is_string($localId) && $localId !== '') {
return $this->firebaseUserId = $localId;
}
return null;
}
/**
* @return non-empty-string|null
*/
public function firebaseTenantId(): ?string
{
if ($this->tenantId !== null) {
return $this->tenantId;
}
if ($this->idToken !== null) {
$idToken = $this->parser->parse($this->idToken);
assert($idToken instanceof UnencryptedToken);
$firebaseClaims = $idToken->claims()->get('firebase', new stdClass());
if (is_object($firebaseClaims) && property_exists($firebaseClaims, 'tenant')) {
return $this->tenantId = $firebaseClaims->tenant;
}
if (is_array($firebaseClaims) && array_key_exists('tenant', $firebaseClaims)) {
return $this->tenantId = $firebaseClaims['tenant'];
}
}
return null;
}
/**
* @return non-empty-string|null
*/
public function accessToken(): ?string
{
return $this->accessToken;
}
/**
* @return non-empty-string|null
*/
public function refreshToken(): ?string
{
return $this->refreshToken;
}
/**
* @return positive-int|null
*/
public function ttl(): ?int
{
return $this->ttl;
}
/**
* @return array<non-empty-string, mixed>
*/
public function data(): array
{
return $this->data;
}
/**
* @return array<non-empty-string, mixed>
*/
public function asTokenResponse(): array
{
return [
'token_type' => 'Bearer',
'access_token' => $this->accessToken,
'id_token' => $this->idToken,
'refresh_token' => $this->refreshToken,
'expires_in' => $this->ttl,
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* @internal
*/
final class SignInWithCustomToken implements IsTenantAware, SignIn
{
private ?string $tenantId = null;
private function __construct(private readonly string $customToken)
{
}
public static function fromValue(string $customToken): self
{
return new self($customToken);
}
public function withTenantId(string $tenantId): self
{
$action = clone $this;
$action->tenantId = $tenantId;
return $action;
}
public function customToken(): string
{
return $this->customToken;
}
public function tenantId(): ?string
{
return $this->tenantId;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* @internal
*/
final class SignInWithEmailAndOobCode implements IsTenantAware, SignIn
{
private ?string $tenantId = null;
private function __construct(private readonly string $email, private readonly string $oobCode)
{
}
public static function fromValues(string $email, string $oobCode): self
{
return new self($email, $oobCode);
}
public function withTenantId(string $tenantId): self
{
$action = clone $this;
$action->tenantId = $tenantId;
return $action;
}
public function email(): string
{
return $this->email;
}
public function oobCode(): string
{
return $this->oobCode;
}
public function tenantId(): ?string
{
return $this->tenantId;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* @internal
*/
final class SignInWithEmailAndPassword implements IsTenantAware, SignIn
{
private ?string $tenantId = null;
private function __construct(private readonly string $email, private readonly string $clearTextPassword)
{
}
public static function fromValues(string $email, string $clearTextPassword): self
{
return new self($email, $clearTextPassword);
}
public function email(): string
{
return $this->email;
}
public function clearTextPassword(): string
{
return $this->clearTextPassword;
}
public function withTenantId(string $tenantId): self
{
$action = clone $this;
$action->tenantId = $tenantId;
return $action;
}
public function tenantId(): ?string
{
return $this->tenantId;
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* @internal
*/
final class SignInWithIdpCredentials implements IsTenantAware, SignIn
{
private ?string $accessToken = null;
private ?string $idToken = null;
private ?string $linkingIdToken = null;
private ?string $oauthTokenSecret = null;
private ?string $rawNonce = null;
private string $requestUri = 'http://localhost';
private ?string $tenantId = null;
private function __construct(private readonly string $provider)
{
}
public static function withAccessToken(string $provider, string $accessToken): self
{
$instance = new self($provider);
$instance->accessToken = $accessToken;
return $instance;
}
public static function withAccessTokenAndOauthTokenSecret(string $provider, string $accessToken, string $oauthTokenSecret): self
{
$instance = self::withAccessToken($provider, $accessToken);
$instance->oauthTokenSecret = $oauthTokenSecret;
return $instance;
}
public static function withIdToken(string $provider, string $idToken): self
{
$instance = new self($provider);
$instance->idToken = $idToken;
return $instance;
}
public function withRawNonce(string $rawNonce): self
{
$instance = clone $this;
$instance->rawNonce = $rawNonce;
return $instance;
}
public function withLinkingIdToken(string $idToken): self
{
$instance = clone $this;
$instance->linkingIdToken = $idToken;
return $instance;
}
public function withRequestUri(string $requestUri): self
{
$instance = clone $this;
$instance->requestUri = $requestUri;
return $instance;
}
public function withTenantId(string $tenantId): self
{
$action = clone $this;
$action->tenantId = $tenantId;
return $action;
}
public function provider(): string
{
return $this->provider;
}
public function oauthTokenSecret(): ?string
{
return $this->oauthTokenSecret;
}
public function accessToken(): ?string
{
return $this->accessToken;
}
public function idToken(): ?string
{
return $this->idToken;
}
public function rawNonce(): ?string
{
return $this->rawNonce;
}
public function linkingIdToken(): ?string
{
return $this->linkingIdToken;
}
public function requestUri(): string
{
return $this->requestUri;
}
public function tenantId(): ?string
{
return $this->tenantId;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* @internal
*/
final class SignInWithRefreshToken implements IsTenantAware, SignIn
{
private ?string $tenantId = null;
private function __construct(private readonly string $refreshToken)
{
}
public static function fromValue(string $refreshToken): self
{
return new self($refreshToken);
}
public function withTenantId(string $tenantId): self
{
$action = clone $this;
$action->tenantId = $tenantId;
return $action;
}
public function refreshToken(): string
{
return $this->refreshToken;
}
public function tenantId(): ?string
{
return $this->tenantId;
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Kreait\Firebase\Util;
use function http_build_query;
use function str_replace;
use function strtr;
/**
* @internal
*/
final class TenantAwareAuthResourceUrlBuilder
{
private const URL_FORMAT = 'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}/tenants/{tenantId}{api}';
private const EMULATOR_URL_FORMAT = 'http://{host}/identitytoolkit.googleapis.com/{version}/projects/{projectId}/tenants/{tenantId}{api}';
private const DEFAULT_API_VERSION = 'v1';
/**
* @param non-empty-string $projectId
* @param non-empty-string $tenantId
* @param non-empty-string $apiVersion
* @param non-empty-string $urlFormat
*/
private function __construct(
private readonly string $projectId,
private readonly string $tenantId,
private readonly string $apiVersion,
private readonly string $urlFormat,
) {
}
/**
* @param non-empty-string $projectId
* @param non-empty-string $tenantId
* @param non-empty-string|null $version
*/
public static function forProjectAndTenant(string $projectId, string $tenantId, ?string $version = null): self
{
$version ??= self::DEFAULT_API_VERSION;
$emulatorHost = Util::authEmulatorHost();
$urlFormat = $emulatorHost !== null
? str_replace('{host}', $emulatorHost, self::EMULATOR_URL_FORMAT)
: self::URL_FORMAT;
return new self($projectId, $tenantId, $version, $urlFormat);
}
/**
* @param array<non-empty-string, scalar>|null $params
*/
public function getUrl(?string $api = null, ?array $params = null): string
{
$api ??= '';
$url = strtr($this->urlFormat, [
'{version}' => $this->apiVersion,
'{projectId}' => $this->projectId,
'{tenantId}' => $this->tenantId,
'{api}' => $api,
]);
if ($params !== null) {
$url .= '?'.http_build_query($params);
}
return $url;
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
/**
* Represents a user's info from a third-party identity provider
* such as Google or Facebook.
*
* @phpstan-type UserInfoShape array{
* uid: non-empty-string,
* providerId: non-empty-string,
* displayName?: non-empty-string,
* email?: non-empty-string,
* photoUrl?: non-empty-string,
* phoneNumber?: non-empty-string
*
* }
* @phpstan-type ProviderUserInfoResponseShape array{
* rawId: non-empty-string,
* providerId: non-empty-string,
* displayName?: non-empty-string,
* email?: non-empty-string,
* federatedId?: non-empty-string,
* photoUrl?: non-empty-string,
* phoneNumber?: non-empty-string
* }
*/
final class UserInfo
{
/**
* @param non-empty-string $uid
* @param non-empty-string $providerId
* @param non-empty-string|null $displayName
* @param non-empty-string|null $email
* @param non-empty-string|null $phoneNumber
* @param non-empty-string|null $photoUrl
*/
public function __construct(
public readonly string $uid,
public readonly string $providerId,
public readonly ?string $displayName,
public readonly ?string $email,
public readonly ?string $phoneNumber,
public readonly ?string $photoUrl,
) {
}
/**
* @internal
*
* @param ProviderUserInfoResponseShape $data
*/
public static function fromResponseData(array $data): self
{
return new self(
$data['rawId'],
$data['providerId'],
$data['displayName'] ?? null,
$data['email'] ?? null,
$data['phoneNumber'] ?? null,
$data['photoUrl'] ?? null,
);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use DateTimeImmutable;
use Kreait\Firebase\Util\DT;
use function array_key_exists;
/**
* @phpstan-type UserMetadataResponseShape array{
* createdAt: non-empty-string,
* lastLoginAt?: non-empty-string,
* passwordUpdatedAt?: non-empty-string,
* lastRefreshAt?: non-empty-string
* }
*/
final class UserMetaData
{
public function __construct(
public readonly DateTimeImmutable $createdAt,
public readonly ?DateTimeImmutable $lastLoginAt,
public readonly ?DateTimeImmutable $passwordUpdatedAt,
public readonly ?DateTimeImmutable $lastRefreshAt,
) {
}
/**
* @internal
*
* @param UserMetadataResponseShape $data
*/
public static function fromResponseData(array $data): self
{
$createdAt = DT::toUTCDateTimeImmutable($data['createdAt']);
$lastLoginAt = array_key_exists('lastLoginAt', $data)
? DT::toUTCDateTimeImmutable($data['lastLoginAt'])
: null;
$passwordUpdatedAt = array_key_exists('passwordUpdatedAt', $data)
? DT::toUTCDateTimeImmutable($data['passwordUpdatedAt'])
: null;
$lastRefreshAt = array_key_exists('lastRefreshAt', $data)
? DT::toUTCDateTimeImmutable($data['lastRefreshAt'])
: null;
return new self($createdAt, $lastLoginAt, $passwordUpdatedAt, $lastRefreshAt);
}
}

View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use JsonSerializable;
use function array_filter;
/**
* @see https://cloud.google.com/identity-platform/docs/reference/rest/v1/projects.accounts/query#request-body
*
* @phpstan-type UserQueryShape array{
* sortBy?: self::FIELD_*,
* order?: self::ORDER_*,
* offset?: int<0, max>,
* limit?: positive-int,
* filter?: array<self::FILTER_*, non-empty-string>
* }
*/
class UserQuery implements JsonSerializable
{
final public const FIELD_CREATED_AT = 'CREATED_AT';
final public const FIELD_LAST_LOGIN_AT = 'LAST_LOGIN_AT';
final public const FIELD_NAME = 'NAME';
final public const FIELD_USER_EMAIL = 'USER_EMAIL';
final public const FIELD_USER_ID = 'USER_ID';
final public const FILTER_EMAIL = 'email';
final public const FILTER_PHONE_NUMBER = 'phoneNumber';
final public const FILTER_USER_ID = 'userId';
final public const ORDER_ASC = 'ASC';
final public const ORDER_DESC = 'DESC';
final public const MAX_LIMIT = 500;
/**
* @var positive-int|null
*/
private ?int $limit = null;
/**
* @var int<0, max>|null
*/
private ?int $offset = null;
/**
* @var self::FIELD_*|null
*/
private ?string $sortBy = null;
/**
* @var self::ORDER_*|null
*/
private ?string $order = null;
/**
* @var array<self::FILTER_*, non-empty-string>|null
*/
private ?array $filter = null;
private function __construct()
{
}
public static function all(): self
{
return new self();
}
/**
* @param UserQueryShape $data
*/
public static function fromArray(array $data): self
{
$query = new self();
$query->sortBy = $data['sortBy'] ?? null;
$query->order = $data['order'] ?? null;
$query->offset = $data['offset'] ?? null;
$query->limit = $data['limit'] ?? null;
$query->filter = $data['filter'] ?? null;
return $query;
}
/**
* @param self::FIELD_* $sortedBy
*/
public function sortedBy(string $sortedBy): self
{
$query = clone $this;
$query->sortBy = $sortedBy;
return $query;
}
public function inAscendingOrder(): self
{
return $this->withOrder(self::ORDER_ASC);
}
public function inDescendingOrder(): self
{
return $this->withOrder(self::ORDER_DESC);
}
/**
* @param int<0, max> $offset
*/
public function withOffset(int $offset): self
{
$query = clone $this;
$query->offset = $offset;
return $query;
}
/**
* @param positive-int $limit
*/
public function withLimit(int $limit): self
{
$query = clone $this;
$query->limit = $limit;
return $query;
}
/**
* @param self::FILTER_* $field
* @param non-empty-string $value
*/
public function withFilter(string $field, string $value): self
{
$query = clone $this;
$query->filter = [$field => $value];
return $query;
}
public function jsonSerialize(): array
{
$data = array_filter([
'returnUserInfo' => true,
'limit' => $this->limit,
'offset' => $this->offset,
'sortBy' => $this->sortBy,
'order' => $this->order,
], fn(int|bool|null|string $value): bool => $value !== null);
if ($this->filter !== null) {
$data['expression'] = $this->filter;
}
return $data;
}
/**
* @param self::ORDER_* $direction
*/
private function withOrder(string $direction): self
{
$query = clone $this;
$query->order = $direction;
return $query;
}
}

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Auth;
use Beste\Json;
use DateTimeImmutable;
use Kreait\Firebase\Util\DT;
use function array_key_exists;
use function array_map;
/**
* @phpstan-import-type ProviderUserInfoResponseShape from UserInfo
* @phpstan-import-type UserMetadataResponseShape from UserMetaData
* @phpstan-import-type MfaInfoResponseShape from MfaInfo
*
* @phpstan-type UserRecordResponseShape array{
* localId: non-empty-string,
* email?: non-empty-string,
* emailVerified?: bool,
* displayName?: non-empty-string,
* photoUrl?: non-empty-string,
* phoneNumber?: non-empty-string,
* disabled?: bool,
* passwordHash?: non-empty-string,
* salt?: non-empty-string,
* customAttributes?: non-empty-string,
* tenantId?: non-empty-string,
* providerUserInfo?: list<ProviderUserInfoResponseShape>,
* mfaInfo?: list<MfaInfoResponseShape>,
* createdAt: non-empty-string,
* lastLoginAt?: non-empty-string,
* passwordUpdatedAt?: non-empty-string,
* lastRefreshAt?: non-empty-string,
* validSince?: non-empty-string
* }
*/
final class UserRecord
{
/**
* @param non-empty-string $uid
* @param non-empty-string|null $email
* @param non-empty-string|null $displayName
* @param non-empty-string|null $phoneNumber
* @param non-empty-string|null $photoUrl
* @param list<UserInfo> $providerData
* @param non-empty-string|null $passwordHash
* @param non-empty-string|null $passwordSalt
* @param array<non-empty-string, mixed> $customClaims
* @param non-empty-string|null $tenantId
*/
public function __construct(
public readonly string $uid,
public readonly ?string $email,
public readonly bool $emailVerified,
public readonly ?string $displayName,
public readonly ?string $phoneNumber,
public readonly ?string $photoUrl,
public readonly bool $disabled,
public readonly UserMetaData $metadata,
public readonly array $providerData,
public readonly ?MfaInfo $mfaInfo,
public readonly ?string $passwordHash,
public readonly ?string $passwordSalt,
public readonly array $customClaims,
public readonly ?string $tenantId,
public readonly ?DateTimeImmutable $tokensValidAfterTime,
) {
}
/**
* @internal
*
* @param UserRecordResponseShape $data
*/
public static function fromResponseData(array $data): self
{
$validSince = array_key_exists('validSince', $data)
? DT::toUTCDateTimeImmutable($data['validSince'])
: null;
$customClaims = array_key_exists('customAttributes', $data)
? Json::decode($data['customAttributes'], true)
: [];
$providerUserInfo = array_key_exists('providerUserInfo', $data)
? self::userInfoFromResponseData($data)
: [];
return new self(
$data['localId'],
$data['email'] ?? null,
$data['emailVerified'] ?? false,
$data['displayName'] ?? null,
$data['phoneNumber'] ?? null,
$data['photoUrl'] ?? null,
$data['disabled'] ?? false,
self::userMetaDataFromResponseData($data),
$providerUserInfo,
self::mfaInfoFromResponseData($data),
$data['passwordHash'] ?? null,
$data['salt'] ?? null,
$customClaims,
$data['tenantId'] ?? null,
$validSince,
);
}
/**
* @param UserMetadataResponseShape $data
*/
private static function userMetaDataFromResponseData(array $data): UserMetaData
{
return UserMetaData::fromResponseData($data);
}
/**
* @param array{
* mfaInfo?: list<MfaInfoResponseShape>
* } $data
*/
private static function mfaInfoFromResponseData(array $data): ?MfaInfo
{
if (!array_key_exists('mfaInfo', $data)) {
return null;
}
$mfaInfo = array_shift($data['mfaInfo']);
if ($mfaInfo === null) {
return null;
}
return MfaInfo::fromResponseData($mfaInfo);
}
/**
* @param array{providerUserInfo: list<ProviderUserInfoResponseShape>} $data
*
* @return list<UserInfo>
*/
private static function userInfoFromResponseData(array $data): array
{
return array_map(
static fn(array $userInfoData): UserInfo => UserInfo::fromResponseData($userInfoData),
$data['providerUserInfo'],
);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Contract;
use Kreait\Firebase\AppCheck\AppCheckToken;
use Kreait\Firebase\AppCheck\AppCheckTokenOptions;
use Kreait\Firebase\AppCheck\VerifyAppCheckTokenResponse;
use Kreait\Firebase\Exception;
use Kreait\Firebase\Exception\AppCheck\FailedToVerifyAppCheckToken;
use Kreait\Firebase\Exception\AppCheck\InvalidAppCheckToken;
use Kreait\Firebase\Exception\AppCheck\InvalidAppCheckTokenOptions;
/**
* @phpstan-import-type AppCheckTokenOptionsShape from AppCheckTokenOptions
*/
interface AppCheck
{
/**
* @param non-empty-string $appId
* @param AppCheckTokenOptions|AppCheckTokenOptionsShape|null $options
*
* @throws InvalidAppCheckTokenOptions
* @throws Exception\AppCheckException
* @throws Exception\FirebaseException
*/
public function createToken(string $appId, $options = null): AppCheckToken;
/**
* @param non-empty-string $appCheckToken
*
* @throws InvalidAppCheckToken
* @throws FailedToVerifyAppCheckToken
* @throws Exception\AppCheckException
* @throws Exception\FirebaseException
*/
public function verifyToken(string $appCheckToken): VerifyAppCheckTokenResponse;
}

View File

@@ -0,0 +1,463 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Contract;
use DateInterval;
use InvalidArgumentException;
use Kreait\Firebase\Auth\ActionCodeSettings;
use Kreait\Firebase\Auth\CreateActionLink\FailedToCreateActionLink;
use Kreait\Firebase\Auth\CreateSessionCookie\FailedToCreateSessionCookie;
use Kreait\Firebase\Auth\DeleteUsersResult;
use Kreait\Firebase\Auth\SendActionLink\FailedToSendActionLink;
use Kreait\Firebase\Auth\SignIn\FailedToSignIn;
use Kreait\Firebase\Auth\SignInResult;
use Kreait\Firebase\Auth\UserQuery;
use Kreait\Firebase\Auth\UserRecord;
use Kreait\Firebase\Exception;
use Kreait\Firebase\Exception\Auth\ExpiredOobCode;
use Kreait\Firebase\Exception\Auth\FailedToVerifySessionCookie;
use Kreait\Firebase\Exception\Auth\FailedToVerifyToken;
use Kreait\Firebase\Exception\Auth\InvalidOobCode;
use Kreait\Firebase\Exception\Auth\OperationNotAllowed;
use Kreait\Firebase\Exception\Auth\RevokedIdToken;
use Kreait\Firebase\Exception\Auth\RevokedSessionCookie;
use Kreait\Firebase\Exception\Auth\UserDisabled;
use Kreait\Firebase\Exception\Auth\UserNotFound;
use Kreait\Firebase\Request\CreateUser;
use Kreait\Firebase\Request\UpdateUser;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\UnencryptedToken;
use Psr\Http\Message\UriInterface;
use Stringable;
use Traversable;
/**
* @phpstan-import-type UserQueryShape from UserQuery
*/
interface Auth
{
/**
* @param Stringable|non-empty-string $uid
*
* @throws UserNotFound
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function getUser(Stringable|string $uid): UserRecord;
/**
* @param non-empty-list<Stringable|non-empty-string> $uids
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*
* @return array<non-empty-string, UserRecord|null>
*/
public function getUsers(array $uids): array;
/**
* @param UserQuery|UserQueryShape $query
*
* @throws Exception\FirebaseException
* @throws Exception\AuthException
*
* @return array<non-empty-string, UserRecord>
*/
public function queryUsers(UserQuery|array $query): array;
/**
* @param positive-int $maxResults
* @param positive-int $batchSize
*
* @throws Exception\FirebaseException
* @throws Exception\AuthException
*
* @return Traversable<UserRecord>
*/
public function listUsers(int $maxResults = 1000, int $batchSize = 1000): Traversable;
/**
* Creates a new user with the provided properties.
*
* @param array<non-empty-string, mixed>|CreateUser $properties
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function createUser(array|CreateUser $properties): UserRecord;
/**
* Updates the given user with the given properties.
*
* @param non-empty-array<non-empty-string, mixed>|UpdateUser $properties
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function updateUser(Stringable|string $uid, array|UpdateUser $properties): UserRecord;
/**
* @param Stringable|non-empty-string $email
* @param Stringable|non-empty-string $password
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function createUserWithEmailAndPassword(Stringable|string $email, Stringable|string $password): UserRecord;
/**
* @param Stringable|non-empty-string $email
*
* @throws UserNotFound
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function getUserByEmail(Stringable|string $email): UserRecord;
/**
* @param Stringable|non-empty-string $phoneNumber
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function getUserByPhoneNumber(Stringable|string $phoneNumber): UserRecord;
/**
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function createAnonymousUser(): UserRecord;
/**
* @param Stringable|non-empty-string $uid
* @param Stringable|non-empty-string $newPassword
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function changeUserPassword(Stringable|string $uid, Stringable|string $newPassword): UserRecord;
/**
* @param Stringable|non-empty-string $uid
* @param Stringable|non-empty-string $newEmail
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function changeUserEmail(Stringable|string $uid, Stringable|string $newEmail): UserRecord;
/**
* @param Stringable|non-empty-string $uid
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function enableUser(Stringable|string $uid): UserRecord;
/**
* @param Stringable|non-empty-string $uid
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function disableUser(Stringable|string $uid): UserRecord;
/**
* @param Stringable|non-empty-string $uid
*
* @throws UserNotFound
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function deleteUser(Stringable|string $uid): void;
/**
* @param iterable<Stringable|non-empty-string> $uids
* @param bool $forceDeleteEnabledUsers Whether to force deleting accounts that are not in disabled state. If false, only disabled accounts will be deleted, and accounts that are not disabled will be added to the errors.
*
* @throws Exception\AuthException
*/
public function deleteUsers(iterable $uids, bool $forceDeleteEnabledUsers = false): DeleteUsersResult;
/**
* @param non-empty-string $type
* @param Stringable|non-empty-string $email
* @param ActionCodeSettings|array<non-empty-string, mixed>|null $actionCodeSettings
* @param non-empty-string|null $locale
*
* @throws FailedToCreateActionLink
*/
public function getEmailActionLink(string $type, Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): string;
/**
* @param non-empty-string $type
* @param Stringable|non-empty-string $email
* @param ActionCodeSettings|array<non-empty-string, mixed>|null $actionCodeSettings
* @param non-empty-string|null $locale
*
* @throws UserNotFound
* @throws FailedToSendActionLink
*/
public function sendEmailActionLink(string $type, Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): void;
/**
* @param Stringable|non-empty-string $email
* @param ActionCodeSettings|array<non-empty-string, mixed>|null $actionCodeSettings
* @param non-empty-string|null $locale
*
* @throws FailedToCreateActionLink
*/
public function getEmailVerificationLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): string;
/**
* @param Stringable|non-empty-string $email
* @param ActionCodeSettings|array<non-empty-string, mixed>|null $actionCodeSettings
* @param non-empty-string|null $locale
*
* @throws FailedToSendActionLink
*/
public function sendEmailVerificationLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): void;
/**
* @param Stringable|non-empty-string $email
* @param ActionCodeSettings|array<non-empty-string, mixed>|null $actionCodeSettings
* @param non-empty-string|null $locale
*
* @throws FailedToCreateActionLink
*/
public function getPasswordResetLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): string;
/**
* @param Stringable|non-empty-string $email
* @param ActionCodeSettings|array<non-empty-string, mixed>|null $actionCodeSettings
* @param non-empty-string|null $locale
*
* @throws FailedToSendActionLink
*/
public function sendPasswordResetLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): void;
/**
* @param Stringable|non-empty-string $email
* @param ActionCodeSettings|array<non-empty-string, mixed>|null $actionCodeSettings
* @param non-empty-string|null $locale
*
* @throws FailedToCreateActionLink
*/
public function getSignInWithEmailLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): string;
/**
* @param Stringable|non-empty-string $email
* @param ActionCodeSettings|array<non-empty-string, mixed>|null $actionCodeSettings
* @param non-empty-string|null $locale
*
* @throws FailedToSendActionLink
*/
public function sendSignInWithEmailLink(Stringable|string $email, $actionCodeSettings = null, ?string $locale = null): void;
/**
* Sets additional developer claims on an existing user identified by the provided UID.
*
* @see https://firebase.google.com/docs/auth/admin/custom-claims
*
* @param Stringable|non-empty-string $uid
* @param array<non-empty-string, mixed>|null $claims
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function setCustomUserClaims(Stringable|string $uid, ?array $claims): void;
/**
* @param array<non-empty-string, mixed> $claims
* @param int<0, 3600>|DateInterval|non-empty-string $ttl
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function createCustomToken(Stringable|string $uid, array $claims = [], int|DateInterval|string $ttl = 3600): UnencryptedToken;
/**
* @param non-empty-string $tokenString
*/
public function parseToken(string $tokenString): UnencryptedToken;
/**
* Creates a new Firebase session cookie with the given lifetime.
*
* The session cookie JWT will have the same payload claims as the provided ID token.
*
* @param Token|non-empty-string $idToken The Firebase ID token to exchange for a session cookie
* @param DateInterval|positive-int $ttl
*
* @throws InvalidArgumentException if the token or TTL is invalid
* @throws FailedToCreateSessionCookie
*/
public function createSessionCookie(Token|string $idToken, DateInterval|int $ttl): string;
/**
* Verifies a JWT auth token.
*
* Returns a token with the token's claims or rejects it if the token could not be verified.
*
* If checkRevoked is set to true, verifies if the session corresponding to the ID token was revoked.
* If the corresponding user's session was invalidated, a RevokedIdToken exception is thrown.
* If not specified the check is not applied.
*
* NOTE: Allowing time inconsistencies might impose a security risk. Do this only when you are not able
* to fix your environment's time to be consistent with Google's servers.
*
* @param Token|non-empty-string $idToken the JWT to verify
* @param bool $checkIfRevoked whether to check if the ID token is revoked
* @param positive-int|null $leewayInSeconds number of seconds to allow a token to be expired, in case that there
* is a clock skew between the signing and the verifying server
*
* @throws FailedToVerifyToken if the token could not be verified
* @throws RevokedIdToken if the token has been revoked
*/
public function verifyIdToken(Token|string $idToken, bool $checkIfRevoked = false, ?int $leewayInSeconds = null): UnencryptedToken;
/**
* Verifies a JWT session cookie.
*
* Returns a token with the cookie's claims or rejects it if the session cookie could not be verified.
*
* If checkRevoked is set to true, verifies if the session corresponding to the ID token was revoked.
* If the corresponding user's session was invalidated, a RevokedSessionCookie exception is thrown.
* If not specified the check is not applied.
*
* NOTE: Allowing time inconsistencies might impose a security risk. Do this only when you are not able
* to fix your environment's time to be consistent with Google's servers.
*
* @param non-empty-string $sessionCookie
* @param positive-int|null $leewayInSeconds
*
* @throws FailedToVerifySessionCookie
* @throws RevokedSessionCookie
*/
public function verifySessionCookie(string $sessionCookie, bool $checkIfRevoked = false, ?int $leewayInSeconds = null): UnencryptedToken;
/**
* Verifies the given password reset code and returns the associated user's email address.
*
* @see https://firebase.google.com/docs/reference/rest/auth#section-verify-password-reset-code
*
* @param non-empty-string $oobCode
*
* @throws ExpiredOobCode
* @throws InvalidOobCode
* @throws OperationNotAllowed
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function verifyPasswordResetCode(string $oobCode): string;
/**
* Applies the password reset requested via the given OOB code and returns the associated user's email address.
*
* @see https://firebase.google.com/docs/reference/rest/auth#section-confirm-reset-password
*
* @param non-empty-string $oobCode the email action code sent to the user's email for resetting the password
* @param Stringable|non-empty-string $newPassword
* @param bool $invalidatePreviousSessions Invalidate sessions initialized with the previous credentials
*
* @throws ExpiredOobCode
* @throws InvalidOobCode
* @throws OperationNotAllowed
* @throws UserDisabled
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function confirmPasswordReset(string $oobCode, Stringable|string $newPassword, bool $invalidatePreviousSessions = true): string;
/**
* Revokes all refresh tokens for the specified user identified by the uid provided.
* In addition to revoking all refresh tokens for a user, all ID tokens issued
* before revocation will also be revoked on the Auth backend. Any request with an
* ID token generated before revocation will be rejected with a token expired error.
*
* @param Stringable|string $uid the user whose tokens are to be revoked
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function revokeRefreshTokens(Stringable|string $uid): void;
/**
* @param Stringable|non-empty-string $uid
* @param list<Stringable|non-empty-string>|Stringable|non-empty-string $provider
*
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function unlinkProvider(Stringable|string $uid, array|Stringable|string $provider): UserRecord;
/**
* @param UserRecord|Stringable|non-empty-string $user
* @param array<non-empty-string, mixed>|null $claims
*
* @throws FailedToSignIn
*/
public function signInAsUser(UserRecord|Stringable|string $user, ?array $claims = null): SignInResult;
/**
* @param Token|non-empty-string $token
*
* @throws FailedToSignIn
*/
public function signInWithCustomToken(Token|string $token): SignInResult;
/**
* @param non-empty-string $refreshToken
*
* @throws FailedToSignIn
*/
public function signInWithRefreshToken(string $refreshToken): SignInResult;
/**
* @param Stringable|non-empty-string $email
* @param Stringable|non-empty-string $clearTextPassword
*
* @throws FailedToSignIn
*/
public function signInWithEmailAndPassword(Stringable|string $email, Stringable|string $clearTextPassword): SignInResult;
/**
* @param Stringable|non-empty-string $email
* @param non-empty-string $oobCode
*
* @throws FailedToSignIn
*/
public function signInWithEmailAndOobCode(Stringable|string $email, string $oobCode): SignInResult;
/**
* @throws FailedToSignIn
*/
public function signInAnonymously(): SignInResult;
/**
* @see https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/signInWithIdp
*
* @param Stringable|non-empty-string $provider
* @param non-empty-string $accessToken
* @param UriInterface|non-empty-string|null $redirectUrl
* @param non-empty-string|null $oauthTokenSecret
* @param non-empty-string|null $linkingIdToken
* @param non-empty-string|null $rawNonce
*
* @throws FailedToSignIn
*/
public function signInWithIdpAccessToken(Stringable|string $provider, string $accessToken, $redirectUrl = null, ?string $oauthTokenSecret = null, ?string $linkingIdToken = null, ?string $rawNonce = null): SignInResult;
/**
* @param Stringable|non-empty-string $provider
* @param Token|non-empty-string $idToken
* @param UriInterface|non-empty-string|null $redirectUrl
* @param non-empty-string|null $linkingIdToken
* @param non-empty-string|null $rawNonce
*
* @throws FailedToSignIn
*/
public function signInWithIdpIdToken(Stringable|string $provider, Token|string $idToken, $redirectUrl = null, ?string $linkingIdToken = null, ?string $rawNonce = null): SignInResult;
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Contract;
use Kreait\Firebase\Database\Reference;
use Kreait\Firebase\Database\RuleSet;
use Kreait\Firebase\Database\Transaction;
use Kreait\Firebase\Exception\DatabaseException;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Kreait\Firebase\Exception\OutOfRangeException;
use Psr\Http\Message\UriInterface;
/**
* The Firebase Realtime Database.
*
* @see https://firebase.google.com/docs/reference/rest/database
*/
interface Database
{
public const SERVER_TIMESTAMP = ['.sv' => 'timestamp'];
/**
* Returns a Reference to the root or the specified path.
*
* @throws InvalidArgumentException
*/
public function getReference(?string $path = null): Reference;
/**
* Returns a reference to the root or the path specified in url.
*
* @param string|UriInterface $uri
*
* @throws InvalidArgumentException If the URL is invalid
* @throws OutOfRangeException If the URL is not in the same domain as the current database
*/
public function getReferenceFromUrl($uri): Reference;
/**
* Retrieve Firebase Database Rules.
*
* @see https://firebase.google.com/docs/database/rest/app-management#retrieving-firebase-realtime-database-rules
*
* @throws DatabaseException
*/
public function getRuleSet(): RuleSet;
/**
* Update Firebase Database Rules.
*
* @see https://firebase.google.com/docs/database/rest/app-management#updating-firebase-realtime-database-rules
*
* @throws DatabaseException
*/
public function updateRules(RuleSet $ruleSet): void;
/**
* @param callable(Transaction $transaction):mixed $callable
*/
public function runTransaction(callable $callable): mixed;
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Contract;
use InvalidArgumentException;
use Kreait\Firebase\DynamicLink;
use Kreait\Firebase\DynamicLink\CreateDynamicLink;
use Kreait\Firebase\DynamicLink\CreateDynamicLink\FailedToCreateDynamicLink;
use Kreait\Firebase\DynamicLink\DynamicLinkStatistics;
use Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink;
use Kreait\Firebase\DynamicLink\ShortenLongDynamicLink;
use Kreait\Firebase\DynamicLink\ShortenLongDynamicLink\FailedToShortenLongDynamicLink;
use Stringable;
/**
* @deprecated 7.14.0 Firebase Dynamic Links is deprecated and should not be used in new projects. The service will
* shut down on August 25, 2025. The component will remain in the SDK until then, but as the
* Firebase service is deprecated, this component is also deprecated
*
* @see https://firebase.google.com/support/dynamic-links-faq Dynamic Links Deprecation FAQ
*
* @see https://firebase.google.com/docs/dynamic-links/rest Create Dynamic Links with the REST API
*
* @phpstan-import-type CreateDynamicLinkShape from CreateDynamicLink
* @phpstan-import-type ShortenLongDynamicLinkShape from ShortenLongDynamicLink
*/
interface DynamicLinks
{
/**
* @param Stringable|non-empty-string|CreateDynamicLink|CreateDynamicLinkShape $url
*
* @throws InvalidArgumentException
* @throws FailedToCreateDynamicLink
*/
public function createUnguessableLink(Stringable|string|CreateDynamicLink|array $url): DynamicLink;
/**
* @param Stringable|non-empty-string|CreateDynamicLink|CreateDynamicLinkShape $url
*
* @throws InvalidArgumentException
* @throws FailedToCreateDynamicLink
*/
public function createShortLink(Stringable|string|CreateDynamicLink|array $url): DynamicLink;
/**
* @param Stringable|non-empty-string|CreateDynamicLink|CreateDynamicLinkShape $actionOrParametersOrUrl
*
* @throws InvalidArgumentException
* @throws FailedToCreateDynamicLink
*/
public function createDynamicLink(Stringable|string|CreateDynamicLink|array $actionOrParametersOrUrl, ?string $suffixType = null): DynamicLink;
/**
* @param Stringable|non-empty-string|ShortenLongDynamicLink|ShortenLongDynamicLinkShape $longDynamicLinkOrAction
*
* @throws InvalidArgumentException
* @throws FailedToShortenLongDynamicLink
*/
public function shortenLongDynamicLink(Stringable|string|ShortenLongDynamicLink|array $longDynamicLinkOrAction, ?string $suffixType = null): DynamicLink;
/**
* @throws InvalidArgumentException
* @throws GetStatisticsForDynamicLink\FailedToGetStatisticsForDynamicLink
*/
public function getStatistics(Stringable|string|GetStatisticsForDynamicLink $dynamicLinkOrAction, ?int $durationInDays = null): DynamicLinkStatistics;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Contract;
use Google\Cloud\Firestore\FirestoreClient;
interface Firestore
{
public function database(): FirestoreClient;
}

View File

@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Contract;
use Kreait\Firebase\Exception\FirebaseException;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Kreait\Firebase\Exception\Messaging\InvalidArgument;
use Kreait\Firebase\Exception\Messaging\InvalidMessage;
use Kreait\Firebase\Exception\MessagingException;
use Kreait\Firebase\Messaging\AppInstance;
use Kreait\Firebase\Messaging\Message;
use Kreait\Firebase\Messaging\Messages;
use Kreait\Firebase\Messaging\MulticastSendReport;
use Kreait\Firebase\Messaging\RegistrationToken;
use Kreait\Firebase\Messaging\RegistrationTokens;
use Kreait\Firebase\Messaging\Topic;
/**
* @phpstan-import-type MessageInputShape from Message
*/
interface Messaging
{
/**
* @deprecated 7.5.0
*/
public const BATCH_MESSAGE_LIMIT = 500;
/**
* @param Message|MessageInputShape $message
*
* @throws MessagingException
* @throws FirebaseException
* @throws InvalidArgumentException
*
* @return array<array-key, mixed>
*/
public function send(Message|array $message, bool $validateOnly = false): array;
/**
* @param Message|MessageInputShape $message
* @param RegistrationTokens|RegistrationToken|list<RegistrationToken|string>|non-empty-string $registrationTokens
*
* @throws InvalidArgumentException if the message is invalid or the list of registration tokens is empty
* @throws MessagingException if the API request failed
* @throws FirebaseException if something very unexpected happened (never :))
*/
public function sendMulticast(Message|array $message, RegistrationTokens|RegistrationToken|array|string $registrationTokens, bool $validateOnly = false): MulticastSendReport;
/**
* @param list<Message|MessageInputShape>|Messages $messages
*
* @throws InvalidArgumentException if the message is invalid
* @throws MessagingException if the API request failed
* @throws FirebaseException if something very unexpected happened (never :))
*/
public function sendAll(array|Messages $messages, bool $validateOnly = false): MulticastSendReport;
/**
* @param Message|MessageInputShape $message
*
* @throws InvalidMessage
* @throws MessagingException
* @throws FirebaseException
* @throws InvalidArgumentException
*
* @return array<non-empty-string, mixed>
*/
public function validate(Message|array $message): array;
/**
* @param RegistrationTokens|RegistrationToken|list<RegistrationToken|non-empty-string>|non-empty-string $registrationTokenOrTokens
*
* @throws MessagingException
* @throws FirebaseException
*
* @return array{
* valid: list<non-empty-string>,
* unknown: list<non-empty-string>,
* invalid: list<non-empty-string>
* }
*/
public function validateRegistrationTokens(RegistrationTokens|RegistrationToken|array|string $registrationTokenOrTokens): array;
/**
* @param Topic|non-empty-string $topic
* @param RegistrationTokens|RegistrationToken|list<RegistrationToken|non-empty-string>|non-empty-string $registrationTokenOrTokens
*
* @return array<string, array<string, string>>
*/
public function subscribeToTopic(string|Topic $topic, RegistrationTokens|RegistrationToken|array|string $registrationTokenOrTokens): array;
/**
* @param iterable<non-empty-string|Topic> $topics
* @param RegistrationTokens|RegistrationToken|list<RegistrationToken|non-empty-string>|non-empty-string $registrationTokenOrTokens
*
* @return array<string, array<string, string>>
*/
public function subscribeToTopics(iterable $topics, RegistrationTokens|RegistrationToken|array|string $registrationTokenOrTokens): array;
/**
* @param Topic|non-empty-string $topic
* @param RegistrationTokens|RegistrationToken|list<RegistrationToken|non-empty-string>|non-empty-string $registrationTokenOrTokens
*
* @return array<string, array<string, string>>
*/
public function unsubscribeFromTopic(string|Topic $topic, RegistrationTokens|RegistrationToken|array|string $registrationTokenOrTokens): array;
/**
* @param array<non-empty-string|Topic> $topics
* @param RegistrationTokens|RegistrationToken|list<RegistrationToken|non-empty-string>|non-empty-string $registrationTokenOrTokens
*
* @return array<string, array<string, string>>
*/
public function unsubscribeFromTopics(array $topics, RegistrationTokens|RegistrationToken|array|string $registrationTokenOrTokens): array;
/**
* @param RegistrationTokens|RegistrationToken|list<RegistrationToken|non-empty-string>|non-empty-string $registrationTokenOrTokens
*
* @return array<string, array<string, string>>
*/
public function unsubscribeFromAllTopics(RegistrationTokens|RegistrationToken|array|string $registrationTokenOrTokens): array;
/**
* @see https://developers.google.com/instance-id/reference/server#results
*
* @param RegistrationToken|non-empty-string $registrationToken
*
* @throws InvalidArgument if the registration token is invalid
* @throws MessagingException
*/
public function getAppInstance(RegistrationToken|string $registrationToken): AppInstance;
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Contract;
use Kreait\Firebase\Exception\RemoteConfig\ValidationFailed;
use Kreait\Firebase\Exception\RemoteConfig\VersionNotFound;
use Kreait\Firebase\Exception\RemoteConfigException;
use Kreait\Firebase\RemoteConfig\FindVersions;
use Kreait\Firebase\RemoteConfig\Template;
use Kreait\Firebase\RemoteConfig\Version;
use Kreait\Firebase\RemoteConfig\VersionNumber;
use Traversable;
/**
* The Firebase Remote Config.
*
* @see https://firebase.google.com/docs/remote-config/use-config-rest
* @see https://firebase.google.com/docs/reference/remote-config/rest
*
* @phpstan-import-type RemoteConfigTemplateShape from Template
* @phpstan-import-type FindVersionsShape from FindVersions
*/
interface RemoteConfig
{
/**
* @param Version|VersionNumber|positive-int|non-empty-string $versionNumber
*
* @throws RemoteConfigException if something went wrong
*/
public function get(Version|VersionNumber|int|string|null $versionNumber = null): Template;
/**
* Validates the given template without publishing it.
*
* @param Template|RemoteConfigTemplateShape $template
*
* @throws ValidationFailed if the validation failed
* @throws RemoteConfigException
*/
public function validate($template): void;
/**
* @param Template|RemoteConfigTemplateShape $template
*
* @throws RemoteConfigException
*
* @return non-empty-string The etag value of the published template that can be compared to in later calls
*/
public function publish($template): string;
/**
* Returns a version with the given number.
*
* @param VersionNumber|positive-int|non-empty-string $versionNumber
*
* @throws VersionNotFound
* @throws RemoteConfigException if something went wrong
*/
public function getVersion(VersionNumber|int|string $versionNumber): Version;
/**
* Returns a version with the given number.
*
* @param VersionNumber|positive-int|non-empty-string $versionNumber
*
* @throws VersionNotFound
* @throws RemoteConfigException if something went wrong
*/
public function rollbackToVersion(VersionNumber|int|string $versionNumber): Template;
/**
* @param FindVersions|FindVersionsShape|null $query
*
* @throws RemoteConfigException if something went wrong
*
* @return Traversable<Version>
*/
public function listVersions($query = null): Traversable;
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Contract;
use Google\Cloud\Storage\Bucket;
use Google\Cloud\Storage\StorageClient;
interface Storage
{
public function getStorageClient(): StorageClient;
public function getBucket(?string $name = null): Bucket;
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Contract\Transitional;
use Kreait\Firebase\Auth\UserRecord;
use Kreait\Firebase\Exception;
use Kreait\Firebase\Exception\Auth\UserNotFound;
use Stringable;
/**
* @TODO: This interface is intended to be integrated into the Auth interface on the next major release.
*/
interface FederatedUserFetcher
{
/**
* @param Stringable|non-empty-string $providerId
* @param Stringable|non-empty-string $providerUid
*
* @throws UserNotFound
* @throws Exception\AuthException
* @throws Exception\FirebaseException
*/
public function getUserByProviderUid(Stringable|string $providerId, Stringable|string $providerUid): UserRecord;
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase;
use GuzzleHttp\Psr7\Uri;
use Kreait\Firebase\Database\ApiClient;
use Kreait\Firebase\Database\Reference;
use Kreait\Firebase\Database\RuleSet;
use Kreait\Firebase\Database\Transaction;
use Kreait\Firebase\Exception\InvalidArgumentException;
use Psr\Http\Message\UriInterface;
use function ltrim;
use function sprintf;
use function trim;
/**
* @internal
*/
final class Database implements Contract\Database
{
public function __construct(
private readonly UriInterface $uri,
private readonly ApiClient $client,
) {
}
public function getReference(?string $path = null): Reference
{
if ($path === null || trim($path) === '') {
$path = '/';
}
$path = '/'.ltrim($path, '/');
try {
return new Reference($this->uri->withPath($path), $this->client);
} catch (\InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
}
public function getReferenceFromUrl($uri): Reference
{
$uri = $uri instanceof UriInterface ? $uri : new Uri($uri);
if (($givenHost = $uri->getHost()) !== ($dbHost = $this->uri->getHost())) {
throw new InvalidArgumentException(sprintf(
'The given URI\'s host "%s" is not covered by the database for the host "%s".',
$givenHost,
$dbHost,
));
}
return $this->getReference($uri->getPath());
}
public function getRuleSet(): RuleSet
{
$rules = $this->client->get('/.settings/rules');
return RuleSet::fromArray($rules);
}
public function updateRules(RuleSet $ruleSet): void
{
$this->client->updateRules('/.settings/rules', $ruleSet);
}
public function runTransaction(callable $callable): mixed
{
$transaction = new Transaction($this->client);
return $callable($transaction);
}
}

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' => [],
];
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase;
use Beste\Json;
use GuzzleHttp\Psr7\Utils;
use JsonSerializable;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Stringable;
use function trim;
/**
* @deprecated 7.14.0 Firebase Dynamic Links is deprecated and should not be used in new projects. The service will
* shut down on August 25, 2025. The component will remain in the SDK until then, but as the
* Firebase service is deprecated, this component is also deprecated
*
* @see https://github.com/googleapis/google-api-nodejs-client/blob/main/src/apis/firebasedynamiclinks/v1.ts
*
* @phpstan-type DynamicLinkWarningShape array{
* warningCode?: non-empty-string,
* warningDocumentLink?: non-empty-string,
* warningMessage?: non-empty-string
* }
* @phpstan-type DynamicLinkShape array{
* shortLink: non-empty-string,
* previewLink?: non-empty-string,
* warning?: list<DynamicLinkWarningShape>
* }
*/
final class DynamicLink implements JsonSerializable, Stringable
{
/**
* @param DynamicLinkShape $data
*/
private function __construct(private readonly array $data)
{
}
public function __toString(): string
{
return (string) $this->uri();
}
/**
* @internal
*/
public static function fromApiResponse(ResponseInterface $response): self
{
/** @var DynamicLinkShape $decoded */
$decoded = Json::decode((string) $response->getBody(), true);
return new self($decoded);
}
public function uri(): UriInterface
{
return Utils::uriFor($this->data['shortLink']);
}
public function previewUri(): ?UriInterface
{
$previewLink = $this->data['previewLink'] ?? null;
return $previewLink !== null ? Utils::uriFor($previewLink) : null;
}
/**
* @return non-empty-string
*/
public function domain(): string
{
$uri = $this->uri();
return $uri->getScheme().'://'.$uri->getHost();
}
public function suffix(): string
{
return trim($this->uri()->getPath(), '/');
}
/**
* @return list<DynamicLinkWarningShape>
*/
public function warnings(): array
{
return $this->data['warning'] ?? [];
}
public function hasWarnings(): bool
{
return $this->warnings() !== [];
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use JsonSerializable;
use Kreait\Firebase\DynamicLink\AnalyticsInfo\GooglePlayAnalytics;
use Kreait\Firebase\DynamicLink\AnalyticsInfo\ITunesConnectAnalytics;
use function array_key_exists;
/**
* @phpstan-import-type GooglePlayAnalyticsShape from GooglePlayAnalytics
* @phpstan-import-type ITunesConnectAnalyticsShape from ITunesConnectAnalytics
*
* @phpstan-type AnalyticsInfoShape array{
* googlePlayAnalytics?: GooglePlayAnalyticsShape,
* itunesConnectAnalytics?: ITunesConnectAnalyticsShape
* }
*/
final class AnalyticsInfo implements JsonSerializable
{
private function __construct(
private readonly ?GooglePlayAnalytics $googlePlayAnalytics,
private readonly ?ITunesConnectAnalytics $iTunesConnectAnalytics,
) {
}
/**
* @param AnalyticsInfoShape $data
*/
public static function fromArray(array $data): self
{
$googlePlayAnalytics = array_key_exists('googlePlayAnalytics', $data)
? GooglePlayAnalytics::fromArray($data['googlePlayAnalytics'])
: null;
$itunesConnectAnalytics = array_key_exists('itunesConnectAnalytics', $data)
? ITunesConnectAnalytics::fromArray($data['itunesConnectAnalytics'])
: null;
return new self($googlePlayAnalytics, $itunesConnectAnalytics);
}
public static function new(): self
{
return new self(null, null);
}
/**
* @param GooglePlayAnalytics|GooglePlayAnalyticsShape $data
*/
public function withGooglePlayAnalyticsInfo($data): self
{
$gpInfo = $data instanceof GooglePlayAnalytics ? $data : GooglePlayAnalytics::fromArray($data);
return new self($gpInfo, $this->iTunesConnectAnalytics);
}
/**
* @param ITunesConnectAnalytics|ITunesConnectAnalyticsShape $data
*/
public function withItunesConnectAnalytics($data): self
{
$icInfo = $data instanceof ITunesConnectAnalytics ? $data : ITunesConnectAnalytics::fromArray($data);
return new self($this->googlePlayAnalytics, $icInfo);
}
/**
* @return AnalyticsInfoShape
*/
public function jsonSerialize(): array
{
return array_filter([
'googlePlayAnalytics' => $this->googlePlayAnalytics?->jsonSerialize(),
'itunesConnectAnalytics' => $this->iTunesConnectAnalytics?->jsonSerialize(),
], fn(?array $value): bool => $value !== null);
}
}

View File

@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink\AnalyticsInfo;
use JsonSerializable;
/**
* @phpstan-type GooglePlayAnalyticsShape array{
* utmSource?: non-empty-string,
* utmMedium?: non-empty-string,
* utmCampaign?: non-empty-string,
* utmTerm?: non-empty-string,
* utmContent?: non-empty-string,
* gclid?: non-empty-string
* }
*/
final class GooglePlayAnalytics implements JsonSerializable
{
/**
* @param GooglePlayAnalyticsShape $data
*/
private function __construct(private readonly array $data)
{
}
/**
* @param GooglePlayAnalyticsShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
public static function new(): self
{
return new self([]);
}
/**
* Identifies the advertiser, site, publication, etc. that is sending traffic to your property,
* for example: google, newsletter4, billboard.
*
* @see https://support.google.com/analytics/answer/1033863#parameters
*
* @param non-empty-string $utmSource
*/
public function withUtmSource(string $utmSource): self
{
$data = $this->data;
$data['utmSource'] = $utmSource;
return new self($data);
}
/**
* The advertising or marketing medium, for example: cpc, banner, email newsletter.
*
* @see https://support.google.com/analytics/answer/1033863#parameters
*
* @param non-empty-string $utmMedium
*/
public function withUtmMedium(string $utmMedium): self
{
$data = $this->data;
$data['utmMedium'] = $utmMedium;
return new self($data);
}
/**
* The individual campaign name, slogan, promo code, etc. for a product.
*
* @see https://support.google.com/analytics/answer/1033863#parameters
*
* @param non-empty-string $utmCampaign
*/
public function withUtmCampaign(string $utmCampaign): self
{
$data = $this->data;
$data['utmCampaign'] = $utmCampaign;
return new self($data);
}
/**
* Identifies paid search keywords. If you're manually tagging paid keyword campaigns, you should also use
* utm_term to specify the keyword.
*
* @see https://support.google.com/analytics/answer/1033863#parameters
*
* @param non-empty-string $utmTerm
*/
public function withUtmTerm(string $utmTerm): self
{
$data = $this->data;
$data['utmTerm'] = $utmTerm;
return new self($data);
}
/**
* Used to differentiate similar content, or links within the same ad. For example, if you have two call-to-action
* links within the same email message, you can use utm_content and set different values for each, so you can tell
* which version is more effective.
*
* @see https://support.google.com/analytics/answer/1033863#parameters
*
* @param non-empty-string $utmContent
*/
public function withUtmContent(string $utmContent): self
{
$data = $this->data;
$data['utmContent'] = $utmContent;
return new self($data);
}
/**
* The Google Click ID.
*
* @see https://support.google.com/analytics/answer/2938246?hl=en
*
* @param non-empty-string $gclid
*/
public function withGclid(string $gclid): self
{
$data = $this->data;
$data['gclid'] = $gclid;
return new self($data);
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink\AnalyticsInfo;
use JsonSerializable;
/**
* @see https://www.macstories.net/tutorials/a-comprehensive-guide-to-the-itunes-affiliate-program/
* @see https://blog.geni.us/parameter-cheat-sheet-for-itunes-and-app-store-links/
*
* @phpstan-type ITunesConnectAnalyticsShape array{
* at?: non-empty-string,
* ct?: non-empty-string,
* mt?: non-empty-string,
* pt?: non-empty-string
* }
*/
final class ITunesConnectAnalytics implements JsonSerializable
{
/**
* @param ITunesConnectAnalyticsShape $data
*/
private function __construct(private readonly array $data)
{
}
/**
* @param ITunesConnectAnalyticsShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
public static function new(): self
{
return new self([]);
}
/**
* The iTunes connect/affiliate partner token.
*
* @see https://blog.geni.us/parameter-cheat-sheet-for-itunes-and-app-store-links/
*
* @param non-empty-string $affiliateToken
*/
public function withAffiliateToken(string $affiliateToken): self
{
$data = $this->data;
$data['at'] = $affiliateToken;
return new self($data);
}
/**
* The iTunes connect/affiliate partner token.
*
* @see https://blog.geni.us/parameter-cheat-sheet-for-itunes-and-app-store-links/
*
* @param non-empty-string $campaignToken
*/
public function withCampaignToken(string $campaignToken): self
{
$data = $this->data;
$data['ct'] = $campaignToken;
return new self($data);
}
/**
* The media type.
*
* @see https://blog.geni.us/parameter-cheat-sheet-for-itunes-and-app-store-links/
*
* @param non-empty-string $mediaType
*/
public function withMediaType(string $mediaType): self
{
$data = $this->data;
$data['mt'] = $mediaType;
return new self($data);
}
/**
* The provider token.
*
* @see https://www.macstories.net/tutorials/a-comprehensive-guide-to-the-itunes-affiliate-program/
*
* @param non-empty-string $providerToken
*/
public function withProviderToken(string $providerToken): self
{
$data = $this->data;
$data['pt'] = $providerToken;
return new self($data);
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use JsonSerializable;
/**
* @phpstan-type AndroidInfoShape array{
* androidPackageName?: non-empty-string,
* androidFallbackLink?: non-empty-string,
* androidMinPackageVersionCode?: non-empty-string
* }
*/
final class AndroidInfo implements JsonSerializable
{
/**
* @param AndroidInfoShape $data
*/
private function __construct(private readonly array $data)
{
}
/**
* @param AndroidInfoShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
public static function new(): self
{
return new self([]);
}
/**
* The package name of the Android app to use to open the link. The app must be connected to your project from the
* Overview page of the Firebase console. Required for the Dynamic Link to open an Android app.
*
* @param non-empty-string $packageName
*/
public function withPackageName(string $packageName): self
{
$data = $this->data;
$data['androidPackageName'] = $packageName;
return new self($data);
}
/**
* The link to open when the app isn't installed. Specify this to do something other than install your app
* from the Play Store when the app isn't installed, such as open the mobile web version of the content,
* or display a promotional page for your app.
*
* @param non-empty-string $fallbackLink
*/
public function withFallbackLink(string $fallbackLink): self
{
$data = $this->data;
$data['androidFallbackLink'] = $fallbackLink;
return new self($data);
}
/**
* The versionCode of the minimum version of your app that can open the link. If the installed app is an older
* version, the user is taken to the Play Store to upgrade the app.
*
* @see https://developer.android.com/studio/publish/versioning#appversioning
*
* @param non-empty-string $minPackageVersionCode
*/
public function withMinPackageVersionCode(string $minPackageVersionCode): self
{
$data = $this->data;
$data['androidMinPackageVersionCode'] = $minPackageVersionCode;
return new self($data);
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use Beste\Json;
use GuzzleHttp\ClientInterface;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use const JSON_FORCE_OBJECT;
/**
* @internal
*/
final class ApiClient
{
public function __construct(
private readonly ClientInterface $client,
private readonly RequestFactoryInterface $requestFactory,
private readonly StreamFactoryInterface $streamFactory,
) {
}
public function createDynamicLinkRequest(CreateDynamicLink $action): RequestInterface
{
return $this->requestFactory
->createRequest('POST', 'https://firebasedynamiclinks.googleapis.com/v1/shortLinks')
->withBody($this->streamFactory->createStream(Json::encode($action, JSON_FORCE_OBJECT)))
->withHeader('Content-Type', 'application/json; charset=UTF-8')
;
}
public function createStatisticsRequest(GetStatisticsForDynamicLink $action): RequestInterface
{
$url = sprintf(
'https://firebasedynamiclinks.googleapis.com/v1/%s/linkStats?durationDays=%d',
rawurlencode($action->dynamicLink()),
$action->durationInDays(),
);
return $this->requestFactory
->createRequest('GET', $url)
->withHeader('Content-Type', 'application/json; charset=UTF-8')
;
}
public function createShortenLinkRequest(ShortenLongDynamicLink $action): RequestInterface
{
return $this->requestFactory
->createRequest('POST', 'https://firebasedynamiclinks.googleapis.com/v1/shortLinks')
->withBody($this->streamFactory->createStream(Json::encode($action, JSON_FORCE_OBJECT)))
->withHeader('Content-Type', 'application/json; charset=UTF-8')
;
}
/**
* @param array<string, mixed> $options
*
* @throws ClientExceptionInterface
*/
public function send(RequestInterface $request, array $options = []): ResponseInterface
{
return $this->client->send($request, $options);
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use Beste\Json;
use JsonSerializable;
use Kreait\Firebase\Value\Url;
use Stringable;
/**
* @phpstan-import-type AnalyticsInfoShape from AnalyticsInfo
* @phpstan-import-type AndroidInfoShape from AndroidInfo
* @phpstan-import-type IOSInfoShape from IOSInfo
* @phpstan-import-type NavigationInfoShape from NavigationInfo
* @phpstan-import-type SocialMetaTagInfoShape from SocialMetaTagInfo
*
* @phpstan-type CreateDynamicLinkShape array{
* dynamicLinkInfo: array{
* link?: non-empty-string,
* domainUriPrefix?: non-empty-string,
* analyticsInfo?: AnalyticsInfoShape,
* androidInfo?: AndroidInfoShape,
* iosInfo?: IOSInfoShape,
* navigationInfo?: NavigationInfoShape,
* socialMetaTagInfo?: SocialMetaTagInfoShape
* },
* suffix: array{
* option: self::WITH_*
* }
* }
*/
final class CreateDynamicLink implements JsonSerializable
{
public const WITH_UNGUESSABLE_SUFFIX = 'UNGUESSABLE';
public const WITH_SHORT_SUFFIX = 'SHORT';
/**
* @param CreateDynamicLinkShape $data
*/
private function __construct(private readonly array $data)
{
}
/**
* @param CreateDynamicLinkShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
public static function new(): self
{
return new self([
'dynamicLinkInfo' => [],
'suffix' => ['option' => self::WITH_UNGUESSABLE_SUFFIX],
]);
}
/**
* The link your app will open. Specify a URL that your app can handle, typically the app's content
* or payload, which initiates app-specific logic (such as crediting the user with a coupon or
* displaying a welcome screen). This link must be a well-formatted URL, be properly
* URL-encoded, use either HTTP or HTTPS, and cannot be another Dynamic Link.
*/
public static function forUrl(Stringable|string $url): self
{
return new self([
'dynamicLinkInfo' => [
'link' => Url::fromString($url)->value,
],
'suffix' => ['option' => self::WITH_UNGUESSABLE_SUFFIX],
]);
}
public function withDynamicLinkDomain(Stringable|string $dynamicLinkDomain): self
{
$data = $this->data;
$data['dynamicLinkInfo']['domainUriPrefix'] = Url::fromString($dynamicLinkDomain)->value;
return new self($data);
}
public function hasDynamicLinkDomain(): bool
{
return (bool) ($this->data['dynamicLinkInfo']['domainUriPrefix'] ?? null);
}
/**
* @param AnalyticsInfo|AnalyticsInfoShape $data
*/
public function withAnalyticsInfo(AnalyticsInfo|array $data): self
{
$info = $data instanceof AnalyticsInfo ? $data : AnalyticsInfo::fromArray($data);
$data = $this->data;
$data['dynamicLinkInfo']['analyticsInfo'] = Json::decode(Json::encode($info), true);
return new self($data);
}
/**
* @param AndroidInfo|AndroidInfoShape $data
*/
public function withAndroidInfo(AndroidInfo|array $data): self
{
$info = $data instanceof AndroidInfo ? $data : AndroidInfo::fromArray($data);
$data = $this->data;
$data['dynamicLinkInfo']['androidInfo'] = Json::decode(Json::encode($info), true);
return new self($data);
}
/**
* @param IOSInfo|IOSInfoShape $data
*/
public function withIOSInfo(IOSInfo|array $data): self
{
$info = $data instanceof IOSInfo ? $data : IOSInfo::fromArray($data);
$data = $this->data;
$data['dynamicLinkInfo']['iosInfo'] = Json::decode(Json::encode($info), true);
return new self($data);
}
/**
* @param NavigationInfo|NavigationInfoShape $data
*/
public function withNavigationInfo(NavigationInfo|array $data): self
{
$info = $data instanceof NavigationInfo ? $data : NavigationInfo::fromArray($data);
$data = $this->data;
$data['dynamicLinkInfo']['navigationInfo'] = Json::decode(Json::encode($info), true);
return new self($data);
}
/**
* @param SocialMetaTagInfo|SocialMetaTagInfoShape $data
*/
public function withSocialMetaTagInfo(SocialMetaTagInfo|array $data): self
{
$info = $data instanceof SocialMetaTagInfo ? $data : SocialMetaTagInfo::fromArray($data);
$data = $this->data;
$data['dynamicLinkInfo']['socialMetaTagInfo'] = Json::decode(Json::encode($info), true);
return new self($data);
}
public function withUnguessableSuffix(): self
{
$data = $this->data;
$data['suffix']['option'] = self::WITH_UNGUESSABLE_SUFFIX;
return new self($data);
}
public function withShortSuffix(): self
{
$data = $this->data;
$data['suffix']['option'] = self::WITH_SHORT_SUFFIX;
return new self($data);
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink\CreateDynamicLink;
use Beste\Json;
use Kreait\Firebase\DynamicLink\CreateDynamicLink;
use Kreait\Firebase\Exception\RuntimeException;
use Psr\Http\Message\ResponseInterface;
use UnexpectedValueException;
final class FailedToCreateDynamicLink extends RuntimeException
{
private ?CreateDynamicLink $action = null;
private ?ResponseInterface $response = null;
public static function withActionAndResponse(CreateDynamicLink $action, ResponseInterface $response): self
{
$fallbackMessage = 'Failed to create dynamic link';
try {
$message = Json::decode((string) $response->getBody(), true)['error']['message'] ?? $fallbackMessage;
} catch (UnexpectedValueException) {
$message = $fallbackMessage;
}
$error = new self($message);
$error->action = $action;
$error->response = $response;
return $error;
}
public function action(): ?CreateDynamicLink
{
return $this->action;
}
public function response(): ?ResponseInterface
{
return $this->response;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use Beste\Json;
use Psr\Http\Message\ResponseInterface;
final class DynamicLinkStatistics
{
/**
* @var array<string, list<array<string, string>>>
*/
private array $rawData = [];
private EventStatistics $events;
private function __construct()
{
$this->events = EventStatistics::fromArray([]);
}
/**
* @internal
*/
public static function fromApiResponse(ResponseInterface $response): self
{
$data = Json::decode((string) $response->getBody(), true);
$link = new self();
$link->rawData = $data;
$link->events = EventStatistics::fromArray($data['linkEventStats'] ?? []);
return $link;
}
public function eventStatistics(): EventStatistics
{
return $this->events;
}
/**
* @return array<string, list<array<string, string>>>
*/
public function rawData(): array
{
return $this->rawData;
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use Countable;
use IteratorAggregate;
use Traversable;
use function array_filter;
/**
* @see https://firebase.google.com/docs/reference/dynamic-links/analytics#response_body
* @see https://github.com/googleapis/google-api-nodejs-client/blob/main/src/apis/firebasedynamiclinks/v1.ts
*
* @phpstan-type EventStatisticsShape array{
* linkEventStats: array{
* count?: non-empty-string,
* event?: non-empty-string,
* platform?: non-empty-string
* }
* }
*
* @implements IteratorAggregate<array>
*/
final class EventStatistics implements Countable, IteratorAggregate
{
public const PLATFORM_ANDROID = 'ANDROID';
public const PLATFORM_DESKTOP = 'DESKTOP';
public const PLATFORM_IOS = 'IOS';
// Any click on a Dynamic Link, irrespective to how it is handled and its destinations
public const TYPE_CLICK = 'CLICK';
// Attempts to redirect users, either to the App Store or Play Store to install or update the app,
// or to some other destination
public const TYPE_REDIRECT = 'REDIRECT';
// Actual installs (only supported by the Play Store)
public const TYPE_APP_INSTALL = 'APP_INSTALL';
// First-opens after an install
public const TYPE_APP_FIRST_OPEN = 'APP_FIRST_OPEN';
// Re-opens of an app
public const TYPE_APP_RE_OPEN = 'APP_RE_OPEN';
/**
* @param list<EventStatisticsShape> $events
*/
private function __construct(private readonly array $events)
{
}
/**
* @param list<EventStatisticsShape> $events
*/
public static function fromArray(array $events): self
{
return new self($events);
}
public function onAndroid(): self
{
return $this->filterByPlatform(self::PLATFORM_ANDROID);
}
public function onDesktop(): self
{
return $this->filterByPlatform(self::PLATFORM_DESKTOP);
}
public function onIOS(): self
{
return $this->filterByPlatform(self::PLATFORM_IOS);
}
public function clicks(): self
{
return $this->filterByType(self::TYPE_CLICK);
}
public function redirects(): self
{
return $this->filterByType(self::TYPE_REDIRECT);
}
public function appInstalls(): self
{
return $this->filterByType(self::TYPE_APP_INSTALL);
}
public function appFirstOpens(): self
{
return $this->filterByType(self::TYPE_APP_FIRST_OPEN);
}
public function appReOpens(): self
{
return $this->filterByType(self::TYPE_APP_RE_OPEN);
}
public function filterByType(string $type): self
{
return $this->filter(static fn(array $event): bool => ($event['event'] ?? null) === $type);
}
public function filterByPlatform(string $platform): self
{
return $this->filter(static fn(array $event): bool => ($event['platform'] ?? null) === $platform);
}
public function filter(callable $filter): self
{
return new self(array_values(array_filter($this->events, $filter)));
}
/**
* @return Traversable<EventStatisticsShape>
*/
public function getIterator(): Traversable
{
yield from $this->events;
}
public function count(): int
{
return array_sum(array_column($this->events, 'count'));
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use Kreait\Firebase\Value\Url;
use Stringable;
final class GetStatisticsForDynamicLink
{
public const DEFAULT_DURATION_IN_DAYS = 7;
private int $durationInDays = self::DEFAULT_DURATION_IN_DAYS;
private function __construct(private readonly string $dynamicLink)
{
}
public static function forLink(Stringable|string $link): self
{
return new self(Url::fromString($link)->value);
}
public function withDurationInDays(int $durationInDays): self
{
$action = clone $this;
$action->durationInDays = $durationInDays;
return $action;
}
public function dynamicLink(): string
{
return $this->dynamicLink;
}
public function durationInDays(): int
{
return $this->durationInDays;
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink;
use Fig\Http\Message\StatusCodeInterface as StatusCode;
use Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink;
use Kreait\Firebase\Exception\RuntimeException;
use Psr\Http\Message\ResponseInterface;
final class FailedToGetStatisticsForDynamicLink extends RuntimeException
{
private ?GetStatisticsForDynamicLink $action = null;
private ?ResponseInterface $response = null;
public static function withActionAndResponse(GetStatisticsForDynamicLink $action, ResponseInterface $response): self
{
['code' => $code, 'message' => $message] = self::getCodeAndMessageFromResponse($response);
$error = new self($message, $code);
$error->action = $action;
$error->response = $response;
return $error;
}
public function action(): ?GetStatisticsForDynamicLink
{
return $this->action;
}
public function response(): ?ResponseInterface
{
return $this->response;
}
/**
* @return array{
* code: int,
* message: string
* }
*/
private static function getCodeAndMessageFromResponse(ResponseInterface $response): array
{
$message = match ($code = $response->getStatusCode()) {
StatusCode::STATUS_FORBIDDEN => <<<'MSG'
Firebase reported missing permissions to access the statistics
for the requested Dynamic Link. Please make sure that the
Google Dynamic Links API is enabled for your project at
https://console.cloud.google.com/apis/library/firebasedynamiclinks.googleapis.com
If the API is enabled, you or the Service Account you're using
might be missing the required permissions. You can check by
visiting
https://console.cloud.google.com/iam-admin/serviceaccounts/
and making sure that the Service Account has one of the following roles
- Firebase Admin
- Firebase Dynamic Links Viewer
- Firebase Dynamic Links Admin
MSG,
default => <<<'MSG'
Failed to get statistics for dynamic link. Please inspect the
response for further details.
If the response type is not covered by the SDK, please create
a new issue in the SDK's GitHub repository and include the
HTTP Status code (`$response->getStatusCode()`) and the
message body (`$response->getBody()->getContents()`)
MSG,
};
return [
'code' => $code,
'message' => $message,
];
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use JsonSerializable;
/**
* @phpstan-type IOSInfoShape array{
* iosBundleId?: non-empty-string,
* iosFallbackLink?: non-empty-string,
* iosCustomScheme?: non-empty-string,
* iosIpadFallbackLink?: non-empty-string,
* iosIpadBundleId?: non-empty-string,
* iosAppStoreId?: non-empty-string
* }
*/
final class IOSInfo implements JsonSerializable
{
/**
* @param IOSInfoShape $data
*/
private function __construct(private readonly array $data)
{
}
/**
* @param IOSInfoShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
public static function new(): self
{
return new self([]);
}
/**
* The bundle ID of the iOS app to use to open the link. The app must be connected to your project from the
* Overview page of the Firebase console. Required for the Dynamic Link to open an iOS app.
*
* @param non-empty-string $bundleId
*/
public function withBundleId(string $bundleId): self
{
$data = $this->data;
$data['iosBundleId'] = $bundleId;
return new self($data);
}
/**
* The link to open when the app isn't installed. Specify this to do something other than install your app from the
* App Store when the app isn't installed, such as open the mobile web version of the content, or display a
* promotional page for your app.
*
* @param non-empty-string $fallbackLink
*/
public function withFallbackLink(string $fallbackLink): self
{
$data = $this->data;
$data['iosFallbackLink'] = $fallbackLink;
return new self($data);
}
/**
* Your app's custom URL scheme, if defined to be something other than your app's bundle ID.
*
* @param non-empty-string $customScheme
*/
public function withCustomScheme(string $customScheme): self
{
$data = $this->data;
$data['iosCustomScheme'] = $customScheme;
return new self($data);
}
/**
* The link to open on iPads when the app isn't installed. Specify this to do something other than install your
* app from the App Store when the app isn't installed, such as open the web version of the content, or
* display a promotional page for your app.
*
* @param non-empty-string $ipadFallbackLink
*/
public function withIPadFallbackLink(string $ipadFallbackLink): self
{
$data = $this->data;
$data['iosIpadFallbackLink'] = $ipadFallbackLink;
return new self($data);
}
/**
* The bundle ID of the iOS app to use on iPads to open the link. The app must be connected to your project from
* the Overview page of the Firebase console.
*
* @param non-empty-string $iPadBundleId
*/
public function withIPadBundleId(string $iPadBundleId): self
{
$data = $this->data;
$data['iosIpadBundleId'] = $iPadBundleId;
return new self($data);
}
/**
* Your app's App Store ID, used to send users to the App Store when the app isn't installed.
*
* @param non-empty-string $appStoreId
*/
public function withAppStoreId(string $appStoreId): self
{
$data = $this->data;
$data['iosAppStoreId'] = $appStoreId;
return new self($data);
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use JsonSerializable;
/**
* @phpstan-type NavigationInfoShape array{
* enableForcedRedirect?: bool
* }
*/
final class NavigationInfo implements JsonSerializable
{
/**
* @param NavigationInfoShape $data
*/
private function __construct(private readonly array $data)
{
}
/**
* @param NavigationInfoShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
public static function new(): self
{
return new self([]);
}
/**
* If set, skip the app preview page when the Dynamic Link is opened, and instead redirect to the app
* or store. The app preview page (enabled by default) can more reliably send users to the most appropriate
* destination when they open Dynamic Links in apps; however, if you expect a Dynamic Link to be opened
* only in apps that can open Dynamic Links reliably without this page, you can disable it with this
* parameter. Note: the app preview page is only shown on iOS currently, but may eventually be
* shown on Android. This parameter will affect the behavior of the Dynamic Link on both
* platforms.
*/
public function withForcedRedirect(): self
{
$data = $this->data;
$data['enableForcedRedirect'] = true;
return new self($data);
}
/**
* @see withForcedRedirect()
*/
public function withoutForcedRedirect(): self
{
$data = $this->data;
unset($data['enableForcedRedirect']);
return new self($data);
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use JsonSerializable;
use Kreait\Firebase\Value\Url;
use Stringable;
/**
* @phpstan-type ShortenLongDynamicLinkShape array{
* longDynamicLink: non-empty-string,
* suffix: array{
* option: self::WITH*
* }
* }
*/
final class ShortenLongDynamicLink implements JsonSerializable
{
public const WITH_UNGUESSABLE_SUFFIX = 'UNGUESSABLE';
public const WITH_SHORT_SUFFIX = 'SHORT';
/**
* @param ShortenLongDynamicLinkShape $data
*/
private function __construct(private readonly array $data)
{
}
/**
* The long dynamic link that has been created as described in {@see https://firebase.google.com/docs/dynamic-links/create-manually}.
*/
public static function forLongDynamicLink(Stringable|string $url): self
{
return new self([
'longDynamicLink' => Url::fromString($url)->value,
'suffix' => ['option' => self::WITH_UNGUESSABLE_SUFFIX],
]);
}
/**
* @param ShortenLongDynamicLinkShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
public function withUnguessableSuffix(): self
{
$data = $this->data;
$data['suffix']['option'] = self::WITH_UNGUESSABLE_SUFFIX;
return new self($data);
}
public function withShortSuffix(): self
{
$data = $this->data;
$data['suffix']['option'] = self::WITH_SHORT_SUFFIX;
return new self($data);
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink\ShortenLongDynamicLink;
use Beste\Json;
use Kreait\Firebase\DynamicLink\ShortenLongDynamicLink;
use Kreait\Firebase\Exception\RuntimeException;
use Psr\Http\Message\ResponseInterface;
use UnexpectedValueException;
final class FailedToShortenLongDynamicLink extends RuntimeException
{
private ?ShortenLongDynamicLink $action = null;
private ?ResponseInterface $response = null;
public static function withActionAndResponse(ShortenLongDynamicLink $action, ResponseInterface $response): self
{
$fallbackMessage = 'Failed to shorten long dynamic link';
try {
$message = Json::decode((string) $response->getBody(), true)['error']['message'] ?? $fallbackMessage;
} catch (UnexpectedValueException) {
$message = $fallbackMessage;
}
$error = new self($message);
$error->action = $action;
$error->response = $response;
return $error;
}
public function action(): ?ShortenLongDynamicLink
{
return $this->action;
}
public function response(): ?ResponseInterface
{
return $this->response;
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\DynamicLink;
use JsonSerializable;
/**
* @phpstan-type SocialMetaTagInfoShape array{
* socialTitle?: non-empty-string,
* socialDescription?: non-empty-string,
* socialImageLink?: non-empty-string
* }
*/
final class SocialMetaTagInfo implements JsonSerializable
{
/**
* @param SocialMetaTagInfoShape $data
*/
private function __construct(private readonly array $data)
{
}
/**
* @param SocialMetaTagInfoShape $data
*/
public static function fromArray(array $data): self
{
return new self($data);
}
public static function new(): self
{
return new self([]);
}
/**
* The title to use when the Dynamic Link is shared in a social post.
*
* @param non-empty-string $title
*/
public function withTitle(string $title): self
{
$data = $this->data;
$data['socialTitle'] = $title;
return new self($data);
}
/**
* The description to use when the Dynamic Link is shared in a social post.
*
* @param non-empty-string $description
*/
public function withDescription(string $description): self
{
$data = $this->data;
$data['socialDescription'] = $description;
return new self($data);
}
/**
* The URL to an image related to this link.
*
* @param non-empty-string $imageLink
*/
public function withImageLink(string $imageLink): self
{
$data = $this->data;
$data['socialImageLink'] = $imageLink;
return new self($data);
}
public function jsonSerialize(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase;
use InvalidArgumentException;
use Kreait\Firebase\DynamicLink\ApiClient;
use Kreait\Firebase\DynamicLink\CreateDynamicLink;
use Kreait\Firebase\DynamicLink\CreateDynamicLink\FailedToCreateDynamicLink;
use Kreait\Firebase\DynamicLink\DynamicLinkStatistics;
use Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink;
use Kreait\Firebase\DynamicLink\GetStatisticsForDynamicLink\FailedToGetStatisticsForDynamicLink;
use Kreait\Firebase\DynamicLink\ShortenLongDynamicLink;
use Kreait\Firebase\DynamicLink\ShortenLongDynamicLink\FailedToShortenLongDynamicLink;
use Kreait\Firebase\Value\Url;
use Psr\Http\Client\ClientExceptionInterface;
use Stringable;
use function is_array;
/**
* @internal
*
* @deprecated 7.14.0 Firebase Dynamic Links is deprecated and should not be used in new projects. The service will
* shut down on August 25, 2025. The component will remain in the SDK until then, but as the
* Firebase service is deprecated, this component is also deprecated
*
* @see https://firebase.google.com/support/dynamic-links-faq Dynamic Links Deprecation FAQ
*
* @phpstan-import-type CreateDynamicLinkShape from CreateDynamicLink
* @phpstan-import-type ShortenLongDynamicLinkShape from ShortenLongDynamicLink
*/
final class DynamicLinks implements Contract\DynamicLinks
{
/**
* @param non-empty-string|null $defaultDynamicLinksDomain
*/
private function __construct(
private readonly ?string $defaultDynamicLinksDomain,
private readonly ApiClient $apiClient,
) {
}
public static function withApiClient(ApiClient $apiClient): self
{
return new self(null, $apiClient);
}
/**
* @param Stringable|non-empty-string $dynamicLinksDomain
*/
public static function withApiClientAndDefaultDomain(ApiClient $apiClient, Stringable|string $dynamicLinksDomain): self
{
$domainUrl = Url::fromString($dynamicLinksDomain)->value;
return new self($domainUrl, $apiClient);
}
public function createUnguessableLink($url): DynamicLink
{
return $this->createDynamicLink($url, CreateDynamicLink::WITH_UNGUESSABLE_SUFFIX);
}
public function createShortLink($url): DynamicLink
{
return $this->createDynamicLink($url, CreateDynamicLink::WITH_SHORT_SUFFIX);
}
public function createDynamicLink($actionOrParametersOrUrl, ?string $suffixType = null): DynamicLink
{
$action = $this->ensureCreateAction($actionOrParametersOrUrl);
if ($this->defaultDynamicLinksDomain !== null && $action->hasDynamicLinkDomain() === false) {
$action = $action->withDynamicLinkDomain($this->defaultDynamicLinksDomain);
}
if ($suffixType === CreateDynamicLink::WITH_SHORT_SUFFIX) {
$action = $action->withShortSuffix();
} elseif ($suffixType === CreateDynamicLink::WITH_UNGUESSABLE_SUFFIX) {
$action = $action->withUnguessableSuffix();
}
$request = $this->apiClient->createDynamicLinkRequest($action);
try {
$response = $this->apiClient->send($request, ['http_errors' => false]);
} catch (ClientExceptionInterface $e) {
throw new FailedToCreateDynamicLink('Failed to create dynamic link: '.$e->getMessage(), $e->getCode(), $e);
}
if ($response->getStatusCode() === 200) {
return DynamicLink::fromApiResponse($response);
}
throw FailedToCreateDynamicLink::withActionAndResponse($action, $response);
}
public function shortenLongDynamicLink($longDynamicLinkOrAction, ?string $suffixType = null): DynamicLink
{
$action = $this->ensureShortenAction($longDynamicLinkOrAction);
if ($suffixType === ShortenLongDynamicLink::WITH_SHORT_SUFFIX) {
$action = $action->withShortSuffix();
} elseif ($suffixType === ShortenLongDynamicLink::WITH_UNGUESSABLE_SUFFIX) {
$action = $action->withUnguessableSuffix();
}
$request = $this->apiClient->createShortenLinkRequest($action);
try {
$response = $this->apiClient->send($request, ['http_errors' => false]);
} catch (ClientExceptionInterface $e) {
throw new FailedToShortenLongDynamicLink('Failed to shorten long dynamic link: '.$e->getMessage(), $e->getCode(), $e);
}
if ($response->getStatusCode() === 200) {
return DynamicLink::fromApiResponse($response);
}
throw FailedToShortenLongDynamicLink::withActionAndResponse($action, $response);
}
public function getStatistics(Stringable|string|GetStatisticsForDynamicLink $dynamicLinkOrAction, ?int $durationInDays = null): DynamicLinkStatistics
{
$action = $this->ensureGetStatisticsAction($dynamicLinkOrAction);
if ($durationInDays !== null && $durationInDays < 1) {
throw new InvalidArgumentException('The duration in days must be a positive integer');
}
if ($durationInDays !== null) {
$action = $action->withDurationInDays($durationInDays);
}
$request = $this->apiClient->createStatisticsRequest($action);
try {
$response = $this->apiClient->send($request, ['http_errors' => false]);
} catch (ClientExceptionInterface $e) {
throw new FailedToGetStatisticsForDynamicLink('Failed to get statistics for Dynamic Link: '.$e->getMessage(), $e->getCode(), $e);
}
if ($response->getStatusCode() === 200) {
return DynamicLinkStatistics::fromApiResponse($response);
}
throw FailedToGetStatisticsForDynamicLink::withActionAndResponse($action, $response);
}
/**
* @param Stringable|non-empty-string|CreateDynamicLink|CreateDynamicLinkShape $actionOrParametersOrUrl
*/
private function ensureCreateAction(Stringable|string|CreateDynamicLink|array $actionOrParametersOrUrl): CreateDynamicLink
{
if (is_array($actionOrParametersOrUrl)) {
return CreateDynamicLink::fromArray($actionOrParametersOrUrl);
}
if ($actionOrParametersOrUrl instanceof CreateDynamicLink) {
return $actionOrParametersOrUrl;
}
return CreateDynamicLink::forUrl((string) $actionOrParametersOrUrl);
}
/**
* @param Stringable|non-empty-string|ShortenLongDynamicLink|ShortenLongDynamicLinkShape $actionOrParametersOrUrl
*/
private function ensureShortenAction(Stringable|string|ShortenLongDynamicLink|array $actionOrParametersOrUrl): ShortenLongDynamicLink
{
if (is_array($actionOrParametersOrUrl)) {
return ShortenLongDynamicLink::fromArray($actionOrParametersOrUrl);
}
if ($actionOrParametersOrUrl instanceof ShortenLongDynamicLink) {
return $actionOrParametersOrUrl;
}
return ShortenLongDynamicLink::forLongDynamicLink((string) $actionOrParametersOrUrl);
}
/**
* @throw InvalidArgumentException
*/
private function ensureGetStatisticsAction(Stringable|string|GetStatisticsForDynamicLink $actionOrUrl): GetStatisticsForDynamicLink
{
if ($actionOrUrl instanceof GetStatisticsForDynamicLink) {
return $actionOrUrl;
}
$actionOrUrl = trim((string) $actionOrUrl);
if ($actionOrUrl === '') {
throw new InvalidArgumentException('A dynamic link must not be empty');
}
return GetStatisticsForDynamicLink::forLink($actionOrUrl);
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Exception\AppCheck;
use Kreait\Firebase\Exception\AppCheckException;
use Kreait\Firebase\Exception\HasErrors;
use Kreait\Firebase\Exception\RuntimeException;
final class ApiConnectionFailed extends RuntimeException implements AppCheckException
{
use HasErrors;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Kreait\Firebase\Exception\AppCheck;
use Kreait\Firebase\Exception\AppCheckException;
use Kreait\Firebase\Exception\RuntimeException;
final class AppCheckError extends RuntimeException implements AppCheckException
{
}

Some files were not shown because too many files have changed in this diff Show More