PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.19
Booktics – Appointment Booking Calendar for Service Businesses v1.0.19
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 1.0.4 1.0.5 1.0.6 1.0.7 All 27 releases
booktics / base / abstracts / post-model.php

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

599 lines 15.8 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 // 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
519 if ( is_string( $value ) && $this->is_serialized( $value ) ) {
520 $value = 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 = 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 // If it isn't a string, return false
583 if ( ! is_string( $data ) ) {
584 return false;
585 }
586
587 // Check if it's a serialized string (Array or Object)
588 $data = trim( $data );
589
590 if ( 'N;' === $data || ! preg_match( '/^([adObis]):/', $data ) ) {
591 return false;
592 }
593
594 $result = @unserialize( $data );
595
596 return $result !== false || $data === 'b:0;'; // Return false for failed unserialization and true for successful
597 }
598 }
599