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

Phpexiftool Laravel Package

alchemy/phpexiftool

PHP wrapper for ExifTool that lets you read, write, and edit metadata in images and media files. Offers a simple object-oriented API for extracting EXIF, IPTC, and XMP data, with batch processing support for Laravel or any PHP project.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin driver around the exiftool binary; 90% of your bugs will be binary related, not PHP related. Install the binary first, then the library.

# Debian/Ubuntu (works in your Dockerfile too)
apt-get install -y libimage-exiftool-perl
# macOS
brew install exiftool
# verify — the wrapper needs a specific version floor
exiftool -ver

composer require alchemy/phpexiftool

Where to look first:

  1. composer.json → confirm whether you got the 3.x or 4.x API. 3.x exposes Reader / Writer / RdfMapper; 4.x is a rewrite around a single long-lived Exiftool process (open() / read() / write() / execute() / close()). The two are not interchangeable — pin the version in composer.json and read the README for that tag, not master.
  2. src/PHPExiftool/Driver/Tag/ — thousands of generated tag classes (EXIF\IFD0\Model, XMP\XMP\Description, IPTC\Application2\Keywords). This is your tag dictionary; grep here instead of guessing tag names.
  3. src/PHPExiftool/Driver/Value/ — Mono and Multi wrappers for scalar vs. list values.

First use case: strip GPS from user uploads, or extract EXIF into an Eloquent column. Both are read/write on a single file and take under a minute to wire up.

use PHPExiftool\Exiftool;

$exiftool = new Exiftool(new \Psr\Log\NullLogger(), '/usr/bin/exiftool');
$exiftool->open();                       // starts `-stay_open` mode
$tags = $exiftool->read($absolutePath); // array keyed like "EXIF:Model", "Composite:GPSLatitude"
$exiftool->close();

In Laravel, bind it once so you're not paying process startup per call:

// AppServiceProvider::register()
$this->app->singleton(Exiftool::class, fn () => tap(
    new Exiftool(app(LoggerInterface::class), config('services.exiftool.binary')),
    fn (Exiftool $e) => $e->open()
));

Implementation Patterns

Strip-on-upload middleware / observer. The most common production use. Read the file, check for Composite:GPSLatitude / GPSLongitude, then write empty values back. Do it in a queued job if uploads are large or bursty — exiftool writes are 50–500 ms each and are pure CPU+IO.

$bag = new MetadataBag();
$bag->add(new Metadata(
    new PHPExiftool\Driver\Tag\EXIF\IFD0\GPSLatitude(),
    new PHPExiftool\Driver\Value\Mono('')
));
$exiftool->write($path, $bag, ['-overwrite_original' => true]);

Extract-to-model on ingest. Read once, map a small whitelist into your model, discard the rest. Do not persist the full tag array — it's 200+ keys, half of them derived Composite: tags that will change on every exiftool upgrade and churn your diffs.

$takenAt = $tags['EXIF:DateTimeOriginal'] ?? null;   // "2024:05:01 12:00:00"
$takenAt = $takenAt ? Carbon::createFromFormat('Y:m:d H:i:s', $takenAt) : null;

Batch reads over directory scans. For imports, prefer one execute('-r', '-json', $dir) over N read() calls. Each read() round-trips through the stdin/stdout pipe and the JSON decode is not the bottleneck — the per-command framing is. If you must loop, keep the process open across the whole loop and close() in a finally.

Integrate with filesystem disks by path, not by stream. The binary needs a real path on the local filesystem. For S3 disks, Storage::path() won't exist; download to storage/app/tmp/, process, upload back, delete. Wrap this in a job with tries = 3 and clean up temp files in finally — orphaned temp files are the #1 operational complaint with this pattern.

Write-then-re-read is your test oracle. Exiftool's write result is optimistic; assert on a subsequent read() rather than the write's return value.

Custom tags and anything not modelled by a Tag class go through raw passthrough — this is also the escape hatch when the generated tag class doesn't exist for a vendor-namespaced XMP field:

