Passed
Push — master ( fcb909...cb681d )
by Robbie
02:26
created

ApiLoader   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 209
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 209
rs 10
c 0
b 0
f 0
wmc 24

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getGuzzleClient() 0 3 1
C doRequest() 0 43 8
A resolveVersion() 0 7 2
A createRequest() 0 9 2
A getCache() 0 7 2
A handleCacheFromResponse() 0 14 4
A setGuzzleClient() 0 4 1
A setToCache() 0 6 1
A setCache() 0 4 1
A getFromCache() 0 10 2
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' => '%$GuzzleHttp\Client',
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, ['http_errors' => false]);
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
     * Attempts to load something from the cache and deserializes from JSON if successful
117
     *
118
     * @return array|bool
119
     * @throws \Psr\SimpleCache\InvalidArgumentException
120
     */
121
    protected function getFromCache()
122
    {
123
        $cacheKey = $this->getCacheKey();
124
        $result = $this->getCache()->get($cacheKey, false);
125
        if ($result === false) {
126
            return false;
127
        }
128
129
        // Deserialize JSON object and return as an array
130
        return Convert::json2array($result);
131
    }
132
133
    /**
134
     * Given a value, set it to the cache with the given key after serializing the value as JSON
135
     *
136
     * @param string $cacheKey
137
     * @param mixed $value
138
     * @param int $ttl
139
     * @return bool
140
     * @throws \Psr\SimpleCache\InvalidArgumentException
141
     */
142
    protected function setToCache($cacheKey, $value, $ttl = null)
143
    {
144
        // Seralize as JSON to ensure array etc can be stored
145
        $value = Convert::raw2json($value);
146
147
        return $this->getCache()->set($cacheKey, $value, $ttl);
148
    }
149
150
    /**
151
     * Check the API response for cache control headers and respect them internally in the SilverStripe
152
     * cache if found
153
     *
154
     * @param Response $response
155
     * @param array|string $result
156
     * @throws \Psr\SimpleCache\InvalidArgumentException
157
     */
158
    protected function handleCacheFromResponse(Response $response, $result)
159
    {
160
        // Handle caching if requested
161
        if ($cacheControl = $response->getHeader('Cache-Control')) {
162
            // Combine separate header rows
163
            $cacheControl = implode(', ', $cacheControl);
164
165
            if (strpos($cacheControl, 'no-store') === false
166
                && preg_match('/(?:max-age=)(\d+)/i', $cacheControl, $matches)
167
            ) {
168
                $duration = (int) $matches[1];
169
170
                $cacheKey = $this->getCacheKey();
171
                $this->setToCache($cacheKey, $result, $duration);
172
            }
173
        }
174
    }
175
176
    /**
177
     * @return CacheInterface
178
     */
179
    public function getCache()
180
    {
181
        if (!$this->cache) {
182
            $this->cache = Injector::inst()->get(CacheInterface::class . '.silverstripeMaintenance');
183
        }
184
185
        return $this->cache;
186
    }
187
188
    /**
189
     * @param CacheInterface $cache
190
     * @return $this
191
     */
192
    public function setCache(CacheInterface $cache)
193
    {
194
        $this->cache = $cache;
195
        return $this;
196
    }
197
198
    /**
199
     * Create a request with some standard headers
200
     *
201
     * @param string $uri
202
     * @param string $method
203
     * @return Request
204
     */
205
    protected function createRequest($uri, $method = 'GET')
206
    {
207
        $headers = [];
208
        $version = $this->resolveVersion();
209
        if (!empty($version)) {
210
            $headers['Silverstripe-Framework-Version'] = $version;
211
        }
212
213
        return new Request($method, $uri, $headers);
214
    }
215
216
    /**
217
     * Resolve the framework version of SilverStripe.
218
     *
219
     * @return string|null
220
     */
221
    protected function resolveVersion()
222
    {
223
        $frameworkPackage = Package::get()->find('Name', 'silverstripe/framework');
224
        if (!$frameworkPackage) {
0 ignored issues
show
introduced by
$frameworkPackage is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
225
            return null;
226
        }
227
        return $frameworkPackage->Version;
228
    }
229
}
230