Passed
Push — develop ( 6e78c9...4aabf2 )
by Stan
04:01 queued 22s
created

Builder::prepare()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 17
nc 3
nop 1
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
    public function __construct(Request $request, array $settings)
53
    {
54
        $this->request = $request;
55
        $this->settings = $settings;
56
    }
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
    public function prepare(array $resources)
68
    {
69
        $supported = collect($resources)
70
            ->merge($this->settings['global_pushes'])
71
            ->filter(function ($resource) {
72
                return array_key_exists($this->getExtension($resource), $this->extensionTypes);
73
            });
74
75
        if ($supported->count() < 1) {
76
            return null;
77
        }
78
79
        $transformed = $this->transform($supported);
80
        $cookie = $this->request->cookie($this->settings['cookie']['name']);
81
82
        $pushable = $this->processCookieCache($transformed, $cookie);
83
84
        if ($pushable->count() < 1) {
85
            return null;
86
        }
87
88
        $link = $this->buildLink($pushable);
89
90
        $cookie = new Cookie(
91
            $this->settings['cookie']['name'],
92
            $transformed->toJson(),
93
            strtotime("+{$this->settings['cookie']['duration']}")
1 ignored issue
show
Bug introduced by
It seems like strtotime(EncapsedNode) can also be of type false; however, parameter $expire of Symfony\Component\HttpFo...n\Cookie::__construct() does only seem to accept integer|string|DateTimeInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
            /** @scrutinizer ignore-type */ strtotime("+{$this->settings['cookie']['duration']}")
Loading history...
94
        );
95
96
        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
        return $collection->map(function ($path) {
109
            $hash = $this->retrieveHash($path);
110
111
            $extension = $this->getExtension($path);
112
113
            $type = $this->extensionTypes[$extension];
114
115
            return compact('path', 'type', 'hash');
116
        });
117
    }
118
119
    /**
120
     * Generate or get a hash of a file.
121
     *
122
     * @param string $path
123
     *
124
     * @return string
125
     */
126
    private function retrieveHash(string $path): string
127
    {
128
        $pieces = parse_url($path);
129
130
        // External url
131
        if (isset($pieces['host'])) {
132
            return substr(hash_file('md5', $path), 0, 12);
1 ignored issue
show
Bug Best Practice introduced by
The expression return substr(hash_file('md5', $path), 0, 12) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
133
        }
134
135
        // TODO: Might want to check for additional version strings other than Mixs'.
136
        preg_match('/id=([a-f0-9]{20})/', $path, $matches);
137
138
        if (last($matches)) {
139
            return substr(last($matches), 0, 12);
1 ignored issue
show
Bug Best Practice introduced by
The expression return substr(last($matches), 0, 12) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
140
        }
141
142
        return substr(hash_file('md5', public_path($path)), 0, 12);
1 ignored issue
show
Bug Best Practice introduced by
The expression return substr(hash_file(...ic_path($path)), 0, 12) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
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
    private function getExtension($path): string
154
    {
155
        return strtok(
156
            pathinfo($path, PATHINFO_EXTENSION),
157
            '?'
158
        );
159
    }
160
161
    /**
162
     * Check which resources already are cached.
163
     *
164
     * @param Collection $pushable
165
     * @param string|null $cache
166
     *
167
     * @return Collection
168
     */
169
    private function processCookieCache(Collection $pushable, $cache = null): Collection
170
    {
171
        if ($cache === null) {
172
            return $pushable;
173
        }
174
175
        if ($cache === $pushable->toJson()) {
176
            return collect();
177
        }
178
179
        $cached = json_decode($cache, true);
180
181
        return $pushable->filter(function ($item) use ($cached) {
182
            return !in_array($item, $cached);
183
        });
184
    }
185
186
    /**
187
     * Create the HTTP2 Server Push link.
188
     *
189
     * @param Collection $pushable
190
     *
191
     * @return string
192
     */
193
    private function buildLink(Collection $pushable): string
194
    {
195
        return $pushable->map(function ($item) {
196
            $push = "<{$item['path']}>; rel=preload; as={$item['type']}";
197
198
            if ($item['type'] === 'font') {
199
                return "{$push}; crossorigin";
200
            }
201
202
            return $push;
203
        })->implode(',');
204
    }
205
}
206