Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Agents
isapp/laravel-imagetools
claude-code
cursor
image
cdn
laravel
optimizer
spatie
ImageTools

Guidance for AI coding agents working on isapp/laravel-imagetools.

Install
php artisan boost:add-skill isapp/laravel-imagetools

Save this content to: AGENTS.md

# AGENTS.md

Guidance for AI coding agents working on **`isapp/laravel-imagetools`**.

## What this is

A Laravel package for **deterministic, query‑driven image generation** (inspired by
vite‑imagetools). `ImageTools::asset('path/to/img.jpg?w=1200&format=webp')` produces a
hash‑named derivative on a configured filesystem disk and records a `path?query → file`
mapping in a PHP manifest, so later calls are cheap. Generation can happen on demand,
ahead of time (scanner command), or on a queue.

## Setup

- PHP **^8.2** with `ext-gd` (the tests draw their fixtures with GD). Install: `composer install`.
- Dependencies live in `vendor/` — never assume an API; read the installed source.

## Commands

- **Tests:** `composer test` (PHPUnit via Orchestra Testbench).
  - Single group: `vendor/bin/phpunit --group s3`.
  - The `s3` group exercises a real S3 endpoint and **skips** unless `AWS_ENDPOINT` +
    `AWS_BUCKET` are set — see the README "Testing" section for the local MinIO recipe.
- **Lint:** `composer lint` (Laravel Pint). Check‑only: `vendor/bin/pint --test`.

## Layout

- `src/ImageTools.php` — the orchestrator (`asset()`, `generate()`, `disk()`, queued
  dispatch). Depends on three injected collaborators in `src/Support/`:
  - `Support/Manifest.php` — the PHP manifest state + persistence (load/has/get/put/putIfAbsent/
    clear/refresh/whileLocked; writes take the `<manifest>.lock` lock).
  - `Support/PathResolver.php` — canonical `seed()` and the deterministic `storedFile()`.
  - `Support/SourceReader.php` — resolves a source to a local path (local or disk→temp), and
    gives its public URL (`publicUrl()`, the `original` fallback of a queued miss).
  Wiring lives in `src/ServiceProvider.php` (`Manifest` is bound with its config path; the
  rest auto‑resolve).
- `src/Jobs/GenerateImageJob.php` — queued generation (`ShouldQueue` + `ShouldBeUnique`).
- `src/Commands/GenerateImagesCommand.php` — scans Blade/PHP (nikic/php-parser) and
  pre‑generates; `src/Commands/ClearGeneratedImagesCommand.php` — removes generated files
  and the manifest; `src/Commands/RegenerateImagesCommand.php` — rebuilds entries whose
  stored file is missing or empty, from the source each entry records;
  `src/Commands/PruneImagesCommand.php` — deletes generated files that no entry of the default
  manifest references.
- `src/Facades/ImageTools.php`, `src/ServiceProvider.php`, `config/image-tools.php`.
- `tests/` — Feature + Unit (Testbench `TestCase`).

## Conventions

- PSR‑4 `Isapp\ImageTools\` → `src/`. `declare(strict_types=1)` in every PHP file.
- Code style is **Laravel Pint** (`pint.json`); run `composer lint` before pushing.
- **Conventional Commits.** Branch off `main` — never commit to `main` directly. PRs are
  **squash‑merged**.
- CI must stay green: matrix (PHP 8.2–8.4 × Laravel 12/13), Pint, the Imagick-driver job, and
  the MinIO S3 job.

## Invariants — do not break

- The manifest key **and** the generated filename both derive from one canonical "seed":
  the query reduced to the supported keys (`w, h, q, fit, format, lossless, optimize`), sorted.
  `lossless` is kept only when on and `optimize` only when off (as `0`), so a switch at its
  default never changes a filename. `asset()`
  (read path) and `generate()` (write path) MUST use the same derivation — both go through
  `PathResolver::seed()` / `storedFile()`. Divergence causes cache misses and broken URLs.
- `queue` is a **control flag**, deliberately excluded from the seed, so it never affects
  the filename or manifest key.
- The **source disk** (set via `disk()`) is folded into the seed so the same path read from
  different disks never collides; it is used only as a key/hash input, never parsed as a path.
- Default (non‑queued, local‑source) behaviour stays fully **synchronous**; only the `queue` flag
  or the `queue` config defers generation. A queued miss dispatches through
  `GenerateImageJob::dispatch()` (a `PendingDispatch`), never `Bus::dispatch()`: only a
  `PendingDispatch` takes the `ShouldBeUnique` lock, which is the one "pending" state. The job's unique id is a hash (fixed key length), and a failed push
  releases the lock. A `sync` connection and a failed dispatch generate in the request. A job
  returns before it encodes when its entry exists already (`ImageTools::has()`).
- On a manifest miss, `asset()` calls `Manifest::refresh()` once. It is read only — never a write.
  `load()` records no signature, because its `require` can return an older opcache copy: the
  first `refresh()` compares the file size with the text `put()` would write for the copy in
  memory, and later calls compare the file's signature (inode, mtime, size).
- A manifest entry records the `source` and `source_disk` it was built from. `imagetools:regenerate`
  rebuilds from those recorded values — the seed in the key is never parsed back into a path and a
  disk. The command also never deletes the manifest or an entry.
- A resize by `w` or `h` never enlarges the source unless `allow_upscale` is on. Such a request
  gets an entry with `path => null` and no file, and `asset()` returns `null` for it. `fit` is
  never affected. `allow_upscale` is not part of the seed. Every reader of an entry (`asset()`,
  `imagetools:clear`, `imagetools:regenerate`) must handle a null `path`.
- `Manifest::put()` writes under a lock (`<manifest>.lock`) and starts from the file on disk, then
  changes one key; `Manifest::clear()` reads and deletes under the same lock. Never write an
  in‑memory copy of a whole manifest back: a queue worker holds the `image-tools` singleton between
  jobs, and its copy can be old. The lock must never make a write fail — it falls back to a
  read‑only handle, then to no lock with a warning.
- A clean-up delete of an old file (`recordOversize()`) comes after the manifest write and never
  fails the request. A refused delete is logged as a warning, both when the disk throws and when
  `delete()` returns `false` (a disk with `'throw' => false`, the Laravel default).
- `driver`, `optimizers`, `put_options` and `media_id` are never part of the seed. `media()` is
  `disk($media->disk)->asset(...)` plus a recorded `media_id`; every entry write goes through
  `ImageTools::entry()`, and the job and `imagetools:regenerate` carry the id along.
- On a miss, `generate()` adopts a non-empty file at the stored name only through
  `Manifest::putIfAbsent()`: the file check and the write share the lock, and only when the file
  has no entry. A first check runs without the lock, so a real miss never holds it during disk
  calls. The source size check runs first. An entry that exists is always encoded again. A disk
  that cannot answer the check (`FilesystemException`) means "not stored": log, then encode.
- `asset()` catches a `Throwable` from the in-request `generate()`, reports it, logs a warning with
  the path and the disk (Laravel does not report a `ValidationException`) and returns `''`.
  `generate()`, the job and the commands still throw.
- `GenerateImageJob::$mediaId` is a plain property with a default, not a promoted one: a payload
  queued by an older version has no `mediaId`, and `unserialize()` never runs the constructor.
- Every delete of a generated file by a command runs inside `whileLocked()`, after the manifest is
  read again, and skips a file that an entry references again. This holds for `imagetools:prune`
  and `imagetools:clear`. Clear falls back to an unlocked delete only when the lock cannot be taken.
- `imagetools:prune` reads only the default manifest. It refuses an empty manifest (always), a
  manifest under `bootstrap/cache` and more than half of the files as orphans (both lifted by
  `--force`; a dry run only warns on the second).
- `Manifest::whileLocked()` is the one lock user without the no-lock fallback: it throws instead,
  because it guards a delete. Code inside it (and inside `putIfAbsent()`'s condition) never writes
  the manifest: a second `flock` in the same process waits for ever. `imagetools:prune` deletes a
  file only inside `whileLocked()`, after it reads the manifest again, and keeps files modified in
  the last hour (an upload comes before its entry write).

## Where to read more

- [README.md](README.md) — usage, query options, queued generation, config, troubleshooting.
- [CHANGELOG.md](CHANGELOG.md) — notable changes.

AGENTS.md

Guidance for AI coding agents working on isapp/laravel-imagetools.

What this is

A Laravel package for deterministic, query‑driven image generation (inspired by vite‑imagetools). ImageTools::asset('path/to/img.jpg?w=1200&format=webp') produces a hash‑named derivative on a configured filesystem disk and records a path?query → file mapping in a PHP manifest, so later calls are cheap. Generation can happen on demand, ahead of time (scanner command), or on a queue.

Setup

  • PHP ^8.2 with ext-gd (the tests draw their fixtures with GD). Install: composer install.
  • Dependencies live in vendor/ — never assume an API; read the installed source.

Commands

  • Tests: composer test (PHPUnit via Orchestra Testbench).
    • Single group: vendor/bin/phpunit --group s3.
    • The s3 group exercises a real S3 endpoint and skips unless AWS_ENDPOINT + AWS_BUCKET are set — see the README "Testing" section for the local MinIO recipe.
  • Lint: composer lint (Laravel Pint). Check‑only: vendor/bin/pint --test.

Layout

  • src/ImageTools.php — the orchestrator (asset(), generate(), disk(), queued dispatch). Depends on three injected collaborators in src/Support/:
    • Support/Manifest.php — the PHP manifest state + persistence (load/has/get/put/putIfAbsent/ clear/refresh/whileLocked; writes take the <manifest>.lock lock).
    • Support/PathResolver.php — canonical seed() and the deterministic storedFile().
    • Support/SourceReader.php — resolves a source to a local path (local or disk→temp), and gives its public URL (publicUrl(), the original fallback of a queued miss). Wiring lives in src/ServiceProvider.php (Manifest is bound with its config path; the rest auto‑resolve).
  • src/Jobs/GenerateImageJob.php — queued generation (ShouldQueue + ShouldBeUnique).
  • src/Commands/GenerateImagesCommand.php — scans Blade/PHP (nikic/php-parser) and pre‑generates; src/Commands/ClearGeneratedImagesCommand.php — removes generated files and the manifest; src/Commands/RegenerateImagesCommand.php — rebuilds entries whose stored file is missing or empty, from the source each entry records; src/Commands/PruneImagesCommand.php — deletes generated files that no entry of the default manifest references.
  • src/Facades/ImageTools.php, src/ServiceProvider.php, config/image-tools.php.
  • tests/ — Feature + Unit (Testbench TestCase).

Conventions

  • PSR‑4 Isapp\ImageTools\ → src/. declare(strict_types=1) in every PHP file.
  • Code style is Laravel Pint (pint.json); run composer lint before pushing.
  • Conventional Commits. Branch off main — never commit to main directly. PRs are squash‑merged.
  • CI must stay green: matrix (PHP 8.2–8.4 × Laravel 12/13), Pint, the Imagick-driver job, and the MinIO S3 job.

Invariants — do not break

  • The manifest key and the generated filename both derive from one canonical "seed": the query reduced to the supported keys (w, h, q, fit, format, lossless, optimize), sorted. lossless is kept only when on and optimize only when off (as 0), so a switch at its default never changes a filename. asset() (read path) and generate() (write path) MUST use the same derivation — both go through PathResolver::seed() / storedFile(). Divergence causes cache misses and broken URLs.
  • queue is a control flag, deliberately excluded from the seed, so it never affects the filename or manifest key.
  • The source disk (set via disk()) is folded into the seed so the same path read from different disks never collides; it is used only as a key/hash input, never parsed as a path.
  • Default (non‑queued, local‑source) behaviour stays fully synchronous; only the queue flag or the queue config defers generation. A queued miss dispatches through GenerateImageJob::dispatch() (a PendingDispatch), never Bus::dispatch(): only a PendingDispatch takes the ShouldBeUnique lock, which is the one "pending" state. The job's unique id is a hash (fixed key length), and a failed push releases the lock. A sync connection and a failed dispatch generate in the request. A job returns before it encodes when its entry exists already (ImageTools::has()).
  • On a manifest miss, asset() calls Manifest::refresh() once. It is read only — never a write. load() records no signature, because its require can return an older opcache copy: the first refresh() compares the file size with the text put() would write for the copy in memory, and later calls compare the file's signature (inode, mtime, size).
  • A manifest entry records the source and source_disk it was built from. imagetools:regenerate rebuilds from those recorded values — the seed in the key is never parsed back into a path and a disk. The command also never deletes the manifest or an entry.
  • A resize by w or h never enlarges the source unless allow_upscale is on. Such a request gets an entry with path => null and no file, and asset() returns null for it. fit is never affected. allow_upscale is not part of the seed. Every reader of an entry (asset(), imagetools:clear, imagetools:regenerate) must handle a null path.
  • Manifest::put() writes under a lock (<manifest>.lock) and starts from the file on disk, then changes one key; Manifest::clear() reads and deletes under the same lock. Never write an in‑memory copy of a whole manifest back: a queue worker holds the image-tools singleton between jobs, and its copy can be old. The lock must never make a write fail — it falls back to a read‑only handle, then to no lock with a warning.
  • A clean-up delete of an old file (recordOversize()) comes after the manifest write and never fails the request. A refused delete is logged as a warning, both when the disk throws and when delete() returns false (a disk with 'throw' => false, the Laravel default).
  • driver, optimizers, put_options and media_id are never part of the seed. media() is disk($media->disk)->asset(...) plus a recorded media_id; every entry write goes through ImageTools::entry(), and the job and imagetools:regenerate carry the id along.
  • On a miss, generate() adopts a non-empty file at the stored name only through Manifest::putIfAbsent(): the file check and the write share the lock, and only when the file has no entry. A first check runs without the lock, so a real miss never holds it during disk calls. The source size check runs first. An entry that exists is always encoded again. A disk that cannot answer the check (FilesystemException) means "not stored": log, then encode.
  • asset() catches a Throwable from the in-request generate(), reports it, logs a warning with the path and the disk (Laravel does not report a ValidationException) and returns ''. generate(), the job and the commands still throw.
  • GenerateImageJob::$mediaId is a plain property with a default, not a promoted one: a payload queued by an older version has no mediaId, and unserialize() never runs the constructor.
  • Every delete of a generated file by a command runs inside whileLocked(), after the manifest is read again, and skips a file that an entry references again. This holds for imagetools:prune and imagetools:clear. Clear falls back to an unlocked delete only when the lock cannot be taken.
  • imagetools:prune reads only the default manifest. It refuses an empty manifest (always), a manifest under bootstrap/cache and more than half of the files as orphans (both lifted by --force; a dry run only warns on the second).
  • Manifest::whileLocked() is the one lock user without the no-lock fallback: it throws instead, because it guards a delete. Code inside it (and inside putIfAbsent()'s condition) never writes the manifest: a second flock in the same process waits for ever. imagetools:prune deletes a file only inside whileLocked(), after it reads the manifest again, and keeps files modified in the last hour (an upload comes before its entry write).

Where to read more

  • README.md — usage, query options, queued generation, config, troubleshooting.
  • CHANGELOG.md — notable changes.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle