PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.11
Booktics – Appointment Booking Calendar for Service Businesses v1.0.11
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
booktics / base / abstracts / post-model.php

post-model.php in Booktics – Appointment Booking Calendar for Service Businesses 1.0.11, at base/abstracts/post-model.php

598 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 'meta_query' => array(
267 array(
268 'key' => $meta_key,
269 'value' => $meta_value,
270 ),
271 ),
272 );
273
274 $posts = get_posts( $args );
275
276 return array_map(
277 function ( $p ) {
278 return new static( $p );
279 }, $posts
280 );
281 }
282
283 /**
284 * Create a single row
285 *
286 * @param array $attributes collection of attributes
287 *
288 * @return null | Post_Model | WP_Error
289 */
290 public static function create( array $attributes ) {
291 $validation = self::validate_attributes( $attributes );
292 if ( is_wp_error( $validation ) ) {
293 return $validation;
294 }
295
296 $post_data = array(
297 'post_type' => ( new static() )->get_post_type(),
298 'post_status' => $attributes['status'] ?? 'deactive',
299 'post_title' => $attributes['title'] ?? '',
300 'post_content' => $attributes['content'] ?? $attributes['description'] ?? '',
301 );
302
303 $post_id = wp_insert_post( $post_data );
304
305 if ( is_wp_error( $post_id ) ) {
306 return null;
307 }
308
309 $instance = new static( $post_id );
310
311 if ( ! is_array( $attributes ) ) {
312 return $instance;
313 }
314
315 foreach ( $attributes as $key => $value ) {
316 if ( ! in_array( $key, array( 'title', 'content', 'status' ) ) ) {
317 update_post_meta( $post_id, $key, $value );
318
319 $instance->$key = $value;
320 }
321 }
322 $instance->link = $instance->get_link( $instance->post );
323
324 return $instance;
325 }
326
327 /**
328 * Update a certain record
329 *
330 * @param array $attributes Collection of attributes
331 *
332 * @return WP_Error|Post_Model
333 */
334 public function update( array $attributes ) {
335 if ( ! $this->post ) {
336 return new WP_Error( 'post_not_found', esc_html__( 'Cannot update a non-existent post.', 'booktics' ) );
337 }
338
339 $validation = self::validate_attributes( $attributes );
340 if ( is_wp_error( $validation ) ) {
341 return $validation;
342 }
343
344 $post_data = array( 'ID' => $this->post->ID );
345
346 if ( isset( $attributes['title'] ) ) {
347 $this->title = $attributes['title'];
348 $post_data['post_title'] = $attributes['title'];
349 }
350
351 if ( isset( $attributes['content'] ) ) {
352 $this->content = $attributes['content'];
353 $post_data['post_content'] = $attributes['content'];
354 }
355
356 if ( isset( $attributes['status'] ) ) {
357 $this->status = $attributes['status'];
358 $post_data['post_status'] = $attributes['status'];
359 }
360
361 // Handle permalink update when 'link' attribute is provided
362 if ( isset( $attributes['link'] ) ) {
363 $new_slug = $this->extract_slug_from_link( $attributes['link'] );
364 if ( $new_slug ) {
365 $post_data['post_name'] = sanitize_title( $new_slug );
366 }
367 }
368
369 wp_update_post( $post_data );
370
371 // Update custom meta fields
372 foreach ( $attributes as $key => $value ) {
373 if ( ! in_array( $key, array( 'title', 'content', 'status', 'link' ) ) ) {
374 update_post_meta( $this->post->ID, $key, $value );
375 $this->attributes[ $key ] = $value;
376 }
377 }
378
379 // Refresh the post object and regenerate the link if permalink was updated
380 if ( isset( $attributes['link'] ) ) {
381 $this->post = get_post( $this->post->ID );
382 $this->attributes['link'] = $this->get_link( $this->post );
383 }
384
385 return $this;
386 }
387
388 /**
389 * Extract slug from a full permalink URL
390 *
391 * @param string $link Full permalink URL
392 * @return string|null Extracted slug or null if invalid
393 */
394 private function extract_slug_from_link( $link ) {
395 // Remove trailing slash
396 $link = rtrim( $link, '/' );
397
398 // Extract the last part of the URL as the slug
399 $slug = basename( $link );
400
401 // Basic validation - ensure it's not empty and contains valid characters
402 if ( empty( $slug ) || $slug === '.' || $slug === '..' ) {
403 return null;
404 }
405
406 return $slug;
407 }
408
409
410 /**
411 * Save a single record
412 *
413 * @return Post_Model instance of the PostModel
414 * @throws Exception
415 */
416 public function save() {
417
418 if ( ! $this->post ) {
419 return static::create( $this->attributes );
420 }
421
422 $post_data = array(
423 'ID' => $this->post->ID,
424 'post_title' => $this->title,
425 'post_content' => $this->content,
426 );
427
428 wp_update_post( $post_data );
429
430 foreach ( $this->attributes as $key => $value ) {
431 if ( ! in_array(
432 $key, array(
433 'ID',
434 'title',
435 'content',
436 )
437 ) && $value !== ( $this->original[ $key ] ?? null ) ) {
438 update_post_meta( $this->post->ID, $key, $value );
439 }
440 }
441
442 return $this;
443 }
444
445 /**
446 * Delete a record
447 *
448 * @return WP_Post|false|null Post data on success, false or null on failure.
449 */
450 public function delete() {
451 if ( $this->post ) {
452 return wp_delete_post( $this->post->ID, true );
453 }
454
455 return false;
456 }
457
458 /**
459 * Query records from posts
460 *
461 * @return Object
462 */
463 public static function query() {
464 return new Query_Builder( new static() );
465 }
466
467 /**
468 * Validate attributes
469 *
470 * @param array $attributes Post data
471 *
472 * @return WP_Error|true
473 */
474 protected static function validate_attributes( $attributes = array() ) {
475 if ( ! is_array( $attributes ) ) {
476 return new WP_Error( 'invalid_attributes', esc_html__( 'Attributes must be an array.', 'booktics' ) );
477 }
478
479 foreach ( $attributes as $key => $value ) {
480 if ( ! isset( ( new static() )->get_fillable()[ $key ] ) && ! in_array(
481 $key, array(
482 'ID',
483 'team_member_details',
484 'category_details',
485 'shortcode',
486 'service_details',
487 'duration_unit',
488 'customer',
489 'service',
490 'team_member',
491 )
492 ) ) {
493 // translators: %s: Property name
494 return new WP_Error( 'invalid_property', sprintf( esc_html__( 'Invalid property: %s', 'booktics' ), $key ) );
495 }
496 }
497
498 return true;
499 }
500
501 /**
502 * Convert the model instance to an array.
503 *
504 * @return array
505 */
506 public function to_array(): array {
507 $data = array();
508
509 foreach ( $this->get_fillable() as $key => $default ) {
510 if ( $key === 'user_email' ) {
511 $data['email'] = $this->$key;
512 continue;
513 }
514
515 $value = $this->$key;
516
517 // Unserialize if necessary
518 if ( is_string( $value ) && $this->is_serialized( $value ) ) {
519 $value = maybe_unserialize( $value );
520 }
521
522 // Normalize value to default type
523 if ( gettype( $value ) !== gettype( $default ) ) {
524 if ( gettype( $default ) === 'array' && is_string( $value ) && $this->is_serialized( $value ) ) {
525 $value = maybe_unserialize( $value );
526 } elseif ( gettype( $default ) === 'array' ) {
527 $value = $default; // Set to default empty array if the value doesn't match
528 }
529 }
530
531 // Cast specific keys to objects or arrays if needed
532 if ( in_array(
533 $key, array(
534 'team_member',
535 'holiday',
536 'categories',
537 'additional_durations',
538 )
539 ) ) {
540 $value = is_array( $value ) ? $value : array();
541 }
542
543 if ( in_array( $key, array( 'schedule', 'custom_field' ) ) ) {
544 $value = empty( $value ) ? (object) array() : $value;
545 }
546
547 // Cast specific keys to boolean
548 if ( in_array(
549 $key, array(
550 'enable_custom_schedule',
551 'enable_custom_holiday',
552 )
553 ) ) {
554 $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
555 }
556
557 $data[ $key ] = $value ?? $default;
558 }
559
560 return $data;
561 }
562
563
564 /**
565 * Specify data which should be serialized to JSON.
566 *
567 * @return array
568 */
569 public function jsonSerialize(): array {
570 return $this->to_array();
571 }
572
573 /**
574 * Check if a string is serialized
575 *
576 * @param string $data The data to check.
577 *
578 * @return bool True if the data is serialized, false otherwise.
579 */
580 protected function is_serialized( $data ) {
581 // If it isn't a string, return false
582 if ( ! is_string( $data ) ) {
583 return false;
584 }
585
586 // Check if it's a serialized string (Array or Object)
587 $data = trim( $data );
588
589 if ( 'N;' === $data || ! preg_match( '/^([adObis]):/', $data ) ) {
590 return false;
591 }
592
593 $result = @unserialize( $data );
594
595 return $result !== false || $data === 'b:0;'; // Return false for failed unserialization and true for successful
596 }
597 }
598