2017-09-26 20:45:41 +00:00
< ? php
2017-11-17 01:23:18 +00:00
/** @ license MIT
* Copyright 2017 J . King , Dustin Wilson et al .
* See LICENSE and AUTHORS files for details */
2017-09-26 20:45:41 +00:00
declare ( strict_types = 1 );
namespace JKingWeb\Arsse\Misc ;
2017-10-19 19:18:58 +00:00
use JKingWeb\Arsse\ExceptionType ;
2017-09-26 20:45:41 +00:00
class ValueInfo {
// universal
const VALID = 1 << 0 ;
const NULL = 1 << 1 ;
// integers
const ZERO = 1 << 2 ;
const NEG = 1 << 3 ;
2017-10-19 19:18:58 +00:00
const FLOAT = 1 << 4 ;
2017-09-26 20:45:41 +00:00
// strings
const EMPTY = 1 << 2 ;
const WHITE = 1 << 3 ;
2018-01-02 21:27:58 +00:00
// normalization types
2017-10-19 19:18:58 +00:00
const T_MIXED = 0 ; // pass through unchanged
const T_NULL = 1 ; // convert to null
const T_BOOL = 2 ; // convert to boolean
const T_INT = 3 ; // convert to integer
const T_FLOAT = 4 ; // convert to floating point
const T_DATE = 5 ; // convert to DateTimeInterface instance
const T_STRING = 6 ; // convert to string
const T_ARRAY = 7 ; // convert to array
2018-01-02 21:27:58 +00:00
// normalization modes
2017-10-19 19:18:58 +00:00
const M_NULL = 1 << 28 ; // pass nulls through regardless of target type
const M_DROP = 1 << 29 ; // drop the value (return null) if the type doesn't match
const M_STRICT = 1 << 30 ; // throw an exception if the type doesn't match
const M_ARRAY = 1 << 31 ; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable
2018-01-02 21:27:58 +00:00
// symbolic date and time formats
const DATE_FORMATS = [ // in out
'iso8601' => [ " !Y-m-d \T H:i:s " , " Y-m-d \T H:i:s \ Z " ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets
'iso8601m' => [ " !Y-m-d \T H:i:s.u " , " Y-m-d \T H:i:s.u \ Z " ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets
'microtime' => [ " U.u " , " 0.u00 U " ], // NOTE: the actual input format at the user level matches the output format; pre-processing is required for PHP not to fail
'http' => [ " !D, d M Y H:i:s \ G \ M \T " , " D, d M Y H:i:s \ G \ M \T " ],
'sql' => [ " !Y-m-d H:i:s " , " Y-m-d H:i:s " ],
'date' => [ " !Y-m-d " , " Y-m-d " ],
'time' => [ " !H:i:s " , " H:i:s " ],
'unix' => [ " U " , " U " ],
'float' => [ " U.u " , " U.u " ],
];
2017-10-19 19:18:58 +00:00
2018-01-02 15:29:24 +00:00
public static function normalize ( $value , int $type , string $dateInFormat = null , $dateOutFormat = null ) {
2017-10-19 19:18:58 +00:00
$allowNull = ( $type & self :: M_NULL );
$strict = ( $type & ( self :: M_STRICT | self :: M_DROP ));
$drop = ( $type & self :: M_DROP );
$arrayVal = ( $type & self :: M_ARRAY );
$type = ( $type & ~ ( self :: M_NULL | self :: M_DROP | self :: M_STRICT | self :: M_ARRAY ));
// if the value is null and this is allowed, simply return
if ( $allowNull && is_null ( $value )) {
return null ;
}
// if the value is supposed to be an array, handle it specially
if ( $arrayVal ) {
$value = self :: normalize ( $value , self :: T_ARRAY );
foreach ( $value as $key => $v ) {
2018-01-02 15:29:24 +00:00
$value [ $key ] = self :: normalize ( $v , $type | ( $allowNull ? self :: M_NULL : 0 ) | ( $strict ? self :: M_STRICT : 0 ) | ( $drop ? self :: M_DROP : 0 ), $dateInFormat , $dateOutFormat );
2017-10-19 19:18:58 +00:00
}
return $value ;
}
switch ( $type ) {
case self :: T_MIXED :
return $value ;
case self :: T_NULL :
return null ;
case self :: T_BOOL :
if ( is_bool ( $value )) {
return $value ;
}
$out = self :: bool ( $value );
if ( $strict && is_null ( $out )) {
// if strict and input is not a boolean, this is an error
if ( $drop ) {
return null ;
}
throw new ExceptionType ( " strictFailure " , $type );
} elseif ( is_float ( $value ) && is_nan ( $value )) {
return false ;
} elseif ( is_null ( $out )) {
// if not strict and input is not a boolean, return a simple type-cast
return ( bool ) $value ;
}
return $out ;
case self :: T_INT :
if ( is_int ( $value )) {
return $value ;
} elseif ( $value instanceof \DateTimeInterface ) {
if ( $strict && ! $drop ) {
throw new ExceptionType ( " strictFailure " , $type );
}
return ( ! $drop ) ? ( int ) $value -> getTimestamp () : null ;
}
$info = self :: int ( $value );
if ( $strict && ! ( $info & self :: VALID )) {
// if strict and input is not an integer, this is an error
if ( $drop ) {
return null ;
}
throw new ExceptionType ( " strictFailure " , $type );
} elseif ( is_bool ( $value )) {
return ( int ) $value ;
} elseif ( $info & ( self :: VALID | self :: FLOAT )) {
$out = strtolower (( string ) $value );
if ( strpos ( $out , " e " )) {
return ( int ) ( float ) $out ;
} else {
return ( int ) $out ;
}
} else {
return 0 ;
}
2017-12-17 15:27:34 +00:00
break ; // @codeCoverageIgnore
2017-10-19 19:18:58 +00:00
case self :: T_FLOAT :
if ( is_float ( $value )) {
return $value ;
} elseif ( $value instanceof \DateTimeInterface ) {
if ( $strict && ! $drop ) {
throw new ExceptionType ( " strictFailure " , $type );
}
return ( ! $drop ) ? ( float ) $value -> getTimestamp () : null ;
} elseif ( is_bool ( $value ) && $strict ) {
if ( $drop ) {
return null ;
}
throw new ExceptionType ( " strictFailure " , $type );
}
$out = filter_var ( $value , \FILTER_VALIDATE_FLOAT );
if ( $strict && $out === false ) {
// if strict and input is not a float, this is an error
if ( $drop ) {
return null ;
}
throw new ExceptionType ( " strictFailure " , $type );
}
return ( float ) $out ;
case self :: T_STRING :
if ( is_string ( $value )) {
return $value ;
}
2018-01-02 21:27:58 +00:00
if ( $value instanceof \DateTimeInterface ) {
$dateOutFormat = $dateOutFormat ? ? " iso8601 " ;
$dateOutFormat = isset ( self :: DATE_FORMATS [ $dateOutFormat ]) ? self :: DATE_FORMATS [ $dateOutFormat ][ 1 ] : $dateOutFormat ;
if ( $value instanceof \DateTimeImmutable ) {
return $value -> setTimezone ( new \DateTimeZone ( " UTC " )) -> format ( $dateOutFormat );
} elseif ( $value instanceof \DateTime ) {
return \DateTimeImmutable :: createFromMutable ( $value ) -> setTimezone ( new \DateTimeZone ( " UTC " )) -> format ( $dateOutFormat );
}
2017-10-19 19:18:58 +00:00
} elseif ( is_float ( $value ) && is_finite ( $value )) {
$out = ( string ) $value ;
2017-10-20 22:41:21 +00:00
if ( ! strpos ( $out , " E " )) {
2017-10-19 19:18:58 +00:00
return $out ;
} else {
$out = sprintf ( " %F " , $value );
return substr ( $out , - 2 ) == " .0 " ? ( string ) ( int ) $out : $out ;
}
}
$info = self :: str ( $value );
if ( ! ( $info & self :: VALID )) {
if ( $drop ) {
return null ;
} elseif ( $strict ) {
// if strict and input is not a string, this is an error
throw new ExceptionType ( " strictFailure " , $type );
} elseif ( ! is_scalar ( $value )) {
return " " ;
} else {
return ( string ) $value ;
}
} else {
return ( string ) $value ;
}
2017-12-17 15:27:34 +00:00
break ; // @codeCoverageIgnore
2017-10-19 19:18:58 +00:00
case self :: T_DATE :
if ( $value instanceof \DateTimeImmutable ) {
return $value -> setTimezone ( new \DateTimeZone ( " UTC " ));
} elseif ( $value instanceof \DateTime ) {
2018-01-02 21:27:58 +00:00
return \DateTimeImmutable :: createFromMutable ( $value ) -> setTimezone ( new \DateTimeZone ( " UTC " ));
2017-10-19 19:18:58 +00:00
} elseif ( is_int ( $value )) {
2018-01-02 21:27:58 +00:00
return \DateTimeImmutable :: createFromFormat ( " U " , ( string ) $value , new \DateTimeZone ( " UTC " ));
2017-10-19 19:18:58 +00:00
} elseif ( is_float ( $value )) {
2018-01-02 21:27:58 +00:00
return \DateTimeImmutable :: createFromFormat ( " U.u " , sprintf ( " %F " , $value ), new \DateTimeZone ( " UTC " ));
2017-10-19 19:18:58 +00:00
} elseif ( is_string ( $value )) {
try {
2018-01-02 15:29:24 +00:00
if ( ! is_null ( $dateInFormat )) {
2017-10-19 19:18:58 +00:00
$out = false ;
2018-01-02 15:29:24 +00:00
if ( $dateInFormat == " microtime " ) {
2017-10-19 19:18:58 +00:00
// PHP is not able to correctly handle the output of microtime() as the input of DateTime::createFromFormat(), so we fudge it to look like a float
if ( preg_match ( " <^0 \ . \ d { 6}00 \ d+ $ > " , $value )) {
2017-10-20 22:41:21 +00:00
$value = substr ( $value , 11 ) . " . " . substr ( $value , 2 , 6 );
2017-10-19 19:18:58 +00:00
} else {
throw new \Exception ;
}
}
2018-01-02 21:27:58 +00:00
$f = isset ( self :: DATE_FORMATS [ $dateInFormat ]) ? self :: DATE_FORMATS [ $dateInFormat ][ 0 ] : $dateInFormat ;
2018-01-02 15:29:24 +00:00
if ( $dateInFormat == " iso8601 " || $dateInFormat == " iso8601m " ) {
2018-01-02 21:27:58 +00:00
// DateTimeImmutable::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works
2018-01-02 15:29:24 +00:00
if ( $dateInFormat == " iso8601m " ) {
2018-01-02 21:27:58 +00:00
$f2 = self :: DATE_FORMATS [ " iso8601 " ][ 0 ];
2017-10-19 19:18:58 +00:00
$zones = [ $f . " " , $f . " \ Z " , $f . " P " , $f . " O " , $f2 . " " , $f2 . " \ Z " , $f2 . " P " , $f2 . " O " ];
} else {
$zones = [ $f . " " , $f . " \ Z " , $f . " P " , $f . " O " ];
}
do {
$ftz = array_shift ( $zones );
2018-01-02 21:27:58 +00:00
$out = \DateTimeImmutable :: createFromFormat ( $ftz , $value , new \DateTimeZone ( " UTC " ));
2017-10-19 19:18:58 +00:00
} while ( ! $out && $zones );
} else {
2018-01-02 21:27:58 +00:00
$out = \DateTimeImmutable :: createFromFormat ( $f , $value , new \DateTimeZone ( " UTC " ));
2017-10-19 19:18:58 +00:00
}
if ( ! $out ) {
throw new \Exception ;
}
return $out ;
} else {
2018-01-02 21:27:58 +00:00
return new \DateTimeImmutable ( $value , new \DateTimeZone ( " UTC " ));
2017-10-19 19:18:58 +00:00
}
} catch ( \Exception $e ) {
if ( $strict && ! $drop ) {
throw new ExceptionType ( " strictFailure " , $type );
}
return null ;
}
} elseif ( $strict && ! $drop ) {
throw new ExceptionType ( " strictFailure " , $type );
}
return null ;
case self :: T_ARRAY :
if ( is_array ( $value )) {
return $value ;
} elseif ( $value instanceof \Traversable ) {
$out = [];
foreach ( $value as $k => $v ) {
$out [ $k ] = $v ;
}
return $out ;
} else {
if ( $drop ) {
return null ;
} elseif ( $strict ) {
// if strict and input is not a string, this is an error
throw new ExceptionType ( " strictFailure " , $type );
} elseif ( is_null ( $value ) || ( is_float ( $value ) && is_nan ( $value ))) {
return [];
} else {
return [ $value ];
}
}
2017-12-17 15:27:34 +00:00
break ; // @codeCoverageIgnore
2017-10-19 19:18:58 +00:00
default :
throw new ExceptionType ( " typeUnknown " , $type ); // @codeCoverageIgnore
}
}
2017-09-26 20:45:41 +00:00
2017-09-28 13:01:43 +00:00
public static function int ( $value ) : int {
2017-09-26 20:45:41 +00:00
$out = 0 ;
if ( is_null ( $value )) {
2017-09-28 02:25:45 +00:00
// check if the input is null
return self :: NULL ;
2017-09-28 12:55:47 +00:00
} elseif ( is_string ( $value ) || ( is_object ( $value ) && method_exists ( $value , " __toString " ))) {
2017-10-19 19:18:58 +00:00
$value = strtolower (( string ) $value );
2017-09-28 02:25:45 +00:00
// normalize a string an integer or float if possible
2017-09-28 12:55:47 +00:00
if ( ! strlen ( $value )) {
2017-09-28 02:25:45 +00:00
// the empty string is equivalent to null when evaluating an integer
return self :: NULL ;
2017-10-19 19:18:58 +00:00
}
// interpret the value as a float
2017-10-20 22:41:21 +00:00
$float = filter_var ( $value , \FILTER_VALIDATE_FLOAT );
2017-10-19 19:18:58 +00:00
if ( $float !== false ) {
if ( ! fmod ( $float , 1 )) {
// an integral float is acceptable
$value = ( int ) ( ! strpos ( $value , " e " ) ? $value : $float );
} else {
$out += self :: FLOAT ;
$value = $float ;
}
} else {
return $out ;
}
} elseif ( is_float ( $value )) {
if ( ! fmod ( $value , 1 )) {
2017-09-28 02:25:45 +00:00
// an integral float is acceptable
$value = ( int ) $value ;
} else {
2017-10-19 19:18:58 +00:00
$out += self :: FLOAT ;
2017-09-26 20:45:41 +00:00
}
2017-09-28 02:25:45 +00:00
} elseif ( ! is_int ( $value )) {
// if the value is not an integer or integral float, stop
2017-09-26 20:45:41 +00:00
return $out ;
}
// mark validity
2017-10-19 19:18:58 +00:00
if ( is_int ( $value )) {
$out += self :: VALID ;
}
2017-09-26 20:45:41 +00:00
// mark zeroness
2017-10-19 19:18:58 +00:00
if ( ! $value ) {
2017-09-26 20:45:41 +00:00
$out += self :: ZERO ;
}
// mark negativeness
if ( $value < 0 ) {
$out += self :: NEG ;
}
return $out ;
}
2017-09-28 13:01:43 +00:00
public static function str ( $value ) : int {
2017-09-26 20:45:41 +00:00
$out = 0 ;
// check if the input is null
if ( is_null ( $value )) {
$out += self :: NULL ;
}
2017-09-28 12:55:47 +00:00
if ( is_object ( $value ) && method_exists ( $value , " __toString " )) {
// if the value is an object which has a __toString method, this is acceptable
$value = ( string ) $value ;
} elseif ( ! is_scalar ( $value ) || is_bool ( $value ) || ( is_float ( $value ) && ! is_finite ( $value ))) {
// otherwise if the value is not scalar, is a boolean, or is infinity or NaN, it cannot be valid
2017-09-26 20:45:41 +00:00
return $out ;
}
// mark validity
$out += self :: VALID ;
if ( ! strlen (( string ) $value )) {
// mark emptiness
$out += self :: EMPTY ;
} elseif ( ! strlen ( trim (( string ) $value ))) {
// mark whitespacedness
$out += self :: WHITE ;
}
return $out ;
}
2017-09-28 02:25:45 +00:00
2017-09-28 13:01:43 +00:00
public static function id ( $value , bool $allowNull = false ) : bool {
2017-09-28 02:25:45 +00:00
$info = self :: int ( $value );
if ( $allowNull && ( $info & self :: NULL )) { // null (and allowed)
return true ;
} elseif ( ! ( $info & self :: VALID )) { // not an integer
return false ;
} elseif ( $info & self :: NEG ) { // negative integer
return false ;
} elseif ( ! $allowNull && ( $info & self :: ZERO )) { // zero (and not allowed)
return false ;
} else { // non-negative integer
return true ;
}
}
2017-10-19 19:18:58 +00:00
public static function bool ( $value , bool $default = null ) {
if ( is_null ( $value ) || ValueInfo :: str ( $value ) & ValueInfo :: WHITE ) {
return $default ;
}
$out = filter_var ( $value , \FILTER_VALIDATE_BOOLEAN , \FILTER_NULL_ON_FAILURE );
if ( is_null ( $out ) && ( ValueInfo :: int ( $value ) & ValueInfo :: VALID )) {
$out = ( int ) filter_var ( $value , \FILTER_VALIDATE_FLOAT );
return ( $out == 1 || $out == 0 ) ? ( bool ) $out : $default ;
}
return ! is_null ( $out ) ? $out : $default ;
}
2017-09-28 13:01:43 +00:00
}