PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.28
Booktics – Appointment Booking Calendar for Service Businesses v1.0.28
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.2 1.0.3 All 31 releases
← All changes | base/abstracts/post-model.php +587 -623 1.0.1 → 1.0.28 View file →
@@ -1,623 +1,587 @@
1 -<?php
2 -
3 -namespace Booktics\Abstracts;
4 -
5 -use Booktics\Query_Builders\Query_Builder;
6 -use Exception;
7 -use JsonSerializable;
8 -use WP_Error;
9 -use WP_Post;
10 -use WP_Query;
11 -
12 -/**
13 - * PostModel class
14 - */
15 -abstract class Post_Model implements JsonSerializable {
16 - /**
17 - * List of accepted post statuses
18 - *
19 - * @var array
20 - */
21 - public static $accepted_statuses = [
22 - 'active',
23 - 'deactive',
24 - 'draft',
25 - 'publish',
26 - 'cancelled',
27 - 'pending'
28 - ];
29 -
30 - /**
31 - * Store post
32 - *
33 - * @var Object
34 - */
35 - protected $post;
36 -
37 - /**
38 - * Store meta property
39 - *
40 - * @var array
41 - */
42 - protected $fillable = [];
43 -
44 - /**
45 - * Store post attributes
46 - *
47 - * @var array
48 - */
49 - protected $attributes = [];
50 -
51 - /**
52 - * Store origianl data
53 - *
54 - * @var array
55 - */
56 - protected $original = [];
57 -
58 - /**
59 - * Store post type
60 - *
61 - * @var string
62 - */
63 - protected $post_type;
64 -
65 - /**
66 - * Constructor for post model class
67 - *
68 - * @param Object $post Post model
69 - *
70 - * @return void
71 - */
72 - public function __construct( $post = null ) {
73 - if ( $post instanceof WP_Post ) {
74 - $this->post = $post;
75 - } elseif ( is_numeric( $post ) ) {
76 - $this->post = get_post( $post );
77 - }
78 -
79 - if ( $this->post && $this->post->post_type !== $this->get_post_type() ) {
80 - $this->post = null;
81 - }
82 -
83 - if ( $this->post ) {
84 - $this->load_attributes();
85 - }
86 -
87 - register_post_status( 'active', [
88 - 'label' => __( 'Active', 'booktics' ),
89 - 'public' => true,
90 - 'publicly_queryable' => true,
91 - 'exclude_from_search' => false,
92 - 'show_in_admin_all_list' => true,
93 - 'show_in_admin_status_list' => true,
94 - // translators: %s: number of items active
95 - 'label_count' => _n_noop( 'Active <span class="count">(%s)</span>', 'Active <span class="count">(%s)</span>', 'booktics' ),
96 - ] );
97 -
98 - register_post_status( 'deactive', [
99 - 'label' => __( 'Deactive', 'booktics' ),
100 - 'public' => true,
101 - 'publicly_queryable' => true,
102 - 'exclude_from_search' => false,
103 - 'show_in_admin_all_list' => true,
104 - 'show_in_admin_status_list' => true,
105 - // translators: %s: number of items deactive
106 - 'label_count' => _n_noop( 'Deactive <span class="count">(%s)</span>', 'Deactive <span class="count">(%s)</span>', 'booktics' ),
107 - ] );
108 -
109 - register_post_status( 'pending', [
110 - 'label' => __( 'Pending', 'booktics' ),
111 - 'public' => true,
112 - 'publicly_queryable' => true,
113 - 'exclude_from_search' => false,
114 - 'show_in_admin_all_list' => true,
115 - 'show_in_admin_status_list' => true,
116 - // translators: %s: number of items pending
117 - 'label_count' => _n_noop( 'Pending <span class="count">(%s)</span>', 'Pending <span class="count">(%s)</span>', 'booktics' ),
118 - ] );
119 -
120 - register_post_status( 'cancelled', [
121 - 'label' => __( 'Cancelled', 'booktics' ),
122 - 'public' => true,
123 - 'publicly_queryable' => true,
124 - 'exclude_from_search' => false,
125 - 'show_in_admin_all_list' => true,
126 - 'show_in_admin_status_list' => true,
127 - // translators: %s: number of items cancelled
128 - 'label_count' => _n_noop( 'Cancelled <span class="count">(%s)</span>', 'Cancelled <span class="count">(%s)</span>', 'booktics' ),
129 - ] );
130 -
131 - register_post_status( 'approved', [
132 - 'label' => __( 'Approved', 'booktics' ),
133 - 'public' => true,
134 - 'publicly_queryable' => true,
135 - 'exclude_from_search' => false,
136 - 'show_in_admin_all_list' => true,
137 - 'show_in_admin_status_list' => true,
138 - // translators: %s: number of items approved
139 - 'label_count' => _n_noop( 'Approved <span class="count">(%s)</span>', 'Approved <span class="count">(%s)</span>', 'booktics' ),
140 - ] );
141 -
142 - register_post_status( 'completed', [
143 - 'label' => __( 'Completed', 'booktics' ),
144 - 'public' => true,
145 - 'publicly_queryable' => true,
146 - 'exclude_from_search' => false,
147 - 'show_in_admin_all_list' => true,
148 - 'show_in_admin_status_list' => true,
149 - // translators: %s: number of items Completed
150 - 'label_count' => _n_noop( 'Completed <span class="count">(%s)</span>', 'Completed <span class="count">(%s)</span>', 'booktics' ),
151 - ] );
152 -
153 - register_post_status( 'not_show', [
154 - 'label' => __( 'Not Show', 'booktics' ),
155 - 'public' => true,
156 - 'publicly_queryable' => true,
157 - 'exclude_from_search' => false,
158 - 'show_in_admin_all_list' => true,
159 - 'show_in_admin_status_list' => true,
160 - // translators: %s: number of items not_show
161 - 'label_count' => _n_noop( 'Not Show <span class="count">(%s)</span>', 'Not Show <span class="count">(%s)</span>', 'booktics' ),
162 - ] );
163 - }
164 -
165 - /**
166 - * Get post type
167 - *
168 - * @return string Get post type name
169 - */
170 - abstract protected function get_post_type();
171 -
172 - /**
173 - * Load attributes
174 - *
175 - * @return void Set all necessary attributes
176 - */
177 - protected function load_attributes() {
178 - $this->attributes = [
179 - 'id' => $this->post->ID,
180 - 'title' => $this->post->post_title,
181 - 'content' => $this->post->post_content,
182 - 'status' => $this->post->post_status,
183 - 'link' => $this->get_link( $this->post ),
184 - ];
185 -
186 - $custom_meta = get_post_meta( $this->post->ID );
187 -
188 - foreach ( $this->get_fillable() as $key => $value ) {
189 - if ( isset( $this->attributes[ $key ] ) && ! empty( $this->attributes[ $key ] ) ) {
190 - continue;
191 - }
192 - $this->attributes[ $key ] = get_post_meta( $this->post->ID, $key, true );
193 - }
194 - $this->original = $this->attributes;
195 - }
196 -
197 - /**
198 - * Get the permalink for the post
199 - *
200 - * @param $post
201 - *
202 - * @return string
203 - */
204 - public function get_link( $post ) {
205 - return $post->post_type && get_post_type_object( $this->post->post_type ) ?
206 - get_post_permalink( $this->post->ID ) : '';
207 - }
208 -
209 - /**
210 - * Get attributes
211 - *
212 - * @param string $key Property name
213 - *
214 - * @return mixed Meta data from any model
215 - * @throws Exception
216 - */
217 - public function __get( $key ) {
218 - if ( isset( $this->get_fillable()[ $key ] ) ) {
219 - return $this->attributes[ $key ] ?? $this->get_fillable()[ $key ];
220 - }
221 -
222 - // translators: %s is the name of the undefined property.
223 - throw new Exception( sprintf( esc_html__( 'Undefined property %s', 'booktics' ), esc_html( $key ) ) );
224 -
225 - }
226 -
227 - /**
228 - * Set attributes
229 - *
230 - * @param string $key Model meta property
231 - * @param mixed $value Post meta data
232 - *
233 - * @return void Set meta data
234 - * @throws Exception
235 - */
236 - public function __set( $key, $value ) {
237 -
238 - if ( ! isset( $this->get_fillable()[ $key ] ) && ! in_array( $key, [ 'link' ] ) ) {
239 -
240 - // translators: %s is the name of the undefined property.
241 - throw new Exception( sprintf( esc_html__( 'Undefined property %s', 'booktics' ), esc_html( $key ) ) );
242 - }
243 -
244 - $this->attributes[ $key ] = $value;
245 - }
246 -
247 - /**
248 - * Fillable getter
249 - *
250 - * @return array
251 - */
252 - protected function get_fillable() {
253 - $this->fillable = apply_filters( 'booktics_post_model_fillable', $this->fillable, $this->get_post_type() );
254 -
255 - return $this->fillable;
256 - }
257 -
258 - /**
259 - * Find any model by using id
260 - *
261 - * @param integer $id Post id
262 - *
263 - * @return null | PostModel
264 - */
265 - public static function find( $id ) {
266 - $post = get_post( $id );
267 - if ( ! $post ) {
268 - return null;
269 - }
270 -
271 - $instance = new static( $post );
272 -
273 - return $instance;
274 - }
275 -
276 - /**
277 - * Get all posts
278 - *
279 - * @param array $args Post arguments
280 - *
281 - * @return array collection of PostModel
282 - */
283 - public static function all( $args = [] ) {
284 - $args = array_merge( [
285 - 'post_type' => ( new static )->get_post_type(),
286 - 'post_status' => static::$accepted_statuses,
287 - 'numberposts' => - 1
288 - ], $args );
289 -
290 - $posts = get_posts( $args );
291 -
292 - return array_map( fn( $p ) => new static( $p ), $posts );
293 - }
294 -
295 - /**
296 - * Pagenate all collections
297 - *
298 - * @param integer $per_page Number of items perpage
299 - * @param integer $page Current page number
300 - * @param array $args other args
301 - *
302 - * @return array collection of records
303 - */
304 - public static function paginate( $per_page = 10, $page = 1, $args = [] ) {
305 - $args = array_merge( [
306 - 'post_type' => ( new static )->get_post_type(),
307 - 'post_status' => static::$accepted_statuses,
308 - 'posts_per_page' => $per_page,
309 - 'paged' => $page,
310 - ], $args );
311 -
312 - $query = new WP_Query( $args );
313 -
314 - return [
315 - 'items' => array_map( function ( $post ) {
316 - return new static( $post );
317 - }, $query->posts ),
318 - 'total' => $query->found_posts,
319 - 'per_page' => $per_page,
320 - 'current_page' => $page,
321 - 'last_page' => ceil( $query->found_posts / $per_page ),
322 - ];
323 - }
324 -
325 -
326 - /**
327 - * Filter post by using condition
328 - *
329 - * @param string $meta_key Meta key for the post model
330 - * @param mixed $meta_value Post meta value
331 - *
332 - * @return array collection of post model
333 - */
334 - public static function where( $meta_key, $meta_value ) {
335 - $args = [
336 - 'post_type' => ( new static )->get_post_type(),
337 - 'post_status' => static::$accepted_statuses,
338 - 'meta_query' => [
339 - [
340 - 'key' => $meta_key,
341 - 'value' => $meta_value,
342 - ]
343 - ]
344 - ];
345 -
346 - $posts = get_posts( $args );
347 -
348 - return array_map( function ( $p ) {
349 - return new static( $p );
350 - }, $posts );
351 - }
352 -
353 - /**
354 - * Create a single row
355 - *
356 - * @param array $attributes collection of attributes
357 - *
358 - * @return null | Post_Model | WP_Error
359 - */
360 - public static function create( array $attributes ) {
361 - $validation = self::validate_attributes( $attributes );
362 - if ( is_wp_error( $validation ) ) {
363 - return $validation;
364 - }
365 -
366 - $post_data = [
367 - 'post_type' => ( new static )->get_post_type(),
368 - 'post_status' => $attributes['status'] ?? 'deactive',
369 - 'post_title' => $attributes['title'] ?? '',
370 - 'post_content' => $attributes['content'] ?? $attributes['description'] ?? '',
371 - ];
372 -
373 - $post_id = wp_insert_post( $post_data );
374 -
375 - if ( is_wp_error( $post_id ) ) {
376 - return null;
377 - }
378 -
379 - $instance = new static( $post_id );
380 -
381 - if ( ! is_array( $attributes ) ) {
382 - return $instance;
383 - }
384 -
385 - foreach ( $attributes as $key => $value ) {
386 - if ( ! in_array( $key, [ 'title', 'content', 'status' ] ) ) {
387 -
388 - update_post_meta( $post_id, $key, $value );
389 -
390 - $instance->$key = $value;
391 - }
392 - }
393 - $instance->link = $instance->get_link( $instance->post );
394 -
395 - return $instance;
396 - }
397 -
398 - /**
399 - * Update a certain record
400 - *
401 - * @param array $attributes Collection of attributes
402 - *
403 - * @return WP_Error
404 - */
405 - public function update( array $attributes ) {
406 - if ( ! $this->post ) {
407 - return new WP_Error( 'post_not_found', esc_html__( 'Cannot update a non-existent post.', 'booktics' ) );
408 - }
409 -
410 - $validation = self::validate_attributes( $attributes );
411 - if ( is_wp_error( $validation ) ) {
412 - return $validation;
413 - }
414 -
415 - $post_data = [ 'ID' => $this->post->ID ];
416 -
417 - if ( isset( $attributes['title'] ) ) {
418 - $this->title = $attributes['title'];
419 - $post_data['post_title'] = $attributes['title'];
420 - }
421 -
422 - if ( isset( $attributes['content'] ) ) {
423 - $this->content = $attributes['content'];
424 - $post_data['post_content'] = $attributes['content'];
425 - }
426 -
427 - if ( isset( $attributes['status'] ) ) {
428 - $this->status = $attributes['status'];
429 - $post_data['post_status'] = $attributes['status'];
430 - }
431 -
432 - wp_update_post( $post_data );
433 -
434 - // Update custom meta fields
435 - foreach ( $attributes as $key => $value ) {
436 - if ( ! in_array( $key, [ 'title', 'content', 'status' ] ) ) {
437 - update_post_meta( $this->post->ID, $key, $value );
438 - $this->attributes[ $key ] = $value;
439 - }
440 - }
441 -
442 - return $this;
443 - }
444 -
445 - /**
446 - * Save a single record
447 - *
448 - * @return Post_Model instance of the PostModel
449 - * @throws Exception
450 - */
451 - public function save() {
452 -
453 - if ( ! $this->post ) {
454 - return static::create( $this->attributes );
455 - }
456 -
457 - $post_data = [
458 - 'ID' => $this->post->ID,
459 - 'post_title' => $this->title,
460 - 'post_content' => $this->content,
461 - ];
462 -
463 - wp_update_post( $post_data );
464 -
465 - foreach ( $this->attributes as $key => $value ) {
466 - if ( ! in_array( $key, [
467 - 'ID',
468 - 'title',
469 - 'content'
470 - ] ) && $value !== ( $this->original[ $key ] ?? null ) ) {
471 - update_post_meta( $this->post->ID, $key, $value );
472 - }
473 - }
474 -
475 - return $this;
476 - }
477 -
478 - /**
479 - * Delete a record
480 - *
481 - * @return WP_Post|false|null Post data on success, false or null on failure.
482 - */
483 - public function delete() {
484 - if ( $this->post ) {
485 - return wp_delete_post( $this->post->ID, true );
486 - }
487 -
488 - return false;
489 - }
490 -
491 - /**
492 - * Query records from posts
493 - *
494 - * @return Object
495 - */
496 - public static function query() {
497 - return new Query_Builder( new static );
498 - }
499 -
500 - /**
501 - * Validate attributes
502 - *
503 - * @param array $attributes Post data
504 - *
505 - * @return WP_Error|true
506 - */
507 - protected static function validate_attributes( $attributes = [] ) {
508 - if ( ! is_array( $attributes ) ) {
509 - return new WP_Error( 'invalid_attributes', esc_html__( 'Attributes must be an array.', 'booktics' ) );
510 - }
511 -
512 - foreach ( $attributes as $key => $value ) {
513 - if ( ! isset( ( new static )->get_fillable()[ $key ] ) && ! in_array( $key, [
514 - 'ID',
515 - 'team_member_details',
516 - 'category_details',
517 - 'shortcode',
518 - 'service_details',
519 - 'duration_unit',
520 - ] ) ) {
521 - // translators: %s: Property name
522 - return new WP_Error( 'invalid_property', sprintf( esc_html__( 'Invalid property: %s', 'booktics' ), $key ) );
523 - }
524 - }
525 -
526 - return true;
527 - }
528 -
529 - /**
530 - * Convert the model instance to an array.
531 - *
532 - * @return array
533 - */
534 - public function to_array(): array {
535 - $data = [];
536 -
537 - foreach ( $this->get_fillable() as $key => $default ) {
538 - if ( $key === 'user_email' ) {
539 - $data['email'] = $this->$key;
540 - continue;
541 - }
542 -
543 - $value = $this->$key;
544 -
545 - // Unserialize if necessary
546 - if ( is_string( $value ) && $this->is_serialized( $value ) ) {
547 - $value = maybe_unserialize( $value );
548 - }
549 -
550 - // Normalize value to default type
551 - if ( gettype( $value ) !== gettype( $default ) ) {
552 - if ( gettype( $default ) === 'array' && is_string( $value ) && $this->is_serialized( $value ) ) {
553 - $value = maybe_unserialize( $value );
554 - } elseif ( gettype( $default ) === 'array' ) {
555 - $value = $default; // Set to default empty array if the value doesn't match
556 - }
557 - }
558 -
559 - // Cast specific keys to objects or arrays if needed
560 - if ( in_array( $key, [
561 - 'team_member',
562 - 'holiday',
563 - 'categories',
564 - 'additional_durations'
565 - ] ) ) {
566 - $value = is_array( $value ) ? $value : [];
567 - }
568 -
569 - if ( in_array( $key, [ 'schedule', 'custom_field' ] ) ) {
570 - $value = empty( $value ) ? (object) [] : $value;
571 - }
572 -
573 - // Cast specific keys to boolean
574 - if ( in_array( $key, [
575 - 'enable_custom_schedule',
576 - 'enable_custom_holiday',
577 - ] ) ) {
578 - $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
579 - }
580 -
581 - $data[ $key ] = $value ?? $default;
582 - }
583 -
584 - return $data;
585 - }
586 -
587 -
588 - /**
589 - * Specify data which should be serialized to JSON.
590 - *
591 - * @return array
592 - */
593 - public function jsonSerialize(): array {
594 - return $this->to_array();
595 - }
596 -
597 - /**
598 - * Check if a string is serialized
599 - *
600 - * @param string $data The data to check.
601 - *
602 - * @return bool True if the data is serialized, false otherwise.
603 - */
604 - protected function is_serialized( $data ) {
605 - // If it isn't a string, return false
606 - if ( ! is_string( $data ) ) {
607 - return false;
608 - }
609 -
610 - // Check if it's a serialized string (Array or Object)
611 - $data = trim( $data );
612 -
613 - if ( 'N;' === $data || ! preg_match( '/^([adObis]):/', $data ) ) {
614 - return false;
615 - }
616 -
617 - $result = @unserialize( $data );
618 -
619 - return $result !== false || $data === 'b:0;'; // Return false for failed unserialization and true for successful
620 - }
621 -
622 -}
623 -
1 +<?php
2 +
3 +namespace Booktics\Abstracts;
4 +
5 +use Booktics\Query_Builders\Query_Builder;
6 +use Exception;
7 +use JsonSerializable;
8 +use WP_Error;
9 +use WP_Post;
10 +use WP_Query;
11 +
12 +/**
13 + * PostModel class
14 + */
15 +abstract class Post_Model implements JsonSerializable {
16 + /**
17 + * List of accepted post statuses
18 + *
19 + * @var array
20 + */
21 + public static $accepted_statuses = array(
22 + 'active',
23 + 'deactive',
24 + 'draft',
25 + 'publish',
26 + 'cancelled',
27 + 'pending',
28 + );
29 +
30 + /**
31 + * Store post
32 + *
33 + * @var Object
34 + */
35 + protected $post;
36 +
37 + /**
38 + * Store meta property
39 + *
40 + * @var array
41 + */
42 + protected $fillable = array();
43 +
44 + /**
45 + * Store post attributes
46 + *
47 + * @var array
48 + */
49 + protected $attributes = array();
50 +
51 + /**
52 + * Store origianl data
53 + *
54 + * @var array
55 + */
56 + protected $original = array();
57 +
58 + /**
59 + * Store post type
60 + *
61 + * @var string
62 + */
63 + protected $post_type;
64 +
65 + /**
66 + * Constructor for post model class
67 + *
68 + * @param Object $post Post model
69 + *
70 + * @return void
71 + */
72 + public function __construct( $post = null ) {
73 + if ( $post instanceof WP_Post ) {
74 + $this->post = $post;
75 + } elseif ( is_numeric( $post ) ) {
76 + $this->post = get_post( $post );
77 + }
78 +
79 + if ( $this->post && $this->post->post_type !== $this->get_post_type() ) {
80 + $this->post = null;
81 + }
82 +
83 + if ( $this->post ) {
84 + $this->load_attributes();
85 + }
86 + }
87 +
88 + /**
89 + * Get post type
90 + *
91 + * @return string Get post type name
92 + */
93 + abstract protected function get_post_type();
94 +
95 + /**
96 + * Load attributes
97 + *
98 + * @return void Set all necessary attributes
99 + */
100 + protected function load_attributes() {
101 + $this->attributes = array(
102 + 'id' => $this->post->ID,
103 + 'title' => $this->post->post_title,
104 + 'content' => $this->post->post_content,
105 + 'status' => $this->post->post_status,
106 + 'link' => $this->get_link( $this->post ),
107 + );
108 +
109 + $custom_meta = get_post_meta( $this->post->ID );
110 +
111 + foreach ( $this->get_fillable() as $key => $value ) {
112 + if ( isset( $this->attributes[ $key ] ) && ! empty( $this->attributes[ $key ] ) ) {
113 + continue;
114 + }
115 + $this->attributes[ $key ] = get_post_meta( $this->post->ID, $key, true );
116 + }
117 + $this->original = $this->attributes;
118 + }
119 +
120 + /**
121 + * Get the permalink for the post
122 + *
123 + * @param $post
124 + *
125 + * @return string
126 + */
127 + public function get_link( $post ) {
128 + return $post->post_type && get_post_type_object( $this->post->post_type ) ?
129 + get_post_permalink( $this->post->ID ) : '';
130 + }
131 +
132 + /**
133 + * Get attributes
134 + *
135 + * @param string $key Property name
136 + *
137 + * @return mixed Meta data from any model
138 + * @throws Exception
139 + */
140 + public function __get( $key ) {
141 + if ( isset( $this->get_fillable()[ $key ] ) ) {
142 + return $this->attributes[ $key ] ?? $this->get_fillable()[ $key ];
143 + }
144 +
145 + // translators: %s is the name of the undefined property.
146 + throw new Exception( sprintf( esc_html__( 'Undefined property %s', 'booktics' ), esc_html( $key ) ) );
147 + }
148 +
149 + /**
150 + * Set attributes
151 + *
152 + * @param string $key Model meta property
153 + * @param mixed $value Post meta data
154 + *
155 + * @return void Set meta data
156 + * @throws Exception
157 + */
158 + public function __set( $key, $value ) {
159 +
160 + if ( ! isset( $this->get_fillable()[ $key ] ) && ! in_array( $key, array( 'link', 'customer' ) ) ) {
161 +
162 + // translators: %s is the name of the undefined property.
163 + throw new Exception( sprintf( esc_html__( 'Undefined property %s', 'booktics' ), esc_html( $key ) ) );
164 + }
165 +
166 + $this->attributes[ $key ] = $value;
167 + }
168 +
169 + /**
170 + * Fillable getter
171 + *
172 + * @return array
173 + */
174 + protected function get_fillable() {
175 + $this->fillable = apply_filters( 'booktics_post_model_fillable', $this->fillable, $this->get_post_type() );
176 +
177 + return $this->fillable;
178 + }
179 +
180 + /**
181 + * Find any model by using id
182 + *
183 + * @param integer $id Post id
184 + *
185 + * @return null | PostModel
186 + */
187 + public static function find( $id ) {
188 + $post = get_post( $id );
189 + if ( ! $post ) {
190 + return null;
191 + }
192 +
193 + $instance = new static( $post );
194 +
195 + return $instance;
196 + }
197 +
198 + /**
199 + * Get all posts
200 + *
201 + * @param array $args Post arguments
202 + *
203 + * @return array collection of PostModel
204 + */
205 + public static function all( $args = array() ) {
206 + $args = array_merge(
207 + array(
208 + 'post_type' => ( new static() )->get_post_type(),
209 + 'post_status' => static::$accepted_statuses,
210 + 'numberposts' => - 1,
211 + ), $args
212 + );
213 +
214 + $posts = get_posts( $args );
215 +
216 + return array_map( fn( $p ) => new static( $p ), $posts );
217 + }
218 +
219 + /**
220 + * Pagenate all collections
221 + *
222 + * @param integer $per_page Number of items perpage
223 + * @param integer $page Current page number
224 + * @param array $args other args
225 + *
226 + * @return array collection of records
227 + */
228 + public static function paginate( $per_page = 10, $page = 1, $args = array() ) {
229 + $args = array_merge(
230 + array(
231 + 'post_type' => ( new static() )->get_post_type(),
232 + 'post_status' => static::$accepted_statuses,
233 + 'posts_per_page' => $per_page,
234 + 'paged' => $page,
235 + ), $args
236 + );
237 +
238 + $query = new WP_Query( $args );
239 +
240 + return array(
241 + 'items' => array_map(
242 + function ( $post ) {
243 + return new static( $post );
244 + }, $query->posts
245 + ),
246 + 'total' => $query->found_posts,
247 + 'per_page' => $per_page,
248 + 'current_page' => $page,
249 + 'last_page' => ceil( $query->found_posts / $per_page ),
250 + );
251 + }
252 +
253 +
254 + /**
255 + * Filter post by using condition
256 + *
257 + * @param string $meta_key Meta key for the post model
258 + * @param mixed $meta_value Post meta value
259 + *
260 + * @return array collection of post model
261 + */
262 + public static function where( $meta_key, $meta_value ) {
263 + $args = array(
264 + 'post_type' => ( new static() )->get_post_type(),
265 + 'post_status' => static::$accepted_statuses,
266 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering posts by meta fields
267 + 'meta_query' => array(
268 + array(
269 + 'key' => $meta_key,
270 + 'value' => $meta_value,
271 + ),
272 + ),
273 + );
274 +
275 + $posts = get_posts( $args );
276 +
277 + return array_map(
278 + function ( $p ) {
279 + return new static( $p );
280 + }, $posts
281 + );
282 + }
283 +
284 + /**
285 + * Create a single row
286 + *
287 + * @param array $attributes collection of attributes
288 + *
289 + * @return null | Post_Model | WP_Error
290 + */
291 + public static function create( array $attributes ) {
292 + $validation = self::validate_attributes( $attributes );
293 + if ( is_wp_error( $validation ) ) {
294 + return $validation;
295 + }
296 +
297 + $post_data = array(
298 + 'post_type' => ( new static() )->get_post_type(),
299 + 'post_status' => $attributes['status'] ?? 'deactive',
300 + 'post_title' => $attributes['title'] ?? '',
301 + 'post_content' => $attributes['content'] ?? $attributes['description'] ?? '',
302 + );
303 +
304 + $post_id = wp_insert_post( $post_data );
305 +
306 + if ( is_wp_error( $post_id ) ) {
307 + return null;
308 + }
309 +
310 + $instance = new static( $post_id );
311 +
312 + if ( ! is_array( $attributes ) ) {
313 + return $instance;
314 + }
315 +
316 + foreach ( $attributes as $key => $value ) {
317 + if ( ! in_array( $key, array( 'title', 'content', 'status' ) ) ) {
318 + update_post_meta( $post_id, $key, $value );
319 +
320 + $instance->$key = $value;
321 + }
322 + }
323 + $instance->link = $instance->get_link( $instance->post );
324 +
325 + return $instance;
326 + }
327 +
328 + /**
329 + * Update a certain record
330 + *
331 + * @param array $attributes Collection of attributes
332 + *
333 + * @return WP_Error|Post_Model
334 + */
335 + public function update( array $attributes ) {
336 + if ( ! $this->post ) {
337 + return new WP_Error( 'post_not_found', esc_html__( 'Cannot update a non-existent post.', 'booktics' ) );
338 + }
339 +
340 + $validation = self::validate_attributes( $attributes );
341 + if ( is_wp_error( $validation ) ) {
342 + return $validation;
343 + }
344 +
345 + $post_data = array( 'ID' => $this->post->ID );
346 +
347 + if ( isset( $attributes['title'] ) ) {
348 + $this->title = $attributes['title'];
349 + $post_data['post_title'] = $attributes['title'];
350 + }
351 +
352 + if ( isset( $attributes['content'] ) ) {
353 + $this->content = $attributes['content'];
354 + $post_data['post_content'] = $attributes['content'];
355 + }
356 +
357 + if ( isset( $attributes['status'] ) ) {
358 + $this->status = $attributes['status'];
359 + $post_data['post_status'] = $attributes['status'];
360 + }
361 +
362 + // Handle permalink update when 'link' attribute is provided
363 + if ( isset( $attributes['link'] ) ) {
364 + $new_slug = $this->extract_slug_from_link( $attributes['link'] );
365 + if ( $new_slug ) {
366 + $post_data['post_name'] = sanitize_title( $new_slug );
367 + }
368 + }
369 +
370 + wp_update_post( $post_data );
371 +
372 + // Update custom meta fields
373 + foreach ( $attributes as $key => $value ) {
374 + if ( ! in_array( $key, array( 'title', 'content', 'status', 'link' ) ) ) {
375 + update_post_meta( $this->post->ID, $key, $value );
376 + $this->attributes[ $key ] = $value;
377 + }
378 + }
379 +
380 + // Refresh the post object and regenerate the link if permalink was updated
381 + if ( isset( $attributes['link'] ) ) {
382 + $this->post = get_post( $this->post->ID );
383 + $this->attributes['link'] = $this->get_link( $this->post );
384 + }
385 +
386 + return $this;
387 + }
388 +
389 + /**
390 + * Extract slug from a full permalink URL
391 + *
392 + * @param string $link Full permalink URL
393 + * @return string|null Extracted slug or null if invalid
394 + */
395 + private function extract_slug_from_link( $link ) {
396 + // Remove trailing slash
397 + $link = rtrim( $link, '/' );
398 +
399 + // Extract the last part of the URL as the slug
400 + $slug = basename( $link );
401 +
402 + // Basic validation - ensure it's not empty and contains valid characters
403 + if ( empty( $slug ) || $slug === '.' || $slug === '..' ) {
404 + return null;
405 + }
406 +
407 + return $slug;
408 + }
409 +
410 +
411 + /**
412 + * Save a single record
413 + *
414 + * @return Post_Model instance of the PostModel
415 + * @throws Exception
416 + */
417 + public function save() {
418 +
419 + if ( ! $this->post ) {
420 + return static::create( $this->attributes );
421 + }
422 +
423 + $post_data = array(
424 + 'ID' => $this->post->ID,
425 + 'post_title' => $this->title,
426 + 'post_content' => $this->content,
427 + );
428 +
429 + wp_update_post( $post_data );
430 +
431 + foreach ( $this->attributes as $key => $value ) {
432 + if ( ! in_array(
433 + $key, array(
434 + 'ID',
435 + 'title',
436 + 'content',
437 + )
438 + ) && $value !== ( $this->original[ $key ] ?? null ) ) {
439 + update_post_meta( $this->post->ID, $key, $value );
440 + }
441 + }
442 +
443 + return $this;
444 + }
445 +
446 + /**
447 + * Delete a record
448 + *
449 + * @return WP_Post|false|null Post data on success, false or null on failure.
450 + */
451 + public function delete() {
452 + if ( $this->post ) {
453 + return wp_delete_post( $this->post->ID, true );
454 + }
455 +
456 + return false;
457 + }
458 +
459 + /**
460 + * Query records from posts
461 + *
462 + * @return Object
463 + */
464 + public static function query() {
465 + return new Query_Builder( new static() );
466 + }
467 +
468 + /**
469 + * Validate attributes
470 + *
471 + * @param array $attributes Post data
472 + *
473 + * @return WP_Error|true
474 + */
475 + protected static function validate_attributes( $attributes = array() ) {
476 + if ( ! is_array( $attributes ) ) {
477 + return new WP_Error( 'invalid_attributes', esc_html__( 'Attributes must be an array.', 'booktics' ) );
478 + }
479 +
480 + foreach ( $attributes as $key => $value ) {
481 + if ( ! isset( ( new static() )->get_fillable()[ $key ] ) && ! in_array(
482 + $key, array(
483 + 'ID',
484 + 'team_member_details',
485 + 'category_details',
486 + 'shortcode',
487 + 'service_details',
488 + 'duration_unit',
489 + 'customer',
490 + 'service',
491 + 'team_member',
492 + )
493 + ) ) {
494 + // translators: %s: Property name
495 + return new WP_Error( 'invalid_property', sprintf( esc_html__( 'Invalid property: %s', 'booktics' ), $key ) );
496 + }
497 + }
498 +
499 + return true;
500 + }
501 +
502 + /**
503 + * Convert the model instance to an array.
504 + *
505 + * @return array
506 + */
507 + public function to_array(): array {
508 + $data = array();
509 +
510 + foreach ( $this->get_fillable() as $key => $default ) {
511 + if ( $key === 'user_email' ) {
512 + $data['email'] = $this->$key;
513 + continue;
514 + }
515 +
516 + $value = $this->$key;
517 +
518 + // Unserialize if necessary (object instantiation disallowed — see helper).
519 + if ( is_string( $value ) && $this->is_serialized( $value ) ) {
520 + $value = \booktics_safe_maybe_unserialize( $value );
521 + }
522 +
523 + // Normalize value to default type
524 + if ( gettype( $value ) !== gettype( $default ) ) {
525 + if ( gettype( $default ) === 'array' && is_string( $value ) && $this->is_serialized( $value ) ) {
526 + $value = \booktics_safe_maybe_unserialize( $value );
527 + } elseif ( gettype( $default ) === 'array' ) {
528 + $value = $default; // Set to default empty array if the value doesn't match
529 + }
530 + }
531 +
532 + // Cast specific keys to objects or arrays if needed
533 + if ( in_array(
534 + $key, array(
535 + 'team_member',
536 + 'holiday',
537 + 'categories',
538 + 'additional_durations',
539 + )
540 + ) ) {
541 + $value = is_array( $value ) ? $value : array();
542 + }
543 +
544 + if ( in_array( $key, array( 'schedule', 'custom_field' ) ) ) {
545 + $value = empty( $value ) ? (object) array() : $value;
546 + }
547 +
548 + // Cast specific keys to boolean
549 + if ( in_array(
550 + $key, array(
551 + 'enable_custom_schedule',
552 + 'enable_custom_holiday',
553 + )
554 + ) ) {
555 + $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
556 + }
557 +
558 + $data[ $key ] = $value ?? $default;
559 + }
560 +
561 + return $data;
562 + }
563 +
564 +
565 + /**
566 + * Specify data which should be serialized to JSON.
567 + *
568 + * @return array
569 + */
570 + public function jsonSerialize(): array {
571 + return $this->to_array();
572 + }
573 +
574 + /**
575 + * Check if a string is serialized
576 + *
577 + * @param string $data The data to check.
578 + *
579 + * @return bool True if the data is serialized, false otherwise.
580 + */
581 + protected function is_serialized( $data ) {
582 + // Use WordPress core's purely structural check. Unlike the previous
583 + // implementation, \is_serialized() never calls unserialize(), so testing
584 + // attacker-controlled meta can no longer trigger PHP Object Injection.
585 + return is_string( $data ) && \is_serialized( $data );
586 + }
587 +}