• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tito10047 / progressive-image-bundle / 20693585928

04 Jan 2026 01:24PM UTC coverage: 89.796%. Remained the same
20693585928

push

github

tito10047
cs fixer

704 of 784 relevant lines covered (89.8%)

252.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.53
/src/Service/ResponsiveAttributeGenerator.php
1
<?php
2

3
/*
4
 * This file is part of the Progressive Image Bundle.
5
 *
6
 * (c) Jozef Môstka <https://github.com/tito10047/progressive-image-bundle>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace Tito10047\ProgressiveImageBundle\Service;
13

14
use Tito10047\ProgressiveImageBundle\DTO\BreakpointAssignment;
15
use Tito10047\ProgressiveImageBundle\UrlGenerator\ResponsiveImageUrlGeneratorInterface;
16

17
final class ResponsiveAttributeGenerator
18
{
19
    /**
20
     * @param array{
21
     *      layouts: array<string, array{
22
     *      min_viewport: int,
23
     *      max_container: int|null
24
     *      }>,
25
     *      columns: int
26
     *      } $gridConfig
27
     * @param array<string, string> $ratioConfig
28
     */
29
    public function __construct(
30
        private array $gridConfig,
31
        private array $ratioConfig,
32
        private readonly PreloadCollector $preloadCollector,
33
        private ResponsiveImageUrlGeneratorInterface $urlGenerator,
34
    ) {
35
    }
413✔
36

37
    /**
38
     * @param BreakpointAssignment[] $assignments
39
     *
40
         * @return array{sizes: string, srcset: string, variables: array<string, string>}
41
     */
42
    public function generate(string $path, array $assignments, int $originalWidth, bool $preload, ?string $pointInterest = null): array
43
    {
44
        $assignments = $this->sortAssignments($assignments);
259✔
45

46
        $sizesParts = [];
259✔
47
        $srcsetParts = [];
259✔
48
                $variables = [];
259✔
49
        $processedWidths = [];
259✔
50

51
        foreach ($assignments as $assignment) {
259✔
52
            $layout = $this->gridConfig['layouts'][$assignment->breakpoint] ?? null;
259✔
53
                        if (!$layout && 'default' === $assignment->breakpoint) {
259✔
54
                                foreach ($this->gridConfig['layouts'] as $l) {
58✔
55
                                        if (0 === $l['min_viewport']) {
58✔
56
                                                $layout = $l;
58✔
57
                                                break;
58✔
58
                                        }
59
                                }
60
                        }
61

62
            if (!$layout) {
259✔
63
                                throw new \InvalidArgumentException(sprintf('Breakpoint "%s" is not defined in the grid configuration.', $assignment->breakpoint));
×
64
            }
65

66
                        [$pixelWidth, $sizeValue, $cssValue] = $this->calculateDimensions($assignment, $layout);
259✔
67

68
            $size = $this->formatSizePart($layout['min_viewport'], $sizeValue);
259✔
69
            $sizesParts[] = $size;
259✔
70

71
            $url = $this->generateUrl($path, $assignment, (int) round($pixelWidth), $originalWidth, $processedWidths, $pointInterest);
259✔
72

73
            if ($url) {
259✔
74
                $actualPixelWidth = (int) round($pixelWidth);
259✔
75
                if ($preload) {
259✔
76
                    $this->preloadCollector->add($url, 'image', 'high', "{$actualPixelWidth}w", $size);
28✔
77
                }
78

79
                $srcsetParts[] = $url." {$actualPixelWidth}w";
259✔
80
            }
81

82
                        $ratio                              = $this->resolveRatio($assignment);
259✔
83
                        $suffix                             = 0 === $layout['min_viewport'] ? '' : '-' . $assignment->breakpoint;
259✔
84
                        $variables['--img-width' . $suffix] = $cssValue;
259✔
85
                        if ($ratio) {
259✔
86
                                $variables['--img-aspect' . $suffix] = (string) $ratio;
175✔
87
                        }
88
        }
89

90
        return [
259✔
91
            'sizes' => implode(', ', $sizesParts),
259✔
92
            'srcset' => implode(', ', $srcsetParts),
259✔
93
                        'variables' => $variables,
259✔
94
        ];
259✔
95
    }
96

97
    private function formatSizePart(int $minViewport, string $sizeValue): string
98
    {
99
        return $minViewport > 0
259✔
100
            ? "(min-width: {$minViewport}px) {$sizeValue}"
244✔
101
            : $sizeValue;
259✔
102
    }
103

104
    /**
105
     * @param BreakpointAssignment[] $assignments
106
     *
107
     * @return BreakpointAssignment[]
108
     */
109
    private function sortAssignments(array $assignments): array
