1 | <?php |
||||
2 | |||||
3 | namespace BringYourOwnIdeas\Maintenance\Util; |
||||
4 | |||||
5 | use BringYourOwnIdeas\Maintenance\Model\Package; |
||||
6 | use GuzzleHttp\Client; |
||||
7 | use GuzzleHttp\Exception\GuzzleException; |
||||
8 | use GuzzleHttp\Psr7\Request; |
||||
9 | use GuzzleHttp\Psr7\Response; |
||||
10 | use Psr\SimpleCache\CacheInterface; |
||||
11 | use RuntimeException; |
||||
12 | use SilverStripe\Core\Convert; |
||||
13 | use SilverStripe\Core\Extensible; |
||||
14 | use SilverStripe\Core\Injector\Injector; |
||||
15 | |||||
16 | /** |
||||
17 | * Handles fetching supported addon details from addons.silverstripe.org |
||||
18 | */ |
||||
19 | abstract class ApiLoader |
||||
20 | { |
||||
21 | use Extensible; |
||||
22 | |||||
23 | private static $dependencies = [ |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
24 | 'GuzzleClient' => '%$' . Client::class, |
||||
25 | ]; |
||||
26 | |||||
27 | /** |
||||
28 | * @var Client |
||||
29 | */ |
||||
30 | protected $guzzleClient; |
||||
31 | |||||
32 | /** |
||||
33 | * @var CacheInterface |
||||
34 | */ |
||||
35 | protected $cache; |
||||
36 | |||||
37 | /** |
||||
38 | * Define a unique cache key for results to be saved for each request (subclass) |
||||
39 | * |
||||
40 | * @return string |
||||
41 | */ |
||||
42 | abstract protected function getCacheKey(); |
||||
43 | |||||
44 | /** |
||||
45 | * Perform an HTTP request for module health information |
||||
46 | * |
||||
47 | * @param string $endpoint API endpoint to check for results |
||||
48 | * @param callable $callback Function to return the result of after loading the API data |
||||
49 | * @return array |
||||
50 | * @throws RuntimeException When the API responds with something that's not module health information |
||||
51 | */ |
||||
52 | public function doRequest($endpoint, callable $callback) |
||||
53 | { |
||||
54 | // Check for a cached value and return if one is available |
||||
55 | if ($result = $this->getFromCache()) { |
||||
56 | return $result; |
||||
0 ignored issues
–
show
|
|||||
57 | } |
||||
58 | |||||
59 | // Otherwise go and request data from the API |
||||
60 | $request = new Request('GET', $endpoint); |
||||
61 | $failureMessage = 'Could not obtain information about module. '; |
||||
62 | |||||
63 | try { |
||||
64 | /** @var Response $response */ |
||||
65 | $response = $this->getGuzzleClient()->send($request, $this->getClientOptions()); |
||||
66 | } catch (GuzzleException $exception) { |
||||
67 | throw new RuntimeException($failureMessage); |
||||
68 | } |
||||
69 | |||||
70 | if ($response->getStatusCode() !== 200) { |
||||
71 | throw new RuntimeException($failureMessage . 'Error code ' . $response->getStatusCode()); |
||||
72 | } |
||||
73 | |||||
74 | if (!in_array('application/json', $response->getHeader('Content-Type'))) { |
||||
75 | throw new RuntimeException($failureMessage . 'Response is not JSON'); |
||||
76 | } |
||||
77 | |||||
78 | $responseBody = Convert::json2array($response->getBody()->getContents()); |
||||
0 ignored issues
–
show
The function
SilverStripe\Core\Convert::json2array() has been deprecated: 4.4.0:5.0.0 Use json_decode() instead
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||
79 | |||||
80 | if (empty($responseBody)) { |
||||
81 | throw new RuntimeException($failureMessage . 'Response could not be parsed'); |
||||
82 | } |
||||
83 | |||||
84 | if (!isset($responseBody['success']) || !$responseBody['success']) { |
||||
85 | throw new RuntimeException($failureMessage . 'Response returned unsuccessfully'); |
||||
86 | } |
||||
87 | |||||
88 | // Allow callback to handle processing of the response body |
||||
89 | $result = $callback($responseBody); |
||||
90 | |||||
91 | // Setting the value to the cache for subsequent requests |
||||
92 | $this->handleCacheFromResponse($response, $result); |
||||
93 | |||||
94 | return $result; |
||||
95 | } |
||||
96 | |||||
97 | /** |
||||
98 | * @return Client |
||||
99 | */ |
||||
100 | public function getGuzzleClient() |
||||
101 | { |
||||
102 | return $this->guzzleClient; |
||||
103 | } |
||||
104 | |||||
105 | /** |
||||
106 | * @param Client $guzzleClient |
||||
107 | * @return $this |
||||
108 | */ |
||||
109 | public function setGuzzleClient(Client $guzzleClient) |
||||
110 | { |
||||
111 | $this->guzzleClient = $guzzleClient; |
||||
112 | return $this; |
||||
113 | } |
||||
114 | |||||
115 | /** |
||||
116 | * Get Guzzle client options |
||||
117 | * |
||||
118 | * @return array |
||||
119 | */ |
||||
120 | public function getClientOptions() |
||||
121 | { |
||||
122 | $options = [ |
||||
123 | 'http_errors' => false, |
||||
124 | ]; |
||||
125 | $this->extend('updateClientOptions', $options); |
||||
126 | return $options; |
||||
127 | } |
||||
128 | |||||
129 | /** |
||||
130 | * Attempts to load something from the cache and deserializes from JSON if successful |
||||
131 | * |
||||
132 | * @return array|bool |
||||
133 | * @throws \Psr\SimpleCache\InvalidArgumentException |
||||
134 | */ |
||||
135 | protected function getFromCache() |
||||
136 | { |
||||
137 | $cacheKey = $this->getCacheKey(); |
||||
138 | $result = $this->getCache()->get($cacheKey, false); |
||||
139 | if ($result === false) { |
||||
140 | return false; |
||||
141 | } |
||||
142 | |||||
143 | // Deserialize JSON object and return as an array |
||||
144 | return Convert::json2array($result); |
||||
0 ignored issues
–
show
The function
SilverStripe\Core\Convert::json2array() has been deprecated: 4.4.0:5.0.0 Use json_decode() instead
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||
145 | } |
||||
146 | |||||
147 | /** |
||||
148 | * Given a value, set it to the cache with the given key after serializing the value as JSON |
||||
149 | * |
||||
150 | * @param string $cacheKey |
||||
151 | * @param mixed $value |
||||
152 | * @param int $ttl |
||||
153 | * @return bool |
||||
154 | * @throws \Psr\SimpleCache\InvalidArgumentException |
||||
155 | */ |
||||
156 | protected function setToCache($cacheKey, $value, $ttl = null) |
||||
157 | { |
||||
158 | // Seralize as JSON to ensure array etc can be stored |
||||
159 | $value = Convert::raw2json($value); |
||||
0 ignored issues
–
show
The function
SilverStripe\Core\Convert::raw2json() has been deprecated: 4.4.0:5.0.0 Use json_encode() instead
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||
160 | |||||
161 | return $this->getCache()->set($cacheKey, $value, $ttl); |
||||
162 | } |
||||
163 | |||||
164 | /** |
||||
165 | * Check the API response for cache control headers and respect them internally in the SilverStripe |
||||
166 | * cache if found |
||||
167 | * |
||||
168 | * @param Response $response |
||||
169 | * @param array|string $result |
||||
170 | * @throws \Psr\SimpleCache\InvalidArgumentException |
||||
171 | */ |
||||
172 | protected function handleCacheFromResponse(Response $response, $result) |
||||
173 | { |
||||
174 | // Handle caching if requested |
||||
175 | if ($cacheControl = $response->getHeader('Cache-Control')) { |
||||
176 | // Combine separate header rows |
||||
177 | $cacheControl = implode(', ', $cacheControl); |
||||
178 | |||||
179 | if (strpos($cacheControl, 'no-store') === false |
||||
180 | && preg_match('/(?:max-age=)(\d+)/i', $cacheControl, $matches) |
||||
181 | ) { |
||||
182 | $duration = (int) $matches[1]; |
||||
183 | |||||
184 | $cacheKey = $this->getCacheKey(); |
||||
185 | $this->setToCache($cacheKey, $result, $duration); |
||||
186 | } |
||||
187 | } |
||||
188 | } |
||||
189 | |||||
190 | /** |
||||
191 | * @return CacheInterface |
||||
192 | */ |
||||
193 | public function getCache() |
||||
194 | { |
||||
195 | if (!$this->cache) { |
||||
196 | $this->cache = Injector::inst()->get(CacheInterface::class . '.silverstripeMaintenance'); |
||||
197 | } |
||||
198 | |||||
199 | return $this->cache; |
||||
200 | } |
||||
201 | |||||
202 | /** |
||||
203 | * @param CacheInterface $cache |
||||
204 | * @return $this |
||||
205 | */ |
||||
206 | public function setCache(CacheInterface $cache) |
||||
207 | { |
||||
208 | $this->cache = $cache; |
||||
209 | return $this; |
||||
210 | } |
||||
211 | |||||
212 | /** |
||||
213 | * Create a request with some standard headers |
||||
214 | * |
||||
215 | * @param string $uri |
||||
216 | * @param string $method |
||||
217 | * @return Request |
||||
218 | */ |
||||
219 | protected function createRequest($uri, $method = 'GET') |
||||
220 | { |
||||
221 | $headers = []; |
||||
222 | $version = $this->resolveVersion(); |
||||
223 | if (!empty($version)) { |
||||
224 | $headers['Silverstripe-Framework-Version'] = $version; |
||||
225 | } |
||||
226 | |||||
227 | return new Request($method, $uri, $headers); |
||||
228 | } |
||||
229 | |||||
230 | /** |
||||
231 | * Resolve the framework version of SilverStripe. |
||||
232 | * |
||||
233 | * @return string|null |
||||
234 | */ |
||||
235 | protected function resolveVersion() |
||||
236 | { |
||||
237 | $frameworkPackage = Package::get()->find('Name', 'silverstripe/framework'); |
||||
238 | if (!$frameworkPackage) { |
||||
239 | return null; |
||||
240 | } |
||||
241 | return $frameworkPackage->Version; |
||||
242 | } |
||||
243 | } |
||||
244 |