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

File Type Detector Laravel Package

wapmorgan/file-type-detector

A lightweight PHP library that detects file types by extension and MIME type, mapping files to broad categories such as image, audio, video, document, archive, and other. Useful for upload validation, file organization, and content handling in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small, dependency-free PHP library — not a Laravel package. There is no service provider, facade, config file, or Artisan command. You wire it in yourself.

Install

composer require wapmorgan/file-type-detector

Where to look first

  1. README.md — the entire API surface is two or three methods.
  2. src/Detector.php (the main class) — confirm the exact namespace/class name; it's wapmorgan\FileTypeDetector\Detector and exposes extension-based and content-based detection.
  3. The data files it ships with — one map of extensions → MIME types, and one map of magic-byte signatures → MIME types. These are plain arrays; skim them before you trust them with a format you care about.

Minimal usage

use wapmorgan\FileTypeDetector\Detector;

$detector = new Detector();

// By extension (never opens the file)
$mime = $detector->detectByFilename('report.pdf');   // application/pdf

// By content (reads the leading bytes / magic number)
$mime = $detector->detectByContent('/tmp/phpA1b2C3'); // application/pdf

First real use case: stop trusting UploadedFile::getClientMimeType() (browser-supplied, trivially spoofed) and getMimeType() (relies on the fileinfo extension, which is disabled on plenty of shared hosts). Detect by content on the uploaded temp file, compare it against the client's original extension, and reject mismatches.

$contentMime = (new Detector())->detectByContent($request->file('avatar')->getRealPath());

Register it once so you're not new Detector() in every controller — the signature/extension tables get loaded per instance:

// AppServiceProvider::register()
$this->app->singleton(\wapmorgan\FileTypeDetector\Detector::class);

Implementation Patterns

1. A reusable validation rule

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use wapmorgan\FileTypeDetector\Detector;

class RealMimeType implements ValidationRule
{
    public function __construct(private array $allowed) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $mime = app(Detector::class)->detectByContent($value->getRealPath());

        if (! in_array($mime, $this->allowed, true)) {
            $fail("The {$attribute} must be one of: " . implode(', ', $this->allowed) . " (detected: {$mime}).");
        }
    }
}
$request->validate([
    'document' => ['required', 'file', 'max:10240', new RealMimeType(['application/pdf'])],
]);

2. Content-vs-extension mismatch check

The strongest signal this library gives you is disagreement between the two detectors. Normalize before comparing — the extension map and the signature map don't return identical strings for every format (image/jpg vs image/jpeg, text/json vs application/json).

$byExt     = $detector->detectByFilename($file->getClientOriginalName());
$byContent = $detector->detectByContent($file->getRealPath());

if ($byExt !== null && ! $this->sameFamily($byExt, $byContent)) {
    // A .jpg that is really text/html is an XSS payload, not a JPEG.
    throw ValidationException::withMessages(['file' => 'File contents do not match its extension.']);
}

3. Extension-based routing without opening the file

For naming/classification logic (icons, previews, "is this an image?") you can skip I/O entirely:

$mime = $detector->detectByFilename($asset->original_name);

$bucket = str_starts_with($mime ?? '', 'image/') ? 'images'
        : (str_starts_with($mime ?? '', 'video/') ? 'videos' : 'other');

4. Media-library / batch import job

Queued job iterating stored files. Detect on the local path, not an S3 URL:

foreach (Storage::disk('public')->allFiles() as $path) {
    $mime = $detector->detectByContent(Storage::disk('public')->path($path));

    MediaAsset::updateOrCreate(
        ['path' => $path],
        ['mime_type' => $mime]
    );
}

5. Serving a correct Content-Type

Storage::response() already guesses, but when you're streaming from S3 or a custom disk you can fill the gap:

return response()->streamDownload(
    fn () => readfile(Storage::path($file->path)),
    $file->name,
    ['Content-Type' => $detector->detectByContent(Storage::path($file->path)) ?? 'application/octet-stream']
);

6. Fallback chain with Symfony Mime

Best practice in a modern Laravel app: use symfony/mime (already a transitive dependency, backed by fileinfo) as the primary, and this library as the pure-PHP fallback for hosts without fileinfo:

$mime = (new MimeTypes())->guessMimeType($path)
     ?? app(Detector::class)->detectByContent($path)
     ?? 'application/octet-stream';

7. Whitelist by family, not by exact string

Since the library's maps are dated and occasionally return legacy aliases, validate against a family:

$allowed = ['image/jpeg', 'image/png', 'image/webp'];
$ok = in_array($mime, $allowed, true) || in_array($mime, ['image/jpg', 'image/pjpeg'], true);

Gotchas and Tips

It's unmaintained since 2017. No release in ~8 years. It still installs and runs on PHP 8.x in practice (it's plain arrays and string functions), but pin it, run your test suite on your target PHP version, and keep a fork in mind. Do not expect fixes for new formats — AVIF, HEIC/HEIF, JPEG XL, and newer WebP variants are almost certainly absent or misidentified, so guard any image pipeline that accepts modern formats.

UploadedFile::getRealPath() has no extension. PHP's upload temp file is /tmp/phpXXXXXX. Calling detectByFilename() on it returns nothing useful. Always pass getClientOriginalName() for extension detection and getRealPath() for content detection — mixing them up is the #1 mistake.

ZIP-based formats collapse to application/zip. docx, xlsx, pptx, epub, odt, and JAR are all ZIP containers. Content detection sees PK\x03\x04 and stops. For Office documents you must combine the extension hint with the content result.

image/svg+xml is an XSS vector. SVG is XML; content detection may return text/xml or text/plain rather than image/svg+xml. Never serve user-uploaded SVGs inline from your own origin, whatever MIME you get back.

This is not a security boundary. Magic-byte sniffing reads a prefix of the file. Polyglots (valid GIF header followed by HTML/JS), truncated files, and crafted headers all defeat it. Treat detection as one layer; add Content-Disposition: attachment, X-Content-Type-Options: nosniff, and CSP on user content.

Normalize case. Extensions arrive as .JPG, .Jpg, or with double extensions (.jpg.php). Lowercase the name yourself before passing it in, and use pathinfo($name, PATHINFO_EXTENSION) from the last dot — the library looks at the final extension, which is what you want for .jpg.php.

Unknown types return null/false (not an exception). Wrap comparisons accordingly; don't str_starts_with() on a null without a null-coalesce.

No extension point, so plan to fork or wrap. The MIME/signature tables are static arrays inside the source. If you need to add a format, either maintain your own small override map in front of the library:

$overrides = ['avif' => 'image/avif', 'heic' => 'image/heic'];
$mime = $overrides[strtolower($ext)] ?? $detector->detectByFilename($name);

…or vendor the class and edit the arrays directly. There is no config, no DI, no events.

Debugging: always print three values side by side — detectByContent(), detectByFilename(), and PHP's finfo_file() / mime_content_type(). Divergence tells you whether it's a map gap (this library wrong) or a genuinely ambiguous file.

dump([
    'content'   => $detector->detectByContent($path),
    'extension' => $detector->detectByFilename($name),
    'finfo'     => (new \finfo(FILEINFO_MIME_TYPE))->file($path),
]);

Large files are fine — content detection reads only the header bytes rather than slurping the file, so a 2 GB upload won't blow memory. Extension detection does no I/O at all.

Don't call new Detector() in a loop. The maps are rebuilt per instance; the singleton binding above matters on batch imports of thousands of files.

Given 0 dependents and 33 stars, assume no community answers exist. Read src/Detector.php end to end — it's small enough to audit in ten minutes, and you'll know exactly which formats you can and cannot rely on before you ship.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator