Completed
Push — master ( 394662...277774 )
by Piotr
02:06
created

EsiTentacles::createSingleRequest()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 2
nop 2
1
<?php
2
3
namespace CrazyGoat\Octophpus;
4
5
use CrazyGoat\Octophpus\Validator\OptionsValidator;
6
use GuzzleHttp\Client;
7
use GuzzleHttp\Exception\ConnectException;
8
use GuzzleHttp\Promise\EachPromise;
9
use GuzzleHttp\Psr7\Response;
10
use Psr\Cache\CacheItemPoolInterface;
11
use Psr\Log\LoggerInterface;
12
13
class EsiTentacles
14
{
15
    const ON_REJECT_EMPTY = 'empty';
16
    const ON_REJECT_EXCEPTION = 'exception';
17
    const ON_TIMEOUT_H_INCLUDE = 'hinclude';
18
    const ON_TIMEOUT_EXCEPTION = 'exception';
19
20
    /**
21
     * @var array options
22
     */
23
    private $options;
24
25
    /**
26
     * @var CacheItemPoolInterface
27
     */
28
    private $cachePool = null;
29
30
    /**
31
     * @var LoggerInterface
32
     */
33
    private $logger;
34
35
    /**
36
     * Mantle constructor.
37
     * @param array $options
38
     */
39
    public function __construct(array $options = [])
40
    {
41
        $validator = new OptionsValidator($options);
42
        $validator->validate();
43
44
        $this->options = array_merge($this->defaultOptions(), $options);
45
        $this->logger = $this->options['logger'];
46
        $this->cachePool = $this->options['cache_pool'];
47
    }
48
49
    public function decorate(string $data): string
50
    {
51
        $parser = new EsiParser();
52
53
        $recurrency = $this->options['recurecny_level'];
54
55
        while ($parser->parse($data) && $recurrency > 0) {
56
57
            /** @var EsiRequest[] $esiRequests */
58
            $esiRequests = $parser->esiRequests();
59
60
            $work = new EachPromise(
61
                $this->createRequestPromises()($esiRequests),
62
                [
63
                    'concurrency' => $this->options['concurrency'],
64
                    'fulfilled' => $this->options['fulfilled']($data, $esiRequests),
65
                    'rejected' => $this->options['rejected']($data, $esiRequests)
66
                ]
67
            );
68
            $work->promise()->wait();
69
            $recurrency--;
70
        }
71
72
        return $data;
73
    }
74
75
    private function createRequestPromises(): \Closure
76
    {
77
        return (function (array $esiRequests) {
78
            $client = new Client($this->clientOptions());
79
            /** @var EsiRequest $esiRequest */
80
            foreach ($esiRequests as $esiRequest) {
81
                yield $this->createSingleRequest($esiRequest, $client);
82
            }
83
        });
84
    }
85
86
    private function createSingleRequest(EsiRequest $esiRequest, Client $client)
87
    {
88
        $cacheKey = $this->options['cache_prefix'] . ':' . base64_encode($esiRequest->getSrc());
89
90
        if ($this->cachePool instanceof CacheItemPoolInterface && $this->cachePool->hasItem($cacheKey)) {
91
            yield new Response(200, [], $this->cachePool->getItem($cacheKey)->get());
92
        }
93
94
        yield $client->requestAsync(
95
            'GET',
96
            $esiRequest->getSrc(),
97
            array_merge($this->options['request_options'], $esiRequest->requestOptions())
98
        );
99
    }
100
101
    private function defaultOptions(): array
102
    {
103
        return [
104
            'concurrency' => 5,
105
            'timeout' => 2.0,
106
            'on_reject' => static::ON_REJECT_EXCEPTION,
107
            'on_timeout' => static::ON_TIMEOUT_EXCEPTION,
108
            'base_uri' => '',
109
            'cache_prefix' => 'esi:include',
110
            'cache_ttl' => 3600,
111
            'request_options' => [],
112
            'recurecny_level' => 1,
113
            'cache_pool' => null,
114
            'logger' => new VoidLogger(),
115
            'fulfilled' => $this->defaultFulfilled(),
116
            'rejected' => $this->defaultReject()
117
        ];
118
    }
119
120
    private function defaultFulfilled(): \Closure
121
    {
122
        return function (string &$data, array $esiRequests) {
123
            return (function (Response $response, int $index) use (&$data, $esiRequests) {
124
                /** @var EsiRequest $esiRequest */
125
                $esiRequest = $esiRequests[$index];
126
                $needle = $esiRequest->getEsiTag();
127
                $pos = strpos($data, $needle);
128
                if ($pos !== false) {
129
                    $contents = $response->getBody()->getContents();
130
                    $this->setCache($esiRequest, $contents);
131
                    $data = substr_replace($data, $contents, $pos, strlen($needle));
132
                } else {
133
                    $this->logger->error('This should not happen. Could not replace previously found esi tag.');
134
                }
135
            });
136
        };
137
    }
138
139
    private function setCache(EsiRequest $esiRequest, string $content)
140
    {
141
        if ($this->cachePool instanceof CacheItemPoolInterface && !$esiRequest->noCache()) {
142
            $cacheKey = $this->options['cache_prefix'] . ':' . base64_encode($esiRequest->getSrc());
143
144
            $this->cachePool->save(
145
                $this->cachePool
146
                    ->getItem($cacheKey)
147
                    ->set($content)
148
                    ->expiresAfter($this->options['cache_ttl'])
149
            );
150
        }
151
    }
152
153
    private function defaultReject(): \Closure
154
    {
155
        return function (string &$data, array $esiRequests) {
156
            return (function (\Exception $reason, int $index) use (&$data, $esiRequests) {
157
                /** @var EsiRequest $esiRequest */
158
                $esiRequest = $esiRequests[$index];
159
160
                $this->logger->error(
161
                    'Could not fetch [' . $esiRequest->getSrc() . ']. Reason: ' . $reason->getMessage()
162
                );
163
                if ($reason instanceof ConnectException &&
164
                    $this->options['on_timeout'] == static::ON_TIMEOUT_H_INCLUDE
165
                ) {
166
                    $data = str_replace(
167
                        $esiRequest->getEsiTag(),
168
                        '<hx:include src="' . $this->options['base_uri'] . $esiRequest->getSrc() . '"></hx:include>',
169
                        $data
170
                    );
171
                    return;
172
                }
173
174
                if ($this->options['on_reject'] == static::ON_REJECT_EMPTY) {
175
                    $data = str_replace($esiRequest->getEsiTag(), '', $data);
176
                } else {
177
                    throw $reason;
178
                }
179
            });
180
        };
181
    }
182
183
    /**
184
     * @param array $options
185
     * @return EsiTentacles
186
     */
187
    public function setOptions(array $options): EsiTentacles
188
    {
189
        $validator = new OptionsValidator($options);
190
        $validator->validate();
191
192
        $this->options = $options;
193
        return $this;
194
    }
195
196
    /**
197
     * @return array
198
     */
199
    public function getOptions(): array
200
    {
201
        return $this->options;
202
    }
203
204
    private function clientOptions(): array
205
    {
206
        return [
207
            'concurrency' => $this->options['concurrency'],
208
            'timeout' => $this->options['timeout'],
209
            'base_uri' => $this->options['base_uri'],
210
        ];
211
    }
212
}