66
77use Closure ;
88use GuzzleHttp \Client as GuzzleClient ;
9- use GuzzleHttp \Exception \ClientException ;
109use GuzzleHttp \Exception \GuzzleException ;
1110use GuzzleHttp \HandlerStack ;
1211use GuzzleHttp \MessageFormatter ;
1817use Psr \Http \Message \RequestInterface ;
1918use Psr \Http \Message \ResponseInterface ;
2019use Psr \Log \LoggerInterface ;
20+ use Psr \Log \NullLogger ;
21+ use Symfony \Component \Validator \Constraints \NotBlank ;
2122use Symfony \Component \Validator \Constraints \Range ;
2223use Symfony \Component \Validator \Constraints \Url ;
2324use Symfony \Component \Validator \ConstraintViolationInterface ;
2425use Symfony \Component \Validator \Validation ;
26+ use Throwable ;
2527
2628abstract class Client
2729{
28- private const DEFAULT_USER_AGENT = 'Notification PHP Client ' ;
29- private const DEFAULT_BACKOFF_RETRIES = 3 ;
3030 private const JSON_DEPTH = 512 ;
31-
31+ protected string $ tokenHeaderName = '' ;
3232 protected GuzzleClient $ guzzle ;
3333
34+ /**
35+ * @param array{
36+ * handler?: HandlerStack,
37+ * backoffMaxTries: int<0, 100>,
38+ * userAgent: string,
39+ * logger?: LoggerInterface
40+ * } $options
41+ */
3442 public function __construct (
3543 string $ baseUrl ,
3644 ?string $ token ,
37- array $ options = []
45+ array $ options
3846 ) {
3947 $ validator = Validation::createValidator ();
4048 $ errors = $ validator ->validate ($ baseUrl , [new Url ()]);
41- if (!empty ($ options ['backoffMaxTries ' ])) {
42- $ errors ->addAll ($ validator ->validate ($ options ['backoffMaxTries ' ], [new Range (['min ' => 0 , 'max ' => 100 ])]));
43- $ options ['backoffMaxTries ' ] = intval ($ options ['backoffMaxTries ' ]);
44- } else {
45- $ options ['backoffMaxTries ' ] = self ::DEFAULT_BACKOFF_RETRIES ;
46- }
47- if (empty ($ options ['userAgent ' ])) {
48- $ options ['userAgent ' ] = self ::DEFAULT_USER_AGENT ;
49- }
49+ $ errors ->addAll ($ validator ->validate (
50+ // @phpstan-ignore-next-line
51+ $ options ['backoffMaxTries ' ] ?? null ,
52+ [new NotBlank (null , '"backoffMaxTries" option must be provided ' )]
53+ ));
54+ $ errors ->addAll ($ validator ->validate (
55+ // @phpstan-ignore-next-line
56+ $ options ['backoffMaxTries ' ] ?? null ,
57+ [new Range (['min ' => 0 , 'max ' => 100 ])]
58+ ));
59+ $ errors ->addAll ($ validator ->validate (
60+ // @phpstan-ignore-next-line
61+ $ options ['userAgent ' ] ?? null ,
62+ [new NotBlank (null , '"userAgent" option must be provided ' )]
63+ ));
5064 if ($ errors ->count () !== 0 ) {
5165 $ messages = '' ;
5266 /** @var ConstraintViolationInterface $error */
@@ -55,63 +69,86 @@ public function __construct(
5569 }
5670 throw new NotificationClientException ('Invalid parameters when creating client: ' . $ messages );
5771 }
72+
5873 $ this ->guzzle = $ this ->initClient ($ baseUrl , $ token , $ options );
5974 }
6075
61- private function createDefaultDecider (int $ maxRetries ): Closure
76+ private function createDefaultDecider (int $ maxRetries, LoggerInterface $ logger ): Closure
6277 {
6378 return function (
6479 $ retries ,
6580 RequestInterface $ request ,
6681 ?ResponseInterface $ response = null ,
67- $ error = null
68- ) use ($ maxRetries ) {
82+ ?Throwable $ error = null
83+ ) use (
84+ $ maxRetries ,
85+ $ logger
86+ ) {
6987 if ($ retries >= $ maxRetries ) {
88+ $ logger ->notice (sprintf ('We have tried this %d times. Giving up. ' , $ maxRetries ));
7089 return false ;
90+ } elseif ($ response && $ response ->getStatusCode () >= 500 ) {
91+ $ logger ->notice (sprintf (
92+ 'Got a %s response for this reason: %s, retrying. ' ,
93+ $ response ->getStatusCode (),
94+ $ response ->getReasonPhrase ()
95+ ));
96+ return true ;
7197 } elseif ($ error && $ error ->getCode () >= 500 ) {
98+ $ logger ->notice (sprintf (
99+ 'Got a %s error with this message: %s, retrying. ' ,
100+ $ error ->getCode (),
101+ $ error ->getMessage ()
102+ ));
72103 return true ;
73104 } else {
74105 return false ;
75106 }
76107 };
77108 }
78109
79- abstract protected function getTokenHeaderName (): ?string ;
80-
81- private function initClient (string $ url , ?string $ token , array $ options = []): GuzzleClient
110+ /**
111+ * @param array{
112+ * handler?: HandlerStack,
113+ * backoffMaxTries: int<0, 100>,
114+ * userAgent: string,
115+ * logger?: LoggerInterface
116+ * } $options
117+ */
118+ private function initClient (string $ url , ?string $ token , array $ options ): GuzzleClient
82119 {
83120 // Initialize handlers (start with those supplied in constructor)
84121 $ handlerStack = HandlerStack::create ($ options ['handler ' ] ?? null );
85- // Set exponential backoff
86- $ handlerStack ->push (Middleware::retry ($ this ->createDefaultDecider ($ options ['backoffMaxTries ' ])));
87- // Set handler to set default headers
88- $ handlerStack ->push (Middleware::mapRequest (
89- function (RequestInterface $ request ) use ($ token , $ options ) {
90- $ request = $ request
91- ->withHeader ('User-Agent ' , $ options ['userAgent ' ])
92- ->withHeader ('Content-type ' , 'application/json ' );
93- if ($ this ->getTokenHeaderName ()) {
94- $ request = $ request ->withHeader ((string ) $ this ->getTokenHeaderName (), (string ) $ token );
95- }
96- return $ request ;
97- }
98- ));
122+
99123 // Set client logger
100- if (isset ($ options ['logger ' ]) && $ options [ ' logger ' ] instanceof LoggerInterface ) {
124+ if (isset ($ options ['logger ' ])) {
101125 $ handlerStack ->push (Middleware::log (
102126 $ options ['logger ' ],
103127 new MessageFormatter (
104128 '{hostname} {req_header_User-Agent} - [{ts}] "{method} {resource} {protocol}/{version}" ' .
105129 ' {code} {res_header_Content-Length} '
106- )
130+ ),
131+ 'debug '
107132 ));
133+ $ logger = $ options ['logger ' ];
134+ } else {
135+ $ logger = new NullLogger ();
108136 }
137+
138+ // Set exponential backoff
139+ $ handlerStack ->push (Middleware::retry ($ this ->createDefaultDecider ($ options ['backoffMaxTries ' ], $ logger )));
140+ $ headers ['User-Agent ' ] = $ options ['userAgent ' ];
141+ if ($ this ->tokenHeaderName ) {
142+ $ headers [$ this ->tokenHeaderName ] = (string ) $ token ;
143+ }
144+
109145 // finally create the instance
110146 return new GuzzleClient ([
111147 'base_uri ' => $ url ,
112148 'handler ' => $ handlerStack ,
113149 RequestOptions::CONNECT_TIMEOUT => 10 ,
114150 RequestOptions::TIMEOUT => 120 ,
151+ 'headers ' => $ headers ,
115152 ]);
116153 }
117154
@@ -123,9 +160,7 @@ protected function sendRequest(Request $request): array
123160 if ($ body === '' ) {
124161 return [];
125162 }
126- return json_decode ($ body , true , self ::JSON_DEPTH , JSON_THROW_ON_ERROR );
127- } catch (ClientException $ e ) {
128- throw new NotificationClientException ($ e ->getMessage (), $ e ->getCode (), $ e );
163+ return (array ) json_decode ($ body , true , self ::JSON_DEPTH , JSON_THROW_ON_ERROR );
129164 } catch (GuzzleException $ e ) {
130165 throw new NotificationClientException ($ e ->getMessage (), $ e ->getCode (), $ e );
131166 } catch (JsonException $ e ) {
0 commit comments