Completed
Push — master ( d1435b...eec67d )
by
unknown
10s
created

ApiLoader::getClientOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 7
rs 10
c 0
b 0
f 0
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());
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);
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);
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) {
0 ignored issues
show
introduced by
$frameworkPackage is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
239
            return null;
240
        }
241
        return $frameworkPackage->Version;
242
    }
243
}
244