ApiLoader::doRequest()   B
last analyzed

Complexity

Conditions 8
Paths 7

Size

Total Lines 43
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 20
c 1
b 0
f 0
nc 7
nop 2
dl 0
loc 43
rs 8.4444
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
The private property $dependencies is not used, and could be removed.
Loading history...
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
Bug Best Practice introduced by
The expression return $result also could return the type true which is incompatible with the documented return type array.
Loading history...
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
Deprecated Code introduced by
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 ignore-deprecated  annotation

78
        $responseBody = /** @scrutinizer ignore-deprecated */ Convert::json2array($response->getBody()->getContents());

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.

Loading history...
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
Deprecated Code introduced by
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 ignore-deprecated  annotation

144
        return /** @scrutinizer ignore-deprecated */ Convert::json2array($result);

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.

Loading history...
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
Deprecated Code introduced by
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 ignore-deprecated  annotation

159
        $value = /** @scrutinizer ignore-deprecated */ Convert::raw2json($value);

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.

Loading history...
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