Here's a definitive solution, which supports negative character classes and the four documented flags.
<?php
if (!function_exists('fnmatch')) {
define('FNM_PATHNAME', 1);
define('FNM_NOESCAPE', 2);
define('FNM_PERIOD', 4);
define('FNM_CASEFOLD', 16);
        function fnmatch($pattern, $string, $flags = 0) {
            return pcre_fnmatch($pattern, $string, $flags);
        }
    }
    function pcre_fnmatch($pattern, $string, $flags = 0) {
$modifiers = null;
$transforms = array(
'\*'    => '.*',
'\?'    => '.',
'\[\!'    => '[^',
'\['    => '[',
'\]'    => ']',
'\.'    => '\.',
'\\'    => '\\\\'
);
if ($flags & FNM_PATHNAME) {
$transforms['\*'] = '[^/]*';
        }
if ($flags & FNM_NOESCAPE) {
            unset($transforms['\\']);
        }
if ($flags & FNM_CASEFOLD) {
$modifiers .= 'i';
        }
if ($flags & FNM_PERIOD) {
            if (strpos($string, '.') === 0 && strpos($pattern, '.') !== 0) return false;
        }
$pattern = '#^'
. strtr(preg_quote($pattern, '#'), $transforms)
            . '$#'
. $modifiers;
        return (boolean)preg_match($pattern, $string);
    }
?>
This probably needs further testing, but it seems to function identically to the native fnmatch implementation.