fileeye/mimemap
A PHP library for mapping MIME types to file extensions and back, using Apache and custom MIME data. Useful for file type detection, extension lookup, and consistent MIME handling in PHP or Laravel projects.
fileeye/mimemap is a pure-data mapping between MIME types and file extensions, built from Apache's mime.types and shipped as a generated PHP array. It does not sniff file contents — that is finfo/Symfony's job. You reach for it whenever you have a MIME string and need an extension (or the reverse) without touching the filesystem.
Install and poke at it in 60 seconds:
composer require fileeye/mimemap
php artisan tinker
use FileEye\MimeMap\MimeMap;
MimeMap::getMimeType('webp'); // 'image/webp'
MimeMap::getMimeTypes('jpg'); // ['image/jpeg']
MimeMap::getExtension('image/jpeg'); // 'jpg' (preferred extension)
MimeMap::getExtensions('image/jpeg'); // ['jpg', 'jpeg', 'jpe', ...]
Where to look first:
FileEye\MimeMap\MimeMap — the entire static API you will use 95% of the time.data/ in the vendor directory) — inspect it when a lookup surprises you.FileEye\MimeMap\MimeMappingBuilder — the extension point for adding vendor types or compiling your own map.First real use case in a Laravel app: normalizing uploaded file names. Browsers send image/jpeg with filenames like photo.JPEG, photo.jfif, or no extension at all. Resolve a canonical extension from the server-detected MIME type:
$mime = $file->getMimeType(); // finfo, server-side
$ext = MimeMap::getExtension($mime) ?? 'bin'; // canonical, lower-case
$name = Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME))
. '.' . $ext;
$path = $file->storeAs('uploads', $name);
That is a five-line improvement over trusting getClientOriginalExtension(), which is attacker-controlled.
Laravel's mimes:jpg,png rule already guesses the extension from the detected MIME, but it fails silently in confusing ways (.jfif, .jpeg, .jpg are all image/jpeg). Being explicit makes the error message useful:
namespace App\Rules;
use Closure;
use FileEye\MimeMap\MimeMap;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Http\UploadedFile;
class ContentMatchesExtension implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! $value instanceof UploadedFile) {
$fail('The :attribute must be an uploaded file.');
return;
}
$ext = strtolower($value->getClientOriginalExtension());
$allowed = array_map('strtolower', MimeMap::getMimeTypes($ext));
if ($allowed === [] || ! in_array(strtolower($value->getMimeType()), $allowed, true)) {
$fail('The :attribute does not contain the type of data its extension claims.');
}
}
}
Note the order of trust: content first (getMimeType() reads the temp file with finfo), map second, client-supplied name last.
The static API is convenient but hard to fake. Wrap it:
// app/Support/MimeResolver.php
class MimeResolver
{
public function extensionFor(string $mime): ?string
{
$mime = strtolower(trim(explode(';', $mime)[0]));
return MimeMap::getExtension($mime);
}
public function mimeFor(string $extension): ?string
{
return MimeMap::getMimeType(strtolower(ltrim($extension, '.')));
}
}
// AppServiceProvider::register()
$this->app->singleton(MimeResolver::class);
Now tests can $this->mock(MimeResolver::class) and you avoid scattering static calls through controllers. If you need a custom map, compile it once and hand it to the library at boot, not per request:
// AppServiceProvider::boot()
MimeMap::setInstance(new MimeMap(storage_path('app/mimemap.php')));
(Check the installed version's constructor/static-instance signature; the pattern is “set it once at boot”.)
The shipped map will not know your product's proprietary formats, and occasionally lags on newer IANA types. Build an overlay:
use FileEye\MimeMap\MimeMappingBuilder;
$builder = MimeMappingBuilder::create();
$builder->add('application/vnd.acme.widget', 'widget');
$builder->add('application/vnd.acme.widget', 'wdgt');
// optionally merge a system mime.types you control
// $builder->addFromFile(base_path('resources/mime.types'));
$builder->save(storage_path('app/mimemap.php'));
Run it from an artisan command or a deploy step, commit the source (the additions file), and treat the compiled PHP as a build artifact. Never let production behaviour depend on the host's /etc/mime.types.
A classic webhook problem — the provider sends Content-Type but no filename:
$mime = strtolower(trim(explode(';', $payload['content_type'])[0]));
$ext = (new MimeResolver)->extensionFor($mime) ?? 'bin';
$filename = Str::uuid() . '.' . $ext;
Storage::disk('s3')->put("attachments/{$filename}", $body);
Same shape applies to email attachments, multipart/form-data parts, and archive extraction.
Storage::response() and response()->file() guess from the path, which breaks for extensionless files or unusual extensions:
$ext = pathinfo($path, PATHINFO_EXTENSION);
$mime = MimeMap::getMimeType($ext) ?? 'application/octet-stream';
return response()->file($absolutePath, [
'Content-Type' => $mime,
'X-Content-Type-Options' => 'nosniff',
]);
X-Content-Type-Options: nosniff pairs well here: you are asserting a type, so tell the browser not to second-guess you.
Mapping 200k legacy rows in a queued job is fine — lookups are array reads. If you are doing millions, chunk and consider memoizing per-process (static array $cache = [] keyed by extension) rather than re-normalizing strings repeatedly.
Both directions are one-to-many; the singular methods pick a winner.
getExtension('image/jpeg') returns one preferred extension among several. Do not treat it as the answer if the extension is user-visible — that is a product decision, not a library one.getMimeTypes('mp4'), ts, dat, bin, and similar extensions are genuinely ambiguous. For validation, accept the set; do not compare against a single MIME.Always sanitize the input string before lookup. MIME is case-insensitive and routinely arrives with parameters:
$mime = strtolower(trim(explode(';', $mime)[0]));
$ext = strtolower(ltrim($ext, '.'));
Skipping this produces intermittent nulls that look like missing map entries.
Handle null/empty returns explicitly. Unknown MIME or extension yields null (or an empty array). Default to application/octet-stream / bin at the boundary, and make that decision in one place.
This is a map, not a detector. Never use it to validate that a file is what it claims. MimeMap::getMimeType('jpg') says “files named .jpg are conventionally image/jpeg” — it does not say your upload is a JPEG. Combine with $file->getMimeType() (finfo) or mime_content_type().
The static instance is global state. If you call MimeMap::setInstance(...) in a test or a job, it leaks into subsequent tests and — under Octane/queue workers — subsequent requests. Set it once in a service provider; never mutate it per request.
Custom maps and Octane. Re-instantiating a MimeMap from a path inside a request means re-parsing a large PHP array on every request. Compile and bind at boot; the map is opcache-friendly because it is a plain array.
Multi-dot filenames. pathinfo('archive.tar.gz', PATHINFO_EXTENSION) is gz, which is correct. But photo.jpg.php is a different conversation — always re-validate the stored name, and never construct storage paths directly from client input even after mapping.
Watch for vendor/legacy oddities. application/x-* types, .jfif, .webp, image/avif, and modern Office types (application/vnd.openxmlformats-officedocument.*) occasionally differ between the library's map, Symfony's MimeTypes, and what a browser sends. Pick one source of truth per code path rather than mixing MimeMap, MimeTypeGuesser, and mimes: rule in the same flow.
Updating. The map is generated at release time. If a new format matters to your product, add it via MimeMappingBuilder rather than waiting on a release — and pin the package version so your compiled map and the library stay in sync.
Licence. LGPL-3.0 is fine for a normal Composer dependency in a closed-source SaaS. It becomes a consideration only if you (a) ship modified library files or (b) statically embed it in a distributed binary — at which point the relinking obligation needs a legal read, not a Stack Overflow one. Using the API and committing your own additions file is unremarkable.
Debugging checklist when a lookup returns null:
; charset=...?data/ file directly.setInstance.--order-by=random to expose it.How can I help you explore Laravel packages today?