post = $post; } elseif ( is_numeric( $post ) ) { $this->post = get_post( $post ); } if ( $this->post && $this->post->post_type !== $this->get_post_type() ) { $this->post = null; } if ( $this->post ) { $this->load_attributes(); } } /** * Get post type * * @return string Get post type name */ abstract protected function get_post_type(); /** * Load attributes * * @return void Set all necessary attributes */ protected function load_attributes() { $this->attributes = array( 'id' => $this->post->ID, 'title' => $this->post->post_title, 'content' => $this->post->post_content, 'status' => $this->post->post_status, 'link' => $this->get_link( $this->post ), ); $custom_meta = get_post_meta( $this->post->ID ); foreach ( $this->get_fillable() as $key => $value ) { if ( isset( $this->attributes[ $key ] ) && ! empty( $this->attributes[ $key ] ) ) { continue; } $this->attributes[ $key ] = get_post_meta( $this->post->ID, $key, true ); } $this->original = $this->attributes; } /** * Get the permalink for the post * * @param $post * * @return string */ public function get_link( $post ) { return $post->post_type && get_post_type_object( $this->post->post_type ) ? get_post_permalink( $this->post->ID ) : ''; } /** * Get attributes * * @param string $key Property name * * @return mixed Meta data from any model * @throws Exception */ public function __get( $key ) { if ( isset( $this->get_fillable()[ $key ] ) ) { return $this->attributes[ $key ] ?? $this->get_fillable()[ $key ]; } // translators: %s is the name of the undefined property. throw new Exception( sprintf( esc_html__( 'Undefined property %s', 'booktics' ), esc_html( $key ) ) ); } /** * Set attributes * * @param string $key Model meta property * @param mixed $value Post meta data * * @return void Set meta data * @throws Exception */ public function __set( $key, $value ) { if ( ! isset( $this->get_fillable()[ $key ] ) && ! in_array( $key, array( 'link', 'customer' ) ) ) { // translators: %s is the name of the undefined property. throw new Exception( sprintf( esc_html__( 'Undefined property %s', 'booktics' ), esc_html( $key ) ) ); } $this->attributes[ $key ] = $value; } /** * Fillable getter * * @return array */ protected function get_fillable() { $this->fillable = apply_filters( 'booktics_post_model_fillable', $this->fillable, $this->get_post_type() ); return $this->fillable; } /** * Find any model by using id * * @param integer $id Post id * * @return null | PostModel */ public static function find( $id ) { $post = get_post( $id ); if ( ! $post ) { return null; } $instance = new static( $post ); return $instance; } /** * Get all posts * * @param array $args Post arguments * * @return array collection of PostModel */ public static function all( $args = array() ) { $args = array_merge( array( 'post_type' => ( new static() )->get_post_type(), 'post_status' => static::$accepted_statuses, 'numberposts' => - 1, ), $args ); $posts = get_posts( $args ); return array_map( fn( $p ) => new static( $p ), $posts ); } /** * Pagenate all collections * * @param integer $per_page Number of items perpage * @param integer $page Current page number * @param array $args other args * * @return array collection of records */ public static function paginate( $per_page = 10, $page = 1, $args = array() ) { $args = array_merge( array( 'post_type' => ( new static() )->get_post_type(), 'post_status' => static::$accepted_statuses, 'posts_per_page' => $per_page, 'paged' => $page, ), $args ); $query = new WP_Query( $args ); return array( 'items' => array_map( function ( $post ) { return new static( $post ); }, $query->posts ), 'total' => $query->found_posts, 'per_page' => $per_page, 'current_page' => $page, 'last_page' => ceil( $query->found_posts / $per_page ), ); } /** * Filter post by using condition * * @param string $meta_key Meta key for the post model * @param mixed $meta_value Post meta value * * @return array collection of post model */ public static function where( $meta_key, $meta_value ) { $args = array( 'post_type' => ( new static() )->get_post_type(), 'post_status' => static::$accepted_statuses, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering posts by meta fields 'meta_query' => array( array( 'key' => $meta_key, 'value' => $meta_value, ), ), ); $posts = get_posts( $args ); return array_map( function ( $p ) { return new static( $p ); }, $posts ); } /** * Create a single row * * @param array $attributes collection of attributes * * @return null | Post_Model | WP_Error */ public static function create( array $attributes ) { $validation = self::validate_attributes( $attributes ); if ( is_wp_error( $validation ) ) { return $validation; } $post_data = array( 'post_type' => ( new static() )->get_post_type(), 'post_status' => $attributes['status'] ?? 'deactive', 'post_title' => $attributes['title'] ?? '', 'post_content' => $attributes['content'] ?? $attributes['description'] ?? '', ); $post_id = wp_insert_post( $post_data ); if ( is_wp_error( $post_id ) ) { return null; } $instance = new static( $post_id ); if ( ! is_array( $attributes ) ) { return $instance; } foreach ( $attributes as $key => $value ) { if ( ! in_array( $key, array( 'title', 'content', 'status' ) ) ) { update_post_meta( $post_id, $key, $value ); $instance->$key = $value; } } $instance->link = $instance->get_link( $instance->post ); return $instance; } /** * Update a certain record * * @param array $attributes Collection of attributes * * @return WP_Error|Post_Model */ public function update( array $attributes ) { if ( ! $this->post ) { return new WP_Error( 'post_not_found', esc_html__( 'Cannot update a non-existent post.', 'booktics' ) ); } $validation = self::validate_attributes( $attributes ); if ( is_wp_error( $validation ) ) { return $validation; } $post_data = array( 'ID' => $this->post->ID ); if ( isset( $attributes['title'] ) ) { $this->title = $attributes['title']; $post_data['post_title'] = $attributes['title']; } if ( isset( $attributes['content'] ) ) { $this->content = $attributes['content']; $post_data['post_content'] = $attributes['content']; } if ( isset( $attributes['status'] ) ) { $this->status = $attributes['status']; $post_data['post_status'] = $attributes['status']; } // Handle permalink update when 'link' attribute is provided if ( isset( $attributes['link'] ) ) { $new_slug = $this->extract_slug_from_link( $attributes['link'] ); if ( $new_slug ) { $post_data['post_name'] = sanitize_title( $new_slug ); } } wp_update_post( $post_data ); // Update custom meta fields foreach ( $attributes as $key => $value ) { if ( ! in_array( $key, array( 'title', 'content', 'status', 'link' ) ) ) { update_post_meta( $this->post->ID, $key, $value ); $this->attributes[ $key ] = $value; } } // Refresh the post object and regenerate the link if permalink was updated if ( isset( $attributes['link'] ) ) { $this->post = get_post( $this->post->ID ); $this->attributes['link'] = $this->get_link( $this->post ); } return $this; } /** * Extract slug from a full permalink URL * * @param string $link Full permalink URL * @return string|null Extracted slug or null if invalid */ private function extract_slug_from_link( $link ) { // Remove trailing slash $link = rtrim( $link, '/' ); // Extract the last part of the URL as the slug $slug = basename( $link ); // Basic validation - ensure it's not empty and contains valid characters if ( empty( $slug ) || $slug === '.' || $slug === '..' ) { return null; } return $slug; } /** * Save a single record * * @return Post_Model instance of the PostModel * @throws Exception */ public function save() { if ( ! $this->post ) { return static::create( $this->attributes ); } $post_data = array( 'ID' => $this->post->ID, 'post_title' => $this->title, 'post_content' => $this->content, ); wp_update_post( $post_data ); foreach ( $this->attributes as $key => $value ) { if ( ! in_array( $key, array( 'ID', 'title', 'content', ) ) && $value !== ( $this->original[ $key ] ?? null ) ) { update_post_meta( $this->post->ID, $key, $value ); } } return $this; } /** * Delete a record * * @return WP_Post|false|null Post data on success, false or null on failure. */ public function delete() { if ( $this->post ) { return wp_delete_post( $this->post->ID, true ); } return false; } /** * Query records from posts * * @return Object */ public static function query() { return new Query_Builder( new static() ); } /** * Validate attributes * * @param array $attributes Post data * * @return WP_Error|true */ protected static function validate_attributes( $attributes = array() ) { if ( ! is_array( $attributes ) ) { return new WP_Error( 'invalid_attributes', esc_html__( 'Attributes must be an array.', 'booktics' ) ); } foreach ( $attributes as $key => $value ) { if ( ! isset( ( new static() )->get_fillable()[ $key ] ) && ! in_array( $key, array( 'ID', 'team_member_details', 'category_details', 'shortcode', 'service_details', 'duration_unit', 'customer', 'service', 'team_member', ) ) ) { // translators: %s: Property name return new WP_Error( 'invalid_property', sprintf( esc_html__( 'Invalid property: %s', 'booktics' ), $key ) ); } } return true; } /** * Convert the model instance to an array. * * @return array */ public function to_array(): array { $data = array(); foreach ( $this->get_fillable() as $key => $default ) { if ( $key === 'user_email' ) { $data['email'] = $this->$key; continue; } $value = $this->$key; // Unserialize if necessary if ( is_string( $value ) && $this->is_serialized( $value ) ) { $value = maybe_unserialize( $value ); } // Normalize value to default type if ( gettype( $value ) !== gettype( $default ) ) { if ( gettype( $default ) === 'array' && is_string( $value ) && $this->is_serialized( $value ) ) { $value = maybe_unserialize( $value ); } elseif ( gettype( $default ) === 'array' ) { $value = $default; // Set to default empty array if the value doesn't match } } // Cast specific keys to objects or arrays if needed if ( in_array( $key, array( 'team_member', 'holiday', 'categories', 'additional_durations', ) ) ) { $value = is_array( $value ) ? $value : array(); } if ( in_array( $key, array( 'schedule', 'custom_field' ) ) ) { $value = empty( $value ) ? (object) array() : $value; } // Cast specific keys to boolean if ( in_array( $key, array( 'enable_custom_schedule', 'enable_custom_holiday', ) ) ) { $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN ); } $data[ $key ] = $value ?? $default; } return $data; } /** * Specify data which should be serialized to JSON. * * @return array */ public function jsonSerialize(): array { return $this->to_array(); } /** * Check if a string is serialized * * @param string $data The data to check. * * @return bool True if the data is serialized, false otherwise. */ protected function is_serialized( $data ) { // If it isn't a string, return false if ( ! is_string( $data ) ) { return false; } // Check if it's a serialized string (Array or Object) $data = trim( $data ); if ( 'N;' === $data || ! preg_match( '/^([adObis]):/', $data ) ) { return false; } $result = @unserialize( $data ); return $result !== false || $data === 'b:0;'; // Return false for failed unserialization and true for successful } }