Completed
Push — develop ( 9b4432...a18168 )
by Stan
03:46
created

Builder::pushable()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 2
dl 0
loc 14
ccs 8
cts 8
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Krenor\Http2Pusher;
4
5
use Illuminate\Http\Request;
6
use Illuminate\Support\Collection;
7
use Symfony\Component\HttpFoundation\Cookie;
8
9
class Builder
10
{
11
    /**
12
     * The current request to read the cookie from.
13
     *
14
     * @var Request
15
     */
16
    protected $request;
17
18
    /**
19
     * Additional cookie and global pushable resources settings.
20
     *
21
     * @var array
22
     */
23
    protected $settings;
24
25
    /**
26
     * The supported extensions to push.
27
     *
28
     * @var array
29
     */
30
    protected $extensionTypes = [
31
        'css'   => 'style',
32
        'js'    => 'script',
33
        'ttf'   => 'font',
34
        'otf'   => 'font',
35
        'woff'  => 'font',
36
        'woff2' => 'font',
37
        'eot'   => 'font',
38
        'jpeg'  => 'image',
39
        'jpg'   => 'image',
40
        'png'   => 'image',
41
        'gif'   => 'image',
42
        'bmp'   => 'image',
43
        'svg'   => 'image',
44
    ];
45
46
    /**
47
     * Builder constructor.
48
     *
49
     * @param Request $request
50
     * @param array $settings
51
     */
52 16
    public function __construct(Request $request, array $settings)
53
    {
54 16
        $this->request = $request;
55 16
        $this->settings = $settings;
56 16
    }
57
58
    /**
59
     * Build the HTTP2 Push link and cache digest cookie.
60
     *
61
     * @see https://w3c.github.io/preload/#server-push-(http/2)
62
     *
63
     * @param array $resources
64
     *
65
     * @return Http2Push|null
66
     */
67 11
    public function prepare(array $resources)
68
    {
69 11
        $supported = collect($resources)
70 11
            ->merge($this->settings['global_pushes'])
71 11
            ->filter(function ($resource) {
72 11
                return array_key_exists($this->extension($resource), $this->extensionTypes);
73 11
            });
74
75 11
        if ($supported->count() < 1) {
76 2
            return null;
77
        }
78
79 9
        $transformed = $this->transform($supported);
80 9
        $cookie = $this->request->cookie($this->settings['cookie']['name']);
81
82 9
        $pushable = $this->pushable($transformed, $cookie);
83
84 9
        if ($pushable->count() < 1) {
85 1
            return null;
86
        }
87
88 8
        $link = $this->link($pushable);
89
90 8
        $cookie = new Cookie(
91 8
            $this->settings['cookie']['name'],
92 8
            $transformed->toJson(),
93 8
            strtotime("+{$this->settings['cookie']['duration']}")
94
        );
95
96 8
        return new Http2Push($pushable, $cookie, $link);
97
    }
98
99
    /**
100
     * Transform the resources to include a pushable type.
101
     *
102
     * @param Collection $collection
103
     *
104
     * @return Collection
105
     */
106
    private function transform(Collection $collection): Collection
107
    {
108 10
        return $collection->map(function ($path) {
109 10
            $hash = $this->hash($path);
110
111 10
            $extension = $this->extension($path);
112
113 10
            $type = $this->extensionTypes[$extension];
114
115 10
            return compact('path', 'type', 'hash');
116 10
        });
117
    }
118
119
    /**
120
     * Generate or get a hash of a file.
121
     *
122
     * @param string $path
123
     *
124
     * @return string
125
     */
126 11
    private function hash(string $path): string
127
    {
128 11
        $pieces = parse_url($path);
129
130
        // External url
131 11
        if (isset($pieces['host'])) {
132 4
            return substr(hash_file('md5', $path), 0, 12);
133
        }
134
135
        // TODO: Might want to check for additional version strings other than Mixs'.
136 11
        preg_match('/id=([a-f0-9]{20})/', $path, $matches);
137
138 11
        if (last($matches)) {
139 3
            return substr(last($matches), 0, 12);
140
        }
141
142 9
        return substr(hash_file('md5', public_path($path)), 0, 12);
143
    }
144
145
146
    /**
147
     * Get the extension of a file and remove query parameters.
148
     *
149
     * @param string $path
150
     *
151
     * @return string
152
     */
153 12
    private function extension($path): string
154
    {
155 12
        return strtok(
156 12
            pathinfo($path, PATHINFO_EXTENSION),
157 12
            '?'
158
        );
159
    }
160
161
    /**
162
     * Check which resources already are cached.
163
     *
164
     * @param Collection $collection
165
     * @param string|null $cache
166
     *
167
     * @return Collection
168
     */
169 9
    private function pushable(Collection $collection, $cache = null): Collection
170
    {
171 9
        if ($cache === null) {
172 7
            return $collection;
173
        }
174
175 2
        if ($cache === $collection->toJson()) {
176 1
            return collect();
177
        }
178
179 1
        $cached = json_decode($cache, true);
180
181 1
        return $collection->reject(function ($item) use ($cached) {
182 1
            return in_array($item, $cached);
183 1
        });
184
    }
185
186
    /**
187
     * Create the HTTP2 Server Push link.
188
     *
189
     * @param Collection $pushable
190
     *
191
     * @return string
192
     */
193
    private function link(Collection $pushable): string
194
    {
195 8
        return $pushable->map(function ($item) {
196 8
            $push = "<{$item['path']}>; rel=preload; as={$item['type']}";
197
198 8
            if ($item['type'] === 'font') {
199 4
                return "{$push}; crossorigin";
200
            }
201
202 8
            return $push;
203 8
        })->implode(',');
204
    }
205
}
206