66
77use ItkDev \F2ApiClient \Exception \ApiException ;
88use ItkDev \F2ApiClient \Exception \RuntimeException ;
9+ use ItkDev \F2ApiClient \Model \AbstractItem ;
910use ItkDev \F2ApiClient \Model \Atom ;
1011use ItkDev \F2ApiClient \Model \CaseFile ;
12+ use ItkDev \F2ApiClient \Model \Document ;
1113use ItkDev \F2ApiClient \Model \Matter ;
14+ use Psr \Cache \CacheItemInterface ;
15+ use Psr \Cache \CacheItemPoolInterface ;
16+ use Symfony \Component \Cache \Adapter \ProxyAdapter ;
1217use Symfony \Component \HttpClient \HttpClient ;
1318use Symfony \Component \HttpFoundation \Request ;
1419use Symfony \Component \HttpFoundation \Response ;
1520use Symfony \Component \OptionsResolver \OptionsResolver ;
21+ use Symfony \Contracts \Cache \CacheInterface ;
1622use Symfony \Contracts \HttpClient \HttpClientInterface ;
1723use Symfony \Contracts \HttpClient \ResponseInterface ;
1824
25+ /**
26+ * @phpstan-type AccessToken array{access_token: string, token_type: string, expires_in: int, refresh_token: string}
27+ */
1928class ApiClient
2029{
2130 private readonly array $ options ;
@@ -40,53 +49,42 @@ public function __construct(array $options)
4049 */
4150 public function getServiceIndex (): array
4251 {
43- $ path = '/F2Rest/ServiceIndex ' ;
44- $ response = $ this ->client ()->request (Request::METHOD_GET , $ path , [
45- 'headers ' => [
46- 'Accept ' => 'application/json ' ,
47- ],
48- ]);
52+ $ cache = $ this ->getCache ();
53+ $ cacheKey = sha1 (__METHOD__ );
54+
55+ return $ cache ->get ($ cacheKey , function (CacheItemInterface $ item ): array {
56+ $ item ->expiresAfter ((int ) $ this ->options ['cache_item_lifetime ' ]);
57+
58+ $ path = '/F2Rest/ServiceIndex ' ;
59+ $ response = $ this ->client ()->request (Request::METHOD_GET , $ path , [
60+ 'headers ' => [
61+ 'Accept ' => 'application/json ' ,
62+ ],
63+ ]);
4964
50- return $ response ->toArray ();
65+ return $ response ->toArray ();
66+ });
5167 }
5268
5369 /**
54- * @param array{q: string, count: int} $query
5570 * @return Atom[]
5671 *
5772 * @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
5873 * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
5974 * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
6075 * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
6176 */
62- public function caseSearch (string $ q , int $ count = 10 ): array
77+ public function caseSearch (string $ searchTerms , int $ count = 10 ): array
6378 {
6479 $ query = [
65- 'q ' => $ q ,
80+ 'searchTerms ' => $ searchTerms ,
6681 'count ' => $ count ,
6782 ];
68- $ path = '/F2Rest/search/cases ' ;
69- // @todo /F2Rest/ServiceIndex.json reports
70- //
71- // "http://cbrain.com/casefile/rel/case-search": {
72- // "href": "/F2Rest/search/cases",
73- // "title": "Case file search"
74- // },
75- //
76- // which refers to the actual search URL: /F2Rest/searches/cases
77- $ path = '/F2Rest/searches/cases ' ;
78- // $path = $this->getRequestUrl('http://cbrain.com/casefile/rel/case-search');
79- $ response = $ this ->request (Request::METHOD_GET , $ path , [
80- 'query ' => $ query ,
81- ]);
8283
83- $ items = [];
84- $ sxe = new \SimpleXMLElement ($ response ->getContent ());
85- foreach ($ sxe ->entry as $ entry ) {
86- $ items [] = Atom::fromSimpleXMLElement ($ entry );
87- }
84+ $ url = $ this ->getSearchRequestUrl ('http://cbrain.com/casefile/rel/case-search ' , $ query );
85+ $ response = $ this ->request (Request::METHOD_GET , $ url );
8886
89- return $ items ;
87+ return $ this -> createSearchResult ( $ response ) ;
9088 }
9189
9290 public function caseById (string $ id ): CaseFile
@@ -100,39 +98,20 @@ public function caseById(string $id): CaseFile
10098 throw new ApiException ($ response );
10199 }
102100
103- return CaseFile:: fromSimpleXMLElement ( new \ SimpleXMLElement ( $ response -> getContent ()) );
101+ return $ this -> createItemResult ( $ response , CaseFile::class );
104102 }
105103
106104 public function matterSearch (string $ searchTerms , int $ count = 10 ): array
107105 {
108106 $ query = [
109- 'searchTerms ' => $ searchTerms ,
110- 'count ' => $ count ,
107+ 'searchTerms ' => $ searchTerms ,
108+ 'count ' => $ count ,
111109 ];
112- // @todo /F2Rest/ServiceIndex.json reports
113- //
114- // "http://cbrain.com/casefile/rel/case-search": {
115- // "href": "/F2Rest/search/cases",
116- // "title": "Case file search"
117- // },
118- //
119- // which refers to the actual search URL: /F2Rest/searches/cases
120- $ url = '/F2Rest/searches/matters ' ;
121- $ url = '/F2Rest/searches/matters?q={searchTerms}&count={count} ' ;
122- $ url = $ this ->replacePlaceholders ($ url , $ query );
123- // $url = $this->getRequestUrl('http://cbrain.com/casefile/rel/matter-search', [
124- // ]);
125- $ response = $ this ->request (Request::METHOD_GET , $ url , [
126- 'query ' => $ query ,
127- ]);
128110
129- $ items = [];
130- $ sxe = new \SimpleXMLElement ($ response ->getContent ());
131- foreach ($ sxe ->entry as $ entry ) {
132- $ items [] = Atom::fromSimpleXMLElement ($ entry );
133- }
111+ $ url = $ this ->getSearchRequestUrl ('http://cbrain.com/casefile/rel/matter-search ' , $ query );
112+ $ response = $ this ->request (Request::METHOD_GET , $ url );
134113
135- return $ items ;
114+ return $ this -> createSearchResult ( $ response ) ;
136115 }
137116
138117 public function matterById (string $ id ): Matter
@@ -146,7 +125,7 @@ public function matterById(string $id): Matter
146125 throw new ApiException ($ response );
147126 }
148127
149- return Matter:: fromSimpleXMLElement ( new \ SimpleXMLElement ( $ response -> getContent ()) );
128+ return $ this -> createItemResult ( $ response , Matter::class );
150129 }
151130
152131 public function matterByMatterNumber (string $ matterNumber ): Matter
@@ -160,38 +139,54 @@ public function matterByMatterNumber(string $matterNumber): Matter
160139 throw new ApiException ($ response );
161140 }
162141
163- return Matter:: fromSimpleXMLElement ( new \ SimpleXMLElement ( $ response -> getContent ()) );
142+ return $ this -> createItemResult ( $ response , Matter::class );
164143 }
165144
145+ public function documentById (string $ id ): Document
146+ {
147+ $ url = $ this ->getRequestUrl ('http://cbrain.com/casefile/rel/document-by-id ' , [
148+ 'id ' => $ id ,
149+ ]);
150+ $ response = $ this ->request (Request::METHOD_GET , $ url );
151+
152+ if (Response::HTTP_OK !== $ response ->getStatusCode ()) {
153+ throw new ApiException ($ response );
154+ }
155+
156+ return $ this ->createItemResult ($ response , Document::class);
157+ }
166158
167159 /**
168- * @return array{access_token: string, token_type: string}
160+ * @return AccessToken
169161 */
170162 protected function getAccessToken (): array
171163 {
172- // @todo Check existing access token is not expired.
173-
174- $ client = $ this ->client ();
175- $ response = $ client ->request (Request::METHOD_POST , '/F2Rest/oauth2/token ' , [
176- 'auth_basic ' => [
177- $ this ->options ['api_username ' ],
178- $ this ->options ['api_secret ' ],
179- ],
180- 'headers ' => [
181- 'accept ' => 'application/json ' ,
182- ],
183- 'body ' => [
184- 'grant_type ' => 'password ' ,
185- 'username ' => $ this ->options ['f2_username ' ],
186- ],
187- ]);
164+ $ cache = $ this ->getCache ();
165+ $ cacheKey = sha1 (__METHOD__ );
166+
167+ return $ cache ->get ($ cacheKey , function (CacheItemInterface $ item ): array {
168+ $ client = $ this ->client ();
169+ $ response = $ client ->request (Request::METHOD_POST , '/F2Rest/oauth2/token ' , [
170+ 'auth_basic ' => [
171+ $ this ->options ['api_username ' ],
172+ $ this ->options ['api_secret ' ],
173+ ],
174+ 'headers ' => [
175+ 'accept ' => 'application/json ' ,
176+ ],
177+ 'body ' => [
178+ 'grant_type ' => 'password ' ,
179+ 'username ' => $ this ->options ['f2_username ' ],
180+ ],
181+ ]);
188182
189- /** @var array{access_token: string, token_type: string} $token */
190- $ token = $ response ->toArray ();
183+ /** @var AccessToken */
184+ $ token = $ response ->toArray ();
191185
192- // @todo Store access token.
186+ $ item -> expiresAfter (( int ) $ token[ ' expires_in ' ] - 60 );
193187
194- return $ token ;
188+ return $ token ;
189+ });
195190 }
196191
197192 protected function request (string $ method , string $ path , array $ options = []): ResponseInterface
@@ -221,17 +216,21 @@ protected function client(): HttpClientInterface
221216
222217 protected function configureOptions (OptionsResolver $ resolver ): void
223218 {
224- $ resolver ->setRequired ([
225- 'api_uri ' ,
226- 'api_username ' ,
227- 'api_secret ' ,
228- 'f2_username ' ,
229- ]);
219+ $ resolver
220+ ->setRequired ([
221+ 'api_uri ' ,
222+ 'api_username ' ,
223+ 'api_secret ' ,
224+ 'f2_username ' ,
225+ ])
226+ ->setDefault ('cache_item_lifetime ' , 86_400 )
227+ ->setAllowedTypes ('cache_item_lifetime ' , 'int ' )
228+ ->setRequired ('cache_item_pool ' )
229+ ->setAllowedTypes ('cache_item_pool ' , CacheItemPoolInterface::class);
230230 }
231231
232232 protected function getRequestUrl (string $ rel , array $ values ): string
233233 {
234- // @todo Cache this!
235234 $ index = $ this ->getServiceIndex ();
236235
237236 $ url = $ index [$ rel ]['href ' ] ?? null ;
@@ -242,20 +241,87 @@ protected function getRequestUrl(string $rel, array $values): string
242241 return $ this ->replacePlaceholders ($ url , $ values );
243242 }
244243
244+ protected function getSearchRequestUrl (string $ rel , array $ values ): string
245+ {
246+ $ url = $ this ->getRequestUrl ($ rel , []);
247+
248+ try {
249+ $ cache = $ this ->getCache ();
250+ $ cacheKey = sha1 (__METHOD__ .'||| ' .$ rel );
251+
252+ $ url = $ cache ->get ($ cacheKey , function (CacheItemInterface $ item ) use ($ url ) {
253+ $ item ->expiresAfter ((int ) $ this ->options ['cache_item_lifetime ' ]);
254+
255+ $ response = $ this ->request (Request::METHOD_GET , $ url );
256+ $ sxe = new \SimpleXMLElement ($ response ->getContent ());
257+
258+ // @mago-ignore analysis:mixed-array-access,non-documented-property
259+ $ searchUrl = (string ) $ sxe ->Url ['template ' ];
260+ if (!filter_var ($ searchUrl , FILTER_VALIDATE_URL )) {
261+ throw new RuntimeException (sprintf ('Cannot get search template URL for %s ' , $ url ));
262+ }
263+
264+ return $ searchUrl ;
265+ });
266+ } catch (\Exception $ e ) {
267+ throw new RuntimeException (sprintf ('Cannot get search URL for rel %s ' , $ rel ), previous: $ e );
268+ }
269+
270+ return $ this ->replacePlaceholders ($ url , $ values );
271+ }
272+
245273 protected function replacePlaceholders (string $ url , array $ values ): string
246274 {
247275 // Replace URL placeholders ('{…}')
248- return preg_replace_callback (
276+ return ( string ) preg_replace_callback (
249277 '/{(?P<name>[^}]+)}/ ' ,
250278 static function (array $ matches ) use ($ url , $ values ): string {
251279 $ name = $ matches ['name ' ];
252280 if (!array_key_exists ($ name , $ values )) {
253281 throw new RuntimeException (sprintf ('Missing value %s for URL %s ' , $ name , $ url ));
254282 }
255283
256- return rawurlencode ((string )$ values [$ name ]);
284+ return rawurlencode ((string ) $ values [$ name ]);
257285 },
258286 $ url ,
259287 );
260288 }
289+
290+ protected function getCache (): CacheInterface
291+ {
292+ /** @var CacheItemPoolInterface $pool */
293+ $ pool = $ this ->options ['cache_item_pool ' ];
294+
295+ return new ProxyAdapter (pool: $ pool );
296+ }
297+
298+ /**
299+ * @return Atom[]
300+ *
301+ * @throws \Exception
302+ */
303+ protected function createSearchResult (ResponseInterface $ response ): array
304+ {
305+ $ items = [];
306+ $ sxe = new \SimpleXMLElement ($ response ->getContent ());
307+ /** @var \SimpleXMLElement $entry */
308+ // @mago-ignore analysis:non-documented-property
309+ foreach ($ sxe ->entry as $ entry ) {
310+ $ items [] = Atom::fromSimpleXMLElement ($ entry );
311+ }
312+
313+ return $ items ;
314+ }
315+
316+ /**
317+ * @template T of AbstractItem
318+ *
319+ * @param class-string<T> $class
320+ *
321+ * @return T
322+ */
323+ protected function createItemResult (ResponseInterface $ response , string $ class ): AbstractItem
324+ {
325+ return $ class ::fromSimpleXMLElement (new \SimpleXMLElement ($ response ->getContent ()));
326+ }
261327}
0 commit comments