Strings7 min readExamples run on PHP 8.5.10

md5_file()

Learn how md5_file() hashes file contents in PHP 8.4, how the binary flag works, when it returns false, and why MD5 should not be used for security.

Signature

md5_file(string $filename, bool $binary = false): string|false
Available since PHP 4.2.07.48.08.18.28.38.48.5All versions →

Purpose

md5_file() reads the file named by its first argument and returns the MD5 digest of the file's contents. Typical uses are quick change detection, cache keys for generated assets, and comparing a file with a checksum published by a trusted source for non-security purposes. Only the contents are hashed. The file name, modification time and permissions do not affect the result.

It behaves like calling md5() on the full contents, but you do not have to load the file into a PHP string yourself.

Signature

Parameters and defaults

  • $filename: the path or stream URL to read. Any stream wrapper that supports reading can be used, not just local paths.
  • $binary: defaults to false. With false, you get a 32-character lowercase hexadecimal string. With true, you get the raw digest as a 16-byte binary string.

Return value and failure

On success you get a string: 32 hex characters, or 16 raw bytes when $binary is true. If the file cannot be opened, the function emits a warning and returns false. Missing files, unreadable files and wrapper failures all end up here.

Because the result is string|false, always compare with === false before using the digest.

A note about the examples

A real call usually looks like md5_file('/path/to/archive.zip'). To keep the examples below self-contained and independent of the disk, each one registers a tiny read-only stream wrapper called mem://. md5_file() reads through it exactly as it would read any other stream. In your own code you would just pass a normal path.

Example 1: hex digest, including an empty file

The hex digest of hello is well known, and an empty file has a digest too. An empty file is not an error.

Example
<?php
class MemoryFile {
    public $context;
    public static array $files = [];
    private string $data = '';
    private int $pos = 0;

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool {
        if (!isset(self::$files[$path])) {
            return false;
        }
        $this->data = self::$files[$path];
        $this->pos = 0;
        return true;
    }

    public function stream_read(int $count): string|false {
        $chunk = substr($this->data, $this->pos, $count);
        $this->pos += strlen($chunk);
        return $chunk;
    }

    public function stream_eof(): bool {
        return $this->pos >= strlen($this->data);
    }

    public function stream_stat(): array|false {
        return ['size' => strlen($this->data)];
    }
}

stream_wrapper_register('mem', MemoryFile::class);
MemoryFile::$files['mem://hello.txt'] = 'hello';
MemoryFile::$files['mem://empty.txt'] = '';

echo json_encode([
    'hello' => md5_file('mem://hello.txt'),
    'empty' => md5_file('mem://empty.txt'),
]);
Output
{"hello":"5d41402abc4b2a76b9719d911017c592","empty":"d41d8cd98f00b204e9800998ecf8427e"}

Example 2: raw binary output

With $binary set to true the result is always 16 bytes. Raw bytes are not printable, so convert them with bin2hex() if you want to display or log them.

Example
<?php
class MemoryFile {
    public $context;
    public static array $files = [];
    private string $data = '';
    private int $pos = 0;

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool {
        if (!isset(self::$files[$path])) {
            return false;
        }
        $this->data = self::$files[$path];
        $this->pos = 0;
        return true;
    }

    public function stream_read(int $count): string|false {
        $chunk = substr($this->data, $this->pos, $count);
        $this->pos += strlen($chunk);
        return $chunk;
    }

    public function stream_eof(): bool {
        return $this->pos >= strlen($this->data);
    }

    public function stream_stat(): array|false {
        return ['size' => strlen($this->data)];
    }
}

stream_wrapper_register('mem', MemoryFile::class);
MemoryFile::$files['mem://abc.txt'] = 'abc';

$raw = md5_file('mem://abc.txt', true);
echo json_encode([strlen($raw), bin2hex($raw)]);
Output
[16,"900150983cd24fb0d6963f7d28e17f72"]

Example 3: matching md5() and handling a missing file

The digest of a file equals md5() of the same bytes. A file that cannot be opened gives false. The @ operator is used here only to keep the demo output clean. In production code, check the return value and log the problem instead of hiding warnings.

Example
<?php
class MemoryFile {
    public $context;
    public static array $files = [];
    private string $data = '';
    private int $pos = 0;

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool {
        if (!isset(self::$files[$path])) {
            return false;
        }
        $this->data = self::$files[$path];
        $this->pos = 0;
        return true;
    }

    public function stream_read(int $count): string|false {
        $chunk = substr($this->data, $this->pos, $count);
        $this->pos += strlen($chunk);
        return $chunk;
    }

    public function stream_eof(): bool {
        return $this->pos >= strlen($this->data);
    }

    public function stream_stat(): array|false {
        return ['size' => strlen($this->data)];
    }
}

stream_wrapper_register('mem', MemoryFile::class);
$text = 'The quick brown fox jumps over the lazy dog';
MemoryFile::$files['mem://fox.txt'] = $text;

echo json_encode([
    'same' => md5_file('mem://fox.txt') === md5($text),
    'missing' => @md5_file('mem://missing.txt'),
]);
Output
{"same":true,"missing":false}

Edge cases

  • An empty file produces the digest d41d8cd98f00b204e9800998ecf8427e, not false.
  • A path that points to a directory, or to something PHP cannot read, fails and returns false.
  • Identical contents give identical digests regardless of file name.
  • Even a one-byte difference changes the digest completely, so a digest is useful only for equality checks, not for measuring how similar two files are.
  • Text mode does not exist here. Line endings are hashed as stored, so a file with \r\n endings differs from the same text with \n.

Common mistakes

  1. Treating the result as always a string. A falsy or false result from a missing file may be stored as a cache key or compared loosely. Use === false.
  2. Using MD5 for security. MD5 is not collision resistant, so it is unsuitable for signatures, password storage or defending against a malicious file author. Use hash_file() with an algorithm such as sha256 instead.
  3. Printing the binary form. With $binary = true, the output is raw bytes. Display it with bin2hex().
  4. Hashing user-supplied paths. Passing unvalidated input lets a caller probe files or stream wrappers. Validate or map names to known locations first.
  5. Comparing digests with different case or format. The hex form is lowercase. Normalize external checksums before comparing.
  • md5() hashes a string already in memory.
  • hash_file() hashes a file with a chosen algorithm.
  • hash_init() starts an incremental hashing context for data that arrives in pieces.

Recap

md5_file() returns the MD5 digest of a file's contents as 32 hex characters, or 16 raw bytes with $binary = true. It returns false with a warning when the file cannot be read. It is fine for checksums and cache keys, but use hash_file() with a stronger algorithm when security matters.

Tested examples

Hex digest of a small file and of an empty file
<?php
class MemoryFile {
    public $context;
    public static array $files = [];
    private string $data = '';
    private int $pos = 0;

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool {
        if (!isset(self::$files[$path])) {
            return false;
        }
        $this->data = self::$files[$path];
        $this->pos = 0;
        return true;
    }

    public function stream_read(int $count): string|false {
        $chunk = substr($this->data, $this->pos, $count);
        $this->pos += strlen($chunk);
        return $chunk;
    }

    public function stream_eof(): bool {
        return $this->pos >= strlen($this->data);
    }

    public function stream_stat(): array|false {
        return ['size' => strlen($this->data)];
    }
}

stream_wrapper_register('mem', MemoryFile::class);
MemoryFile::$files['mem://hello.txt'] = 'hello';
MemoryFile::$files['mem://empty.txt'] = '';

echo json_encode([
    'hello' => md5_file('mem://hello.txt'),
    'empty' => md5_file('mem://empty.txt'),
]);
Output
{"hello":"5d41402abc4b2a76b9719d911017c592","empty":"d41d8cd98f00b204e9800998ecf8427e"}
Raw binary digest converted with bin2hex()
<?php
class MemoryFile {
    public $context;
    public static array $files = [];
    private string $data = '';
    private int $pos = 0;

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool {
        if (!isset(self::$files[$path])) {
            return false;
        }
        $this->data = self::$files[$path];
        $this->pos = 0;
        return true;
    }

    public function stream_read(int $count): string|false {
        $chunk = substr($this->data, $this->pos, $count);
        $this->pos += strlen($chunk);
        return $chunk;
    }

    public function stream_eof(): bool {
        return $this->pos >= strlen($this->data);
    }

    public function stream_stat(): array|false {
        return ['size' => strlen($this->data)];
    }
}

stream_wrapper_register('mem', MemoryFile::class);
MemoryFile::$files['mem://abc.txt'] = 'abc';

$raw = md5_file('mem://abc.txt', true);
echo json_encode([strlen($raw), bin2hex($raw)]);
Output
[16,"900150983cd24fb0d6963f7d28e17f72"]
Comparing with md5() and handling a missing file
<?php
class MemoryFile {
    public $context;
    public static array $files = [];
    private string $data = '';
    private int $pos = 0;

    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool {
        if (!isset(self::$files[$path])) {
            return false;
        }
        $this->data = self::$files[$path];
        $this->pos = 0;
        return true;
    }

    public function stream_read(int $count): string|false {
        $chunk = substr($this->data, $this->pos, $count);
        $this->pos += strlen($chunk);
        return $chunk;
    }

    public function stream_eof(): bool {
        return $this->pos >= strlen($this->data);
    }

    public function stream_stat(): array|false {
        return ['size' => strlen($this->data)];
    }
}

stream_wrapper_register('mem', MemoryFile::class);
$text = 'The quick brown fox jumps over the lazy dog';
MemoryFile::$files['mem://fox.txt'] = $text;

echo json_encode([
    'same' => md5_file('mem://fox.txt') === md5($text),
    'missing' => @md5_file('mem://missing.txt'),
]);
Output
{"same":true,"missing":false}