PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.29
Booktics – Appointment Booking Calendar for Service Businesses v1.0.29
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/user-model.php +621 -600 1.0.1 → 1.0.29 View file →
@@ -1,601 +1,622 @@
1 -<?php
2 -/**
3 - * User Model Abstract for managing all wp_users and wp_usermeta
4 - *
5 - * @package Booktics/Abstracts
6 - */
7 -
8 -namespace Booktics\Abstracts;
9 -
10 -use JsonSerializable;
11 -use WP_User_Query;
12 -
13 -abstract class User_Model implements JsonSerializable {
14 -
15 - /**
16 - * Store specific user role
17 - *
18 - * @var string
19 - */
20 - protected $role = 'administrator';
21 -
22 - /**
23 - * Store specific meta prefix
24 - *
25 - * @var string
26 - */
27 - protected $meta_prefix = '';
28 -
29 - /**
30 - * Store user id
31 - *
32 - * @var integer
33 - */
34 - protected $id;
35 -
36 - /**
37 - * Store user data
38 - *
39 - * @var array
40 - */
41 - protected $fillable = [
42 - 'id' => '',
43 - 'first_name' => '',
44 - 'last_name' => '',
45 - 'user_email' => '',
46 - 'image' => '',
47 - 'user_login' => '',
48 - 'phone' => '',
49 - 'status' => 0,
50 - 'user_registered' => '',
51 - ];
52 -
53 - /**
54 - * Store Filter conditions
55 - *
56 - * @var array
57 - */
58 - protected $filters = [];
59 -
60 - /**
61 - * Store error
62 - *
63 - * @var \WP_Error
64 - */
65 - protected $error;
66 -
67 - /**
68 - * User constructor
69 - *
70 - * @param $user
71 - *
72 - * return void
73 - */
74 - public function __construct( $user = 0 ) {
75 - require_once( ABSPATH . 'wp-admin/includes/user.php' );
76 -
77 - if ( $user instanceof self ) {
78 - $this->set_id( $user->get_id() );
79 - } elseif ( ! empty( $user->ID ) ) {
80 - $this->set_id( $user->ID );
81 - } elseif ( is_numeric( $user ) && $user > 0 ) {
82 - $this->set_id( $user );
83 - }
84 - }
85 -
86 - /**
87 - * Get user id
88 - *
89 - * @return integer
90 - */
91 - public function get_id(): int {
92 - return $this->id;
93 - }
94 -
95 - /**
96 - * Set user id
97 - *
98 - * @param int $id
99 - *
100 - * @return void
101 - */
102 - public function set_id( int $id ) {
103 - $this->id = $id;
104 - }
105 -
106 - /**
107 - * Get user data
108 - *
109 - * @param string $name
110 - *
111 - * @return string|null
112 - */
113 - public function __get( string $name ) {
114 - if ( $name === 'id' ) {
115 - return $this->id;
116 - }
117 - if ( ! isset( $this->fillable[ $name ] ) ) {
118 - return null;
119 - }
120 -
121 - $possible_function_name = 'get_' . booktics_snake_case( $name ) . '_attribute';
122 - $data = $this->get_prop( $name, $this->fillable[ $name ] );
123 - if ( method_exists( $this, $possible_function_name ) ) {
124 - return $this->$possible_function_name( $this );
125 - }
126 -
127 - return $data;
128 - }
129 -
130 - /**
131 - * Get either user data or default data
132 - *
133 - * @param string $prop
134 - * @param string|array $default
135 - *
136 - * @return string|array
137 - */
138 - private function get_prop( $prop = '', $default = '' ) {
139 - return $this->get_metadata( $prop ) ?? $default;
140 - }
141 -
142 - /**
143 - * Get user metadata
144 - *
145 - * @param string $prop
146 - *
147 - * @return mixed
148 - */
149 - private function get_metadata( string $prop = '' ) {
150 - $meta_key = $this->meta_prefix . $prop;
151 -
152 - return get_user_meta( $this->id, $meta_key, true );
153 - }
154 -
155 - /**
156 - * Set user values
157 - *
158 - * @param string $name
159 - * @param string $value
160 - *
161 - * @return void
162 - */
163 - public function __set( string $name, string $value ) {
164 - if ( strpos( $name, $this->meta_prefix ) !== false ) {
165 - $name = str_replace( $this->meta_prefix, '', $name );
166 - }
167 -
168 - $this->fillable[ $name ] = $value;
169 - }
170 -
171 - /**
172 - * Save or update user
173 - *
174 - * @param array $args
175 - *
176 - * @return mixed|string
177 - */
178 - public function save( array $args = [] ) {
179 - if ( isset( $args['email'] ) ) {
180 - $args['user_email'] = $args['email'];
181 - unset( $args['email'] );
182 - }
183 -
184 - $user_id = $args['ID'] ?? '';
185 -
186 - $user_data = [
187 - 'ID' => $user_id,
188 - 'role' => $this->role,
189 - 'password' => wp_generate_password(),
190 - ];
191 -
192 - // Add required user fields
193 - foreach ( $args as $key => $value ) {
194 - if ( ! isset( $this->fillable[ $key ] ) && ! in_array( $key, [
195 - 'email',
196 - 'user_email'
197 - ] ) ) {
198 - continue;
199 - }
200 - $user_data[ $key ] = $value;
201 - }
202 -
203 - $user_id = wp_insert_user( $user_data );
204 - if ( is_wp_error( $user_id ) ) {
205 - return $user_id;
206 - }
207 -
208 - $user = new \WP_User( $user_id );
209 - $user->add_role( $this->role );
210 - $this->save_metadata( $args );
211 -
212 - if ( $user_id ) {
213 - wp_update_user( $user_data );
214 - } else {
215 - $user_id = wp_insert_user( $user_data );
216 - }
217 -
218 - $user = get_userdata( $user_id );
219 - $user->add_role( $this->role );
220 - if ( ! is_wp_error( $user_id ) ) {
221 - $this->set_id( $user_id );
222 - $this->save_metadata( $user_data );
223 - $this->retrieve_password();
224 - }
225 -
226 - return $user_id;
227 - }
228 -
229 - /**
230 - * Update specific field in user or meta data
231 - *
232 - * @param array $data
233 - *
234 - * @return bool
235 - */
236 - public function update( array $data = [] ): bool {
237 - if ( ! $this->id ) {
238 - $this->error = new \WP_Error( 'no_id', __( 'User ID is required to update.', 'booktics' ) );
239 -
240 - return false;
241 - }
242 -
243 - $user_data = [ 'ID' => $this->id ];
244 - $meta_data = [];
245 -
246 - foreach ( $data as $key => $value ) {
247 - if ( in_array( $key, [ 'schedule', 'custom', 'holiday' ] ) ) {
248 - continue;
249 - }
250 - if ( isset( $this->fillable[ $key ] ) && ! strpos( $key, $this->meta_prefix ) ) {
251 - if ( in_array( $key, [ 'user_email', 'user_login' ] ) ) {
252 - $user_data[ $key ] = $value;
253 - }
254 - $meta_data[ $key ] = $value;
255 - }
256 - }
257 - $result = wp_update_user( $user_data );
258 - if ( is_wp_error( $result ) ) {
259 - $this->error = $result;
260 -
261 - return false;
262 - }
263 -
264 - $this->save_metadata( $meta_data );
265 -
266 - return true;
267 - }
268 -
269 - /**
270 - * Store user meta data
271 - *
272 - * @param array $data
273 - *
274 - * @return void
275 - */
276 - public function save_metadata( array $data = [] ) {
277 - foreach ( $data as $key => $value ) {
278 - if ( ! isset( $this->fillable[ $key ] ) ) {
279 - continue;
280 - }
281 - $meta_key = $this->meta_prefix . $key;
282 -
283 - update_user_meta( $this->id, $meta_key, $value );
284 - }
285 - }
286 -
287 - /**
288 - * Send password reset email
289 - *
290 - * @return void
291 - */
292 - public function retrieve_password() {
293 - if ( 0 === intval( $this->status ) ) {
294 - retrieve_password( $this->email );
295 - }
296 - }
297 -
298 - /**
299 - * Delete user
300 - *
301 - * @return boolean
302 - */
303 - public function delete(): bool {
304 - $user = get_userdata( $this->id );
305 -
306 - if ( ! $user ) {
307 - return false;
308 - }
309 -
310 - if ( count( $user->roles ) == 1 && in_array( $this->role, $user->roles ) ) {
311 - return wp_delete_user( $this->id );
312 - }
313 -
314 - $user->remove_role( $this->role );
315 -
316 - return true;
317 - }
318 -
319 - /**
320 - * Append user filter condition
321 - *
322 - * @param string $field
323 - * @param $value
324 - * @param string $compare
325 - *
326 - * @return $this
327 - */
328 - public function where( string $field, $value, string $compare = '=' ): self {
329 - $this->filters[] = [
330 - 'field' => $field,
331 - 'value' => $value,
332 - 'compare' => $compare,
333 - ];
334 -
335 - return $this;
336 - }
337 -
338 - /**
339 - * Add OR conditions in where
340 - *
341 - * @param string $field
342 - * @param $value
343 - * @param string $compare
344 - *
345 - * @return $this
346 - */
347 - public function or_where( string $field, $value, string $compare = '=' ): self {
348 - $this->filters[] = [
349 - 'field' => $field,
350 - 'value' => $value,
351 - 'compare' => $compare,
352 - 'relation' => 'OR',
353 - ];
354 -
355 - return $this;
356 - }
357 -
358 - /**
359 - * Conditionally apply query filters
360 - *
361 - * @param mixed $value
362 - * @param callable $callback
363 - * @param callable|null $default
364 - *
365 - * @return $this
366 - */
367 - public function when( $value, callable $callback, callable $default = null ): self {
368 - if ( $value ) {
369 - $callback( $this, $value );
370 - } elseif ( $default ) {
371 - $default( $this, $value );
372 - }
373 -
374 - return $this;
375 - }
376 -
377 - /**
378 - * Get all users paginated data
379 - *
380 - * @param array $args
381 - *
382 - * @return array
383 - */
384 - public function all( array $args = [] ): array {
385 - $defaults = [
386 - 'role' => $this->role,
387 - 'number' => 20,
388 - 'paged' => 1,
389 - 'orderby' => 'ID',
390 - 'order' => 'DESC',
391 - ];
392 -
393 - $args = wp_parse_args( $args, $defaults );
394 -
395 - if ( ! empty( $this->filters ) ) {
396 - $meta_query = [];
397 - $relation = 'AND';
398 -
399 - foreach ( $this->filters as $filter ) {
400 - if ( in_array( $filter['field'], [
401 - 'user_email',
402 - 'user_login'
403 - ] ) ) {
404 - $args['search'] = '*' . esc_attr( $filter['value'] ) . '*';
405 - $args['search_columns'] = [ $filter['field'] ];
406 - } else {
407 - $meta_query[] = [
408 - 'key' => $this->meta_prefix . $filter['field'],
409 - 'value' => $filter['value'],
410 - 'compare' => $filter['compare'],
411 - ];
412 - }
413 -
414 - if ( isset( $filter['relation'] ) && $filter['relation'] === 'OR' ) {
415 - $relation = 'OR';
416 - }
417 - }
418 - if ( ! empty( $meta_query ) ) {
419 - $args['meta_query'] = [
420 - 'relation' => $relation,
421 - ...$meta_query
422 - ];
423 - }
424 - }
425 -
426 - $users = new WP_User_Query( $args );
427 - $items = [];
428 - foreach ( $users->get_results() as $result ) {
429 - $cloneObj = clone $this;
430 - $userMeta = get_user_meta( $result->ID );
431 -
432 - $items[] = $cloneObj->hydrate( $result->ID, array_merge(
433 - (array) $result->data,
434 - $userMeta
435 - ) )->to_array();
436 - }
437 -
438 - $this->filters = [];
439 -
440 - return [
441 - 'total' => $users->get_total(),
442 - 'per_page' => $args['number'],
443 - 'current_page' => $args['paged'],
444 - 'last_page' => ceil( $users->get_total() / $args['number'] ),
445 - 'items' => $items,
446 - ];
447 - }
448 -
449 - /**
450 - * Find user by id
451 - *
452 - * @param $id
453 - *
454 - * @return $this
455 - */
456 - public function find( $id ) {
457 - $user = get_user_by( 'ID', $id );
458 -
459 - if ( ! $user ) {
460 - return null;
461 - }
462 -
463 - $meta = array_map( function ( $value ) {
464 - return is_array( $value ) && count( $value ) === 1 ? $value[0] : $value;
465 - }, get_user_meta( $user->ID ) );
466 -
467 - $user_data = (array) $user->data;
468 -
469 - return $this->hydrate( $id, array_merge( $user_data, $meta ) );
470 - }
471 -
472 - /**
473 - * Hydrate each result with this class
474 - *
475 - * @param $id
476 - * @param $result
477 - *
478 - * @return $this
479 - */
480 - public function hydrate( $id, $result ): self {
481 - $this->set_id( $id );
482 - if ( ! is_array( $result ) ) {
483 - return $this;
484 - }
485 -
486 - foreach ( $result as $key => $value ) {
487 - $key = str_replace( $this->meta_prefix, '', $key );
488 - if ( ! isset( $this->fillable[ $key ] ) && ! in_array( $key, [
489 - 'user_email',
490 - 'email'
491 - ] ) ) {
492 - continue;
493 - }
494 - if ( is_array( $value ) ) {
495 - $value = implode( ',', $value );
496 - }
497 -
498 - if ( $key === 'user_registered' ) {
499 - $this->fillable['user_registered'] = ! empty( $result['user_registered'] ) ?
500 - gmdate( 'j F, Y', strtotime( $result['user_registered'] ) ) : '';
501 - continue;
502 - }
503 -
504 - $this->$key = empty( $value ) ? $this->fillable[ $key ] : $value;
505 - }
506 -
507 - return $this;
508 - }
509 -
510 - /**
511 - * Generate username from email
512 - *
513 - * @param string $email
514 - *
515 - * @return string
516 - */
517 - public static function generate_username( string $email ) {
518 - $username = strtok( $email, '@' );
519 -
520 - if ( username_exists( $username ) ) {
521 - $username = $username . wp_rand( 10, 100 );
522 - }
523 -
524 - return $username;
525 - }
526 -
527 - /**
528 - * Convert User model object to array
529 - *
530 - * @return array
531 - */
532 - public function to_array(): array {
533 - $data = [];
534 -
535 - foreach ( $this->fillable as $key => $default ) {
536 - if ( $key === 'user_email' ) {
537 - $data['email'] = $this->fillable[$key];
538 - continue;
539 - }
540 -
541 - $value = $this->$key ?? $this->fillable[ $key ];
542 -
543 - // Unserialize if necessary
544 - if ( is_string( $value ) && $this->is_serialized( $value ) ) {
545 - $value = maybe_unserialize( $value );
546 - }
547 -
548 - // Normalize value to default type
549 - if ( gettype( $value ) !== gettype( $default ) ) {
550 - if ( gettype( $default ) === 'array' && is_string( $value ) && $this->is_serialized( $value ) ) {
551 - $value = maybe_unserialize( $value );
552 - } elseif ( gettype( $default ) === 'array' ) {
553 - $value = $default;
554 - }
555 - }
556 -
557 - // Cast specific keys to objects
558 - if ( $key == 'schedule' ) {
559 - $value = (object) $value;
560 - }
561 -
562 - // Cast specific keys to boolean
563 - if ( in_array( $key, [
564 - 'enable_custom_schedule',
565 - 'enable_custom_holiday'
566 - ] ) ) {
567 - $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
568 - }
569 -
570 - if ( $key == 'user_registered' ) {
571 - $value = $default;
572 - }
573 -
574 - $data[ $key ] = $value ?? $default;
575 - }
576 -
577 - $data['ID'] = $this->id;
578 -
579 - return $data;
580 - }
581 -
582 - /**
583 - * Check if a string is serialized data
584 - *
585 - * @param mixed $data Data to check for serialization
586 - *
587 - * @return bool True if data is serialized, false otherwise
588 - */
589 - function is_serialized( $data ) {
590 - return is_string( $data ) && @unserialize( $data ) !== false;
591 - }
592 -
593 - /**
594 - * Specify data which should be serialized to JSON.
595 - *
596 - * @return array
597 - */
598 - public function jsonSerialize(): array {
599 - return $this->to_array();
600 - }
1 +<?php
2 +/**
3 + * User Model Abstract for managing all wp_users and wp_usermeta
4 + *
5 + * @package Booktics/Abstracts
6 + */
7 +
8 +namespace Booktics\Abstracts;
9 +
10 +if ( ! defined( 'ABSPATH' ) ) exit;
11 +
12 +use JsonSerializable;
13 +use WP_User_Query;
14 +
15 +abstract class User_Model implements JsonSerializable {
16 +
17 + /**
18 + * Store specific user role
19 + *
20 + * @var string
21 + */
22 + protected $role = 'administrator';
23 +
24 + /**
25 + * Store specific meta prefix
26 + *
27 + * @var string
28 + */
29 + protected $meta_prefix = '';
30 +
31 + /**
32 + * Store user id
33 + *
34 + * @var integer
35 + */
36 + protected $id;
37 +
38 + /**
39 + * Store user data
40 + *
41 + * @var array
42 + */
43 + protected $fillable = [
44 + 'id' => '',
45 + 'first_name' => '',
46 + 'last_name' => '',
47 + 'user_email' => '',
48 + 'image' => '',
49 + 'user_login' => '',
50 + 'phone' => '',
51 + 'status' => 0,
52 + 'user_registered' => '',
53 + ];
54 +
55 + /**
56 + * Store Filter conditions
57 + *
58 + * @var array
59 + */
60 + protected $filters = [];
61 +
62 + /**
63 + * Store error
64 + *
65 + * @var \WP_Error
66 + */
67 + protected $error;
68 +
69 + /**
70 + * User constructor
71 + *
72 + * @param $user
73 + *
74 + * return void
75 + */
76 + public function __construct( $user = 0 ) {
77 + require_once( ABSPATH . 'wp-admin/includes/user.php' );
78 +
79 + if ( $user instanceof self ) {
80 + $this->set_id( $user->get_id() );
81 + } elseif ( ! empty( $user->ID ) ) {
82 + $this->set_id( $user->ID );
83 + } elseif ( is_numeric( $user ) && $user > 0 ) {
84 + $this->set_id( $user );
85 + }
86 + }
87 +
88 + /**
89 + * Get user id
90 + *
91 + * @return integer
92 + */
93 + public function get_id(): int {
94 + return $this->id;
95 + }
96 +
97 + /**
98 + * Set user id
99 + *
100 + * @param int $id
101 + *
102 + * @return void
103 + */
104 + public function set_id( int $id ) {
105 + $this->id = $id;
106 + }
107 +
108 + /**
109 + * Get user data
110 + *
111 + * @param string $name
112 + *
113 + * @return string|null
114 + */
115 + public function __get( string $name ) {
116 + if ( $name === 'id' ) {
117 + return $this->id;
118 + }
119 + if ( ! isset( $this->fillable[ $name ] ) ) {
120 + return null;
121 + }
122 +
123 + $possible_function_name = 'get_' . booktics_snake_case( $name ) . '_attribute';
124 + $data = $this->get_prop( $name, $this->fillable[ $name ] );
125 + if ( method_exists( $this, $possible_function_name ) ) {
126 + return $this->$possible_function_name( $this );
127 + }
128 +
129 + return $data;
130 + }
131 +
132 + /**
133 + * Get either user data or default data
134 + *
135 + * @param string $prop
136 + * @param string|array $default
137 + *
138 + * @return string|array
139 + */
140 + private function get_prop( $prop = '', $default = '' ) {
141 + return $this->get_metadata( $prop ) ?? $default;
142 + }
143 +
144 + /**
145 + * Get user metadata
146 + *
147 + * @param string $prop
148 + *
149 + * @return mixed
150 + */
151 + private function get_metadata( string $prop = '' ) {
152 + $meta_key = $this->meta_prefix . $prop;
153 +
154 + return get_user_meta( $this->id, $meta_key, true );
155 + }
156 +
157 + /**
158 + * Set user values
159 + *
160 + * @param string $name
161 + * @param string $value
162 + *
163 + * @return void
164 + */
165 + public function __set( string $name, string $value ) {
166 + if ( strpos( $name, $this->meta_prefix ) !== false ) {
167 + $name = str_replace( $this->meta_prefix, '', $name );
168 + }
169 +
170 + $this->fillable[ $name ] = $value;
171 + }
172 +
173 + /**
174 + * Save or update user
175 + *
176 + * @param array $args
177 + *
178 + * @return mixed|string
179 + */
180 + public function save( array $args = [] ) {
181 + if ( isset( $args['email'] ) ) {
182 + $args['user_email'] = $args['email'];
183 + unset( $args['email'] );
184 + }
185 +
186 + $user_id = $args['ID'] ?? '';
187 +
188 + $user_data = [
189 + 'ID' => $user_id,
190 + 'role' => $this->role,
191 + 'password' => wp_generate_password(),
192 + ];
193 +
194 + // Add required user fields
195 + foreach ( $args as $key => $value ) {
196 + if ( ! isset( $this->fillable[ $key ] ) && ! in_array( $key, [
197 + 'email',
198 + 'user_email'
199 + ] ) ) {
200 + continue;
201 + }
202 + $user_data[ $key ] = $value;
203 + }
204 +
205 + $user_id = wp_insert_user( $user_data );
206 + if ( is_wp_error( $user_id ) ) {
207 + return $user_id;
208 + }
209 +
210 + $user = new \WP_User( $user_id );
211 + $user->add_role( $this->role );
212 + $this->save_metadata( $args );
213 +
214 + if ( $user_id ) {
215 + wp_update_user( $user_data );
216 + } else {
217 + $user_id = wp_insert_user( $user_data );
218 + }
219 +
220 + $user = get_userdata( $user_id );
221 + $user->add_role( $this->role );
222 + if ( ! is_wp_error( $user_id ) ) {
223 + $this->set_id( $user_id );
224 + $this->save_metadata( $user_data );
225 + $this->retrieve_password();
226 + }
227 +
228 + return $user_id;
229 + }
230 +
231 + /**
232 + * Update specific field in user or meta data
233 + *
234 + * @param array $data
235 + *
236 + * @return bool
237 + */
238 + public function update( array $data = [] ): bool {
239 + if ( ! $this->id ) {
240 + $this->error = new \WP_Error( 'no_id', __( 'User ID is required to update.', 'booktics' ) );
241 +
242 + return false;
243 + }
244 +
245 + $user_data = [ 'ID' => $this->id ];
246 + $meta_data = [];
247 +
248 + foreach ( $data as $key => $value ) {
249 + if ( in_array( $key, [ 'schedule', 'custom', 'holiday' ] ) ) {
250 + continue;
251 + }
252 + if ( isset( $this->fillable[ $key ] ) && ! strpos( $key, $this->meta_prefix ) ) {
253 + if ( in_array( $key, [ 'user_email', 'user_login' ] ) ) {
254 + $user_data[ $key ] = $value;
255 + }
256 + $meta_data[ $key ] = $value;
257 + }
258 + }
259 + $result = wp_update_user( $user_data );
260 + if ( is_wp_error( $result ) ) {
261 + $this->error = $result;
262 +
263 + return false;
264 + }
265 +
266 + $this->save_metadata( $meta_data );
267 +
268 + return true;
269 + }
270 +
271 + /**
272 + * Store user meta data
273 + *
274 + * @param array $data
275 + *
276 + * @return void
277 + */
278 + public function save_metadata( array $data = [] ) {
279 + foreach ( $data as $key => $value ) {
280 + if ( ! isset( $this->fillable[ $key ] ) ) {
281 + continue;
282 + }
283 + $meta_key = $this->meta_prefix . $key;
284 +
285 + update_user_meta( $this->id, $meta_key, $value );
286 + }
287 + }
288 +
289 + /**
290 + * Send password reset email
291 + *
292 + * @return void
293 + */
294 + public function retrieve_password() {
295 + if ( 0 === intval( $this->status ) ) {
296 + retrieve_password( $this->email );
297 + }
298 + }
299 +
300 + /**
301 + * Delete user
302 + *
303 + * @return boolean
304 + */
305 + public function delete(): bool {
306 + $user = get_userdata( $this->id );
307 +
308 + if ( ! $user ) {
309 + return false;
310 + }
311 +
312 + if ( count( $user->roles ) == 1 && in_array( $this->role, $user->roles ) ) {
313 + return wp_delete_user( $this->id );
314 + }
315 + $user->remove_role( $this->role );
316 +
317 + return true;
318 + }
319 +
320 + /**
321 + * Append user filter condition
322 + *
323 + * @param string $field
324 + * @param $value
325 + * @param string $compare
326 + *
327 + * @return $this
328 + */
329 + public function where( string $field, $value, string $compare = '=' ): self {
330 + $this->filters[] = [
331 + 'field' => $field,
332 + 'value' => $value,
333 + 'compare' => $compare,
334 + ];
335 +
336 + return $this;
337 + }
338 +
339 + /**
340 + * Add OR conditions in where
341 + *
342 + * @param string $field
343 + * @param $value
344 + * @param string $compare
345 + *
346 + * @return $this
347 + */
348 + public function or_where( string $field, $value, string $compare = '=' ): self {
349 + $this->filters[] = [
350 + 'field' => $field,
351 + 'value' => $value,
352 + 'compare' => $compare,
353 + 'relation' => 'OR',
354 + ];
355 +
356 + return $this;
357 + }
358 +
359 + /**
360 + * Conditionally apply query filters
361 + *
362 + * @param mixed $value
363 + * @param callable $callback
364 + * @param callable|null $default
365 + *
366 + * @return $this
367 + */
368 + public function when( $value, callable $callback, callable $default = null ): self {
369 + if ( $value ) {
370 + $callback( $this, $value );
371 + } elseif ( $default ) {
372 + $default( $this, $value );
373 + }
374 +
375 + return $this;
376 + }
377 +
378 + /**
379 + * Get all users paginated data
380 + *
381 + * @param array $args
382 + *
383 + * @return array
384 + */
385 + public function all( array $args = [] ): array {
386 + $defaults = [
387 + 'role' => $this->role,
388 + 'number' => 20,
389 + 'paged' => 1,
390 + 'orderby' => 'ID',
391 + 'order' => 'DESC',
392 + ];
393 +
394 + $args = wp_parse_args( $args, $defaults );
395 +
396 + if ( ! empty( $this->filters ) ) {
397 + $meta_query = [];
398 + $relation = 'AND';
399 +
400 + foreach ( $this->filters as $filter ) {
401 + if ( in_array( $filter['field'], [
402 + 'user_email',
403 + 'user_login'
404 + ] ) ) {
405 + $args['search'] = '*' . esc_attr( $filter['value'] ) . '*';
406 + $args['search_columns'] = [ $filter['field'] ];
407 + } else {
408 + $meta_query[] = [
409 + 'key' => $this->meta_prefix . $filter['field'],
410 + 'value' => $filter['value'],
411 + 'compare' => $filter['compare'],
412 + ];
413 + }
414 +
415 + if ( isset( $filter['relation'] ) && $filter['relation'] === 'OR' ) {
416 + $relation = 'OR';
417 + }
418 + }
419 + if ( ! empty( $meta_query ) ) {
420 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering users by meta fields
421 + $args['meta_query'] = [
422 + 'relation' => $relation,
423 + ...$meta_query
424 + ];
425 + }
426 + }
427 +
428 + $users = new WP_User_Query( $args );
429 + $items = [];
430 + foreach ( $users->get_results() as $result ) {
431 + $cloneObj = clone $this;
432 + $userMeta = get_user_meta( $result->ID );
433 +
434 + $items[] = $cloneObj->hydrate( $result->ID, array_merge(
435 + (array) $result->data,
436 + $userMeta
437 + ) )->to_array();
438 + }
439 +
440 + $this->filters = [];
441 +
442 + return [
443 + 'total' => $users->get_total(),
444 + 'per_page' => $args['number'],
445 + 'current_page' => $args['paged'],
446 + 'last_page' => ceil( $users->get_total() / $args['number'] ),
447 + 'items' => $items,
448 + ];
449 + }
450 +
451 + /**
452 + * Find user by id
453 + *
454 + * @param $id
455 + *
456 + * @return $this
457 + */
458 + public function find( $id ) {
459 + $user = get_user_by( 'ID', $id );
460 +
461 + if ( ! $user ) {
462 + return null;
463 + }
464 +
465 + $meta = array_map( function ( $value ) {
466 + return is_array( $value ) && count( $value ) === 1 ? $value[0] : $value;
467 + }, get_user_meta( $user->ID ) );
468 +
469 + $user_data = (array) $user->data;
470 +
471 + return $this->hydrate( $id, array_merge( $user_data, $meta ) );
472 + }
473 +
474 + /**
475 + * Hydrate each result with this class
476 + *
477 + * @param $id
478 + * @param $result
479 + *
480 + * @return $this
481 + */
482 + public function hydrate( $id, $result ): self {
483 + $this->set_id( $id );
484 + if ( ! is_array( $result ) ) {
485 + return $this;
486 + }
487 +
488 + foreach ( $result as $key => $value ) {
489 + $key = str_replace( $this->meta_prefix, '', $key );
490 + if ( ! isset( $this->fillable[ $key ] ) && ! in_array( $key, [
491 + 'user_email',
492 + 'email'
493 + ] ) ) {
494 + continue;
495 + }
496 + if ( is_array( $value ) ) {
497 + $value = implode( ',', $value );
498 + }
499 +
500 + if ( $key === 'user_registered' ) {
501 + $this->fillable['user_registered'] = ! empty( $result['user_registered'] ) ?
502 + gmdate( 'j F, Y', strtotime( $result['user_registered'] ) ) : '';
503 + continue;
504 + }
505 +
506 + $this->$key = empty( $value ) ? $this->fillable[ $key ] : $value;
507 + }
508 +
509 + return $this;
510 + }
511 +
512 + /**
513 + * Generate username from email
514 + *
515 + * @param string $email
516 + *
517 + * @return string
518 + */
519 + public static function generate_username( string $email ) {
520 + $username = strtok( $email, '@' );
521 +
522 + if ( username_exists( $username ) ) {
523 + $username = $username . wp_rand( 10, 100 );
524 + }
525 +
526 + return $username;
527 + }
528 +
529 + /**
530 + * Convert User model object to array
531 + *
532 + * @return array
533 + */
534 + public function to_array(): array {
535 + $data = [];
536 +
537 + foreach ( $this->fillable as $key => $default ) {
538 + if ( $key === 'user_email' ) {
539 + $data['email'] = $this->fillable[$key];
540 + continue;
541 + }
542 +
543 + $value = $this->$key ?? $this->fillable[ $key ];
544 +
545 + // Unserialize if necessary (object instantiation disallowed — see helper).
546 + if ( is_string( $value ) && $this->is_serialized( $value ) ) {
547 + $value = \booktics_safe_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 = \booktics_safe_maybe_unserialize( $value );
554 + } elseif ( gettype( $default ) === 'array' ) {
555 + $value = $default;
556 + }
557 + }
558 +
559 + // Cast specific keys to objects
560 + if ( 'schedule' == $key ) {
561 + $value = (object) $value;
562 + }
563 +
564 + // Cast specific keys to boolean
565 + if ( in_array( $key, [
566 + 'enable_custom_schedule',
567 + 'enable_custom_holiday',
568 + 'enable_custom_availability',
569 + ] ) ) {
570 + $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
571 + }
572 +
573 + if ( 'user_registered' == $key ) {
574 + $value = $default;
575 + }
576 +
577 + $data[ $key ] = $value ?? $default;
578 + }
579 +
580 + $data['ID'] = $this->id;
581 +
582 + return $data;
583 + }
584 +
585 + /**
586 + * Check if a string is serialized data
587 + *
588 + * @param mixed $data Data to check for serialization
589 + *
590 + * @return bool True if data is serialized, false otherwise
591 + */
592 + function is_serialized( $data ) {
593 + // Structural check only (WordPress core) — never calls unserialize(), so it
594 + // cannot trigger PHP Object Injection on attacker-controlled user meta.
595 + return is_string( $data ) && \is_serialized( $data );
596 + }
597 +
598 + /**
599 + * Specify data which should be serialized to JSON.
600 + *
601 + * @return array
602 + */
603 + public function jsonSerialize(): array {
604 + return $this->to_array();
605 + }
606 +
607 + /**
608 + * Get the first user matching the current filters.
609 + *
610 + * @param array $args
611 + * @return self|null
612 + */
613 + public function first(array $args = []): ?self {
614 + $args['number'] = 1;
615 + $args['paged'] = 1;
616 + $result = $this->all($args);
617 + if (!empty($result['items'][0])) {
618 + return $this->hydrate($result['items'][0]['ID'], $result['items'][0]);
619 + }
620 + return null;
621 + }
601 622 }