(MediaExtension::transformStringToMedia()) calls setAlt($caption, true) whenever content containing caption renders, mutating alt in memory only — the DB still holds the previous value, and the render runs before AfterEntityUpdatedEvent. Any rename or diff heuristic must read the persisted alt...
php artisan boost:add-skill pushword/version
Save this content to: .cursor/rules/media.md
# Media and image cache
- **`Media.alt` has two values during a request.** The `media_from_string()` Twig function
(`MediaExtension::transformStringToMedia()`) calls `setAlt($caption, true)` whenever
content containing `` renders, mutating alt **in memory only** — the DB
still holds the previous value, and the render runs before `AfterEntityUpdatedEvent`.
Any rename or diff heuristic must read the *persisted* alt via
`getUnitOfWork()->getOriginalEntityData($media)['alt']`, not the live one.
`Media::isFileNameAutoGenerated(?string $referenceAlt)` takes the reference alt for
exactly this reason.
- **A 0-byte derivative means an in-place optimizer truncated it, not an encode failure.**
`ImageEncoder` (vips) is atomic and rejects empty payloads — a real encode failure
throws and leaves the file *missing*. A genuinely 0-byte file is the fingerprint of an
external binary writing its own target (`cwebp … 'x.webp' -o 'x.webp'`) and being killed
mid-write. Optimize into a temp copy, validate (non-empty + `getimagesize`), then
atomically rename. Treat 0-byte cache files as missing everywhere they are read.
- Generalize that shape: any "truncated cache file, non-deterministic, size-correlated,
silent RC=0" symptom points at an external binary writing in place, not at the primary
encoder.
- **A gd decode counts against `memory_limit`, and overrunning it is a fatal no `catch`
reaches.** `ImageReader::read()` asks for width × height × 4 bytes in one allocation —
192 MB for a 48 Mpx master — so any decode on a request-side path has to be declined
first (`ImageCacheGenerator::decodeFitsIn()`), never wrapped in a `try`. imagick and vips
allocate outside the limit, which is why the check is gd-only. Beware when reproducing:
whether gd allocations are counted depends on the PHP build (they were under FrankenPHP's
8.4, not under the local CLI 8.5), so a local `memory_get_peak_usage()` of 17 MB proves
nothing about production — reproduce through the real SAPI.
- **Filesystem work owed to a media write belongs after the commit, never in `preUpdate`.**
`MediaStorageListener` decides a rename in `preUpdate` (so an impossible one still rolls
the row back) and moves the file in `postFlush`. A move inside the transaction is
destructive by construction: anything throwing later in the same flush rolls back the row
and leaves the DB naming a file that was renamed away — the whole site 404s on it. The
`preRemove` file delete still has that shape.
- **Derivatives are written to `pw.media_cache_dir`, never to `public_dir`/`public_media_dir`.**
The default is `public/{public_media_dir}/`, so nothing moves for a site; `public_media_dir`
alone stays what the *browser* path is built from. Compute a derivative path from anything
else and you reintroduce the leak that made a `pw:static` build ship 17k OG images of pages
that only ever existed inside a test.
Media.alt has two values during a request. The media_from_string() Twig function
(MediaExtension::transformStringToMedia()) calls setAlt($caption, true) whenever
content containing  renders, mutating alt in memory only — the DB
still holds the previous value, and the render runs before AfterEntityUpdatedEvent.
Any rename or diff heuristic must read the persisted alt via
getUnitOfWork()->getOriginalEntityData($media)['alt'], not the live one.
Media::isFileNameAutoGenerated(?string $referenceAlt) takes the reference alt for
exactly this reason.ImageEncoder (vips) is atomic and rejects empty payloads — a real encode failure
throws and leaves the file missing. A genuinely 0-byte file is the fingerprint of an
external binary writing its own target (cwebp … 'x.webp' -o 'x.webp') and being killed
mid-write. Optimize into a temp copy, validate (non-empty + getimagesize), then
atomically rename. Treat 0-byte cache files as missing everywhere they are read.memory_limit, and overrunning it is a fatal no catch
reaches. ImageReader::read() asks for width × height × 4 bytes in one allocation —
192 MB for a 48 Mpx master — so any decode on a request-side path has to be declined
first (ImageCacheGenerator::decodeFitsIn()), never wrapped in a try. imagick and vips
allocate outside the limit, which is why the check is gd-only. Beware when reproducing:
whether gd allocations are counted depends on the PHP build (they were under FrankenPHP's
8.4, not under the local CLI 8.5), so a local memory_get_peak_usage() of 17 MB proves
nothing about production — reproduce through the real SAPI.preUpdate.
MediaStorageListener decides a rename in preUpdate (so an impossible one still rolls
the row back) and moves the file in postFlush. A move inside the transaction is
destructive by construction: anything throwing later in the same flush rolls back the row
and leaves the DB naming a file that was renamed away — the whole site 404s on it. The
preRemove file delete still has that shape.pw.media_cache_dir, never to public_dir/public_media_dir.
The default is public/{public_media_dir}/, so nothing moves for a site; public_media_dir
alone stays what the browser path is built from. Compute a derivative path from anything
else and you reintroduce the leak that made a pw:static build ship 17k OG images of pages
that only ever existed inside a test.How can I help you explore Laravel packages today?