Skip to content

Commit a8aac89

Browse files
committed
Shortcut image rendering
1 parent 1b27f3b commit a8aac89

9 files changed

Lines changed: 122 additions & 192 deletions

File tree

src/Image/config/services.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@
191191
$services->set('ux_image.twig.runtime', ImageRuntime::class)
192192
->arg('$renderer', service(ImageRendererInterface::class))
193193
->tag('twig.runtime')
194+
->tag('ux.twig_component.twig_renderer', ['key' => 'ux:image'])
194195
;
195196

196197
$services->set('ux_image.twig.extension', ImageExtension::class)

src/Image/doc/integrations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ The Twig functions are the lowest-friction rendering API:
134134
Use `<twig:ux:image>` when the application already composes TwigComponents.
135135
The component service is registered only when `TwigComponentBundle` is active
136136
in the kernel; merely having the package in `vendor/` does not enable it.
137-
Override its template at
138-
`templates/bundles/UXImageBundle/components/Image.html.twig`.
137+
The self-closing component renders directly through the UX Image runtime; pass
138+
classes and other safe HTML attributes on the component itself.
139139

140140
Prefer styling an application-owned wrapper or class over replacing the
141141
semantic `<picture>` structure. Bootstrap, Tailwind and custom design systems

src/Image/doc/rendering.md

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ When `symfony/ux-twig-component` is installed **and**
8686
/>
8787
```
8888

89-
The `<twig:ux:image>` component renders via `ux_picture()` internally. The template is at
90-
`templates/components/Image.html.twig` and can be overridden under
91-
`templates/bundles/UXImageBundle/components/Image.html.twig`.
89+
Like `<twig:ux:icon>` and `<twig:ux:map>`, the self-closing component uses a
90+
dedicated TwigComponent runtime renderer. It produces the same `<picture>`
91+
markup as `ux_picture()` without loading an intermediate component template.
9292

9393
## HTML output
9494

@@ -291,17 +291,6 @@ return new Response($rendered->toHtml());
291291
In Twig, both functions return a string. Pipe through `|raw` is not needed as the functions
292292
already output safe HTML.
293293

294-
## Overriding templates
295-
296-
The TwigComponent template lives at:
297-
`vendor/symfony/ux-image/templates/components/Image.html.twig`
298-
299-
Override it by creating:
300-
`templates/bundles/UXImageBundle/components/Image.html.twig`
301-
302-
The override receives the same `rendered` variable (`RenderedImage` object) with `toHtml()` and
303-
`toImgHtml()` methods.
304-
305294
## Reference
306295

307296
### Twig functions

src/Image/src/Twig/Components/Image.php

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,14 @@
1212
namespace Symfony\UX\Image\Twig\Components;
1313

1414
use Symfony\UX\Image\ImageAsset;
15-
use Symfony\UX\Image\Renderer\ImageRenderOptions;
16-
use Symfony\UX\Image\Renderer\RenderedImage;
17-
use Symfony\UX\Image\Twig\ImageRuntime;
18-
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
19-
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
20-
use Twig\Extra\Html\HtmlExtension;
2115

2216
/**
2317
* Twig component for rendering images with responsive features.
2418
*
2519
* @author Simon André <smn.andre@gmail.com>
20+
*
21+
* @internal
2622
*/
27-
#[AsTwigComponent('ux:image')]
2823
final class Image
2924
{
3025
public ?ImageAsset $src = null;
@@ -37,34 +32,4 @@ final class Image
3732
public ?string $sizes = null;
3833
public ?string $fetchpriority = null;
3934
public ?string $decoding = 'async';
40-
41-
private ?RenderedImage $rendered = null;
42-
43-
public function __construct(private readonly ImageRuntime $runtime)
44-
{
45-
}
46-
47-
#[ExposeInTemplate]
48-
public function rendered(array $attributes = []): ?RenderedImage
49-
{
50-
if (!$this->src) {
51-
return null;
52-
}
53-
54-
foreach ($attributes as $name => $value) {
55-
$attributes[$name] = HtmlExtension::htmlAttrValue($name, $value);
56-
}
57-
58-
return $this->rendered ??= $this->runtime->render($this->src, new ImageRenderOptions(
59-
sizes: $this->sizes,
60-
alt: $this->alt ?? '',
61-
lazy: $this->lazy,
62-
fetchPriority: $this->fetchpriority ?? ($this->lazy ? 'auto' : 'high'),
63-
class: $this->class ?? '',
64-
decoding: $this->decoding ?? 'async',
65-
variant: $this->variant,
66-
srcset: $this->srcset,
67-
attributes: $attributes,
68-
));
69-
}
7035
}

src/Image/src/Twig/ImageRuntime.php

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,47 @@
1515
use Symfony\UX\Image\Renderer\ImageRendererInterface;
1616
use Symfony\UX\Image\Renderer\ImageRenderOptions;
1717
use Symfony\UX\Image\Renderer\RenderedImage;
18+
use Twig\Extension\RuntimeExtensionInterface;
19+
use Twig\Extra\Html\HtmlExtension;
1820

1921
/**
2022
* Runtime helpers used by the ux_image Twig component.
2123
*
2224
* @author Simon André <smn.andre@gmail.com>
2325
*/
24-
final class ImageRuntime
26+
final class ImageRuntime implements RuntimeExtensionInterface
2527
{
2628
public function __construct(private ImageRendererInterface $renderer)
2729
{
2830
}
2931

30-
public function render(ImageAsset $asset, ?ImageRenderOptions $options = null): RenderedImage
32+
/**
33+
* @param array<string, mixed> $args
34+
*/
35+
public function render(array $args = []): string
36+
{
37+
$asset = $args['src'] ?? null;
38+
unset($args['src']);
39+
40+
if (null === $asset) {
41+
return '';
42+
}
43+
if (!$asset instanceof ImageAsset) {
44+
throw new \TypeError(\sprintf('The "src" argument must be an instance of "%s", "%s" given.', ImageAsset::class, get_debug_type($asset)));
45+
}
46+
47+
$options = array_intersect_key($args, array_flip(['sizes', 'alt', 'lazy', 'fetchpriority', 'class', 'decoding', 'variant', 'srcset']));
48+
$attributes = array_diff_key($args, $options);
49+
foreach ($attributes as $name => $value) {
50+
$attributes[$name] = HtmlExtension::htmlAttrValue($name, $value);
51+
}
52+
$options['attributes'] = $attributes;
53+
$options['fetchpriority'] ??= false === ($options['lazy'] ?? true) ? 'high' : 'auto';
54+
55+
return $this->renderPicture($asset, $options);
56+
}
57+
58+
private function renderAsset(ImageAsset $asset, ?ImageRenderOptions $options = null): RenderedImage
3159
{
3260
return $this->renderer->render($asset, $options);
3361
}
@@ -63,7 +91,7 @@ public function renderConfigured(ImageAsset $asset, array $options = []): Render
6391
$srcset = $options['srcset'] ?? null;
6492
$attributes = $options['attributes'] ?? [];
6593

66-
return $this->render($asset, new ImageRenderOptions(
94+
return $this->renderAsset($asset, new ImageRenderOptions(
6795
sizes: \is_string($sizes) ? $sizes : null,
6896
alt: \is_string($alt) ? $alt : '',
6997
lazy: \is_bool($lazy) ? $lazy : true,
@@ -83,32 +111,32 @@ class: \is_string($class) ? $class : '',
83111
*/
84112
public function getSources(ImageAsset $asset): array
85113
{
86-
return $this->render($asset)->sources;
114+
return $this->renderAsset($asset)->sources;
87115
}
88116

89117
/**
90118
* Returns the fallback src URL using the first jpeg variant or the asset path.
91119
*/
92120
public function getFallbackSrc(ImageAsset $asset): string
93121
{
94-
return $this->render($asset)->fallbackSrc;
122+
return $this->renderAsset($asset)->fallbackSrc;
95123
}
96124

97125
/**
98126
* Returns srcset string for the fallback <img> tag.
99127
*/
100128
public function getFallbackSrcset(ImageAsset $asset): ?string
101129
{
102-
return $this->render($asset)->fallbackSrcset;
130+
return $this->renderAsset($asset)->fallbackSrcset;
103131
}
104132

105133
public function getWidth(ImageAsset $asset): ?int
106134
{
107-
return $this->render($asset)->width;
135+
return $this->renderAsset($asset)->width;
108136
}
109137

110138
public function getHeight(ImageAsset $asset): ?int
111139
{
112-
return $this->render($asset)->height;
140+
return $this->renderAsset($asset)->height;
113141
}
114142
}

src/Image/templates/components/Image.html.twig

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/Image/tests/Integration/BundleInitializationTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
use Symfony\Component\Config\Loader\LoaderInterface;
1919
use Symfony\Component\DependencyInjection\ContainerBuilder;
2020
use Symfony\Component\HttpKernel\Kernel;
21+
use Symfony\UX\Image\ImageAsset;
2122
use Symfony\UX\Image\UXImageBundle;
2223
use Symfony\UX\TwigComponent\TwigComponentBundle;
24+
use Twig\Environment;
2325

2426
/**
2527
* @requires class Symfony\Bundle\FrameworkBundle\FrameworkBundle
@@ -57,6 +59,28 @@ public function testBundleSetsParameters()
5759

5860
$kernel->shutdown();
5961
}
62+
63+
public function testTwigComponentUsesRuntimeRenderer()
64+
{
65+
$kernel = new UxImageTestKernel('test', true);
66+
$kernel->boot();
67+
68+
$twig = $kernel->getContainer()->get('test.service_container')->get('twig');
69+
self::assertInstanceOf(Environment::class, $twig);
70+
$template = $twig->createTemplate('<twig:ux:image :src="asset" alt="Photo" :lazy="false" id="hero" />');
71+
$html = $template->render([
72+
'asset' => new ImageAsset('default', 'https://example.com/photo.jpg', width: 1200, height: 800),
73+
]);
74+
75+
self::assertStringStartsWith('<picture>', $html);
76+
self::assertStringContainsString('src="https://example.com/photo.jpg"', $html);
77+
self::assertStringContainsString('alt="Photo"', $html);
78+
self::assertStringContainsString('loading="eager"', $html);
79+
self::assertStringContainsString('fetchpriority="high"', $html);
80+
self::assertStringContainsString('id="hero"', $html);
81+
82+
$kernel->shutdown();
83+
}
6084
}
6185

6286
class UxImageTestKernel extends Kernel

src/Image/tests/Twig/Components/ImageComponentTest.php

Lines changed: 0 additions & 127 deletions
This file was deleted.

0 commit comments

Comments
 (0)