ÿØÿà JFIF    ÿÛ „  ( %"1!%)+...383,7(-.+  -+++--++++---+-+-----+---------------+---+-++7-----ÿÀ  ß â" ÿÄ     ÿÄ H    !1AQaq"‘¡2B±ÁÑð#R“Ò Tbr‚²á3csƒ’ÂñDS¢³$CÿÄ   ÿÄ %  !1AQa"23‘ÿÚ   ? ôÿ ¨pŸªáÿ —åYõõ\?àÒü©ŠÄï¨pŸªáÿ —åYõõ\?àÓü©ŠÄá 0Ÿªáÿ Ÿå[úƒ ú®ði~TÁbqÐ8OÕpÿ ƒOò¤Oè`–RÂáœá™êi€ßÉ< FtŸI“öÌ8úDf´°å}“¾œ6  öFá°y¥jñÇh†ˆ¢ã/ÃÐ:ªcÈ "Y¡ðÑl>ÿ ”ÏËte:qž\oäŠe÷󲍷˜HT4&ÿ ÓÐü6ö®¿øþßèô Ÿ•7Ñi’•j|“ñì>b…þS?*Óôÿ ÓÐü*h¥£ír¶ü UãS炟[AÐaè[ûª•õ&õj?†Éö+EzP—WeÒírJFt ‘BŒ†Ï‡%#tE Øz ¥OÛ«!1›üä±Í™%ºÍãö]°î(–:@<‹ŒÊö×òÆt¦ãº+‡¦%ÌÁ²h´OƒJŒtMÜ>ÀÜÊw3Y´•牋4ǍýʏTì>œú=Íwhyë,¾Ôò×õ¿ßÊa»«þˆѪQ|%6ž™A õ%:øj<>É—ÿ Å_ˆCbõ¥š±ý¯Ýƒï…¶|RëócÍf溪“t.СøTÿ *Ä¿-{†çàczůŽ_–^XþŒ±miB[X±d 1,é”zEù»& î9gœf™9Ð'.;—™i}!ôšåîqêÛ٤ёý£½ÆA–àôe"A$˝Úsäÿ ÷Û #°xŸëí(l »ý3—¥5m! rt`†0~'j2(]S¦¦kv,ÚÇ l¦øJA£Šƒ J3E8ÙiŽ:cÉžúeZ°€¯\®kÖ(79«Ž:¯X”¾³Š&¡* ….‰Ž(ÜíŸ2¥ª‡×Hi²TF¤ò[¨íÈRëÉ䢍mgÑ.Ÿ<öäS0í„ǹÁU´f#Vß;Õ–…P@3ío<ä-±»Ž.L|kªÀê›fÂ6@»eu‚|ÓaÞÆŸ…¨ááå>åŠ?cKü6ùTÍÆ”†sĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ­‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg A2Z"i¸vdÄ÷.iõ®§)¿]¤À†–‡É&ä{V¶iŽ”.Ó×Õÿ û?h¬Mt–íª[ÿ Ñÿ ÌV(í}=ibÔ¡›¥¢±b Lô¥‡piη_Z<‡z§èŒ)iÖwiÇ 2hÙ3·=’d÷8éŽ1¦¸c¤µ€7›7Ø ð\á)} ¹fËí›pAÃL%âc2 í§æQz¿;T8sæ°qø)QFMð‰XŒÂ±N¢aF¨…8¯!U  Z©RÊ ÖPVÄÀÍin™Ì-GˆªÅËŠ›•zË}º±ŽÍFò¹}Uw×#ä5B¤{î}Ð<ÙD é©¤&‡ïDbàÁôMÁ." ¤‡ú*õ'VŽ|¼´Úgllº¼klz[Æüï÷Aób‡Eÿ dÑ»Xx9ÃÜ£ÁT/`¼¸vI±Ýµ·Ë‚“G³þ*Ÿû´r|*}<¨îºœ @¦mÄ’M¹”.œ«Y–|6ÏU¤jç¥ÕÞqO ˜kDÆÁ¨5ÿ š;ÐЦ¦€GÙk \ –Þ=â¼=SͧµªS°ÚÍpÜãQűÀõ¬?ÃÁ1Ñ•õZà?hóœ€ L¦l{Y*K˜Ù›zc˜–ˆâ ø+¾ ­-Ök¥%ùEÜA'}ˆ><ÊIè“bpÍ/qÞâvoX€w,\úªò6Z[XdÒæ­@Ö—€$òJí#é>'°Ú ôª˜<)4ryÙ£|óAÅn5žêŸyÒäMÝ2{"}‰–¤l÷ûWX\l¾Á¸góÉOÔ /óñB¤f¸çñ[.P˜ZsÊË*ßT܈§QN¢’¡¨§V¼(Üù*eÕ“”5T¨‹Âê¥FŒã½Dü[8'Ò¥a…Ú¶k7a *•›¼'Ò·\8¨ª\@\õ¢¦íq+DÙrmÎ…_ªæ»ŠÓœ¡¯’Ré9MÅ×D™lælffc+ŒÑ,ý™ÿ ¯þǤ=Å’Á7µ÷ÚÛ/“Ü€ñýã¼àí¾ÕÑ+ƒ,uµMâÀÄbm:ÒÎPæ{˜Gz[ƒ¯«® KHà`ߨŠéí¯P8Aq.C‰ à€kòpj´kN¶qô€…Õ,ÜNŠª-­{Zö’æû44‰sŽè‰îVíRœÕm" 6?³D9¡ÇTíÅꋇ`4«¸ÝÁô ï’ýorqКÇZ«x4Žâéþuïf¹µö[P ,Q£éaX±`PÉÍZ ¸äYúg üAx ’6Lê‚xÝÓ*äQ  Ï’¨hÍ =²,6ï#rÃ<¯–£»ƒ‹,–ê•€ aÛsñ'%Æ"®ÛüìBᝠHÚ3ß°©$“XnœÖ’î2ËTeûìxîß ¦å¿çÉ ðK§þ{‘t‚Ϋ¬jéîZ[ ”š7L¥4VÚCE×]m¤Øy”ä4-dz£œ§¸x.*ãÊÊ b÷•h:©‡¦s`BTÁRû¾g⻩‹jø sF¢àJøFl‘È•Xᓁà~*j¯ +(ÚÕ6-£¯÷GŠØy‚<Ç’.F‹Hœw(+)ÜÜâÈzÄäT§FߘãÏ;DmVœ3Àu@mÚüXÝü•3B¨òÌÁÛ<·ÃÜ z,Ì@õÅ·d2]ü8s÷IôÞ¯^Ç9¢u„~ëAŸï4«M? K]­ÅàPl@s_ p:°¬ZR”´›JC[CS.h‹ƒïËœ«Æ]–÷ó‚wR×k7X‰k›‘´ù¦=¡«‰¨¨Â')—71ó’c‡Ðúµ `é.{§p¹ój\Ž{1h{o±Ý=áUÊïGÖŒõ–-BÄm+AZX¶¡ ïHðæ¥JmÙ;…䡟ˆ¦ ° äšiÉg«$üMk5¤L“’çÊvïâï ,=f“"íἊ5ô¬x6{ɏžID0e¸vçmi'︧ºð9$ò¹÷*£’9ÿ ²TÔ…×>JV¥}Œ}$p[bÔ®*[jzS*8 ”·T›Í–ñUîƒwo$áè=LT™ç—~ô·¤ÈÚ$榍q‰„+´kFm)ž‹©i–ËqÞŠ‰à¶ü( ‚•§ •°ò·‡#5ª•µÊ﯅¡X¨šÁ*F#TXJÊ ušJVÍ&=iÄs1‚3•'fý§5Ñ<=[íÞ­ PÚ;ѱÌ_~Ä££8rÞ ²w;’hDT°>ÈG¬8Á²ÚzŽ®ò®qZcqJêäÞ-ö[ܘbň±çb“ж31²n×iƒðÕ;1¶þÉ ªX‰,ßqÏ$>•î íZ¥Z 1{ç൵+ƒÕµ¥°T$§K]á»Ûï*·¤tMI’ÂZbŽÕiÒ˜}bÓ0£ª5›¨ [5Ž^ÝœWøÂÝh° ¢OWun£¤5 a2Z.G2³YL]jåtì”ä ÁÓ‘%"©<Ôúʰsº UZvä‡ÄiÆÒM .÷V·™ø#kèýiíÌ–ª)µT[)BˆõÑ xB¾B€ÖT¨.¥~ð@VĶr#¸ü*åZNDŽH;âi ],©£öØpù(šºãö¼T.uCê•4@ÿ GÕÛ)Cx›®0ø#:ÏðFÒbR\(€€Ä®fã4Þ‰Fä¯HXƒÅ,†öEÑÔÜ]Öv²?tLÃvBY£ú6Êu5ÅAQ³1‘’¬x–HŒÐ‡ ^ ¸KwJôÖŽ5×CÚ¨vÜ«/B0$×k°=ðbÇ(Ï)w±A†Á† 11Í=èQšµ626ŒÜ/`G«µ<}—-Ö7KEHÈÉðóȤmݱû±·ø«Snmá=“䫚mݱŸ¡¶~ó·“äUóJæúòB|E LêŽy´jDÔ$G¢þÐñ7óR8ýÒ…Ç› WVe#·Ÿ p·Fx~•ݤF÷0Èÿ K¯æS<6’¡WШ; ´ÿ ¥Êø\Òuî†åÝ–VNœkÒ7oòX¨Á­Ø÷FÎÑä±g÷ÿ M~Çî=p,X´ ÝÌÚÅ‹’ÃjÖ.ØöÏñ qïQ¤ÓZE†° =6·]܈ s¸>v•Ž^Ý\wq9r‰Î\¸¡kURÒ$­*‹Nq?Þª*!sŠÆ:TU_u±T+øX¡ ®¹¡,ÄâÃBTsÜ$Ø›4m椴zÜK]’’›Pƒ @€#â˜`é¹=I‡fiV•Ôî“nRm+µFPOhÍ0B£ €+¬5c v•:P'ÒyÎ ‰V~‚Ó†ÖuókDoh$å\*ö%Ю=£«…aȼ½÷Û.-½VŒŠ¼'lyî±1¬3ó#ÞE¿ÔS¤gV£m›=§\û"—WU¤ÚǼÿ ÂnÁGŒÃ ‚õN D³õNÚíŒÕ;HôyÄÈ©P¹Ä{:?R‘Ô¨âF÷ø£bÅó® JS|‚R÷ivýáâ€Æé¡è³´IئÑT!§˜•ت‚¬â@q€wnïCWÄ@JU€ê¯m6]Ï:£âx'+ÒðXvÓ¦Úm=–´7œ $ì“B£~p%ÕŸUþ« N@¼üï~w˜ñø5®—'Ôe»¤5ã//€ž~‰Tþ›Å7•#¤× Íö pÄ$ùeåì*«ÓŠEØWEÈsßg ¦ûvžSsLpºÊW–âµEWöˬH; ™!CYõZ ÃÄf æ#1W. \uWâ\,\Çf j’<qTbên›Î[vxx£ë 'ö¨1›˜ÀM¼Pÿ H)ƒêêŒA7s,|F“ 꺸k³9Ìö*ç®;Ö!Ö$Eiž•¹ÒÚ†ýóéÝû¾ÕS®ó$’NÝäŸz¤5r¦ãÄÃD÷Üø!°ø‡Ô&@m™Ì^Ãä­d q5Lnÿ N;.6½·N|#ä"1Nƒx“ã<3('&ñßt  ~ªu”1Tb㫨9ê–›–bìd$ߣ=#ÕãÒmU¯eí$EFù5ýYô櫨æì™Ç—±ssM]·á¿0ÕåJRÓªîiƒ+O58ÖñªŠÒx" \µâá¨i’¤i —Ö ” M+M¤ë9‚‰A¦°Qõ¾ßøK~¼Ã‘g…Ö´~÷Ï[3GUœÒ½#…kàÔ®Ò”‰³·dWV‰IP‰Ú8u¹”E ÖqLj¾êÕCBš{A^Âß;–¨`¯¬ìö ˼ ×tìø.tƐm*n¨y4o&Àx¥n¦×î‡aupáÛj8¿m›è¶ã!o½;ß0y^ý×^EÑ¿ÒjzŒ­)vÚÑnÄL …^ªô× ‡—‚3k Îý­hï]içå–îÏ*÷ñþ»Ô CÒjøjÍznˆ´ ¹#b'Fô‹ ‰v¥'’à'T´ƒHýÍ%M‰ ƒ&ÆÇŒï1 ‘ –Þ ‰i¬s žR-Ÿ kЬá¬7:þ 0ŒÅÒÕ/aÙ¬ÃÝ#Úøœ ©aiVc‰. ¹¦ãµ” ›Yg¦›ÆÎýº°f³7ƒhá·¸­}&D9¡ÂsÉÙÞèŠõØàC™¨ñbFC|´Ü(ŸƒÚÒ-%»'a Ì¿)ËÇn¿úÿ ÞŽX…4ÊÅH^ôΑí@ù¹Eh¶“L8Çjù ¼ÎåVªóR©Ï5uà V4lZß®=€xÖŸ–ÑÈ ÷”¨°¾__yM1tÉ?uÆþIkÄgæ@þ[¢†°XÃJ£j·:nkÅ¢u ‘}âGzö­/IµèЬ¼48q¦F°ŽR¼=ûì{´¯RýicS ÕÛ íNtÍÙï£,w4rêì®»~x(©Uñ§#Ñ&œÕ¤>ÎåÍÓ9’Ö{9eV­[Öjâ²ãu]˜å2›qÑšÕJç0€sÄ|Êëè0튔bÁ>“{×_F`Ø©ºê:µä,v¤ðfc1±"«ÔÍän1#=· Âøv~H½ÐßA¾¿Ü€Óš]Õ; I¾÷ç‚Qi†î¹9ywÔKG˜áñ zQY—§ÃÕZ07§X‚ Áh;ÁM)iÌCH-¯T‘ë|A0{Ò½LÚ–TâÖkÜ’dÀ“rmm»”جPF³ÖcbE§T€ÒxKºû’Ó®7±²(\4ŽÃ¸Uu@j™yĵ;³µ!Á¢b.W¤=mõ´êµK k ¸K^ÜÛ#p*Ü14qkZç5ïë †°5Ï%ÍÛ<Õ¤×Ô¥ê†C Õ´¼ú$ƒÖ“”]Ù¬qÞÚ[4©ý!ûÏ—Áb쳐XµA¬â~`›Çr¸8ìùÝ䫦<>ä÷«?xs´ÇÑ /á;¹øüÊÈÙà{"@Žïzâ¬[âß‚ U_<ÇŸ½4èN˜ú61®qŠu ¦þF£»äJ_ˆÙÎ~ ÞAã–݄ϗrŠD;xTž‘ô`É«…suãO`?³à™ô Lý#Íc5öoæØ‚y´´÷«ZR§<&JÇ+éâô´€i!Àˆ0æAoàðLèÖ-2ŸõW.’t^–(KÁmHµV@xÜÇy®Ñø­â^:Ú3w· 7½¹°ñ¸â¹®:',«Mœ—n­Á+Ãbš LÈ‘ÄnRÓÅœ%¦²‰¨ùQ:¤f‚ "PÕtô¸…cæl…&˜Ú˜Ôkv‹ž+vŠ,=¢v­6—Xy*¥t£«<™:“aîϲ=¦6rO]XI¿Œ÷¤zÚ­›¶ 6÷”w\d ü~v®ˆÌk«^m<ÿ ¢‰Õ\)ùºŽ;… lîÙÅEŠ®cѾ@vnMÏ,¼“ñ•ŽBxðÃzãÇç%3ˆ"}Ù•Åî> BÉú;Ò]V+P˜F_´ßé> Øše|ï‡ÄOmFæÇ ãqÞ$/xÐx­z`ï9"œÜij‚!7.\Td…9M‡•iŽ‹¾‘50ÞŽn¥ß4ÉôO ¹*í^QêËÜÇÌ8=ާs‰'ÂëÙ«á%Pú[O †ÅP¯Vsް.‰,kc¶ ¬A9n˜XÎ-ÞšN["¹QÕ‰ƒMýÁߺXJæÍaLj¾×Ãmã¾ãÚ uñÒþåQô¦¥ /ÄUx:‚ÍÜ’ Đ©ØÝ3V¨‰ÕnÐ6ó*óúK­«…c ¯U òhsý­jóÔj#,ímŒRµ«lbïUTŒÑ8†Ä0œÏr`ð¡¬É Ї ë"À² ™ 6¥ f¶ ¢ÚoܱԷ-<Àî)†a¶ž'Ú»¨TXqØæ¶÷YÄHy˜9ÈIW­YÀuMFë ºÏ’AqÌ4·/Ú †ô'i$øä­=Ä Ý|öK×40è|È6p‘0§)o¥ctî§H+CA-“ xØ|ÐXАç l8íºð3Ø:³¤¬KX¯UÿÙ $middlewareCallables */ private array $middlewareCallables = []; private array $transportCallMethods = [ Call::UNARY_CALL => 'startUnaryCall', Call::BIDI_STREAMING_CALL => 'startBidiStreamingCall', Call::CLIENT_STREAMING_CALL => 'startClientStreamingCall', Call::SERVER_STREAMING_CALL => 'startServerStreamingCall', ]; private bool $backwardsCompatibilityMode; /** * Add a middleware to the call stack by providing a callable which will be * invoked at the start of each call, and will return an instance of * {@see MiddlewareInterface} when invoked. * * The callable must have the following method signature: * * callable(MiddlewareInterface): MiddlewareInterface * * An implementation may look something like this: * ``` * $client->addMiddleware(function (MiddlewareInterface $handler) { * return new class ($handler) implements MiddlewareInterface { * public function __construct(private MiddlewareInterface $handler) { * } * * public function __invoke(Call $call, array $options) { * // modify call and options (pre-request) * $response = ($this->handler)($call, $options); * // modify the response (post-request) * return $response; * } * }; * }); * ``` * * @param callable $middlewareCallable A callable which returns an instance * of {@see MiddlewareInterface} when invoked with a * MiddlewareInterface instance as its first argument. * @return void */ public function addMiddleware(callable $middlewareCallable): void { $this->middlewareCallables[] = $middlewareCallable; } /** * Initiates an orderly shutdown in which preexisting calls continue but new * calls are immediately cancelled. * * @experimental */ public function close() { $this->transport->close(); } /** * Get the transport for the client. This method is protected to support * use by customized clients. * * @access private * @return TransportInterface */ protected function getTransport() { return $this->transport; } /** * Get the credentials for the client. This method is protected to support * use by customized clients. * * @access private * @return CredentialsWrapper */ protected function getCredentialsWrapper() { return $this->credentialsWrapper; } /** * Configures the GAPIC client based on an array of options. * * @param array $options { * An array of required and optional arguments. * * @type string $apiEndpoint * The address of the API remote host, for example "example.googleapis.com. May also * include the port, for example "example.googleapis.com:443" * @type bool $disableRetries * Determines whether or not retries defined by the client configuration should be * disabled. Defaults to `false`. * @type string|array $clientConfig * Client method configuration, including retry settings. This option can be either a * path to a JSON file, or a PHP array containing the decoded JSON data. * By default this settings points to the default client config file, which is provided * in the resources folder. * @type string|array|FetchAuthTokenInterface|CredentialsWrapper $credentials * The credentials to be used by the client to authorize API calls. This option * accepts either a path to a credentials file, or a decoded credentials file as a * PHP array. * *Advanced usage*: In addition, this option can also accept a pre-constructed * \Google\Auth\FetchAuthTokenInterface object or \Google\ApiCore\CredentialsWrapper * object. Note that when one of these objects are provided, any settings in * $authConfig will be ignored. * @type array $credentialsConfig * Options used to configure credentials, including auth token caching, for the client. * For a full list of supporting configuration options, see * \Google\ApiCore\CredentialsWrapper::build. * @type string|TransportInterface $transport * The transport used for executing network requests. May be either the string `rest`, * `grpc`, or 'grpc-fallback'. Defaults to `grpc` if gRPC support is detected on the system. * *Advanced usage*: Additionally, it is possible to pass in an already instantiated * TransportInterface object. Note that when this objects is provided, any settings in * $transportConfig, and any `$apiEndpoint` setting, will be ignored. * @type array $transportConfig * Configuration options that will be used to construct the transport. Options for * each supported transport type should be passed in a key for that transport. For * example: * $transportConfig = [ * 'grpc' => [...], * 'rest' => [...], * 'grpc-fallback' => [...], * ]; * See the GrpcTransport::build and RestTransport::build * methods for the supported options. * @type string $versionFile * The path to a file which contains the current version of the client. * @type string $descriptorsConfigPath * The path to a descriptor configuration file. * @type string $serviceName * The name of the service. * @type string $libName * The name of the client application. * @type string $libVersion * The version of the client application. * @type string $gapicVersion * The code generator version of the GAPIC library. * @type callable $clientCertSource * A callable which returns the client cert as a string. * } * @throws ValidationException */ private function setClientOptions(array $options) { // serviceAddress is now deprecated and acts as an alias for apiEndpoint if (isset($options['serviceAddress'])) { $options['apiEndpoint'] = $this->pluck('serviceAddress', $options, false); } $this->validateNotNull($options, [ 'apiEndpoint', 'serviceName', 'descriptorsConfigPath', 'clientConfig', 'disableRetries', 'credentialsConfig', 'transportConfig', ]); $this->traitValidate($options, [ 'credentials', 'transport', 'gapicVersion', 'libName', 'libVersion', ]); if ($this->isBackwardsCompatibilityMode()) { if (is_string($options['clientConfig'])) { // perform validation for V1 surfaces which is done in the // ClientOptions class for v2 surfaces. $options['clientConfig'] = json_decode( file_get_contents($options['clientConfig']), true ); self::validateFileExists($options['descriptorsConfigPath']); } } else { // cast to ClientOptions for new surfaces only $options = new ClientOptions($options); } $this->serviceName = $options['serviceName']; $this->retrySettings = RetrySettings::load( $this->serviceName, $options['clientConfig'], $options['disableRetries'] ); $headerInfo = [ 'libName' => $options['libName'], 'libVersion' => $options['libVersion'], 'gapicVersion' => $options['gapicVersion'], ]; // Edge case: If the client has the gRPC extension installed, but is // a REST-only library, then the grpcVersion header should not be set. if ($this->transport instanceof GrpcTransport) { $headerInfo['grpcVersion'] = phpversion('grpc'); } elseif ($this->transport instanceof RestTransport || $this->transport instanceof GrpcFallbackTransport) { $headerInfo['restVersion'] = Version::getApiCoreVersion(); } $this->agentHeader = AgentHeader::buildAgentHeader($headerInfo); // Set "client_library_name" depending on client library surface being used $userAgentHeader = sprintf( 'gcloud-php-%s/%s', $this->isBackwardsCompatibilityMode() ? 'legacy' : 'new', $options['gapicVersion'] ); $this->agentHeader['User-Agent'] = [$userAgentHeader]; self::validateFileExists($options['descriptorsConfigPath']); $descriptors = require($options['descriptorsConfigPath']); $this->descriptors = $descriptors['interfaces'][$this->serviceName]; $this->credentialsWrapper = $this->createCredentialsWrapper( $options['credentials'], $options['credentialsConfig'], $options['universeDomain'] ); $transport = $options['transport'] ?: self::defaultTransport(); $this->transport = $transport instanceof TransportInterface ? $transport : $this->createTransport( $options['apiEndpoint'], $transport, $options['transportConfig'], $options['clientCertSource'] ); } /** * @param string $apiEndpoint * @param string $transport * @param TransportOptions|array $transportConfig * @param callable $clientCertSource * @return TransportInterface * @throws ValidationException */ private function createTransport( string $apiEndpoint, $transport, $transportConfig, ?callable $clientCertSource = null ) { if (!is_string($transport)) { throw new ValidationException( "'transport' must be a string, instead got:" . print_r($transport, true) ); } $supportedTransports = self::supportedTransports(); if (!in_array($transport, $supportedTransports)) { throw new ValidationException(sprintf( 'Unexpected transport option "%s". Supported transports: %s', $transport, implode(', ', $supportedTransports) )); } $configForSpecifiedTransport = $transportConfig[$transport] ?? []; if (is_array($configForSpecifiedTransport)) { $configForSpecifiedTransport['clientCertSource'] = $clientCertSource; } else { $configForSpecifiedTransport->setClientCertSource($clientCertSource); $configForSpecifiedTransport = $configForSpecifiedTransport->toArray(); } switch ($transport) { case 'grpc': // Setting the user agent for gRPC requires special handling if (isset($this->agentHeader['User-Agent'])) { if ($configForSpecifiedTransport['stubOpts']['grpc.primary_user_agent'] ??= '') { $configForSpecifiedTransport['stubOpts']['grpc.primary_user_agent'] .= ' '; } $configForSpecifiedTransport['stubOpts']['grpc.primary_user_agent'] .= $this->agentHeader['User-Agent'][0]; } return GrpcTransport::build($apiEndpoint, $configForSpecifiedTransport); case 'grpc-fallback': return GrpcFallbackTransport::build($apiEndpoint, $configForSpecifiedTransport); case 'rest': if (!isset($configForSpecifiedTransport['restClientConfigPath'])) { throw new ValidationException( "The 'restClientConfigPath' config is required for 'rest' transport." ); } $restConfigPath = $configForSpecifiedTransport['restClientConfigPath']; return RestTransport::build($apiEndpoint, $restConfigPath, $configForSpecifiedTransport); default: throw new ValidationException( "Unexpected 'transport' option: $transport. " . "Supported values: ['grpc', 'rest', 'grpc-fallback']" ); } } /** * @param array $options * @return OperationsClient */ private function createOperationsClient(array $options) { $this->pluckArray([ 'serviceName', 'clientConfig', 'descriptorsConfigPath', ], $options); // User-supplied operations client if ($operationsClient = $this->pluck('operationsClient', $options, false)) { return $operationsClient; } // operationsClientClass option $operationsClientClass = $this->pluck('operationsClientClass', $options, false) ?: OperationsCLient::class; return new $operationsClientClass($options); } /** * @return string */ private static function defaultTransport() { return self::getGrpcDependencyStatus() ? 'grpc' : 'rest'; } private function validateCallConfig(string $methodName) { // Ensure a method descriptor exists for the target method. if (!isset($this->descriptors[$methodName])) { throw new ValidationException("Requested method '$methodName' does not exist in descriptor configuration."); } $methodDescriptors = $this->descriptors[$methodName]; // Ensure required descriptor configuration exists. if (!isset($methodDescriptors['callType'])) { throw new ValidationException("Requested method '$methodName' does not have a callType " . 'in descriptor configuration.'); } $callType = $methodDescriptors['callType']; // Validate various callType specific configurations. if ($callType == Call::LONGRUNNING_CALL) { if (!isset($methodDescriptors['longRunning'])) { throw new ValidationException("Requested method '$methodName' does not have a longRunning config " . 'in descriptor configuration.'); } // @TODO: check if the client implements `OperationsClientInterface` instead if (!method_exists($this, 'getOperationsClient')) { throw new ValidationException('Client missing required getOperationsClient ' . "for longrunning call '$methodName'"); } } elseif ($callType == Call::PAGINATED_CALL) { if (!isset($methodDescriptors['pageStreaming'])) { throw new ValidationException("Requested method '$methodName' with callType PAGINATED_CALL does not " . 'have a pageStreaming in descriptor configuration.'); } } // LRO are either Standard LRO response type or custom, which are handled by // startOperationCall, so no need to validate responseType for those callType. if ($callType != Call::LONGRUNNING_CALL) { if (!isset($methodDescriptors['responseType'])) { throw new ValidationException("Requested method '$methodName' does not have a responseType " . 'in descriptor configuration.'); } } return $methodDescriptors; } /** * @param string $methodName * @param Message $request * @param array $optionalArgs { * Call Options * * @type array $headers [optional] key-value array containing headers * @type int $timeoutMillis [optional] the timeout in milliseconds for the call * @type array $transportOptions [optional] transport-specific call options * @type RetrySettings|array $retrySettings [optional] A retry settings override for the call. * } * * @experimental * * @return PromiseInterface */ private function startAsyncCall( string $methodName, Message $request, array $optionalArgs = [] ) { // Convert method name to the UpperCamelCase of RPC names from lowerCamelCase of GAPIC method names // in order to find the method in the descriptor config. $methodName = ucfirst($methodName); $methodDescriptors = $this->validateCallConfig($methodName); $callType = $methodDescriptors['callType']; switch ($callType) { case Call::PAGINATED_CALL: return $this->getPagedListResponseAsync( $methodName, $optionalArgs, $methodDescriptors['responseType'], $request, $methodDescriptors['interfaceOverride'] ?? $this->serviceName ); case Call::SERVER_STREAMING_CALL: case Call::CLIENT_STREAMING_CALL: case Call::BIDI_STREAMING_CALL: throw new ValidationException("Call type '$callType' of requested method " . "'$methodName' is not supported for async execution."); } return $this->startApiCall($methodName, $request, $optionalArgs); } /** * @param string $methodName * @param Message $request * @param array $optionalArgs { * Call Options * * @type array $headers [optional] key-value array containing headers * @type int $timeoutMillis [optional] the timeout in milliseconds for the call * @type array $transportOptions [optional] transport-specific call options * @type RetrySettings|array $retrySettings [optional] A retry settings * override for the call. * } * * @experimental * * @return PromiseInterface|PagedListResponse|BidiStream|ClientStream|ServerStream */ private function startApiCall( string $methodName, ?Message $request = null, array $optionalArgs = [] ) { $methodDescriptors =$this->validateCallConfig($methodName); $callType = $methodDescriptors['callType']; // Prepare request-based headers, merge with user-provided headers, // which take precedence. $headerParams = $methodDescriptors['headerParams'] ?? []; $requestHeaders = $this->buildRequestParamsHeader($headerParams, $request); $optionalArgs['headers'] = array_merge($requestHeaders, $optionalArgs['headers'] ?? []); // Default the interface name, if not set, to the client's protobuf service name. $interfaceName = $methodDescriptors['interfaceOverride'] ?? $this->serviceName; // Handle call based on call type configured in the method descriptor config. if ($callType == Call::LONGRUNNING_CALL) { return $this->startOperationsCall( $methodName, $optionalArgs, $request, $this->getOperationsClient(), $interfaceName, // Custom operations will define their own operation response type, whereas standard // LRO defaults to the same type. $methodDescriptors['responseType'] ?? null ); } // Fully-qualified name of the response message PHP class. $decodeType = $methodDescriptors['responseType']; if ($callType == Call::PAGINATED_CALL) { return $this->getPagedListResponse($methodName, $optionalArgs, $decodeType, $request, $interfaceName); } // Unary, and all Streaming types handled by startCall. return $this->startCall($methodName, $decodeType, $optionalArgs, $request, $callType, $interfaceName); } /** * @param string $methodName * @param string $decodeType * @param array $optionalArgs { * Call Options * * @type array $headers [optional] key-value array containing headers * @type int $timeoutMillis [optional] the timeout in milliseconds for the call * @type array $transportOptions [optional] transport-specific call options * @type RetrySettings|array $retrySettings [optional] A retry settings * override for the call. * } * @param Message $request * @param int $callType * @param string $interfaceName * * @return PromiseInterface|BidiStream|ClientStream|ServerStream */ private function startCall( string $methodName, string $decodeType, array $optionalArgs = [], ?Message $request = null, int $callType = Call::UNARY_CALL, ?string $interfaceName = null ) { $optionalArgs = $this->configureCallOptions($optionalArgs); $callStack = $this->createCallStack( $this->configureCallConstructionOptions($methodName, $optionalArgs) ); $descriptor = $this->descriptors[$methodName]['grpcStreaming'] ?? null; $call = new Call( $this->buildMethod($interfaceName, $methodName), $decodeType, $request, $descriptor, $callType ); switch ($callType) { case Call::UNARY_CALL: $this->modifyUnaryCallable($callStack); break; case Call::BIDI_STREAMING_CALL: case Call::CLIENT_STREAMING_CALL: case Call::SERVER_STREAMING_CALL: $this->modifyStreamingCallable($callStack); break; } return $callStack($call, $optionalArgs + array_filter([ 'audience' => self::getDefaultAudience() ])); } /** * @param array $callConstructionOptions { * Call Construction Options * * @type RetrySettings $retrySettings [optional] A retry settings override * For the call. * } * * @return callable */ private function createCallStack(array $callConstructionOptions) { $quotaProject = $this->credentialsWrapper->getQuotaProject(); $fixedHeaders = $this->agentHeader; if ($quotaProject) { $fixedHeaders += [ 'X-Goog-User-Project' => [$quotaProject] ]; } $callStack = function (Call $call, array $options) { $startCallMethod = $this->transportCallMethods[$call->getCallType()]; return $this->transport->$startCallMethod($call, $options); }; $callStack = new CredentialsWrapperMiddleware($callStack, $this->credentialsWrapper); $callStack = new FixedHeaderMiddleware($callStack, $fixedHeaders, true); $callStack = new RetryMiddleware($callStack, $callConstructionOptions['retrySettings']); $callStack = new OptionsFilterMiddleware($callStack, [ 'headers', 'timeoutMillis', 'transportOptions', 'metadataCallback', 'audience', 'metadataReturnType' ]); foreach (\array_reverse($this->middlewareCallables) as $fn) { /** @var MiddlewareInterface $callStack */ $callStack = $fn($callStack); } return $callStack; } /** * @param string $methodName * @param array $optionalArgs { * Optional arguments * * @type RetrySettings|array $retrySettings [optional] A retry settings * override for the call. * } * * @return array */ private function configureCallConstructionOptions(string $methodName, array $optionalArgs) { $retrySettings = $this->retrySettings[$methodName]; // Allow for retry settings to be changed at call time if (isset($optionalArgs['retrySettings'])) { if ($optionalArgs['retrySettings'] instanceof RetrySettings) { $retrySettings = $optionalArgs['retrySettings']; } else { $retrySettings = $retrySettings->with( $optionalArgs['retrySettings'] ); } } return [ 'retrySettings' => $retrySettings, ]; } /** * @return array */ private function configureCallOptions(array $optionalArgs): array { if ($this->isBackwardsCompatibilityMode()) { return $optionalArgs; } // cast to CallOptions for new surfaces only return (new CallOptions($optionalArgs))->toArray(); } /** * @param string $methodName * @param array $optionalArgs { * Call Options * * @type array $headers [optional] key-value array containing headers * @type int $timeoutMillis [optional] the timeout in milliseconds for the call * @type array $transportOptions [optional] transport-specific call options * } * @param Message $request * @param OperationsClient|object $client * @param string $interfaceName * @param string $operationClass If provided, will be used instead of the default * operation response class of {@see \Google\LongRunning\Operation}. * * @return PromiseInterface */ private function startOperationsCall( string $methodName, array $optionalArgs, Message $request, $client, ?string $interfaceName = null, ?string $operationClass = null ) { $optionalArgs = $this->configureCallOptions($optionalArgs); $callStack = $this->createCallStack( $this->configureCallConstructionOptions($methodName, $optionalArgs) ); $descriptor = $this->descriptors[$methodName]['longRunning']; $metadataReturnType = null; // Call the methods supplied in "additionalArgumentMethods" on the request Message object // to build the "additionalOperationArguments" option for the operation response. if (isset($descriptor['additionalArgumentMethods'])) { $additionalArgs = []; foreach ($descriptor['additionalArgumentMethods'] as $additionalArgsMethodName) { $additionalArgs[] = $request->$additionalArgsMethodName(); } $descriptor['additionalOperationArguments'] = $additionalArgs; unset($descriptor['additionalArgumentMethods']); } if (isset($descriptor['metadataReturnType'])) { $metadataReturnType = $descriptor['metadataReturnType']; } $callStack = new OperationsMiddleware($callStack, $client, $descriptor); $call = new Call( $this->buildMethod($interfaceName, $methodName), $operationClass ?: Operation::class, $request, [], Call::UNARY_CALL ); $this->modifyUnaryCallable($callStack); return $callStack($call, $optionalArgs + array_filter([ 'metadataReturnType' => $metadataReturnType, 'audience' => self::getDefaultAudience() ])); } /** * @param string $methodName * @param array $optionalArgs * @param string $decodeType * @param Message $request * @param string $interfaceName * * @return PagedListResponse */ private function getPagedListResponse( string $methodName, array $optionalArgs, string $decodeType, Message $request, ?string $interfaceName = null ) { return $this->getPagedListResponseAsync( $methodName, $optionalArgs, $decodeType, $request, $interfaceName )->wait(); } /** * @param string $methodName * @param array $optionalArgs * @param string $decodeType * @param Message $request * @param string $interfaceName * * @return PromiseInterface */ private function getPagedListResponseAsync( string $methodName, array $optionalArgs, string $decodeType, Message $request, ?string $interfaceName = null ) { $optionalArgs = $this->configureCallOptions($optionalArgs); $callStack = $this->createCallStack( $this->configureCallConstructionOptions($methodName, $optionalArgs) ); $descriptor = new PageStreamingDescriptor( $this->descriptors[$methodName]['pageStreaming'] ); $callStack = new PagedMiddleware($callStack, $descriptor); $call = new Call( $this->buildMethod($interfaceName, $methodName), $decodeType, $request, [], Call::UNARY_CALL ); $this->modifyUnaryCallable($callStack); return $callStack($call, $optionalArgs + array_filter([ 'audience' => self::getDefaultAudience() ])); } /** * @param string $interfaceName * @param string $methodName * * @return string */ private function buildMethod(?string $interfaceName = null, ?string $methodName = null) { return sprintf( '%s/%s', $interfaceName ?: $this->serviceName, $methodName ); } /** * @param array $headerParams * @param Message|null $request * * @return array */ private function buildRequestParamsHeader(array $headerParams, ?Message $request = null) { $headers = []; // No request message means no request-based headers. if (!$request) { return $headers; } foreach ($headerParams as $headerParam) { $msg = $request; $value = null; foreach ($headerParam['fieldAccessors'] as $accessor) { $value = $msg->$accessor(); // In case the field in question is nested in another message, // skip the header param when the nested message field is unset. $msg = $value; if (is_null($msg)) { break; } } $keyName = $headerParam['keyName']; // If there are value pattern matchers configured and the target // field was set, evaluate the matchers in the order that they were // annotated in with last one matching wins. $original = $value; $matchers = isset($headerParam['matchers']) && !is_null($value) ? $headerParam['matchers'] : []; foreach ($matchers as $matcher) { $matches = []; if (preg_match($matcher, $original, $matches)) { $value = $matches[$keyName]; } } // If there are no matches or the target field was unset, skip this // header param. if (!$value) { continue; } $headers[$keyName] = $value; } $requestParams = new RequestParamsHeaderDescriptor($headers); return $requestParams->getHeader(); } /** * The SERVICE_ADDRESS constant is set by GAPIC clients */ private static function getDefaultAudience() { if (!defined('self::SERVICE_ADDRESS')) { return null; } return 'https://' . self::SERVICE_ADDRESS . '/'; // @phpstan-ignore-line } /** * Modify the unary callable. * * @param callable $callable * @access private */ protected function modifyUnaryCallable(callable &$callable) { // Do nothing - this method exists to allow callable modification by partial veneers. } /** * Modify the streaming callable. * * @param callable $callable * @access private */ protected function modifyStreamingCallable(callable &$callable) { // Do nothing - this method exists to allow callable modification by partial veneers. } /** * @internal */ private function isBackwardsCompatibilityMode(): bool { return $this->backwardsCompatibilityMode ?? $this->backwardsCompatibilityMode = substr(__CLASS__, -11) === 'GapicClient'; } }