_way( $data ): void { foreach ( $data as $key => $val ) { // Check if we have a safe list. if ( ! empty( $this->safe ) && ! in_array( $key, $this->safe, true ) ) { continue; } $allowed = array_keys( $this->export() ); if ( ! in_array( $key, $allowed, true ) ) { continue; } if ( $this->has_property( $key ) ) { $this->$key = $val; } } } /** * Import data into the model. * * @param mixed $data The data array to import values from. * * @return void */ public function import( $data ): void { if ( empty( $this->annotations ) ) { $this->import_old_way( $data ); } else { foreach ( array_keys( $this->annotations ) as $property ) { if ( isset( $data[ $property ] ) && $this->has_property( $property ) ) { $this->$property = $data[ $property ]; } } $this->sanitize(); } } /** * This method prepares the data for saving in the database. * * @param array $data The data array to import values from. * * @return array The prepared data array. */ protected function prepare_data( $data = array() ): array { $scenario = 'import'; if ( ! count( $data ) ) { $data = $this->export(); $scenario = 'export'; } if ( empty( $this->mapping ) ) { return $data; } foreach ( $this->mapping as $key => $val ) { if ( 'export' === $scenario && isset( $data[ $key ] ) ) { $data[ $val ] = $data[ $key ]; unset( $data[ $key ] ); } elseif ( 'import' === $scenario && isset( $data[ $val ] ) ) { $data[ $key ] = $data[ $val ]; unset( $data[ $val ] ); } } return $data; } /** * Run a filter for casting type. * * @return void */ protected function sanitize() { if ( empty( $this->annotations ) ) { return; } foreach ( $this->annotations as $property => $meta ) { if ( ! $this->has_property( $property ) ) { // Todo: log it as this is not a good behavior. continue; } $type = $meta['type']; if ( false === $type ) { // Without a type, won't allow it. $this->$property = null; continue; } $value = $this->$property; // Cast it first. if ( 'boolean' === $type || 'bool' === $type ) { $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN ); } else { settype( $value, $type ); } if ( false !== $meta['sanitize'] ) { $func = $meta['sanitize']; if ( ! function_exists( $func ) ) { // The formatting.php still need to be included. include_once ABSPATH . WPINC . '/formatting.php'; } if ( is_array( $value ) ) { $value = $this->sanitize_array( $value, $func ); } else { $value = $func( $value ); } } $this->$property = $value; } } /** * Sanitize an array recursively. * * @param array $arr The array to be sanitized. * @param callable $sanitize The function to sanitize the array values. * * @return array The sanitized array. */ protected function sanitize_array( $arr, $sanitize ) { foreach ( $arr as &$value ) { if ( is_array( $value ) ) { $value = $this->sanitize_array( $value, $sanitize ); } else { $value = $sanitize( $value ); } } return $arr; } /** * Prepare the validation object. * * @return Validator */ protected function get_validate_rules(): Validator { $v = new Validator( $this->export() ); foreach ( $this->annotations as $property => $meta ) { if ( ! $this->has_property( $property ) ) { continue; } if ( false === $meta['rule'] ) { continue; } $rules = explode( '|', $meta['rule'] ); foreach ( $rules as $str ) { $v->rule( $str, $property ); } } return $v; } /** * Export the data type of public properties. * * @since 4.8.0 * @return array */ public function export_type(): array { $reflection = new ReflectionClass( $this ); $props = $reflection->getProperties( ReflectionProperty::IS_PUBLIC ); if ( empty( $this->annotations ) ) { return $this->export_type_oldway(); } $types = array(); foreach ( array_keys( $this->annotations ) as $property ) { if ( $this->has_property( $property ) ) { $type = isset( $this->annotations[ $property ]['type'] ) ? (string) $this->annotations[ $property ]['type'] : 'string'; $types[ $property ] = $this->map_format( $type ); } } return $types; } /** * Backward compatibility for exporting the data type of class properties. * * @since 4.8.0 * @return array */ private function export_type_oldway(): array { $types = array(); $reflection = new ReflectionClass( $this ); $props = $reflection->getProperties( ReflectionProperty::IS_PUBLIC ); foreach ( $props as $prop ) { $rp = new ReflectionProperty( $prop->class, $prop->name ); if ( preg_match( '/@var\s+([^\s]+)/', $rp->getDocComment(), $matches ) ) { $type = isset( $matches[1] ) ? (string) $matches[1] : 'string'; $types[] = $this->map_format( $type ); } } return $types; } /** * Map the format for a given data type. * * @param string $type The data type. * * @since 4.8.0 * @return string */ private function map_format( string $type ): string { $format = ''; switch ( $type ) { case 'int': case 'integer': case 'bool': case 'boolean': $format = '%d'; break; case 'float': $format = '%f'; break; // Handle other data types like array, object, string. default: $format = '%s'; break; } return $format; } }