$exiftool->execute('-XMP-dc:Subject+=holiday', '-overwrite_original', $path);

Queued pipeline shape that scales:

  1. ProcessImageMetadata job → read → persist whitelist → dispatch StripLocation.
  2. StripLocation → conditional write → Storage::copy to the public variant.
  3. Both jobs call $exiftool->open() / close() themselves; never share an instance across jobs (see below).

Gotchas and Tips

  • -stay_open processes leak. The wrapper holds a child process and two pipes. If you don't close(), you get zombie exiftool processes and, in long-running workers, a slowly growing pipe buffer. Always try { ... } finally { $exiftool->close(); }, or bind with scoped() in Laravel so Octane resets it per request. Never make it a hard singleton in a queue worker — the binary can die mid-stream and every subsequent call hangs.
  • A dead exiftool process hangs your PHP process. The wrapper writes to stdin and blocks reading stdout. If the binary crashes (Segmentation fault on exotic HEIC/CR3 files is real), there's no built-in timeout. Guard with pcntl_alarm or run the whole call inside a Process with a timeout for untrusted formats.
  • _original backup files. By default exiftool writes photo.jpg_original next to every modified file. Pass -overwrite_original (and -P to preserve the mtime) or you'll litter your storage/app tree and break Storage::size() assertions.
  • Exit code 0 does not mean success. Exiftool reports per-tag problems on stdout as Warning: / Error: entries while still exiting 0, especially with -m (ignore minor errors). Parse those keys and log them; otherwise corrupt writes pass silently.
  • Composite: and System: tags are read-only and computed. Composite:ImageSize, Composite:GPSPosition, System:FileModifyDate cannot be written. Filter by group before you build a MetadataBag, or you'll get cryptic Tag 'X' is not writable failures.
  • EXIF dates are not ISO 8601. Always Y:m:d H:i:s with colons in the date. Feed exiftool an ISO string and it will silently store garbage that other tools mis-parse. Also set EXIF:OffsetTimeOriginal alongside DateTimeOriginal or your dates drift in every viewer that respects timezone.
  • Group prefix matters on write, not on read. Reads come back as EXIF:Model; writes need the class (e.g. EXIF\IFD0\Model) so exiftool knows which IFD to target. When in doubt, pass the flat string form to execute() — -EXIF:Model=X and -XMP:Creator=Y.
  • Use -n for numbers, -api QuickTimeUTC for video. Otherwise GPS coordinates come back as "51 deg 30' 0.00\" N" and QuickTime dates get interpreted as local time. Both are per-call flags — set them everywhere, including inside the wrapper's option array.
  • Non-ASCII paths. Exiftool guesses charset from the extension; a héllo.jpg on a filesystem with a non-UTF8 locale fails with "file not found". Pass -charset filename=UTF8 (and set LANG=C.UTF-8 in your container) rather than debugging for an hour.
  • The tag classes are ~10k generated PHP files. They're in the composer classmap, so composer dump-autoload -o gets slower and your autoloader gets fatter. OPcache helps; if you only use a handful of tags, execute() with raw -Tag=Value strings is measurably lighter than instantiating Tag objects.
  • Docker: the alchemy/phpexiftool image and the Debian package both need libimage-exiftool-perl; the pure-PHP library bundles nothing. Pin the binary version in your image and write a test that asserts exiftool -ver is within the range you tested — upstream exiftool changes tag output between releases and your Composite: keys will shift.
  • Security: exiftool parses hostile, user-supplied files. Keep it patched (there have been parsing CVEs), never pass user-controlled strings into execute() without argument-array form, never let a user supply -config, and process uploads in a worker with a resource limit rather than in the web request.
  • Maintenance risk: this is a 41-star, single-maintainer package with no dependents and no Laravel wrapper. Expect to own the Exiftool service provider, the error handling, and any bug fixes yourself. If you only need -json reads, shelling out to exiftool -json -n via symfony/process is ~30 lines and gives you the same result with zero dependency surface — the wrapper's value is really just the long-lived process and the typed tag classes.
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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo