Completed
Branch master (8400a7)
by Piotr
02:05
created

EsiTentacles   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 169
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 17
lcom 1
cbo 11
dl 0
loc 169
rs 10
c 0
b 0
f 0

7 Methods

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