ImageTransformer::clientHints()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Psr7Middlewares\Middleware;
4
5
use Psr7Middlewares\Utils;
6
use Psr\Http\Message\ServerRequestInterface;
7
use Psr\Http\Message\ResponseInterface;
8
use Psr\Cache\CacheItemPoolInterface;
9
use Imagecow\Image;
10
11
/**
12
 * Middleware to manipulate images on demand.
13
 */
14
class ImageTransformer
15
{
16
    use Utils\CacheMessageTrait;
17
    use Utils\AttributeTrait;
18
    use Utils\StreamTrait;
19
20
    const KEY_GENERATOR = 'IMAGE_TRANSFORMER';
21
22
    /**
23
     * @var array|false Enable client hints
24
     */
25
    private $clientHints = false;
26
27
    /**
28
     * @var array Available sizes
29
     */
30
    private $sizes = [];
31
32
    /**
33
     * Returns a callable to generate urls.
34
     *
35
     * @param ServerRequestInterface $request
36
     *
37
     * @return callable|null
38
     */
39
    public static function getGenerator(ServerRequestInterface $request)
40
    {
41
        return self::getAttribute($request, self::KEY_GENERATOR);
42
    }
43
44
    /**
45
     * Define the available sizes, for example:
46
     * [
47
     *    'small'  => 'resizeCrop,50,50',
48
     *    'medium' => 'resize,500',
49
     *    'big'    => 'resize,1000',
50
     * ].
51
     *
52
     * @param array $sizes
53
     */
54
    public function __construct(array $sizes)
55
    {
56
        foreach ($sizes as $prefix => $transform) {
57
            if (strpos($prefix, '/') === false) {
58
                $path = '';
59
            } else {
60
                $path = pathinfo($prefix, PATHINFO_DIRNAME);
61
                $prefix = pathinfo($prefix, PATHINFO_BASENAME);
62
            }
63
64
            if (!isset($this->sizes[$prefix])) {
65
                $this->sizes[$prefix] = [$path => $transform];
66
            } else {
67
                $this->sizes[$prefix][$path] = $transform;
68
            }
69
        }
70
    }
71
72
    /**
73
     * To save the transformed images in the cache.
74
     *
75
     * @param CacheItemPoolInterface $cache
76
     *
77
     * @return self
78
     */
79
    public function cache(CacheItemPoolInterface $cache)
80
    {
81
        $this->cache = $cache;
82
83
        return $this;
84
    }
85
86
    /**
87
     * Enable the client hints.
88
     *
89
     * @param array $clientHints
90
     *
91
     * @return self
92
     */
93
    public function clientHints($clientHints = ['Dpr', 'Viewport-Width', 'Width'])
94
    {
95
        $this->clientHints = $clientHints;
96
97
        return $this;
98
    }
99
100
    /**
101
     * Execute the middleware.
102
     *
103
     * @param ServerRequestInterface $request
104
     * @param ResponseInterface      $response
105
     * @param callable               $next
106
     *
107
     * @return ResponseInterface
108
     */
109
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
110
    {
111
        switch (Utils\Helpers::getMimeType($response)) {
112
            case 'image/jpeg':
113
            case 'image/gif':
114
            case 'image/png':
115
                $key = $this->getCacheKey($request);
116
117
                //Get from the cache
118
                if ($cached = $this->getFromCache($key, $response)) {
119
                    return $cached;
120
                }
121
122
                $info = $this->parsePath($request->getUri()->getPath());
123
124
                if (!$info) {
125
                    break;
126
                }
127
128
                //Removes the transform info in the path
129
                list($path, $transform) = $info;
130
                $request = $request->withUri($request->getUri()->withPath($path));
131
                $response = $next($request, $response);
132
133
                //Transform
134
                if ($response->getStatusCode() === 200 && $response->getBody()->getSize()) {
135
                    $response = $this->transform($request, $response, $transform);
136
137
                    //Save in the cache
138
                    $this->saveIntoCache($key, $response);
139
                }
140
141
                return $response;
142
143
            case 'text/html':
144
                $generator = function ($path, $transform) {
145
                    $info = pathinfo($path);
146
147
                    if (!isset($this->sizes[$transform])) {
148
                        throw new \InvalidArgumentException(sprintf('The image size "%s" is not valid', $transform));
149
                    }
150
151
                    return Utils\Helpers::joinPath($info['dirname'], $transform.$info['basename']);
152
                };
153
154
                $request = self::setAttribute($request, self::KEY_GENERATOR, $generator);
155
                $response = $next($request, $response);
156
157
                if (!empty($this->clientHints)) {
158
                    return $response->withHeader('Accept-CH', implode(',', $this->clientHints));
159
                }
160
161
                return $response;
162
        }
163
164
        return $next($request, $response);
165
    }
166
167
    /**
168
     * Transform the image.
169
     *
170
     * @param ServerRequestInterface $request
171
     * @param ResponseInterface      $response
172
     * @param string                 $transform
173
     *
174
     * @return ResponseInterface
175
     */
176
    private function transform(ServerRequestInterface $request, ResponseInterface $response, $transform)
177
    {
178
        $image = Image::fromString((string) $response->getBody());
179
        $hints = $this->getClientHints($request);
180
181
        if ($hints) {
182
            $image->setClientHints($hints);
183
            $response = $response->withHeader('Vary', implode(', ', $hints));
184
        }
185
186
        $image->transform($transform);
187
188
        $body = self::createStream($response->getBody());
189
        $body->write($image->getString());
190
191
        return $response
192
            ->withBody($body)
193
            ->withHeader('Content-Type', $image->getMimeType());
194
    }
195
196
    /**
197
     * Parses the path and return the file and transform values.
198
     * For example, the path "/images/small.avatar.jpg" returns:
199
     * ["/images/avatar.jpg", "resizeCrop,50,50"].
200
     *
201
     * @param string $path
202
     *
203
     * @return false|array [file, transform]
204
     */
205
    private function parsePath($path)
206
    {
207
        $info = pathinfo($path);
208
        $basename = $info['basename'];
209
        $dirname = $info['dirname'];
210
211
        foreach ($this->sizes as $prefix => $paths) {
212
            if (strpos($basename, $prefix) !== 0) {
213
                continue;
214
            }
215
216
            foreach ($paths as $path => $transform) {
217
                $needle = $path === '' ? '' : substr($dirname, -strlen($path));
218
219
                if ($path === $needle) {
220
                    return [Utils\Helpers::joinPath($dirname, substr($basename, strlen($prefix))), $transform];
221
                }
222
            }
223
        }
224
225
        return false;
226
    }
227
228
    /**
229
     * Returns the client hints sent.
230
     *
231
     * @param ServerRequestInterface $request
232
     *
233
     * @return array|null
234
     */
235
    private function getClientHints(ServerRequestInterface $request)
236
    {
237
        if (!empty($this->clientHints)) {
238
            $hints = [];
239
240
            foreach ($this->clientHints as $name) {
241
                if ($request->hasHeader($name)) {
242
                    $hints[$name] = $request->getHeaderLine($name);
243
                }
244
            }
245
246
            return $hints;
247
        }
248
    }
249
250
    /**
251
     * Generates the key used to save the image in cache.
252
     *
253
     * @param ServerRequestInterface $request
254
     *
255
     * @return string
256
     */
257
    private function getCacheKey(ServerRequestInterface $request)
258
    {
259
        $id = base64_encode((string) $request->getUri());
260
        $hints = $this->getClientHints($request);
261
262
        if ($hints) {
263
            $id .= '.'.base64_encode(json_encode($hints));
264
        }
265
266
        return $id;
267
    }
268
}
269