ralouphie/mimey
Mimey is a lightweight PHP library that maps file extensions to MIME types and back. It ships with a comprehensive built-in mapping, requires no extensions, and offers simple methods like getMimeTypeFromExtension and getExtensionFromMimeType for reliable content-type detection in Laravel or any PHP...
What it is: a zero-dependency PHP class (~330KB of static mapping table) that converts file extensions to MIME types and back. One class, four methods, no facade, no service provider, no config file.
You probably already have it. laravel/framework depends on mimey/mimey because the mimes and mimetypes validation rules use it under the hood. Confirm with:
composer show mimey/mimey
For a non-Laravel or bare install: composer require ralouphie/mimey.
First use — extension → MIME:
use Mimey\MimeTypes;
$mimes = new MimeTypes;
$mimes->getMimeType('json'); // "application/json"
$mimes->getMimeType('unknown'); // null
$mimes->getExtension('image/jpeg'); // "jpg" (first/preferred match)
The full API you'll use 95% of the time:
| Method | Returns |
|---|---|
getMimeType($ext) |
MIME string or null |
getExtension($mime) |
extension string or null |
getAllMimeTypes($ext) |
array of MIMEs (empty if unknown) |
getAllExtensions($mime) |
array of extensions |
Where to look first:
vendor/ralouphie/mimey/src/MimeTypes.php — the built-in table is a static array near the top; skim it to see coverage for your domain.Illuminate\Validation\Concerns\ValidatesAttributes — grep for MimeTypes to see how Laravel itself wires it up (it resolves MimeTypes from the container).First real use case: deriving a browser accept attribute and a validation rule from one config list of allowed extensions.
<input type="file" name="file" accept="{{ app(MimeTypes::class)->getMimeType('pdf') }}">
The constructor's only argument is optional, so the container resolves it automatically:
public function __construct(private \Mimey\MimeTypes $mimes) {}
Custom formats (.geojson, .heic, internal extensions) go through the constructor. Pass a mapping and it augments the built-in table:
// AppServiceProvider::register()
$this->app->singleton(\Mimey\MimeTypes::class, fn () => new \Mimey\MimeTypes([
'geojson' => ['application/geo+json'],
'heic' => ['image/heic', 'image/heif'],
]));
Registering it as a singleton with a constructor mapping keeps the extension isolated and testable.
accept + validationThe anti-pattern is two lists that drift. Derive both from config:
// config/uploads.php
return ['allowed' => ['jpg', 'jpeg', 'png', 'pdf', 'geojson']];
<input type="file" name="file"
accept="{{ collect(config('uploads.allowed'))
->map(fn ($ext) => app(\Mimey\MimeTypes::class)->getMimeType($ext))
->filter()
->unique()
->implode(',') }}">
// FormRequest
public function rules(): array
{
return [
'file' => ['required', 'file', 'mimes:'.implode(',', config('uploads.allowed'))],
];
}
Mimey alone cannot validate an upload. Combine it with $file->getMimeType() (Symfony finfo) to confirm the extension is consistent with the bytes:
class AllowedUpload implements ValidationRule
{
public function __construct(private array $extensions) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$mimes = app(\Mimey\MimeTypes::class);
$ext = strtolower($value->getClientOriginalExtension());
if (! in_array($ext, $this->extensions, true)) {
$fail("The {$attribute} extension is not allowed.");
return;
}
$allowed = $mimes->getAllMimeTypes($ext); // [] if unknown
$actual = $value->getMimeType(); // from file contents
if (! in_array($actual, $allowed, true)) {
$fail("The {$attribute} contents do not match a .{$ext} file.");
}
}
}
Content-Type on manual streamsWhen you serve a file outside Storage::download():
return response()->streamDownload(
fn () => print file_get_contents($path),
basename($path),
['Content-Type' => $mimes->getMimeType(pathinfo($path, PATHINFO_EXTENSION))
?? 'application/octet-stream'],
);
Write mime_type into your uploads row when the file lands, so listing screens and API responses don't need to touch disk to render a badge or build a link. Conversely, use getExtension() when an external system hands you a MIME (email attachments, webhook payloads, S3 Content-Type).
finfo/$file->getMimeType(), or just use Laravel's mimes/mimetypes rules, which already do the right thing.null. Type-hint ?string and always coalesce at the boundary (?? 'application/octet-stream'). getAllMimeTypes() returns [], not null, for unknown extensions — that's a second, different failure shape to handle..jpg and .jpeg both map to image/jpeg; image/jpeg maps back to jpg (the first/preferred entry). Round-tripping an extension is lossy. Use getAllMimeTypes()/getAllExtensions() when you need the complete set.getMimeType('JPG') returns null. Normalize with strtolower() before every call.text/plain; charset=utf-8 will miss. Split on ; and trim() first.add() mutates. MimeTypes is mutable. Calling add() on a container singleton leaks the mapping across the whole request/app. Prefer the constructor mapping in your binding.Mimey::getMimeType(...) — it doesn't exist. If you like the sugar, register a tiny facade in app/Facades; the container already binds MimeTypes.mimes vs. mimetypes debugging. mimes:jpg validates the client-supplied extension, mimetypes:image/jpeg validates detected content. Mimey underpins the former. If a validation "should" pass and doesn't, check which rule you actually used — they read different columns.vendor/.MimeTypes instance (or memoize a reverse index from getAllMimeTypes()) instead of constructing inside the loop — the constructor is cheap, but repeated getAllExtensions() calls on the same 50k-row import add up.How can I help you explore Laravel packages today?