77use Beste \Cache \InMemoryCache ;
88use Beste \Clock \SystemClock ;
99use Beste \Clock \WrappingClock ;
10- use Beste \Json ;
1110use Firebase \JWT \CachedKeySet ;
1211use Google \Auth \ApplicationDefaultCredentials ;
1312use Google \Auth \Credentials \ServiceAccountCredentials ;
3837use Kreait \Firebase \JWT \SessionCookieVerifier ;
3938use Kreait \Firebase \Messaging \AppInstanceApiClient ;
4039use Kreait \Firebase \Messaging \RequestFactory ;
40+ use Kreait \Firebase \Valinor \Mapper ;
41+ use Kreait \Firebase \Valinor \Normalizer ;
42+ use Kreait \Firebase \Valinor \Source ;
4143use Psr \Cache \CacheItemPoolInterface ;
4244use Psr \Clock \ClockInterface ;
4345use Psr \Http \Message \UriInterface ;
4446use Psr \Log \LoggerInterface ;
4547use Psr \Log \LogLevel ;
4648use Stringable ;
4749use Throwable ;
48- use UnexpectedValueException ;
4950
5051use function array_filter ;
5152use function is_string ;
5253use function sprintf ;
5354use function trim ;
5455
55- /**
56- * @phpstan-type ServiceAccountShape array{
57- * project_id: non-empty-string,
58- * client_email: non-empty-string,
59- * private_key: non-empty-string,
60- * type: 'service_account'
61- * }
62- */
6356final class Factory
6457{
6558 public const API_CLIENT_SCOPES = [
@@ -83,10 +76,7 @@ final class Factory
8376 */
8477 private ?string $ defaultStorageBucket = null ;
8578
86- /**
87- * @var ServiceAccountShape|null
88- */
89- private ?array $ serviceAccount = null ;
79+ private ?ServiceAccount $ serviceAccount = null ;
9080
9181 private ?FetchAuthTokenInterface $ googleAuthTokenCredentials = null ;
9282
@@ -134,6 +124,14 @@ final class Factory
134124 */
135125 private array $ firestoreClientConfig = [];
136126
127+ private mixed $ mapperCache = null ;
128+
129+ private mixed $ normalizerCache = null ;
130+
131+ private ?Mapper $ mapper = null ;
132+
133+ private ?Normalizer $ normalizer = null ;
134+
137135 public function __construct ()
138136 {
139137 $ this ->clock = SystemClock::create ();
@@ -144,40 +142,16 @@ public function __construct()
144142 }
145143
146144 /**
147- * @param non-empty-string|ServiceAccountShape $value
145+ * @param string|array<mixed> $value
146+ *
147+ * @throws InvalidArgumentException
148148 */
149149 public function withServiceAccount (string |array $ value ): self
150150 {
151- if (is_string ($ value ) && str_starts_with ($ value , '{ ' )) {
152- try {
153- /** @var ServiceAccountShape $serviceAccount */
154- $ serviceAccount = Json::decode ($ value , true );
155- } catch (UnexpectedValueException $ e ) {
156- throw new InvalidArgumentException ('Invalid service account: ' .$ e ->getMessage (), $ e ->getCode (), $ e );
157- }
158-
159- $ factory = clone $ this ;
160- $ factory ->serviceAccount = $ serviceAccount ;
161-
162- return $ factory ;
163- }
164-
165- if (is_string ($ value )) {
166- try {
167- /** @var ServiceAccountShape $serviceAccount */
168- $ serviceAccount = Json::decodeFile ($ value , true );
169-
170- $ factory = clone $ this ;
171- $ factory ->serviceAccount = $ serviceAccount ;
172-
173- return $ factory ;
174- } catch (UnexpectedValueException $ e ) {
175- throw new InvalidArgumentException ('Invalid service account: ' .$ e ->getMessage (), $ e ->getCode (), $ e );
176- }
177- }
151+ $ serviceAccount = $ this ->mapServiceAccount ($ value );
178152
179153 $ factory = clone $ this ;
180- $ factory ->serviceAccount = $ value ;
154+ $ factory ->serviceAccount = $ serviceAccount ;
181155
182156 return $ factory ;
183157 }
@@ -308,6 +282,24 @@ public function withKeySetCache(CacheItemPoolInterface $cache): self
308282 return $ factory ;
309283 }
310284
285+ public function withMapperCache (mixed $ cache ): self
286+ {
287+ $ factory = clone $ this ;
288+ $ factory ->mapperCache = $ cache ;
289+ $ factory ->mapper = null ;
290+
291+ return $ factory ;
292+ }
293+
294+ public function withNormalizerCache (mixed $ cache ): self
295+ {
296+ $ factory = clone $ this ;
297+ $ factory ->normalizerCache = $ cache ;
298+ $ factory ->normalizer = null ;
299+
300+ return $ factory ;
301+ }
302+
311303 public function withHttpClientOptions (HttpClientOptions $ options ): self
312304 {
313305 $ factory = clone $ this ;
@@ -385,8 +377,8 @@ public function createAppCheck(): Contract\AppCheck
385377 return new AppCheck (
386378 new AppCheck \ApiClient ($ http ),
387379 new AppCheckTokenGenerator (
388- $ serviceAccount[ ' client_email ' ] ,
389- $ serviceAccount[ ' private_key ' ] ,
380+ $ serviceAccount-> clientEmail ,
381+ $ serviceAccount-> privateKey ,
390382 $ this ->clock ,
391383 ),
392384 new AppCheckTokenVerifier ($ projectId , $ keySet ),
@@ -519,16 +511,7 @@ public function createStorage(): Contract\Storage
519511 * @deprecated 7.20.0
520512 * @codeCoverageIgnore
521513 *
522- * @return array{
523- * credentialsType: string|null,
524- * databaseUrl: string,
525- * defaultStorageBucket: string|null,
526- * serviceAccount: string|array<string, string>|null,
527- * projectId: string,
528- * tenantId: non-empty-string|null,
529- * tokenCacheType: class-string,
530- * verifierCacheType: class-string,
531- * }
514+ * @return array<mixed>
532515 */
533516 public function getDebugInfo (): array
534517 {
@@ -616,13 +599,7 @@ public function createApiClient(?array $config = null, ?array $middlewares = nul
616599 }
617600
618601 /**
619- * @return array{
620- * projectId: non-empty-string,
621- * authCache: CacheItemPoolInterface,
622- * credentialsFetcher?: FetchAuthTokenInterface,
623- * keyFile?: ServiceAccountShape,
624- * keyFilePath?: non-empty-string
625- * }
602+ * @return array<non-empty-string, mixed>
626603 */
627604 private function googleCloudClientConfig (): array
628605 {
@@ -638,7 +615,7 @@ private function googleCloudClientConfig(): array
638615
639616 $ serviceAccount = $ this ->getServiceAccount ();
640617 if ($ serviceAccount !== null ) {
641- $ config ['keyFile ' ] = $ serviceAccount ;
618+ $ config ['keyFile ' ] = $ this -> normalizeServiceAccount ( $ serviceAccount) ;
642619 }
643620
644621 return $ config ;
@@ -658,6 +635,8 @@ private function getProjectId(): string
658635 ? $ credentials ->getProjectId ()
659636 : Util::getenv ('GOOGLE_CLOUD_PROJECT ' );
660637
638+ $ projectId ??= $ this ->getServiceAccount ()?->projectId;
639+
661640 if (is_string ($ projectId ) && $ projectId !== '' ) {
662641 return $ this ->projectId = $ projectId ;
663642 }
@@ -670,23 +649,15 @@ private function getProjectId(): string
670649 */
671650 private function getDatabaseUrl (): string
672651 {
673- if ($ this ->databaseUrl === null ) {
674- $ this ->databaseUrl = sprintf ('https://%s.firebaseio.com ' , $ this ->getProjectId ());
675- }
676-
677- return $ this ->databaseUrl ;
652+ return $ this ->databaseUrl ??= sprintf ('https://%s.firebaseio.com ' , $ this ->getProjectId ());
678653 }
679654
680655 /**
681656 * @return non-empty-string
682657 */
683658 private function getStorageBucketName (): string
684659 {
685- if ($ this ->defaultStorageBucket === null ) {
686- $ this ->defaultStorageBucket = sprintf ('%s.appspot.com ' , $ this ->getProjectId ());
687- }
688-
689- return $ this ->defaultStorageBucket ;
660+ return $ this ->defaultStorageBucket ??= sprintf ('%s.appspot.com ' , $ this ->getProjectId ());
690661 }
691662
692663 private function createCustomTokenGenerator (): ?CustomTokenViaGoogleCredentials
@@ -711,46 +682,49 @@ private function createIdTokenVerifier(): IdTokenVerifier
711682 return $ verifier ->withExpectedTenantId ($ this ->tenantId );
712683 }
713684
714- private function createSessionCookieVerifier (): SessionCookieVerifier
685+ private function getMapper (): Mapper
715686 {
716- return SessionCookieVerifier:: createWithProjectIdAndCache ( $ this ->getProjectId (), $ this -> verifierCache ?? $ this ->defaultCache );
687+ return $ this ->mapper ??= new Mapper ( $ this ->mapperCache );
717688 }
718689
719- /**
720- * @return ServiceAccountShape|null
721- */
722- private function getServiceAccount (): ?array
690+ private function getNormalizer (): Normalizer
723691 {
724- if ( $ this ->serviceAccount === null ) {
725- $ googleApplicationCredentials = Util:: getenv ( ' GOOGLE_APPLICATION_CREDENTIALS ' );
692+ return $ this ->normalizer ??= new Normalizer ( $ this -> normalizerCache );
693+ }
726694
727- if ($ googleApplicationCredentials === null ) {
728- return null ;
729- }
695+ private function createSessionCookieVerifier (): SessionCookieVerifier
696+ {
697+ return SessionCookieVerifier::createWithProjectIdAndCache ($ this ->getProjectId (), $ this ->verifierCache ?? $ this ->defaultCache );
698+ }
730699
731- if (!str_starts_with ($ googleApplicationCredentials , '{ ' )) {
732- return null ;
733- }
700+ private function getServiceAccount (): ?ServiceAccount
701+ {
702+ if ($ this ->serviceAccount !== null ) {
703+ return $ this ->serviceAccount ;
704+ }
734705
735- /** @var ServiceAccountShape $serviceAccount */
736- $ serviceAccount = Json::decode ($ googleApplicationCredentials , true );
706+ $ googleApplicationCredentials = Util::getenv ('GOOGLE_APPLICATION_CREDENTIALS ' );
737707
738- $ this ->serviceAccount = $ serviceAccount ;
708+ if ($ googleApplicationCredentials === null ) {
709+ return $ this ->serviceAccount ;
739710 }
740711
741- return $ this ->serviceAccount ;
712+ return $ this ->serviceAccount = $ this -> mapServiceAccount ( $ googleApplicationCredentials ) ;
742713 }
743714
744715 private function getGoogleAuthTokenCredentials (): ?FetchAuthTokenInterface
745716 {
746- if ($ this ->googleAuthTokenCredentials !== null ) {
717+ if ($ this ->googleAuthTokenCredentials instanceof FetchAuthTokenInterface ) {
747718 return $ this ->googleAuthTokenCredentials ;
748719 }
749720
750721 $ serviceAccount = $ this ->getServiceAccount ();
751722
752723 if ($ serviceAccount !== null ) {
753- return $ this ->googleAuthTokenCredentials = new ServiceAccountCredentials (self ::API_CLIENT_SCOPES , $ serviceAccount );
724+ return $ this ->googleAuthTokenCredentials = new ServiceAccountCredentials (
725+ self ::API_CLIENT_SCOPES ,
726+ $ this ->normalizeServiceAccount ($ serviceAccount ),
727+ );
754728 }
755729
756730 try {
@@ -759,4 +733,22 @@ private function getGoogleAuthTokenCredentials(): ?FetchAuthTokenInterface
759733 return null ;
760734 }
761735 }
736+
737+ private function mapServiceAccount (mixed $ value ): ServiceAccount
738+ {
739+ return $ this ->getMapper ()
740+ ->allowSuperfluousKeys ()
741+ ->snakeToCamelCase ()
742+ ->map (ServiceAccount::class, Source::parse ($ value ));
743+ }
744+
745+ /**
746+ * @return array<non-empty-string, mixed>
747+ */
748+ private function normalizeServiceAccount (ServiceAccount $ serviceAccount ): array
749+ {
750+ return $ this ->getNormalizer ()
751+ ->camelToSnakeCase ()
752+ ->toArray ($ serviceAccount );
753+ }
762754}
0 commit comments