Completed
Push — master ( 0ff0ab...646508 )
by Piotr
02:59
created

EsiTentacles::defaultFulfilled()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 1
nop 0
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_lever'];
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
82
                $cacheKey = $this->options['cache_prefix'] . ':' . base64_encode($esiRequest->getSrc());
83
84
                if ($this->cachePool instanceof CacheItemPoolInterface &&
85
                    $this->cachePool->hasItem($cacheKey)
86
                ) {
87
                    $value = $this->cachePool->getItem($cacheKey)->get();
88
                    yield new Response(200, [], $value);
89
                    continue;
90
                }
91
92
                yield $client->requestAsync(
93
                    'GET',
94
                    $esiRequest->getSrc(),
95
                    array_merge($this->options['request_options'], $esiRequest->requestOptions())
96
                );
97
            }
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_lever' => 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
130
                    if ($this->cachePool instanceof CacheItemPoolInterface && !$esiRequest->isNoCache()) {
131
                        $cacheKey = $this->options['cache_prefix'] . ':' . base64_encode($esiRequest->getSrc());
132
133
                        $this->cachePool->save(
134
                            $this->cachePool
135
                                ->getItem($cacheKey)
136
                                ->set($response->getBody()->getContents())
137
                                ->expiresAfter($this->options['cache_ttl'])
138
                        );
139
                    }
140
141
                    $data = substr_replace($data, $response->getBody()->getContents(), $pos, strlen($needle));
142
                } else {
143
                    $this->logger->error('This should not happen. Could not replace previously found esi tag.');
144
                }
145
            });
146
        };
147
    }
148
149
    private function defaultReject(): \Closure
150
    {
151
        return function (string &$data, array $esiRequests) {
152
            return (function (\Exception $reason, int $index) use (&$data, $esiRequests) {
153
                /** @var EsiRequest $esiRequest */
154
                $esiRequest = $esiRequests[$index];
155
156
                $this->logger->error(
157
                    'Could not fetch [' . $esiRequest->getSrc() . ']. Reason: ' . $reason->getMessage()
158
                );
159
                if ($reason instanceof ConnectException && (
160
                        $this->options['on_timeout'] == static::ON_TIMEOUT_H_INCLUDE
161
                    )
162
                ) {
163
                    $data = str_replace(
164
                        $esiRequest->getEsiTag(),
165
                        '<hx:include src="' . $this->options['base_uri'] . $esiRequest->getSrc() . '"></hx:include>',
166
                        $data
167
                    );
168
                    return;
169
                }
170
171
                if ($this->options['on_reject'] == static::ON_REJECT_EMPTY) {
172
                    $data = str_replace($esiRequest->getEsiTag(), '', $data);
173
                } else {
174
                    throw $reason;
175
                }
176
            });
177
        };
178
    }
179
180
    /**
181
     * @param array $options
182
     * @return EsiTentacles
183
     */
184
    public function setOptions(array $options): EsiTentacles
185
    {
186
        $validator = new OptionsValidator($options);
187
        $validator->validate();
188
189
        $this->options = $options;
190
        return $this;
191
    }
192
193
    /**
194
     * @return array
195
     */
196
    public function getOptions(): array
197
    {
198
        return $this->options;
199
    }
200
201
    private function clientOptions(): array
202
    {
203
        return [
204
            'concurrency' => $this->options['concurrency'],
205
            'timeout' => $this->options['timeout'],
206
            'base_uri' => $this->options['base_uri'],
207
        ];
208
    }
209
}