* new WP_SQLite_PDO_User_Defined_Functions(ref_to_pdo_obj);
*
*
* This automatically enables ref_to_pdo_obj to replace the function in the SQL statement
* to the ones defined here.
*/
class WP_SQLite_PDO_User_Defined_Functions {
/**
* Registers the user defined functions for SQLite to a PDO instance.
* The functions are registered using PDO::sqliteCreateFunction().
*
* @param PDO|PDO\SQLite $pdo The PDO object.
*/
public static function register_for( $pdo ): self {
$instance = new self();
foreach ( $instance->functions as $f => $t ) {
if ( $pdo instanceof PDO\SQLite ) {
$pdo->createFunction( $f, array( $instance, $t ) );
} else {
$pdo->sqliteCreateFunction( $f, array( $instance, $t ) );
}
}
return $instance;
}
/**
* Array to define MySQL function => function defined with PHP.
*
* Replaced functions must be public.
*
* @var array
*/
private $functions = array(
'throw' => 'throw',
'month' => 'month',
'monthnum' => 'month',
'year' => 'year',
'day' => 'day',
'hour' => 'hour',
'minute' => 'minute',
'second' => 'second',
'week' => 'week',
'weekday' => 'weekday',
'dayofweek' => 'dayofweek',
'dayofmonth' => 'dayofmonth',
'unix_timestamp' => 'unix_timestamp',
'now' => 'now',
'md5' => 'md5',
'curdate' => 'curdate',
'rand' => 'rand',
'from_unixtime' => 'from_unixtime',
'localtime' => 'now',
'localtimestamp' => 'now',
'isnull' => 'isnull',
'if' => '_if',
'regexp' => 'regexp',
'field' => 'field',
'log' => 'log',
'least' => 'least',
'greatest' => 'greatest',
'get_lock' => 'get_lock',
'release_lock' => 'release_lock',
'ucase' => 'ucase',
'lcase' => 'lcase',
'unhex' => 'unhex',
'from_base64' => 'from_base64',
'to_base64' => 'to_base64',
'inet_ntoa' => 'inet_ntoa',
'inet_aton' => 'inet_aton',
'datediff' => 'datediff',
'locate' => 'locate',
'utc_date' => 'utc_date',
'utc_time' => 'utc_time',
'utc_timestamp' => 'utc_timestamp',
'version' => 'version',
// Internal helper functions.
'_helper_like_to_glob_pattern' => '_helper_like_to_glob_pattern',
);
/**
* First element of the RAND(N) LCG state (the value the output is derived from).
*
* @var int|null
*/
private $rand_seed1 = null;
/**
* Second element of the RAND(N) LCG state (the paired value used in the recurrence).
*
* @var int|null
*/
private $rand_seed2 = null;
/**
* Last seed value passed to RAND(N) in the current statement.
*
* Used to detect whether the rand sequence is advancing with the same seed
* (e.g. "SELECT RAND(3) FROM t"), or reseeding (starting a new sequence).
*
* @var int|null
*/
private $rand_last_seed = null;
/**
* Clear any per-statement state held by the UDFs.
*/
public function flush(): void {
$this->rand_seed1 = null;
$this->rand_seed2 = null;
$this->rand_last_seed = null;
}
/**
* A helper function to throw an error from SQLite expressions.
*
* @param string $message The error message.
*
* @throws Exception The error message.
* @return void
*/
public function throw( $message ): void {
throw new Exception( $message );
}
/**
* Method to return the unix timestamp.
*
* Used without an argument, it returns PHP time() function (total seconds passed
* from '1970-01-01 00:00:00' GMT). Used with the argument, it changes the value
* to the timestamp.
*
* @param string $field Representing the date formatted as '0000-00-00 00:00:00'.
*
* @return number of unsigned integer
*/
public function unix_timestamp( $field = null ) {
return is_null( $field ) ? time() : strtotime( $field );
}
/**
* Method to emulate MySQL FROM_UNIXTIME() function.
*
* @param int $field The unix timestamp.
* @param string $format Indicate the way of formatting(optional).
*
* @return string
*/
public function from_unixtime( $field, $format = null ) {
// Convert to ISO time.
$date = gmdate( 'Y-m-d H:i:s', $field );
return is_null( $format ) ? $date : $this->dateformat( $date, $format );
}
/**
* Method to emulate MySQL NOW() function.
*
* @return string representing current time formatted as '0000-00-00 00:00:00'.
*/
public function now() {
return gmdate( 'Y-m-d H:i:s' );
}
/**
* Method to emulate MySQL CURDATE() function.
*
* @return string representing current time formatted as '0000-00-00'.
*/
public function curdate() {
return gmdate( 'Y-m-d' );
}
/**
* Method to emulate MySQL MD5() function.
*
* @param string $field The string to be hashed.
*
* @return string of the md5 hash value of the argument.
*/
public function md5( $field ) {
return md5( $field );
}
/**
* Method to emulate MySQL's seeded RAND(N) function.
*
* Implements MySQL's deterministic LCG (Linear Congruential Generator),
* producing bit-exact output for a given seed.
*
* Known divergences from MySQL:
*
* 1. In MySQL, RAND(N) behaves differently depending on whether the seed
* is constant expression or varies per invocation:
* - Constant seed (e.g. "SELECT RAND(3) FROM t"):
* LCG is initialized once per statement and advanced for each row.
* - Non-constant seed (e.g. "SELECT RAND(col) FROM t"):
* LCG is initialized for every row with its seed value.
*
* A SQLite UDF cannot tell whether the seed expression is constant, so
* we just compare the seed against its last value. This diverges from
* MySQL in rare cases, and we can consider improving it in the future.
*
* 2. The LCG state is shared across call sites in the same query, so
* "SELECT RAND(1), RAND(1)" yields different results here than in MySQL.
* This is a rare edge case that we can consider improving in the future.
*
* Unseeded RAND() never reaches this function. The AST driver translates it
* directly to a more efficient SQLite-native expression.
*
* @param int|float|string|null $seed Seed value.
*
* @return float A value in [0, 1).
*/
public function rand( $seed ) {
// Requires 64-bit PHP. Seed * 0x10000001 can exceed PHP_INT_MAX on 32-bit.
$max_value = 0x3FFFFFFF;
if ( null === $seed ) {
// MySQL treats NULL seed as 0.
$seed = 0;
} elseif ( ! is_int( $seed ) ) {
/*
* MySQL rounds float values and numeric strings take the same path.
* Reduce the value to a 32-bit range using "fmod" to avoid firing
* the "out-of-range float to int" cast deprecation on PHP 8.1+.
*/
$seed = (int) fmod( round( (float) $seed, 0, PHP_ROUND_HALF_EVEN ), 0x100000000 );
}
// Initialize MySQL's internal 30-bit seeds.
if ( $seed !== $this->rand_last_seed ) {
/*
* MySQL casts to uint32, and the intermediate results wrap at 32-bit
* unsigned boundaries. We emulate this with & 0xFFFFFFFF masks.
*/
$seed_u32 = $seed & 0xFFFFFFFF;
$this->rand_seed1 = ( ( $seed_u32 * 0x10001 + 55555555 ) & 0xFFFFFFFF ) % $max_value;
$this->rand_seed2 = ( ( $seed_u32 * 0x10000001 ) & 0xFFFFFFFF ) % $max_value;
$this->rand_last_seed = $seed;
}
/*
* MySQL's LCG recurrence:
* seed1 = (seed1 * 3 + seed2) % max_value
* seed2 = (seed1 + seed2 + 33) % max_value
*
* Note that seed1 is updated first and the new value is used for seed2.
*/
$this->rand_seed1 = ( $this->rand_seed1 * 3 + $this->rand_seed2 ) % $max_value;
$this->rand_seed2 = ( $this->rand_seed1 + $this->rand_seed2 + 33 ) % $max_value;
return (float) $this->rand_seed1 / (float) $max_value;
}
/**
* Method to emulate MySQL DATEFORMAT() function.
*
* @param string $date Formatted as '0000-00-00' or datetime as '0000-00-00 00:00:00'.
* @param string $format The string format.
*
* @return string formatted according to $format
*/
public function dateformat( $date, $format ) {
$mysql_php_date_formats = array(
'%a' => 'D',
'%b' => 'M',
'%c' => 'n',
'%D' => 'jS',
'%d' => 'd',
'%e' => 'j',
'%H' => 'H',
'%h' => 'h',
'%I' => 'h',
'%i' => 'i',
'%j' => 'z',
'%k' => 'G',
'%l' => 'g',
'%M' => 'F',
'%m' => 'm',
'%p' => 'A',
'%r' => 'h:i:s A',
'%S' => 's',
'%s' => 's',
'%T' => 'H:i:s',
'%U' => 'W',
'%u' => 'W',
'%V' => 'W',
'%v' => 'W',
'%W' => 'l',
'%w' => 'w',
'%X' => 'Y',
'%x' => 'o',
'%Y' => 'Y',
'%y' => 'y',
);
$time = strtotime( $date );
$format = strtr( $format, $mysql_php_date_formats );
return gmdate( $format, $time );
}
/**
* Method to extract the month value from the date.
*
* @param string $field Representing the date formatted as 0000-00-00.
*
* @return string Representing the number of the month between 1 and 12.
*/
public function month( $field ) {
/*
* MySQL returns 0 for MONTH('0000-00-00') and for dates with
* zero month parts like '2020-00-15'. PHP's strtotime() can't
* parse these, so we extract the month directly from the string.
*/
if ( preg_match( '/^\d{4}-(\d{2})/', $field, $matches ) ) {
return intval( $matches[1] );
}
/*
* From https://www.php.net/manual/en/datetime.format.php:
*
* n - Numeric representation of a month, without leading zeros.
* 1 through 12
*/
return intval( gmdate( 'n', strtotime( $field ) ) );
}
/**
* Method to extract the year value from the date.
*
* @param string $field Representing the date formatted as 0000-00-00.
*
* @return string Representing the number of the year.
*/
public function year( $field ) {
/*
* MySQL returns 0 for YEAR('0000-00-00'). PHP's strtotime()
* can't parse zero dates, so we extract the year directly.
*/
if ( preg_match( '/^(\d{4})-\d{2}/', $field, $matches ) ) {
return intval( $matches[1] );
}
/*
* From https://www.php.net/manual/en/datetime.format.php:
*
* Y - A full numeric representation of a year, 4 digits.
*/
return intval( gmdate( 'Y', strtotime( $field ) ) );
}
/**
* Method to extract the day value from the date.
*
* @param string $field Representing the date formatted as 0000-00-00.
*
* @return string Representing the number of the day of the month from 1 and 31.
*/
public function day( $field ) {
/*
* MySQL returns 0 for DAY('0000-00-00') and for dates with
* zero day parts like '2020-01-00'. PHP's strtotime() can't
* parse these, so we extract the day directly from the string.
*/
if ( preg_match( '/^\d{4}-\d{2}-(\d{2})/', $field, $matches ) ) {
return intval( $matches[1] );
}
/*
* From https://www.php.net/manual/en/datetime.format.php:
*
* j - Day of the month without leading zeros.
* 1 to 31.
*/
return intval( gmdate( 'j', strtotime( $field ) ) );
}
/**
* Method to emulate MySQL SECOND() function.
*
* @see https://www.php.net/manual/en/datetime.format.php
*
* @param string $field Representing the time formatted as '00:00:00'.
*
* @return number Unsigned integer
*/
public function second( $field ) {
/*
* From https://www.php.net/manual/en/datetime.format.php:
*
* s - Seconds, with leading zeros (00 to 59)
*/
return intval( gmdate( 's', strtotime( $field ) ) );
}
/**
* Method to emulate MySQL MINUTE() function.
*
* @param string $field Representing the time formatted as '00:00:00'.
*
* @return int
*/
public function minute( $field ) {
/*
* From https://www.php.net/manual/en/datetime.format.php:
*
* i - Minutes with leading zeros.
* 00 to 59.
*/
return intval( gmdate( 'i', strtotime( $field ) ) );
}
/**
* Method to emulate MySQL HOUR() function.
*
* Returns the hour for time, in 24-hour format, from 0 to 23.
* Importantly, midnight is 0, not 24.
*
* @param string $time Representing the time formatted, like '14:08:12'.
*
* @return int
*/
public function hour( $time ) {
/*
* From https://www.php.net/manual/en/datetime.format.php:
*
* H 24-hour format of an hour with leading zeros.
* 00 through 23.
*/
return intval( gmdate( 'H', strtotime( $time ) ) );
}
/**
* Covers MySQL WEEK() function.
*
* Always assumes $mode = 1.
*
* @TODO: Support other modes.
*
* From https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_week:
*
* > Returns the week number for date. The two-argument form of WEEK()
* > enables you to specify whether the week starts on Sunday or Monday
* > and whether the return value should be in the range from 0 to 53
* > or from 1 to 53. If the mode argument is omitted, the value of the
* > default_week_format system variable is used.
* >
* > The following table describes how the mode argument works:
* >
* > Mode First day of week Range Week 1 is the first week …
* > 0 Sunday 0-53 with a Sunday in this year
* > 1 Monday 0-53 with 4 or more days this year
* > 2 Sunday 1-53 with a Sunday in this year
* > 3 Monday 1-53 with 4 or more days this year
* > 4 Sunday 0-53 with 4 or more days this year
* > 5 Monday 0-53 with a Monday in this year
* > 6 Sunday 1-53 with 4 or more days this year
* > 7 Monday 1-53 with a Monday in this year
*
* @param string $field Representing the date.
* @param int $mode The mode argument.
*/
public function week( $field, $mode ) {
/*
* From https://www.php.net/manual/en/datetime.format.php:
*
* W - ISO-8601 week number of year, weeks starting on Monday.
* Example: 42 (the 42nd week in the year)
*
* Week 1 is the first week with a Thursday in it.
*/
return intval( gmdate( 'W', strtotime( $field ) ) );
}
/**
* Simulates WEEKDAY() function in MySQL.
*
* Returns the day of the week as an integer.
* The days of the week are numbered 0 to 6:
* * 0 for Monday
* * 1 for Tuesday
* * 2 for Wednesday
* * 3 for Thursday
* * 4 for Friday
* * 5 for Saturday
* * 6 for Sunday
*
* @param string $field Representing the date.
*
* @return int
*/
public function weekday( $field ) {
/*
* date('N') returns 1 (for Monday) through 7 (for Sunday)
* That's one more than MySQL.
* Let's subtract one to make it compatible.
*/
return intval( gmdate( 'N', strtotime( $field ) ) ) - 1;
}
/**
* Method to emulate MySQL DAYOFMONTH() function.
*
* @see https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_dayofmonth
*
* @param string $field Representing the date.
*
* @return int Returns the day of the month for date as a number in the range 1 to 31.
*/
public function dayofmonth( $field ) {
return intval( gmdate( 'j', strtotime( $field ) ) );
}
/**
* Method to emulate MySQL DAYOFWEEK() function.
*
* > Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday).
* > These index values correspond to the ODBC standard. Returns NULL if date is NULL.
*
* @param string $field Representing the date.
*
* @return int Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday).
*/
public function dayofweek( $field ) {
/**
* From https://www.php.net/manual/en/datetime.format.php:
*
* `w` – Numeric representation of the day of the week
* 0 (for Sunday) through 6 (for Saturday)
*/
return intval( gmdate( 'w', strtotime( $field ) ) ) + 1;
}
/**
* Method to emulate MySQL DATE() function.
*
* @see https://www.php.net/manual/en/datetime.format.php
*
* @param string $date formatted as unix time.
*
* @return string formatted as '0000-00-00'.
*/
public function date( $date ) {
return gmdate( 'Y-m-d', strtotime( $date ) );
}
/**
* Method to emulate MySQL ISNULL() function.
*
* This function returns true if the argument is null, and true if not.
*
* @param mixed $field The field to be tested.
*
* @return boolean
*/
public function isnull( $field ) {
return is_null( $field );
}
/**
* Method to emulate MySQL IF() function.
*
* As 'IF' is a reserved word for PHP, function name must be changed.
*
* @param mixed $expression The statement to be evaluated as true or false.
* @param mixed $truthy Statement or value returned if $expression is true.
* @param mixed $falsy Statement or value returned if $expression is false.
*
* @return mixed
*/
public function _if( $expression, $truthy, $falsy ) {
return ( true === $expression ) ? $truthy : $falsy;
}
/**
* Method to emulate MySQL REGEXP() function.
*
* @param string $pattern Regular expression to match.
* @param string $field Haystack.
*
* @return integer 1 if matched, 0 if not matched.
*/
public function regexp( $pattern, $field ) {
/*
* If the original query says REGEXP BINARY
* the comparison is byte-by-byte and letter casing now
* matters since lower- and upper-case letters have different
* byte codes.
*
* The REGEXP function can't be easily made to accept two
* parameters, so we'll have to use a hack to get around this.
*
* If the first character of the pattern is a null byte, we'll
* remove it and make the comparison case-sensitive. This should
* be reasonably safe since PHP does not allow null bytes in
* regular expressions anyway.
*/
if ( "\x00" === $pattern[0] ) {
$pattern = substr( $pattern, 1 );
$flags = '';
} else {
// Otherwise, the search is case-insensitive.
$flags = 'i';
}
$pattern = str_replace( '/', '\/', $pattern );
$pattern = '/' . $pattern . '/' . $flags;
return preg_match( $pattern, $field );
}
/**
* Method to emulate MySQL FIELD() function.
*
* This function gets the list argument and compares the first item to all the others.
* If the same value is found, it returns the position of that value. If not, it
* returns 0.
*
* @return int
*/
public function field() {
$num_args = func_num_args();
if ( $num_args < 2 || is_null( func_get_arg( 0 ) ) ) {
return 0;
}
$arg_list = func_get_args();
$search_string = strtolower( array_shift( $arg_list ) );
for ( $i = 0; $i < $num_args - 1; $i++ ) {
if ( strtolower( $arg_list[ $i ] ) === $search_string ) {
return $i + 1;
}
}
return 0;
}
/**
* Method to emulate MySQL LOG() function.
*
* Used with one argument, it returns the natural logarithm of X.
*
* LOG(X)
*
* Used with two arguments, it returns the natural logarithm of X base B.
*
* LOG(B, X)
*
* In this case, it returns the value of log(X) / log(B).
*
* Used without an argument, it returns false. This returned value will be
* rewritten to 0, because SQLite doesn't understand true/false value.
*
* @return double|null
*/
public function log() {
$num_args = func_num_args();
if ( 1 === $num_args ) {
$arg1 = func_get_arg( 0 );
return log( $arg1 );
}
if ( 2 === $num_args ) {
$arg1 = func_get_arg( 0 );
$arg2 = func_get_arg( 1 );
return log( $arg1 ) / log( $arg2 );
}
return null;
}
/**
* Method to emulate MySQL LEAST() function.
*
* This function rewrites the function name to SQLite compatible function name.
*
* @return mixed
*/
public function least() {
$arg_list = func_get_args();
return min( $arg_list );
}
/**
* Method to emulate MySQL GREATEST() function.
*
* This function rewrites the function name to SQLite compatible function name.
*
* @return mixed
*/
public function greatest() {
$arg_list = func_get_args();
return max( $arg_list );
}
/**
* Method to dummy out MySQL GET_LOCK() function.
*
* This function is meaningless in SQLite, so we do nothing.
*
* @param string $name Not used.
* @param integer $timeout Not used.
*
* @return string
*/
public function get_lock( $name, $timeout ) {
return '1=1';
}
/**
* Method to dummy out MySQL RELEASE_LOCK() function.
*
* This function is meaningless in SQLite, so we do nothing.
*
* @param string $name Not used.
*
* @return string
*/
public function release_lock( $name ) {
return '1=1';
}
/**
* Method to emulate MySQL UCASE() function.
*
* This is MySQL alias for upper() function. This function rewrites it
* to SQLite compatible name upper().
*
* @param string $content String to be converted to uppercase.
*
* @return string SQLite compatible function name.
*/
public function ucase( $content ) {
return "upper($content)";
}
/**
* Method to emulate MySQL LCASE() function.
*
* This is MySQL alias for lower() function. This function rewrites it
* to SQLite compatible name lower().
*
* @param string $content String to be converted to lowercase.
*
* @return string SQLite compatible function name.
*/
public function lcase( $content ) {
return "lower($content)";
}
/**
* Method to emulate MySQL UNHEX() function.
*
* For a string argument str, UNHEX(str) interprets each pair of characters
* in the argument as a hexadecimal number and converts it to the byte represented
* by the number. The return value is a binary string.
*
* @param string $number Number to be unhexed.
*
* @return string Binary string
*/
public function unhex( $number ) {
return pack( 'H*', $number );
}
/**
* Method to emulate MySQL FROM_BASE64() function.
*
* Takes a base64-encoded string and returns the decoded result as a binary
* string. Returns NULL if the argument is NULL or is not a valid base64 string.
*
* @param string|null $str The base64-encoded string.
*
* @return string|null Decoded binary string, or NULL.
*/
public function from_base64( $str ) {
if ( null === $str ) {
return null;
}
$decoded = base64_decode( $str, true );
if ( false === $decoded ) {
return null;
}
return $decoded;
}
/**
* Method to emulate MySQL TO_BASE64() function.
*
* Takes a string and returns a base64-encoded result.
* Returns NULL if the argument is NULL.
*
* @param string|null $str The string to encode.
*
* @return string|null Base64-encoded string, or NULL.
*/
public function to_base64( $str ) {
if ( null === $str ) {
return null;
}
return base64_encode( $str );
}
/**
* Method to emulate MySQL INET_NTOA() function.
*
* This function gets 4 or 8 bytes integer and turn it into the network address.
*
* @param integer $num Long integer.
*
* @return string
*/
public function inet_ntoa( $num ) {
return long2ip( $num );
}
/**
* Method to emulate MySQL INET_ATON() function.
*
* This function gets the network address and turns it into integer.
*
* @param string $addr Network address.
*
* @return int long integer
*/
public function inet_aton( $addr ) {
return absint( ip2long( $addr ) );
}
/**
* Method to emulate MySQL DATEDIFF() function.
*
* This function compares two dates value and returns the difference.
*
* @param string $start Start date.
* @param string $end End date.
*
* @return string
*/
public function datediff( $start, $end ) {
$start_date = new DateTime( $start );
$end_date = new DateTime( $end );
$interval = $end_date->diff( $start_date, false );
return $interval->format( '%r%a' );
}
/**
* Method to emulate MySQL LOCATE() function.
*
* This function returns the position if $substr is found in $str. If not,
* it returns 0. If mbstring extension is loaded, mb_strpos() function is
* used.
*
* @param string $substr Needle.
* @param string $str Haystack.
* @param integer $pos Position.
*
* @return integer
*/
public function locate( $substr, $str, $pos = 0 ) {
if ( ! extension_loaded( 'mbstring' ) ) {
$val = strpos( $str, $substr, $pos );
if ( false !== $val ) {
return $val + 1;
}
return 0;
}
$val = mb_strpos( $str, $substr, $pos );
if ( false !== $val ) {
return $val + 1;
}
return 0;
}
/**
* Method to return GMT date in the string format.
*
* @return string formatted GMT date 'dddd-mm-dd'
*/
public function utc_date() {
return gmdate( 'Y-m-d', time() );
}
/**
* Method to return GMT time in the string format.
*
* @return string formatted GMT time '00:00:00'
*/
public function utc_time() {
return gmdate( 'H:i:s', time() );
}
/**
* Method to return GMT time stamp in the string format.
*
* @return string formatted GMT timestamp 'yyyy-mm-dd 00:00:00'
*/
public function utc_timestamp() {
return gmdate( 'Y-m-d H:i:s', time() );
}
/**
* Method to return MySQL version.
*
* This function only returns the current newest version number of MySQL,
* because it is meaningless for SQLite database.
*
* @return string representing the version number: major_version.minor_version
*/
public function version() {
return '5.5';
}
/**
* A helper to covert LIKE pattern to a GLOB pattern for "LIKE BINARY" support.
* @TODO: Some of the MySQL string specifics described below are likely to
* affect also other patterns than just "LIKE BINARY". We should
* consider applying some of the conversions more broadly.
*
* @param string $pattern
* @return string
*/
public function _helper_like_to_glob_pattern( $pattern ) {
if ( null === $pattern ) {
return null;
}
/*
* 1. Escape characters that have special meaning in GLOB patterns.
*
* We need to:
* 1. Escape "]" as "[]]" to avoid interpreting "[...]" as a character class.
* 2. Escape "*" as "[*]" (must be after 1 to avoid being escaped).
* 3. Escape "?" as "[?]" (must be after 1 to avoid being escaped).
*/
$pattern = str_replace( ']', '[]]', $pattern );
$pattern = str_replace( '*', '[*]', $pattern );
$pattern = str_replace( '?', '[?]', $pattern );
/*
* 2. Convert LIKE wildcards to GLOB wildcards ("%" -> "*", "_" -> "?").
*
* We need to convert them only when they don't follow any backslashes,
* or when they follow an even number of backslashes (as "\\" is "\").
*/
$pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)%/', '$1*', $pattern );
$pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)_/', '$1?', $pattern );
/*
* 3. Unescape LIKE escape sequences.
*
* While in MySQL LIKE patterns, a backslash is usually used to escape
* special characters ("%", "_", and "\"), it works with all characters.
*
* That is:
* SELECT '\\x' prints '\x', but LIKE '\\x' is equivalent to LIKE 'x'.
*
* This is true also for multi-byte characters:
* SELECT '\\©' prints '\©', but LIKE '\\©' is equivalent to LIKE '©'.
*
* However, the multi-byte behavior is likely to depend on the charset.
* For now, we'll assume UTF-8 and thus the "u" modifier for the regex.
*/
$pattern = preg_replace( '/\\\\(.)/u', '$1', $pattern );
return $pattern;
}
}