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

Php Exif Laravel Package

lychee-org/php-exif

A PHP library for reading, writing, and manipulating EXIF metadata in images. It provides an object-oriented API to extract camera, GPS, and exposure details or edit and remove metadata, making it useful for photo galleries, media managers, and image-processing workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require lychee-org/php-exif

No service provider is auto-discovered and there is no config file — this is a plain library. You wire it up yourself.

Where to look first

  • src/Reader/Reader.php — the entry point (Reader::factory(...), ->read($path)).
  • src/Adapter/ — the actual implementations. The native Exif adapter wraps PHP's exif_read_data(); the FFProbe adapter shells out to ffprobe for video containers. Whatever adapters exist in your installed version is what you can actually use — read this directory before designing around a format.
  • src/Exif.php — the value object returned by the reader. Getters like getCameraMake(), getCameraModel(), getDateTimeOriginal(), getOrientation(), getWidth(), getHeight(), getLatitude(), getLongitude(), getKeywords(), getMimeType(). Check the file for the exact getter list in your version; the Lychee fork adds getters the upstream package doesn't have, and every getter can return null.

First use case — capture metadata on upload

use PHPExif\Reader\Reader;

$reader = Reader::factory(Reader::TYPE_NATIVE);

try {
    $exif = $reader->read($request->file('photo')->getRealPath());
} catch (\Throwable $e) {
    $exif = null; // never let missing EXIF break an upload
}

$takenAt = $exif?->getDateTimeOriginal();

The reader takes a local filesystem path, not a stream and not an UploadedFile. UploadedFile::getRealPath() and Storage::disk('local')->path($p) work; Storage::disk('s3') does not — copy to a temp file first.

Implementation Patterns

Wrap it in one injectable service. Don't call Reader::factory() from controllers. Bind a small ExifExtractor as a singleton (or scoped) in a service provider and return a plain array/DTO — that keeps the third-party namespace out of your app and makes it swappable later.

final class ExifExtractor
{
    public function extract(string $absolutePath): array
    {
        $reader = Reader::factory(Reader::TYPE_NATIVE);
        $exif = $reader->read($absolutePath); // throws; caller decides

        return array_filter([
            'camera_make'  => $exif->getCameraMake(),
            'camera_model' => $exif->getCameraModel(),
            'taken_at'     => $exif->getDateTimeOriginal(),
            'orientation'  => $exif->getOrientation(),
            'width'        => $exif->getWidth(),
            'height'       => $exif->getHeight(),
            'lat'          => $exif->getLatitude(),
            'lng'          => $exif->getLongitude(),
        ], fn ($v) => $v !== null);
    }
}

Run it in a queued job, after the file is persisted. EXIF parsing touches disk and can shell out (FFProbe), so it does not belong in the request/response cycle. Store the file, dispatch ExtractExif($mediaId), and write results back onto the model.

Persist as a JSON column, not fifty columns. EXIF key sets are wildly inconsistent across cameras. Store the normalized array in a json/jsonb column plus a handful of promoted, indexed columns (taken_at, camera_model, lat, lng) that you actually query on.

Adapter chaining with fallbacks. Pick the adapter by MIME type rather than by trial and error: native EXIF for JPEG/TIFF, FFProbe for MP4/MOV, and a no-op for anything else. Wrap in try/catch (\Throwable) and treat "no metadata" as a normal outcome — screenshots, exported PNGs, and images re-saved by a messaging app have had their EXIF stripped.

Timestamp fallback ladder. Prefer getDateTimeOriginal(), then file mtime, then now(). Never let a null date block the model from being created.

Normalize orientation before storing derivatives. The reader reports getOrientation() (1–8) but does not rotate anything. Apply it with Intervention Image / GD when generating thumbnails, and strip EXIF on the derivative so browsers don't double-rotate.

GPS into a spatial column. For PostGIS/MySQL spatial, convert lat/lng to a point after validating ranges: latitude ∈ [-90, 90], longitude ∈ [-180, 180]. Reject anything else as garbage metadata.

Testing. Use Storage::fake('local') and a handful of small committed fixture JPEGs with known EXIF (one with GPS, one without, one rotated). Assert on your ExifExtractor output, not on the library's object, so a dependency swap doesn't rewrite your test suite.

Gotchas and Tips

  • Local path only. Streams, URLs, and S3 objects fail. Copy to storage_path('app/tmp/...'), parse, delete in a finally.
  • exif_read_data() must be enabled. Some hardened PHP builds and minimal Docker images compile it out or disable it via disable_functions. Guard with function_exists('exif_read_data') and fail open.
  • HEIC/HEIF is not supported by the native adapter, which is exactly what iPhones produce. Either convert server-side first or accept "no metadata" for those uploads.
  • EXIF is attacker-controlled input. Make, model, and ImageDescription routinely contain newlines, control bytes, and non-UTF-8 sequences that will blow up json_encode (silently returning false) or a Blade render. Sanitize, truncate to sane lengths, and encode with JSON_INVALID_UTF8_SUBSTITUTE.
  • DateTimeOriginal has no timezone. It's floating local time at the camera. If OffsetTimeOriginal is present you can anchor it; otherwise store it without assuming UTC, or your "taken at" ordering will drift across a user's travel history.
  • (0, 0) is ambiguous — it's both "no GPS" and a real point in the Gulf of Guinea. Check whether the getter returns null versus 0.0 and gate on null explicitly; don't use empty().
  • Everything returns null, including things that feel mandatory. getWidth()/getHeight() can be missing on valid files. Type your consumers as nullable and give every field a fallback.
  • Exceptions are broad. Unreadable files, unsupported formats, and "no EXIF section" can all surface as different exception types from the adapter. Catch \Throwable at the boundary and log at debug — an upload without EXIF is not an error.
  • FFProbe is an external binary. Not in most php:8.x-fpm images. Either install it or disable that adapter; configure the binary path explicitly via the adapter constructor rather than relying on PATH.
  • Low-traffic fork — pin it. Eleven stars and zero dependents means the API surface is not battle-hardened by a crowd. Commit to an exact version in composer.json, keep the library behind your own service class, and expect that you may eventually swap in exiftool or a hand-rolled exif_read_data() call without touching call sites.
  • Extension point: implementing the adapter interface lets you plug in a custom backend — an exiftool JSON wrapper, a pure-PHP HEIC parser, or a metadata microservice — and keeps the Reader API unchanged. That's the seam to use if the built-in adapters don't cover your formats.
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.
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
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core