Laravel 教程

Laravel Image Responses: Serve Resized Images From Routes

发布
Laravel Image Responses: Serve Resized Images From Routes image

拉拉维尔的 first-party image API has been good at the write path since 13.20: take an upload, transform it, put it on a disk. The read path was less tidy. Serving a resized image over HTTP meant calling toBytes() , building a response, and setting the content type yourself, which is three lines of boilerplate in every controller that does it.

Laravel 13.25 makes Image 实施 Responsable contract, so an image instance is a valid return value from a route or controller. Two related additions landed in the same release, Image::fromStream() and a public toFormat() , and together the three cover most of what an image endpoint needs.

Returning an Image

使用 Illuminate\Support\Facades\Image ;
路线 :: 得到 '/avatars/{user}' , 功能 用户 $user) {
返回 图像 :: fromStorage ($用户) -> avatar_path)
-> cover 200 , 200
-> toWebp ()
-> 质量 80 (英文):
});

That is the whole thing. The framework calls toResponse() , which runs the pipeline, returns the processed bytes with a 200, and sets Content-Type from the output rather than the source. The route above returns image/webp even though the stored file is a JPEG, because the header is read from what the pipeline produced.

Everything that returns an Image works the same way, so a controller method, an invokable controller, or a value returned from a route model binding closure are all fine. The instance is lazy until something asks for bytes, which means the transformation does not run when the framework is only deciding what kind of response it has.

Adding Cache Headers

The default response has no caching headers at all, which is the right default for a framework and the wrong default for an endpoint that resizes an image on every request. Call toResponse() yourself when you want to add to it:

路线 :: 得到 '/avatars/{user}' , 功能 要求 请求, 用户 $user) {
返回 图像 :: fromStorage ($用户) -> avatar_path)
-> cover 200 , 200
-> toWebp ()
-> 质量 80
-> 响应 (请求)
-> setMaxAge 31536000
-> setPublic ();
});

toResponse() 返回 Illuminate\Http\Response , so the full response API is available: header() , setEtag() , setLastModified() , and the rest. Pair a long max-age with a URL that changes when the image changes, either a hash in the path or a query string built from the model's updated_at , and browsers stop asking after the first request.

For anything with real traffic, resizing per request is still work you are doing over and over. The pattern that scales is to write the derived file on the first request and serve it from the disk after that:

路线 :: 得到 '/thumbs/{photo}' , 功能 要求 请求, 照片 $photo) {
$路径 = "thumbs/{ $照片 -> ID }-{ $照片 -> 更新于 -> 时间戳 }.webp" ;
如果 贮存 :: 磁盘 '民众' -> 存在 ($path)) {
图像 :: fromStorage ($photo -> path)
-> cover 400 , 400
-> toWebp ()
-> 质量 80
-> storeAs 'thumbs' , basename ($path), '民众' (英文):
}
返回 贮存 :: 磁盘 '民众' -> 回复 (路径)
});

Putting the timestamp in the filename means an updated photo produces a new path, so old thumbnails fall out of use without a cache to invalidate.

Dynamic Formats With toFormat()

An endpoint that accepts a format from the request used to need a match statement to turn the string into the right method call. toFormat() is now public and takes the format directly:

路线 :: 得到 '/photos/{photo}.{format}' , 功能 照片 $photo, 细绳 $format) {
返回 图像 :: fromStorage ($photo -> path)
-> 规模 宽度 : 1200
-> toFormat ($format)
-> 质量 80 (英文):
}) -> 在哪里 '格式' , 'webp|avif|jpg' (英文):

The accepted values are webp , jpg , jpeg , png , gif , avif , heic , heif , 和 bmp , with heif normalized to heic . Anything else throws an ImageException with the format in the message, which is a 500 rather than a 404, so constrain the parameter in the route as above or validate the value before you pass it. The same method backs optimize() , which is the version to reach for when you also want a quality in one call.

This is what makes an AVIF-with-fallback endpoint short. Serve whichever format the request asked for, and let the <picture> element decide which URL the browser hits.

Building From a Stream

Image::fromStream() creates an instance from a stream resource, which covers the sources the other factory methods do not:

$图像 = 图像 :: 来自流 贮存 :: 磁盘 's3' -> readStream ($path));

The read is lazy. fromStream() wraps the resource in a closure and does not touch it until the pipeline runs, so creating an instance you end up not using costs nothing. A stream that yields no data throws an ImageException with the message "Invalid stream image data." at that point rather than at construction.

Alongside fromPath() , fromStorage() , fromUpload() , fromUrl() , fromBytes() , 和 fromBase64() , the stream variant is the one for anything you already hold a handle to: a php://input body on a raw upload endpoint, a file being read out of a zip, or a stream handed to you by another library.

A Complete Endpoint

Putting the three together, an image endpoint that takes a width and a format, reads from S3, and caches for a year:

使用 照亮\Http\请求 ;
使用 Illuminate\Support\Facades\Image ;
使用 照明\支持\立面\存储 ;
路线 :: 得到 '/media/{media}' , 功能 要求 请求, 媒体 $media) {
$已验证 = $请求 -> 证实 ([
'在' => [ ‘整数’ , 'between:32,2000' ],
'格式' => [ 'in:webp,avif,jpg' ],
]);
返回 图像 :: 来自流 贮存 :: 磁盘 's3' -> readStream ($media -> path))
-> 规模 宽度 : $validated[ '在' ] ?? 800
-> toFormat ($已验证[ '格式' ] ?? 'webp'
-> 质量 80
-> 响应 (请求)
-> setMaxAge 31536000
-> setPublic ();
}) -> 中间件 'signed' (英文):

Two details worth keeping. The width is bounded, because an unvalidated dimension on a public endpoint is an invitation to ask for a 20,000 pixel resize. And the route is signed, which stops anyone from generating arbitrary variants against your storage bill. Laravel's signed routes give you that with a signedRoute() call in the view.

进一步阅读

保罗·雷德蒙德照片

Laravel News 特约撰稿人。全栈 Web 开发人员兼作家。

赞助

laravelcloud logo
Laravel 云

轻松创建和管理服务器,并在几秒钟内部署 Laravel 应用程序。

访问 Laravel Cloud
Compoships: Eloquent Relationships on Multiple Columns image

Compoships: Eloquent Relationships on Multiple Columns

阅读文章
MKSine: A Filament CMS with Plugins, Themes, and Blocks image

MKSine: A Filament CMS with Plugins, Themes, and Blocks

阅读文章
Cancel In-Flight Form Submissions in Inertia.js v3.7 image

Cancel In-Flight Form Submissions in Inertia.js v3.7

阅读文章
Laravel Starter Kits Now Ship with Vite+ image

Laravel Starter Kits Now Ship with Vite+

阅读文章
Pessimistic Locking in Laravel Eloquent with refreshForUpdate() image

Pessimistic Locking in Laravel Eloquent with refreshForUpdate()

阅读文章
Mask Query Bindings in Laravel Exception Messages image

Mask Query Bindings in Laravel Exception Messages

阅读文章