110
    {
111
        usort($assignments, fn ($a, $b) => ($this->gridConfig['layouts'][$b->breakpoint]['min_viewport'] ?? 0) <=>
259✔
112
            ($this->gridConfig['layouts'][$a->breakpoint]['min_viewport'] ?? 0)
170✔
113
        );
259✔
114

115
        return $assignments;
259✔
116
    }
117

118
    /**
119
     * @param array{min_viewport: int, max_container: int|null} $layout
120
     *
121
         * @return array{0: float, 1: string, 2: string}
122
     */
123
    private function calculateDimensions(BreakpointAssignment $assignment, array $layout): array
124
    {
125
                if (null !== $assignment->widthPercent) {
259✔
126
                        $percentValue = (float) rtrim($assignment->widthPercent, '%');
44✔
127
                        $cssValue     = $assignment->widthPercent;
44✔
128

129
                        $maxContainer = $layout['max_container'];
44✔
130
                        if ($maxContainer) {
44✔
131
                                $pixelWidth = ($percentValue / 100) * $maxContainer;
44✔
132
                        } else {
133
                                $pixelWidth = ($percentValue / 100) * 1920;
29✔
134
                        }
135

136
                        return [$pixelWidth, round($pixelWidth) . 'px', $cssValue];
44✔
137
                }
138

139
                if (null !== $assignment->width) {
215✔
140
                        $pixelWidth = (float) $assignment->width;
15✔
141
                        $sizeValue  = $assignment->width . 'px';
15✔
142

143
                        return [$pixelWidth, $sizeValue, $sizeValue];
15✔
144
                }
145

146
        $totalCols = $this->gridConfig['columns'];
200✔
147
        $maxContainer = $layout['max_container'];
200✔
148

149
        if ($maxContainer) {
200✔
150
                        // Fixed container (e.g. 1320px) -> width in px
151
            $pixelWidth = ($assignment->columns / $totalCols) * $maxContainer;
185✔
152
            $sizeValue = round($pixelWidth).'px';
185✔
153
        } else {
154
                        // Fluid (null) -> width in vw
155
            $vwWidth = ($assignment->columns / $totalCols) * 100;
156✔
156
            $sizeValue = round($vwWidth).'vw';
156✔
157
                        // For URL calculation we estimate px width from some reasonable max-width (e.g. 1920)
158
            $pixelWidth = ($vwWidth / 100) * 1920;
156✔
159
        }
160

161
                return [$pixelWidth, $sizeValue, $sizeValue];
200✔
162
    }
163

164
    /**
165
     * @param array<int, bool> $processedWidths
166
     */
167
    private function generateUrl(
168
        string $path,
169
        BreakpointAssignment $assignment,
170
        int $basePixelWidth,
171
        int $originalWidth,
172
        array &$processedWidths,
173
        ?string $pointInterest = null,
174
        ): string {
175
        $ratio = $this->resolveRatio($assignment);
259✔
176

177
                $requestedWidth = $basePixelWidth;
259✔
178
                if ($originalWidth > 0 && $basePixelWidth > $originalWidth) {
259✔
179
                        $basePixelWidth = $originalWidth;
84✔
180
        }
181

182
        $targetH = $ratio ? (int) round($basePixelWidth / $ratio) : null;
259✔
183
        $url = $this->urlGenerator->generateUrl($path, $basePixelWidth, $targetH, $pointInterest);
259✔
184

185
        return $url;
259✔
186
    }
187

188
    private function resolveRatio(BreakpointAssignment $assignment): ?float
189
    {
190
        $ratioString = $assignment->ratio ?? null;
259✔
191
        if (!$ratioString) {
259✔
192
            return null;
98✔
193
        }
194

195
                // If it's a key in ratioConfig, use that
196
        if (isset($this->ratioConfig[$ratioString])) {
175✔
197
                        $ratioString = $this->ratioConfig[$ratioString];
103✔
198
                }
199

200
                if (is_numeric($ratioString)) {
175✔
201
                        return (float) $ratioString;
118✔
202
        }
203

204
                // Otherwise try to parse format "16/9" or "3-4"
205
        if (preg_match('/^(\d+)[\/-](\d+)$/', $ratioString, $matches)) {
115✔
206
                        return (float) $matches[1] / (float) $matches[2];
100✔
207
                }
208

209
                // Or format "400x500"
210
                if (preg_match('/^(\d+)x(\d+)$/', $ratioString, $matches)) {
58✔
211
            return (float) $matches[1] / (float) $matches[2];
58✔
212
        }
213

214
                throw new \InvalidArgumentException(sprintf('Invalid ratio format or missing ratio configuration for: "%s"', $ratioString));
×
215
    }
216
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc