Tested in a number of configurations on a number of file and symlink combinations. Written for PHP 5.3+, though should work in earlier versions if you change "$home = $_ENV['HOME'] ?: '.';" to "$home = $_ENV['HOME'] ? $_ENV['HOME'] : '.';". Designed for Linux/Unix, though probably works on Windows if you specify true for $semiColon - not tested though.

No licence. Free to use or change without credit. If you find this useful, it would be nice to hear from you in this thread.

PHP Code:
/**
 * Searches a path list for the requested filename, with the requested permissions. Returns the first match.
 *
 * @param string $name
 *         Filename to find
 *
 * @param string $path
 *         Colon separated path list. If omitted or null, $_ENV['path'] is used. This will use realpath() to translate
 *         individual paths, and translate ~/ at the start of a path to $_ENV['home']. If no portions of this are
 *         valid paths, an E_USER_WARNING is triggered.
 *
 * @param bool $readable
 *         If true, only files that are readable will match
 *
 * @param bool $writable
 *         If true, only files that are writable will match
 *
 * @param bool $executable
 *         If true, only files that are executable will match
 *
 * @param bool $semiColon
 *         If true, uses the Windows path separator instead
 *
 * @return bool|string
 *         Either the full, real path without a trailing "/" or false if it couldn't be found
 */
function find_file($name$path null$readable true$writable false$executable false$semiColon false) {
    
// Handle default path
    
if (is_null($path)) {
        
$path $_ENV['PATH'];
    }

    
// Get a value for home, defaulting to cwd
    
$home $_ENV['HOME'] ?: '.';

    
// Split the path
    
$paths explode(($semiColon ';' ':'), $path);

    
// Setup to detect fully invalid $path for a E_USER_WARNING
    
$hasValid false;

    foreach(
$paths as $pathItem) {
        
// realpath() doesn't support ~, so compensate for that
        
if (($pathItem === '~') || (substr($pathItem02) === '~/')) {
            
$pathItem $home '/' substr($pathItem2);
        }

        
// Check this is a valid path
        
if (($pathItem === '') || !is_dir($pathItem)) {
            continue;
        }

        
// We got here, it's a valid path
        
$hasValid true;

        
// Setup fullName and pathItem with the real, full paths
        
$fullName = ($pathItem realpath($pathItem)) . '/' $name;

        
// Check criteria and return first match
        
if (file_exists($fullName) && !is_dir($fullName)
            && (!
$readable || is_readable($fullName))
            && (!
$writable || is_writable($fullName))
            && (!
$executable || is_executable($fullName))
        ) {
            return 
$pathItem;
        }
    }

    
// If $path had no valid paths, trigger a warning
    
if (!$hasValid) {
        
trigger_error('No valid paths given'E_USER_WARNING);
    }

    return 
false;

Examples of use:

Find a readable my.cnf in some likely locations:

PHP Code:
find_file('my.cnf''/etc:/etc/mysql:/etc/MySQL:~:~/mysql'); 
Find an executable convert in the system provided path:

PHP Code:
find_file('convert'nulltruefalsetrue);