Passed
Push — master ( 8a3439...c62c23 )
by Kane
04:52
created

HtmlBuilder::applyOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 7
c 2
b 0
f 0
dl 0
loc 11
rs 10
cc 1
nc 1
nop 3
1
<?php
2
namespace Cohensive\OEmbed;
3
4
class HtmlBuilder
5
{
6
    const TYPE_RAW = 'raw';
7
    const TYPE_IFRAME = 'iframe';
8
    const TYPE_VIDEO = 'video';
9
10
    public function __construct(
11
        protected string $type,
12
        protected string | array $html,
13
        protected ?string $script = null
14
    ) {
15
    }
16
17
    /**
18
     * Returns current type.
19
     */
20
    public function type(): string
21
    {
22
        return $this->type;
23
    }
24
25
    /**
26
     * Returns HTML code for media provider.
27
     */
28
    public function html(array $options = [], array $globalOptions = [], bool $amp = false): string
29
    {
30
        if (is_array($this->html)) {
31
            $attrs = $this->applyOptions($this->html, $options, $globalOptions);
32
33
            if ($this->type === self::TYPE_IFRAME) {
34
                return $this->iframe($attrs, $amp);
35
            }
36
37
            if ($this->type === self::TYPE_VIDEO) {
38
                return $this->video($attrs, $amp);
39
            }
40
41
            return '';
42
        } else {
43
            return $this->html;
44
        }
45
    }
46
47
    /**
48
     * Return AMP-friendly HTML for media provider.
49
     */
50
    public function ampHtml(array $options = [], array $globalOptions = []): string
51
    {
52
        return $this->html($options, $globalOptions, true);
53
    }
54
55
    /**
56
     * Returns URL for a given media provider embed. Returned url type depends on embed type:
57
     * iframe - string
58
     * video - string[]
59
     * raw - null
60
     */
61
    public function src(array $options = [], array $globalOptions = []): mixed
62
    {
63
        if (is_array($this->html)) {
64
            $attrs = $this->applyOptions($this->html, $options, $globalOptions);
65
66
            if ($this->type === self::TYPE_IFRAME) {
67
                return $attrs['src'] ?? null;
68
            }
69
70
            if ($this->type === self::TYPE_VIDEO) {
71
                return array_map(function ($source) {
72
                    return $source['src'];
73
                }, $attrs['source']);
74
            }
75
        }
76
77
        return null;
78
    }
79
80
    /**
81
     * Constructs <iframe> HTML-element based on array of provider attributes.
82
     */
83
    protected function iframe(array $attrs, bool $amp = false): string
84
    {
85
        $tag = $amp ? 'amp-iframe' : 'iframe';
86
87
        $html = "<$tag";
88
        foreach ($attrs as $attr => $val) {
89
            $html .= sprintf(' %s="%s"', $attr, $val);
90
        }
91
        $html .= "></$tag>";
92
93
        return $html;
94
    }
95
96
    /**
97
     * Constructs <video> HTML-element based on an array of provider attributes.
98
     */
99
    protected function video(array $attrs, bool $amp = false): string
100
    {
101
        $tag = $amp ? 'amp-video' : 'video';
102
103
        $inner = '';
104
105
        $html = "<$tag";
106
        foreach ($attrs as $attr => $val) {
107
            if (is_array($val)) {
108
                foreach ($val as $child) {
109
                    $inner .= "<$attr";
110
                    foreach ($child as $iattr => $ival) {
111
                        $inner .= sprintf(' %s="%s"', $iattr, $ival);
112
                    }
113
                    $inner .= ">";
114
                }
115
            } else {
116
                $html .= sprintf(' %s="%s"', $attr, $val);
117
            }
118
        }
119
        $html .= ">";
120
121
        $html .= $inner;
122
123
        $html .= "</$tag>";
124
125
        return $html;
126
    }
127
128
    /**
129
     * Returns script source if available.
130
     */
131
    public function script(): ?string
132
    {
133
        return $this->script;
134
    }
135
136
    /**
137
     * Converts class to an array.
138
     */
139
    public function toArray(): array
140
    {
141
        return [
142
            'type' => $this->type,
143
            'html' => $this->html,
144
        ];
145
    }
146
147
    /**
148
     * Extracts and returns an array of options for a current HTML element type.
149
     */
150
    protected function getTypeOptions(array $options): array
151
    {
152
        if (isset($options['html'])) {
153
            return $options['html'][$this->type] ?? [];
154
        }
155
156
        return [];
157
    }
158
159
    /**
160
     * Merge and apply local and global options to the provider attributes.
161
     */
162
    protected function applyOptions(array $attrs, array $options, array $globalOptions): array
163
    {
164
        $mergedOptions = array_merge($globalOptions['attributes'] ?? [], $options);
165
        $attrs = $this->applyDimensions($attrs, $mergedOptions);
166
        $attrs = $this->applyAutoplay($attrs, $mergedOptions);
167
168
        $typeOptions = $this->getTypeOptions($globalOptions);
169
170
        return array_filter(
171
            array_merge($typeOptions, $mergedOptions, $attrs),
172
            fn($value) => $value !== null
173
        );
174
    }
175
176
    /**
177
     * Apply width and height dimensions with aspect ratio calculations.
178
     */
179
    protected function applyDimensions(array $attrs, array $options): array
180
    {
181
        $width = $options['width'] ?? null;
182
        $height = $options['height'] ?? null;
183
184
        if ($this->hasNumericDimensions($attrs)) {
185
            $attrs = $this->applyAspectRatioDimensions($attrs, $width, $height);
186
        } elseif ($width || $height) {
187
            $attrs = $this->applyManualDimensions($attrs, $width, $height);
188
        }
189
190
        return $attrs;
191
    }
192
193
    /**
194
     * Check if attributes have numeric width and height values.
195
     */
196
    protected function hasNumericDimensions(array $attrs): bool
197
    {
198
        return isset($attrs['width'], $attrs['height'])
199
            && is_numeric($attrs['width'])
200
            && is_numeric($attrs['height']);
201
    }
202
203
    /**
204
     * Apply dimensions while maintaining aspect ratio.
205
     */
206
    protected function applyAspectRatioDimensions(array $attrs, mixed $width, mixed $height): array
207
    {
208
        $ratio = $attrs['width'] / $attrs['height'];
209
210
        $attrs['width'] = $width ?: round(($height ?: $attrs['height']) * $ratio);
211
        $attrs['height'] = $height ?: round($attrs['width'] / $ratio);
212
213
        return $attrs;
214
    }
215
216
    /**
217
     * Apply dimensions without aspect ratio calculations.
218
     */
219
    protected function applyManualDimensions(array $attrs, ?int $width, ?int $height): array
220
    {
221
        if ($width) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $width of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
222
            $attrs['width'] = $width;
223
        }
224
225
        if ($height) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $height of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
226
            $attrs['height'] = $height;
227
        }
228
229
        return $attrs;
230
    }
231
232
    /**
233
     * Handle autoplay functionality, especially for iframe embeds.
234
     */
235
    protected function applyAutoplay(array $attrs, array &$options): array
236
    {
237
        if (! ($options['autoplay'] ?? false)) {
238
            return $attrs;
239
        }
240
241
        if ($this->type === self::TYPE_IFRAME) {
242
            $attrs['src'] = $this->addUrlParam(
243
                $attrs['src'],
244
                sprintf('autoplay=%s', $options['autoplay'])
245
            );
246
            unset($options['autoplay']);
247
        } else {
248
            $attrs['autoplay'] = $options['autoplay'];
249
        }
250
251
        return $attrs;
252
    }
253
254
    /**
255
     * Append custom parameter to the end of the url.
256
     */
257
    protected function addUrlParam(string $url, string $param): string
258
    {
259
        $operator = strpos($url, '?') >= 0 ? '&' : '?';
260
        return $url . $operator . $param;
261
    }
262
}
263