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 ;
4847use Psr \Log \LogLevel ;
4948use Stringable ;
5049use Throwable ;
51- use UnexpectedValueException ;
5250
5351use function array_filter ;
5452use function is_string ;
5553use function sprintf ;
5654use function trim ;
5755
58- /**
59- * @phpstan-type ServiceAccountShape array{
60- * project_id: non-empty-string,
61- * client_email: non-empty-string,
62- * private_key: non-empty-string,
63- * type: 'service_account'
64- * }
65- */
6656final class Factory
6757{
6858 public const API_CLIENT_SCOPES = [
@@ -86,10 +76,7 @@ final class Factory
8676 */
8777 private ?string $ defaultStorageBucket = null ;
8878
89- /**
90- * @var ServiceAccountShape|null
91- */
92- private ?array $ serviceAccount = null ;
79+ private ?ServiceAccount $ serviceAccount = null ;
9380
9481 private ?FetchAuthTokenInterface $ googleAuthTokenCredentials = null ;
9582
@@ -155,40 +142,16 @@ public function __construct()
155142 }
156143
157144 /**
158- * @param non-empty-string|ServiceAccountShape $value
145+ * @param string|array<mixed> $value
146+ *
147+ * @throws InvalidArgumentException
159148 */
160149 public function withServiceAccount (string |array $ value ): self
161150 {
162- if (is_string ($ value ) && str_starts_with ($ value , '{ ' )) {
163- try {
164- /** @var ServiceAccountShape $serviceAccount */
165- $ serviceAccount = Json::decode ($ value , true );
166- } catch (UnexpectedValueException $ e ) {
167- throw new InvalidArgumentException ('Invalid service account: ' .$ e ->getMessage (), $ e ->getCode (), $ e );
168- }
169-
170- $ factory = clone $ this ;
171- $ factory ->serviceAccount = $ serviceAccount ;
172-
173- return $ factory ;
174- }
175-
176- if (is_string ($ value )) {
177- try {
178- /** @var ServiceAccountShape $serviceAccount */
179- $ serviceAccount = Json::decodeFile ($ value , true );
180-
181- $ factory = clone $ this ;
182- $ factory ->serviceAccount = $ serviceAccount ;
183-
184- return $ factory ;
185- } catch (UnexpectedValueException $ e ) {
186- throw new InvalidArgumentException ('Invalid service account: ' .$ e ->getMessage (), $ e ->getCode (), $ e );
187- }
188- }
151+ $ serviceAccount = $ this ->mapServiceAccount ($ value );
189152
190153 $ factory = clone $ this ;
191- $ factory ->serviceAccount = $ value ;
154+ $ factory ->serviceAccount = $ serviceAccount ;
192155
193156 return $ factory ;
194157 }
@@ -414,8 +377,8 @@ public function createAppCheck(): Contract\AppCheck
414377 return new AppCheck (
415378 new AppCheck \ApiClient ($ http ),
416379 new AppCheckTokenGenerator (
417- $ serviceAccount[ ' client_email ' ] ,
418- $ serviceAccount[ ' private_key ' ] ,
380+ $ serviceAccount-> clientEmail ,
381+ $ serviceAccount-> privateKey ,
419382 $ this ->clock ,
420383 ),
421384 new AppCheckTokenVerifier ($ projectId , $ keySet ),
@@ -548,16 +511,7 @@ public function createStorage(): Contract\Storage
548511 * @deprecated 7.20.0
549512 * @codeCoverageIgnore
550513 *
551- * @return array{
552- * credentialsType: string|null,
553- * databaseUrl: string,
554- * defaultStorageBucket: string|null,
555- * serviceAccount: string|array<string, string>|null,
556- * projectId: string,
557- * tenantId: non-empty-string|null,
558- * tokenCacheType: class-string,
559- * verifierCacheType: class-string,
560- * }
514+ * @return array<mixed>
561515 */
562516 public function getDebugInfo (): array
563517 {
@@ -645,13 +599,7 @@ public function createApiClient(?array $config = null, ?array $middlewares = nul
645599 }
646600
647601 /**
648- * @return array{
649- * projectId: non-empty-string,
650- * authCache: CacheItemPoolInterface,
651- * credentialsFetcher?: FetchAuthTokenInterface,
652- * keyFile?: ServiceAccountShape,
653- * keyFilePath?: non-empty-string
654- * }
602+ * @return array<non-empty-string, mixed>
655603 */
656604 private function googleCloudClientConfig (): array
657605 {
@@ -667,7 +615,7 @@ private function googleCloudClientConfig(): array
667615
668616 $ serviceAccount = $ this ->getServiceAccount ();
669617 if ($ serviceAccount !== null ) {
670- $ config ['keyFile ' ] = $ serviceAccount ;
618+ $ config ['keyFile ' ] = $ this -> normalizeServiceAccount ( $ serviceAccount) ;
671619 }
672620
673621 return $ config ;
@@ -687,6 +635,8 @@ private function getProjectId(): string
687635 ? $ credentials ->getProjectId ()
688636 : Util::getenv ('GOOGLE_CLOUD_PROJECT ' );
689637
638+ $ projectId ??= $ this ->getServiceAccount ()?->projectId;
639+
690640 if (is_string ($ projectId ) && $ projectId !== '' ) {
691641 return $ this ->projectId = $ projectId ;
692642 }
@@ -699,23 +649,15 @@ private function getProjectId(): string
699649 */
700650 private function getDatabaseUrl (): string
701651 {
702- if ($ this ->databaseUrl === null ) {
703- $ this ->databaseUrl = sprintf ('https://%s.firebaseio.com ' , $ this ->getProjectId ());
704- }
705-
706- return $ this ->databaseUrl ;
652+ return $ this ->databaseUrl ??= sprintf ('https://%s.firebaseio.com ' , $ this ->getProjectId ());
707653 }
708654
709655 /**
710656 * @return non-empty-string
711657 */
712658 private function getStorageBucketName (): string
713659 {
714- if ($ this ->defaultStorageBucket === null ) {
715- $ this ->defaultStorageBucket = sprintf ('%s.appspot.com ' , $ this ->getProjectId ());
716- }
717-
718- return $ this ->defaultStorageBucket ;
660+ return $ this ->defaultStorageBucket ??= sprintf ('%s.appspot.com ' , $ this ->getProjectId ());
719661 }
720662
721663 private function createCustomTokenGenerator (): ?CustomTokenViaGoogleCredentials
@@ -755,41 +697,34 @@ private function createSessionCookieVerifier(): SessionCookieVerifier
755697 return SessionCookieVerifier::createWithProjectIdAndCache ($ this ->getProjectId (), $ this ->verifierCache ?? $ this ->defaultCache );
756698 }
757699
758- /**
759- * @return ServiceAccountShape|null
760- */
761- private function getServiceAccount (): ?array
700+ private function getServiceAccount (): ?ServiceAccount
762701 {
763- if ($ this ->serviceAccount === null ) {
764- $ googleApplicationCredentials = Util::getenv ('GOOGLE_APPLICATION_CREDENTIALS ' );
765-
766- if ($ googleApplicationCredentials === null ) {
767- return null ;
768- }
769-
770- if (!str_starts_with ($ googleApplicationCredentials , '{ ' )) {
771- return null ;
772- }
702+ if ($ this ->serviceAccount !== null ) {
703+ return $ this ->serviceAccount ;
704+ }
773705
774- /** @var ServiceAccountShape $serviceAccount */
775- $ serviceAccount = Json::decode ($ googleApplicationCredentials , true );
706+ $ googleApplicationCredentials = Util::getenv ('GOOGLE_APPLICATION_CREDENTIALS ' );
776707
777- $ this ->serviceAccount = $ serviceAccount ;
708+ if ($ googleApplicationCredentials === null ) {
709+ return $ this ->serviceAccount ;
778710 }
779711
780- return $ this ->serviceAccount ;
712+ return $ this ->serviceAccount = $ this -> mapServiceAccount ( $ googleApplicationCredentials ) ;
781713 }
782714
783715 private function getGoogleAuthTokenCredentials (): ?FetchAuthTokenInterface
784716 {
785- if ($ this ->googleAuthTokenCredentials !== null ) {
717+ if ($ this ->googleAuthTokenCredentials instanceof FetchAuthTokenInterface ) {
786718 return $ this ->googleAuthTokenCredentials ;
787719 }
788720
789721 $ serviceAccount = $ this ->getServiceAccount ();
790722
791723 if ($ serviceAccount !== null ) {
792- 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+ );
793728 }
794729
795730 try {
@@ -798,4 +733,22 @@ private function getGoogleAuthTokenCredentials(): ?FetchAuthTokenInterface
798733 return null ;
799734 }
800735 }
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+ }
801754}
0 commit comments