PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.3
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.3
4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 4.2.1 All 138 releases
learnpress / inc / Models / CourseModel.php

CourseModel.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.3, at inc/Models/CourseModel.php

1,542 lines 39.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class Course Model
5 * Purpose: Use to map property separate table learnpress_course
6 * Field json for store all value of single course.
7 * Another fields for query list courses faster
8 *
9 * @package LearnPress/Classes
10 * @version 1.0.5
11 * @since 4.2.6.9
12 */
13
14 namespace LearnPress\Models;
15
16 use Exception;
17 use LearnPress\Databases\Course\CourseSectionItemsDB;
18 use LearnPress\Filters\PostFilter;
19 use LearnPress\Models\UserItems\UserCourseModel;
20 use LearnPress\Models\UserItems\UserItemModel;
21 use LP_Course_Cache;
22 use LP_Course_DB;
23 use LP_Course_Item;
24 use LP_Course_JSON_DB;
25 use LP_Course_JSON_Filter;
26 use LP_Datetime;
27 use LP_Debug;
28 use LP_Helper;
29 use LP_Section_Items_Filter;
30 use LP_Settings;
31 use stdClass;
32 use Throwable;
33 use WP_Error;
34 use WP_Post;
35
36 class CourseModel {
37 /**
38 * Auto increment, Primary key
39 *
40 * @var int
41 */
42 public $ID = 0;
43 /**
44 * @var string author id, foreign key
45 */
46 public $post_author = 0;
47 /**
48 * @var string post date gmt
49 */
50 public $post_date_gmt = null;
51 /**
52 * @var string post content
53 */
54 public $post_content = '';
55 /**
56 * @var string Post title
57 */
58 public $post_title = '';
59 /**
60 * @var string Post Status (publish, draft, ...)
61 */
62 public $post_status = '';
63 /**
64 * @var string Post name (slug for link)
65 */
66 public $post_name = '';
67 /**
68 * @var float price only using for filter courses, don't use for course detail
69 * Because price can change by date if set schedule sale
70 */
71 public $price_to_sort = 0;
72 public $is_sale = 0;
73 /**
74 * @var string JSON Store all data a single course
75 */
76 public $json = null; // Only set when save, don't set when get
77 /**
78 * @var string lang of Course
79 */
80 public $lang = null;
81 /********** Field not on table **********/
82 /**
83 * @var stdClass all meta data
84 */
85 public $meta_data = null;
86 public $image_url = null;
87 public $permalink = '';
88 public $categories;
89 public $tags;
90 private $price; // Not save in database, must auto reload calculate
91 private $passing_condition = '';
92 public $post_excerpt = '';
93 /**
94 * @var int ID of first item
95 */
96 public $first_item_id;
97 /**
98 * @var null|object info total items {'count_items': 20, 'lp_lesson': 10, 'lp_quiz': 10, ...}
99 */
100 public $total_items;
101 /**
102 * @var array list sections items
103 */
104 public $sections_items;
105 /**
106 * @var array|null cached items of course (per instance)
107 */
108 protected array $only_items;
109
110 /**
111 * If data get from database, map to object.
112 * Else create new object to save data to database.
113 *
114 * @param array|object|mixed $data
115 */
116 public function __construct( $data = null ) {
117 if ( $data ) {
118 $this->map_to_object( $data );
119 }
120
121 if ( is_null( $this->meta_data ) ) {
122 $this->meta_data = new stdClass();
123 }
124 }
125
126 /**
127 * Map array, object data to CourseModel.
128 * Use for data get from database.
129 *
130 * @param array|object|mixed $data
131 *
132 * @return CourseModel
133 */
134 public function map_to_object( $data ): CourseModel {
135 foreach ( $data as $key => $value ) {
136 if ( property_exists( $this, $key ) ) {
137 $this->{$key} = $value;
138 }
139 }
140
141 return $this;
142 }
143
144 /**
145 * Get course id
146 *
147 * @return int
148 */
149 public function get_id(): int {
150 return $this->ID;
151 }
152
153 /**
154 * Get post model
155 *
156 * @return CoursePostModel
157 * @since 4.3.6
158 * @version 1.0.0
159 */
160 public function get_post_model(): CoursePostModel {
161 return new CoursePostModel( $this );
162 }
163
164 public function get_title(): string {
165 $course_post = new CoursePostModel( $this );
166
167 return $course_post->get_the_title();
168 }
169
170 /**
171 * Get image url
172 * if not check get from Post
173 *
174 * @param string|int[] $size
175 *
176 * @return string
177 * @since 4.2.6.9
178 * @version 1.0.2
179 */
180 public function get_image_url( $size = 'post-thumbnail' ): string {
181 /**
182 * Comment code isset( $this->image_url )
183 * To apply for many size on a course
184 * To apply cache need handle cache before, where set size for image.
185 */
186 /*if ( isset( $this->image_url ) ) {
187 return $this->image_url;
188 }*/
189
190 $post = new CoursePostModel( $this );
191 $image_url = $post->get_image_url( $size );
192
193 $this->image_url = $image_url;
194
195 return $image_url;
196 }
197
198 /**
199 * Get author model
200 * Check has data on table learnpress_courses return
201 * if not check get from Post
202 *
203 * @return UserModel|false
204 */
205 public function get_author_model() {
206 $post = new CoursePostModel( $this );
207
208 return $post->get_author_model();
209 }
210
211 /**
212 * Get status of course
213 *
214 * @return string
215 * @since 4.2.7.3
216 * @version 1.0.0
217 */
218 public function get_status(): string {
219 return $this->post_status;
220 }
221
222 /**
223 * Get categories
224 * Check has data on table learnpress_courses return
225 * if not check get from Post
226 *
227 * @return array
228 */
229 public function get_categories(): array {
230 if ( isset( $this->categories ) ) {
231 return $this->categories;
232 }
233
234 $post = new PostModel( $this );
235 $categories = $post->get_categories();
236
237 $this->categories = $categories;
238
239 return $this->categories;
240 }
241
242 /**
243 * Get tags of course.
244 *
245 * @return array
246 * @since 4.2.7.2
247 * @version 1.0.0
248 */
249 public function get_tags(): array {
250 if ( isset( $this->tags ) ) {
251 return $this->tags;
252 }
253
254 $post = new PostModel( $this );
255 $tags = $post->get_tags();
256
257 $this->tags = $tags;
258
259 return $this->tags;
260 }
261
262 /**
263 * Get price
264 *
265 * @return float
266 */
267 public function get_price(): float {
268 /*if ( ! empty( $this->price ) ) {
269 return $this->price;
270 }*/
271
272 if ( $this->has_sale_price() ) {
273 $price = $this->get_sale_price();
274 } else {
275 $price = $this->get_regular_price();
276 }
277
278 $this->price = (float) $price;
279 $this->meta_data->{CoursePostModel::META_KEY_PRICE} = (float) $price;
280
281 return apply_filters( 'learnPress/course/price', (float) $price, $this->get_id() );
282 }
283
284 /**
285 * Get regular price
286 * Check has data on table learnpress_courses return
287 * if not check get from Post
288 * Value can be string empty if not set
289 *
290 * @return float|string
291 */
292 public function get_regular_price() {
293 $key = CoursePostModel::META_KEY_REGULAR_PRICE;
294 if ( $this->meta_data && isset( $this->meta_data->{$key} ) ) {
295 return $this->meta_data->{$key};
296 }
297
298 $coursePost = new CoursePostModel( $this );
299 $this->meta_data->{$key} = $coursePost->get_regular_price();
300
301 return $this->meta_data->{$key};
302 }
303
304 /**
305 * Get sale price
306 * Sale price can is string empty if not set
307 * Sale price set if is number >= 0
308 * Check has data on table learnpress_courses return
309 * if not check get from Post
310 *
311 * @return float|string
312 */
313 public function get_sale_price() {
314 $key = CoursePostModel::META_KEY_SALE_PRICE;
315 if ( $this->meta_data && isset( $this->meta_data->{$key} ) ) {
316 return $this->meta_data->{$key};
317 }
318
319 $coursePost = new CoursePostModel( $this );
320 $sale_price = $coursePost->get_sale_price();
321 $this->meta_data->{$key} = $sale_price;
322
323 return $this->meta_data->{$key};
324 }
325
326 /**
327 * Check course has 'sale price'
328 *
329 * @return bool
330 */
331 public function has_sale_price(): bool {
332 $has_sale_price = false;
333 $regular_price = $this->get_regular_price();
334 $sale_price = $this->get_sale_price();
335 $start_date = $this->get_sale_start();
336 $end_date = $this->get_sale_end();
337
338 if ( $sale_price !== '' && (float) $regular_price > (float) $sale_price ) {
339 $has_sale_price = true;
340 }
341
342 // Check in days sale
343 if ( $has_sale_price && ! empty( $start_date ) && ! empty( $end_date ) ) {
344 $nowObj = new LP_Datetime();
345 // Compare via timezone WP
346 $nowStr = $nowObj->toSql( true );
347 $now = strtotime( $nowStr );
348 $end = strtotime( $end_date );
349 $start = strtotime( $start_date );
350
351 $has_sale_price = $now >= $start && $now <= $end;
352 }
353
354 return apply_filters( 'learnPress/course/has-sale-price', $has_sale_price, $this );
355 }
356
357 /**
358 * Get date sale start
359 *
360 * @return mixed
361 */
362 public function get_sale_start() {
363 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_SALE_START );
364 }
365
366 /**
367 * Get date sale end
368 *
369 * @return mixed
370 */
371 public function get_sale_end() {
372 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_SALE_END );
373 }
374
375 /**
376 * Check if a course is Free
377 *
378 * @return bool
379 */
380 public function is_free(): bool {
381 return $this->get_price() == 0;
382 }
383
384 /**
385 * Check if a course is enabled Offline
386 *
387 * @return bool
388 */
389 public function is_offline(): bool {
390 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_OFFLINE_COURSE, 'no' ) === 'yes';
391 }
392
393 /**
394 * Check option "Block course when expire" enable.
395 *
396 * @return bool
397 * @since 4.2.7.3
398 * @version 1.0.0
399 */
400 public function enable_block_when_expire(): bool {
401 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_BLOCK_EXPIRE_DURATION, 'no' ) === 'yes';
402 }
403
404 /**
405 * Check option "Block course when finished" enable.
406 *
407 * @return bool
408 * @since 4.2.7.6
409 * @version 1.0.0
410 */
411 public function enable_block_when_finished(): bool {
412 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_BLOCK_FINISH, 'no' ) === 'yes';
413 }
414
415 /**
416 * Get first item of course
417 *
418 * @return int
419 */
420 public function get_first_item_id(): int {
421 if ( isset( $this->first_item_id ) ) {
422 return $this->first_item_id;
423 }
424
425 try {
426 $this->first_item_id = LP_Course_DB::getInstance()->get_first_item_id( $this->get_id() );
427 } catch ( Throwable $e ) {
428 $this->first_item_id = 0;
429 }
430
431 return $this->first_item_id;
432 }
433
434 /**
435 * Get total items of course
436 *
437 * @return null|object
438 */
439 public function get_total_items() {
440 if ( isset( $this->total_items ) ) {
441 return $this->total_items;
442 }
443
444 try {
445 $this->total_items = LP_Course_DB::getInstance()->get_total_items( $this->get_id() );
446 } catch ( Throwable $e ) {
447 $this->total_items = null;
448 }
449
450 return $this->total_items;
451 }
452
453 /**
454 * Get total sections of course
455 *
456 * @return int
457 * @since 4.2.7.6
458 * @version 1.0.0
459 */
460 public function get_total_sections(): int {
461 $section_items = $this->get_section_items();
462
463 return count( $section_items );
464 }
465
466 /**
467 * Get total items of course
468 *
469 * @return array
470 */
471 public function get_section_items(): array {
472 if ( isset( $this->sections_items ) ) {
473 return $this->sections_items;
474 }
475
476 try {
477 $this->sections_items = $this->get_sections_and_items_course_from_db_and_sort();
478 } catch ( Throwable $e ) {
479 $this->sections_items = [];
480 }
481
482 return $this->sections_items;
483 }
484
485 /**
486 * Get only items of course
487 *
488 * @return array
489 * @since 4.3.2
490 * @version 1.0.1
491 */
492 public function get_only_items(): array {
493 if ( isset( $this->only_items ) ) {
494 return $this->only_items;
495 }
496
497 $this->only_items = [];
498
499 foreach ( $this->get_section_items() as $section ) {
500 foreach ( $section->items as $item ) {
501 $this->only_items[ $item->item_id ] = $item;
502 }
503 }
504
505 return $this->only_items;
506 }
507
508 /**
509 * Get section id of item
510 *
511 * @param int $item_id
512 *
513 * @return int
514 * @since 4.2.8
515 * @version 1.0.0
516 */
517 public function get_section_of_item( int $item_id ): int {
518 $section_id = 0;
519
520 $section_items = $this->get_section_items();
521 foreach ( $section_items as $section ) {
522 foreach ( $section->items as $item ) {
523 $item_id_check = (int) ( $item->item_id ?? $item->id ?? 0 );
524 if ( $item_id_check === $item_id ) {
525 $section_id = $section->section_id ?? $section->id ?? 0;
526 break;
527 }
528 }
529 }
530
531 return (int) $section_id;
532 }
533
534 /**
535 * Get course Evaluation type.
536 *
537 * @return string
538 * @since 4.2.7.3
539 * @version 1.0.1
540 */
541 public function get_evaluation_type(): string {
542 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_EVALUATION_TYPE, 'evaluate_lesson' );
543 }
544
545 /**
546 * Get course Evaluation type.
547 *
548 * @param string $type
549 *
550 * @return array
551 * @since 4.3.2.6
552 * @version 1.0.1
553 */
554 public static function get_evaluation_types( string $type = '' ): array {
555 if ( has_filter( 'learnpress/course-evaluation/methods' ) ) {
556 $methods = apply_filters( 'learnpress/course-evaluation/methods', [], 0 );
557 }
558
559 $types = apply_filters(
560 'learn-press/course/evaluation-types',
561 array(
562 'evaluate_lesson' => [
563 'label' => __( 'Evaluate via completed lessons', 'learnpress' ),
564 'tip' => __( 'Course will be completed when all lessons are completed.', 'learnpress' ),
565 ],
566 'evaluate_final_quiz' => [
567 'label' => __( 'Evaluate via final quiz', 'learnpress' ),
568 'tip' => __( 'Course will be completed when the final quiz is passed.', 'learnpress' ),
569 ],
570 'evaluate_quiz' => [
571 'label' => __( 'Evaluate via quizzes', 'learnpress' ),
572 'tip' => __( 'Course will be completed when all quizzes are passed.', 'learnpress' ),
573 ],
574 'evaluate_questions' => [
575 'label' => __( 'Evaluate via questions', 'learnpress' ),
576 'tip' => __( 'Course will be completed when all questions are answered correctly.', 'learnpress' ),
577 ],
578 'evaluate_mark' => [
579 'label' => __( 'Evaluate via marks', 'learnpress' ),
580 'tip' => __( 'Course will be completed when the passing mark is achieved.', 'learnpress' ),
581 ],
582 ),
583 );
584
585 if ( empty( $type ) ) {
586 return $types;
587 } elseif ( empty( $types[ $type ] ) && ! empty( $methods[ $type ] ) ) {
588 $types[ $type ] = [
589 'label' => strip_tags( $methods[ $type ] ),
590 'tip' => strip_tags( $methods[ $type ] ),
591 ];
592 }
593
594 return $types[ $type ] ?? [];
595 }
596
597 /**
598 * Get course passing condition value.
599 *
600 * @return float
601 * @since 4.2.7.3
602 * @version 1.0.0
603 */
604 public function get_passing_condition(): float {
605 return (float) $this->get_meta_value_by_key( CoursePostModel::META_KEY_PASSING_CONDITION, 80 );
606 }
607
608 /**
609 * Get final quiz id
610 *
611 * @return int
612 */
613 public function get_final_quiz(): int {
614 $key = '_lp_final_quiz';
615 if ( ! empty( $this->meta_data->{$key} ) ) {
616 return $this->meta_data->$key;
617 }
618
619 $final_quiz = 0;
620
621 // Not use array_reverse, it's make change object
622 $section_items = $this->get_section_items();
623 $found = 0;
624 for ( $i = count( $section_items ); $i > 0; $i-- ) {
625 $section = $section_items[ $i - 1 ];
626 for ( $j = count( $section->items ); $j > 0; $j-- ) {
627 $item = $section->items[ $j - 1 ];
628 if ( learn_press_get_post_type( $item->id ) === LP_QUIZ_CPT ) {
629 $final_quiz = $item->id;
630 $found = 1;
631 break;
632 }
633 }
634
635 if ( $found ) {
636 break;
637 }
638 }
639
640 $evaluation_type = $this->get_evaluation_type();
641 if ( $evaluation_type === 'evaluate_final_quiz' ) {
642 if ( isset( $final_quiz ) ) {
643 update_post_meta( $this->ID, $key, $final_quiz );
644 } else {
645 delete_post_meta( $this->ID, $key );
646 }
647 }
648
649 $this->meta_data->{$key} = $final_quiz;
650
651 return $final_quiz;
652 }
653
654 /**
655 * Get all sections and items from database, then handle sort
656 * Only call when data change or not set
657 *
658 * @return array
659 * @since 4.1.6.9
660 * @version 1.0.4
661 * @author tungnx
662 */
663 private function get_sections_and_items_course_from_db_and_sort(): array {
664 $sections_items = [];
665 $course_id = $this->get_id();
666 $lp_course_db = LP_Course_DB::getInstance();
667
668 try {
669 $sections_results = $lp_course_db->get_sections( $course_id );
670 $sections_items_results = $lp_course_db->get_full_sections_and_items_course( $course_id );
671 $count_items = count( $sections_items_results );
672 $index_items_last = $count_items - 1;
673 $section_current = 0;
674
675 /**
676 * @var $section_order_plus int
677 * @var $item_order_plus int
678 * To fixed case: section order start from 0, item order start from 0
679 */
680 $section_order_plus = 0;
681 $item_order_plus = 0;
682 foreach ( $sections_items_results as $index => $sections_item ) {
683 $section_new = $sections_item->section_id;
684 $section_order = (int) $sections_item->section_order;
685 if ( $index === 0 && $section_order === 0 ) {
686 $section_order_plus = 1;
687 }
688
689 $section_order += $section_order_plus;
690 $item = new stdClass();
691 $item->id = $sections_item->item_id;
692 $item->item_id = $sections_item->item_id;
693 $item_order = (int) $sections_item->item_order;
694 if ( $index === 0 && $item_order === 0 ) {
695 $item_order_plus = 1;
696 }
697
698 $item_order += $item_order_plus;
699 $item->order = $item_order;
700 $item->item_order = $item_order;
701 $item->type = $sections_item->item_type;
702 $item->item_type = $sections_item->item_type;
703 $item_tmp = LP_Course_Item::get_item( $item->id );
704 if ( $item_tmp ) {
705 $item->title = html_entity_decode( $item_tmp->get_title() );
706 $item->preview = $item_tmp->is_preview();
707 }
708
709 if ( $section_new != $section_current ) {
710 $sections_items[ $section_new ] = new stdClass();
711 $sections_items[ $section_new ]->id = $section_new; // old field will be deprecated in future
712 $sections_items[ $section_new ]->section_id = $section_new; // new field
713 $sections_items[ $section_new ]->order = $section_order; // old field will be deprecated in future
714 $sections_items[ $section_new ]->section_order = $section_order; // new field
715 $sections_items[ $section_new ]->title = html_entity_decode( $sections_item->section_name ); // old field will be deprecated in future
716 $sections_items[ $section_new ]->section_name = html_entity_decode( $sections_item->section_name ); // new field
717 $sections_items[ $section_new ]->description = html_entity_decode( $sections_item->section_description ); // old field will be deprecated in future
718 $sections_items[ $section_new ]->section_description = html_entity_decode( $sections_item->section_description ); // new field
719 $sections_items[ $section_new ]->items = [];
720
721 // Sort item by item_order
722 if ( $section_current != 0 ) {
723 usort(
724 $sections_items[ $section_current ]->items,
725 function ( $item1, $item2 ) {
726 return $item1->item_order - $item2->item_order;
727 }
728 );
729 }
730
731 $section_current = $section_new;
732 }
733
734 $sections_items[ $section_new ]->items[ $item->item_id ] = $item;
735
736 if ( $index_items_last === $index ) {
737 usort(
738 $sections_items[ $section_current ]->items,
739 function ( $item1, $item2 ) {
740 return $item1->item_order - $item2->item_order;
741 }
742 );
743 }
744 }
745
746 // Check case if section empty items
747 foreach ( $sections_results as $section ) {
748 $section_id = $section->section_id;
749 if ( isset( $sections_items[ $section_id ] ) ) {
750 continue;
751 }
752
753 $section_obj = new stdClass();
754 $section_obj->id = $section_id;
755 $section_obj->section_id = $section_id;
756 $section_obj->order = $section->section_order;
757 $section_obj->section_order = $section->section_order;
758 $section_obj->title = html_entity_decode( $section->section_name );
759 $section_obj->section_name = html_entity_decode( $section->section_name );
760 $section_obj->description = html_entity_decode( $section->section_description );
761 $section_obj->section_description = html_entity_decode( $section->section_description );
762 $section_obj->items = [];
763 $sections_items[ $section_id ] = $section_obj;
764 }
765
766 // Sort section by section_order
767 usort(
768 $sections_items,
769 function ( $section1, $section2 ) {
770 return $section1->section_order - $section2->section_order;
771 }
772 );
773 } catch ( Throwable $e ) {
774 LP_Debug::error_log( $e );
775 }
776
777 return $sections_items;
778 }
779
780 /**
781 * Get permalink course
782 *
783 * @return string
784 */
785 public function get_permalink(): string {
786 if ( ! empty( $this->permalink ) ) {
787 return $this->permalink;
788 }
789
790 try {
791 $coursePostModel = new CoursePostModel( $this );
792 $this->permalink = $coursePostModel->get_permalink();
793 } catch ( Throwable $e ) {
794 $this->permalink = '';
795 }
796
797 return $this->permalink;
798 }
799
800 /**
801 * Get value option No enroll requirement
802 *
803 * @return mixed
804 */
805 public function get_no_enroll_requirement() {
806 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_NO_REQUIRED_ENROLL, 'no' );
807 }
808
809 /**
810 * Get description of Course
811 *
812 * @return string
813 */
814 public function get_description(): string {
815 $course_post = new CoursePostModel( $this );
816
817 return $course_post->get_the_content();
818 }
819
820 /**
821 * Get short description of Course
822 *
823 * @return string
824 */
825 public function get_short_description(): string {
826 $course_post = new CoursePostModel( $this );
827
828 return $course_post->get_the_excerpt();
829 }
830
831 /**
832 * Get value option No enroll requirement
833 *
834 * @return bool
835 */
836 public function has_no_enroll_requirement(): bool {
837 return $this->get_no_enroll_requirement() === 'yes';
838 }
839
840 /**
841 * Get value from meta data by key
842 *
843 * @param string $key
844 * @param mixed|false $default_value
845 * @param bool $single
846 *
847 * @return false|mixed
848 * @since 4.2.6.9
849 * @version 1.0.2
850 */
851 public function get_meta_value_by_key( string $key, $default_value = false, bool $single = true ) {
852 if ( $this->meta_data instanceof stdClass && isset( $this->meta_data->{$key} ) ) {
853 $value = maybe_unserialize( $this->meta_data->{$key} );
854 } else {
855 $coursePost = new CoursePostModel( $this );
856 $value = $coursePost->get_meta_value_by_key( $key, $default_value, $single );
857 }
858
859 $value = maybe_unserialize( $value );
860 $this->meta_data->{$key} = $value;
861
862 return $value;
863 }
864
865 /**
866 * Check course is in stock
867 * True is in stock, False is out of stock
868 *
869 * @return mixed
870 * @since 3.0.0
871 * @version 1.0.1
872 */
873 public function is_in_stock() {
874 $in_stock = true;
875 $max_allowed = (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_MAX_STUDENTS, 0 );
876
877 if ( $max_allowed ) {
878 $in_stock = $max_allowed > $this->get_total_user_enrolled_or_purchased();
879 }
880
881 return apply_filters( 'learn-press/is-in-stock', $in_stock, $this->get_id() );
882 }
883
884 /**
885 * Check course is enable repurchase
886 *
887 * @return bool
888 * @since 4.2.7.2
889 * @version 1.0.1
890 */
891 public function enable_allow_repurchase(): bool {
892 $enable = $this->get_meta_value_by_key( CoursePostModel::META_KEY_ALLOW_COURSE_REPURCHASE, 'no' );
893
894 return 'yes' === $enable;
895 }
896
897 /**
898 * Type repurchase
899 *
900 * @return string
901 * @since 4.2.7.3
902 * @version 1.0.0
903 */
904 public function get_type_repurchase(): string {
905 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_COURSE_REPURCHASE_OPTION, 'reset' );
906 }
907
908 /**
909 * Get external link
910 *
911 * @return string
912 */
913 public function get_external_link(): string {
914 return esc_url_raw(
915 $this->get_meta_value_by_key( CoursePostModel::META_KEY_EXTERNAL_LINK_BY_COURSE, '' )
916 );
917 }
918
919 /**
920 * Get item's link
921 * @move from LP_Abstract_Course
922 *
923 * @param int $item_id
924 * @param string $item_type
925 *
926 * @return string
927 * @since 3.0.0
928 * @version 1.0.2
929 */
930 public function get_item_link( int $item_id, string $item_type = '' ): string {
931 if ( empty( $item_type ) ) {
932 $item_type = get_post_type( $item_id );
933 }
934 $course_permalink = trailingslashit( $this->get_permalink() );
935 $item_slug = get_post_field( 'post_name', $item_id );
936
937 $slug_prefixes = apply_filters(
938 'learn-press/course/custom-item-prefixes',
939 array(
940 LP_QUIZ_CPT => sanitize_title_with_dashes( LP_Settings::get_option( 'quiz_slug', 'quizzes' ) ),
941 LP_LESSON_CPT => sanitize_title_with_dashes( LP_Settings::get_option( 'lesson_slug', 'lessons' ) ),
942 ),
943 $this->get_id()
944 );
945
946 $slug_prefix = trailingslashit( $slug_prefixes[ $item_type ] ?? '' );
947 $item_link = trailingslashit( $course_permalink . $slug_prefix . $item_slug );
948
949 return apply_filters( 'learn-press/course/item-link', $item_link, $item_id, $this );
950 }
951
952 /**
953 * Get total user enrolled, purchased or finished
954 *
955 * @move from LP_Abstract_Course
956 * @return int
957 * @version 1.0.1
958 * @since 4.1.4
959 */
960 public function get_total_user_enrolled_or_purchased(): int {
961 $total = 0;
962 $lp_course_cache = new LP_Course_Cache( true );
963
964 try {
965 $total = $lp_course_cache->get_total_students_enrolled_or_purchased( $this->get_id() );
966 if ( false !== $total ) {
967 return $total;
968 }
969
970 $lp_course_db = LP_Course_DB::getInstance();
971 $total = $lp_course_db->get_total_user_enrolled_or_purchased( $this->get_id() );
972 $lp_course_cache->set_total_students_enrolled_or_purchased( $this->get_id(), $total );
973 } catch ( Throwable $e ) {
974 LP_Debug::error_log( $e );
975 }
976
977 return $total;
978 }
979
980 /**
981 * Get fake students.
982 *
983 * @return int
984 */
985 public function get_fake_students(): int {
986 return (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_STUDENTS, 0 );
987 }
988
989 /**
990 * Count number of students enrolled course.
991 * Check global settings `enrolled_students_number`
992 * and add the fake value if both are set.
993 *
994 * @return int
995 * @move from LP_Abstract_Course
996 */
997 public function count_students(): int {
998 $total = $this->get_total_user_enrolled_or_purchased();
999 $total += $this->get_fake_students();
1000
1001 return $total;
1002 }
1003
1004 /**
1005 * Count total items in Course
1006 * item_type empty will return all items if exists.
1007 *
1008 * @param string $item_type
1009 *
1010 * @return int
1011 * @since 4.2.7.3
1012 * @version 1.0.1
1013 */
1014 public function count_items( string $item_type = '' ): int {
1015 $count = 0;
1016
1017 $total_items = $this->get_total_items();
1018 if ( empty( $item_type ) ) {
1019 $count = $total_items->count_items ?? 0;
1020 } elseif ( isset( $total_items->{$item_type} ) ) {
1021 return $total_items->{$item_type};
1022 }
1023
1024 return $count;
1025 }
1026
1027 /**
1028 * Get Duration of course
1029 * Timestamp in second
1030 *
1031 * @return string
1032 */
1033 public function get_duration(): string {
1034 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_DURATION, '10 week' );
1035 }
1036
1037 /**
1038 * Check user can enroll course.
1039 * @move from can_enroll_course method of LP_User class, since 4.1.1
1040 *
1041 * @param UserModel|false $user
1042 *
1043 * @return bool|WP_Error
1044 * @since 4.2.7.3
1045 * @version 1.0.2
1046 */
1047 public function can_enroll( $user ) {
1048 $can_enroll = true;
1049 $error_code = '';
1050
1051 $user_id = 0;
1052 if ( $user instanceof UserModel ) {
1053 $user_id = $user->get_id();
1054 }
1055
1056 try {
1057 if ( ! in_array( $this->post_status, [ 'publish', 'private' ] ) ) {
1058 $error_code = 'course_not_publish';
1059 throw new Exception( __( 'The course is not public', 'learnpress' ) );
1060 }
1061
1062 $userCourseModel = UserCourseModel::find( $user_id, $this->get_id(), true );
1063 $enable_no_required_enroll = $this->has_no_enroll_requirement();
1064 $out_of_stock = ! $this->is_in_stock();
1065
1066 // Case user can retake course.
1067 if ( $userCourseModel && $userCourseModel->can_retake() ) {
1068 $error_code = 'course_can_retry';
1069 throw new Exception( esc_html__( 'Course can retake.', 'learnpress' ) );
1070 }
1071
1072 // Case course is out of stock, show message when user is not login or user_item not exits
1073 if ( $out_of_stock &&
1074 ( ! $user || ! $userCourseModel || ! $userCourseModel->has_enrolled_or_finished() ) ) {
1075 $error_code = 'course_out_of_stock';
1076 throw new Exception( __( 'The course is full of students.', 'learnpress' ) );
1077 }
1078
1079 // Case user is logged in and user_item exists
1080 if ( $userCourseModel && $user ) {
1081 if ( $userCourseModel->has_enrolled() ) {
1082 $error_code = 'course_is_enrolled';
1083 throw new Exception( __( 'This course is already enrolled!', 'learnpress' ) );
1084 } elseif ( $userCourseModel->has_finished() ) {
1085 $error_code = 'course_is_finished';
1086 throw new Exception( __( 'The course is finished.', 'learnpress' ) );
1087 }
1088 }
1089
1090 if ( $enable_no_required_enroll ) {
1091 if ( ! $user ) {
1092 $error_code = 'course_is_no_required_enroll_not_login';
1093 throw new Exception(
1094 __(
1095 'Enrollment in the course is not mandatory. You can access course for learning now.',
1096 'learnpress'
1097 )
1098 );
1099 } else {
1100
1101 }
1102 } else {
1103 if ( ! empty( $this->get_external_link() )
1104 && ( ! $user || ! $userCourseModel
1105 || $userCourseModel->get_status() === UserItemModel::STATUS_CANCEL )
1106 && ! $this->is_offline() ) {
1107 $error_code = 'course_is_external';
1108 throw new Exception( __( 'The course is external', 'learnpress' ) );
1109 }
1110
1111 if ( ! $this->is_free() ) {
1112 if ( ! $user ) {
1113 $error_code = 'course_is_not_purchased_not_login';
1114 throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
1115 } elseif ( ! $userCourseModel || ! $userCourseModel->has_purchased() ) {
1116 $error_code = 'course_is_not_purchased';
1117 throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
1118 }
1119 }
1120 }
1121 } catch ( Throwable $e ) {
1122 if ( empty( $error_code ) ) {
1123 $error_code = 'course_can_not_enroll';
1124 }
1125 $can_enroll = new WP_Error( $error_code, $e->getMessage() );
1126 }
1127
1128 // Hook old
1129 if ( has_filter( 'learn-press/user/can-enroll-course' ) ) {
1130 $output = new stdClass();
1131 $output->check = true;
1132 $output->message = '';
1133 if ( $can_enroll instanceof WP_Error ) {
1134 $output->check = false;
1135 $output->message = $can_enroll->get_error_message();
1136 }
1137
1138 $course_old = learn_press_get_course( $this->get_id() );
1139 $user_old = learn_press_get_user( $user_id );
1140 $output = apply_filters( 'learn-press/user/can-enroll-course', $output, $course_old, false, $user_old );
1141 if ( $output === false ) {
1142 $can_enroll = new WP_Error( '', '' );
1143 } elseif ( ! $output->check && $output->message ) {
1144 $can_enroll = new WP_Error( 'error_custom', $output->message );
1145 }
1146 //_deprecated_function( 'The learn-press/user/can-enroll-course filter', '4.2.7.3', 'learn-press/user/can-enroll/course' );
1147 }
1148
1149 return apply_filters( 'learn-press/user/can-enroll/course', $can_enroll, $this, $user );
1150 }
1151
1152 /**
1153 * Check user can purchase course.
1154 * @move from can_purchase_course method of LP_User class, since 4.0.8
1155 * @use LP_User::can_purchase_course
1156 *
1157 * @param UserModel|false $user
1158 *
1159 * @return bool|WP_Error
1160 * @since 4.2.7.3
1161 * @version 1.0.0
1162 */
1163 public function can_purchase( $user ) {
1164 $can_purchase = true;
1165 $error_code = '';
1166
1167 $user_id = 0;
1168 if ( $user instanceof UserModel ) {
1169 $user_id = $user->get_id();
1170 }
1171
1172 try {
1173 $can_enroll = $this->can_enroll( $user );
1174 if ( $can_enroll instanceof WP_Error ) {
1175 $error_code_return = [
1176 'course_is_not_purchased_not_login',
1177 'course_is_not_purchased',
1178 'course_is_enrolled',
1179 'course_is_finished',
1180 ];
1181 if ( ! in_array( $can_enroll->get_error_code(), $error_code_return ) ) {
1182 $error_code = $can_enroll->get_error_code();
1183 throw new Exception( $can_enroll->get_error_message() );
1184 }
1185 }
1186
1187 if ( $this->is_free() ) {
1188 $error_code = 'course_is_free';
1189 throw new Exception( __( 'The course is free.', 'learnpress' ) );
1190 }
1191
1192 $enable_no_required_enroll = $this->has_no_enroll_requirement();
1193 if ( $enable_no_required_enroll ) {
1194 $error_code = 'course_is_no_required_enroll';
1195 throw new Exception(
1196 __( 'Enrollment in the course is not mandatory. You can access course for learning now.', 'learnpress' )
1197 );
1198 }
1199
1200 $userCourseModel = UserCourseModel::find( $user_id, $this->get_id(), true );
1201 if ( $user ) {
1202 if ( $userCourseModel ) {
1203 if ( $userCourseModel->has_purchased() ) {
1204 $error_code = 'course_purchased';
1205 throw new Exception( __( 'Course is purchased', 'learnpress' ) );
1206 }
1207
1208 if ( $this->enable_allow_repurchase() ) {
1209 if ( $userCourseModel->has_enrolled() && $userCourseModel->timestamp_remaining_duration() !== 0 ) {
1210 $error_code = 'course_is_enrolled';
1211 throw new Exception( 'This course is already enrolled!' );
1212 }
1213 } else {
1214 if ( $userCourseModel->has_enrolled_or_finished() ) {
1215 $error_code = 'course_is_enrolled_or_finished';
1216 throw new Exception( __( 'Course is enrolled or finished', 'learnpress' ) );
1217 }
1218 }
1219 }
1220 }
1221 } catch ( Throwable $e ) {
1222 if ( empty( $error_code ) ) {
1223 $error_code = 'course_can_not_purchase';
1224 }
1225 $can_purchase = new WP_Error( $error_code, $e->getMessage() );
1226 }
1227
1228 // Hook old
1229 if ( has_filter( 'learn-press/user/can-purchase-course' ) ) {
1230 $can_purchase = apply_filters( 'learn-press/user/can-purchase-course', $can_purchase, $user_id, $this->get_id() );
1231 if ( $can_purchase === false ) {
1232 $can_purchase = new WP_Error( '', '' );
1233 }
1234 //_deprecated_function( 'The learn-press/user/can-purchase-course filter', '4.2.7.3', 'learn-press/user/can-purchase/course' );
1235 }
1236
1237 return apply_filters( 'learn-press/user/can-purchase/course', $can_purchase, $this, $user );
1238 }
1239
1240 /**
1241 * Check user is author or co-in of course.
1242 *
1243 * @param UserModel|false $userModel
1244 *
1245 * @return bool
1246 * @since 4.2.7.6
1247 * @version 1.0.1
1248 */
1249 public function check_user_is_author( $userModel ): bool {
1250 $is_author = false;
1251
1252 if ( $userModel instanceof UserModel
1253 && $userModel->get_id() === $this->post_author ) {
1254 $is_author = true;
1255 }
1256
1257 return apply_filters( 'learn-press/course/is-author', $is_author, $this, $userModel );
1258 }
1259
1260 /**
1261 * Get item model assigned to this course
1262 *
1263 * @param int $item_id
1264 * @param string $item_type
1265 * @param bool $check_assign | default true check assign item to course
1266 *
1267 * @return mixed|false|null|WP_Post|PostModel
1268 * @since v4.2.7.6
1269 * @version 1.0.2
1270 */
1271 public function get_item_model( int $item_id, string $item_type, bool $check_assign = true ) {
1272 try {
1273 $item = false;
1274
1275 // Find item has in section
1276 $section_id = $this->get_section_of_item( $item_id );
1277 if ( $check_assign && ! $section_id ) {
1278 return $item;
1279 }
1280
1281 switch ( $item_type ) {
1282 case LP_LESSON_CPT:
1283 $item = LessonPostModel::find( $item_id, true );
1284 break;
1285 case LP_QUIZ_CPT:
1286 $item = QuizPostModel::find( $item_id, true );
1287 break;
1288 case LP_QUESTION_CPT:
1289 break;
1290 default:
1291 $item = apply_filters( 'learn-press/course/get-item-model', $item, $item_id, $item_type, $this );
1292 break;
1293 }
1294
1295 // If not defined class, get post default
1296 if ( ! $item ) {
1297 $filter = new PostFilter();
1298 $filter->ID = $item_id;
1299 $filter->post_type = $item_type;
1300 $item = PostModel::get_item_model_from_db( $filter );
1301 }
1302 } catch ( Exception $e ) {
1303 LP_Debug::error_log( $e );
1304 }
1305
1306 return $item;
1307 }
1308
1309 /**
1310 * Get item model if query success.
1311 * If not exists, return false.
1312 * If exists, return PostModel.
1313 *
1314 * @param LP_Course_JSON_Filter $filter
1315 *
1316 * @return CourseModel|false|static
1317 * @since 4.2.6.9
1318 * @version 1.0.2
1319 */
1320 public static function get_item_model_from_db( LP_Course_JSON_Filter $filter ) {
1321 $course_model = false;
1322
1323 try {
1324 $filter->only_fields = [ 'json', 'post_content' ];
1325
1326 $course_rs = self::get_course_from_db( $filter );
1327 if ( $course_rs instanceof stdClass && isset( $course_rs->json ) ) {
1328 $course_obj = LP_Helper::json_decode( $course_rs->json );
1329 $course_model = new static( $course_obj );
1330 //$course_model->json = $course_rs->json;
1331 $course_model->post_content = $course_rs->post_content;
1332 $course_model->get_author_model();
1333 }
1334 } catch ( Throwable $e ) {
1335 LP_Debug::error_log( $e );
1336 }
1337
1338 return $course_model;
1339 }
1340
1341 /**
1342 * Get course by ID
1343 *
1344 * @param int $course_id
1345 * @param bool $check_cache
1346 *
1347 * @return false|CourseModel|static
1348 * @since 4.2.6.9
1349 * @version 1.0.2
1350 */
1351 public static function find( int $course_id, bool $check_cache = false ) {
1352 if ( ! $course_id ) {
1353 return false;
1354 }
1355
1356 $filter_course = new LP_Course_JSON_Filter();
1357 $filter_course->ID = $course_id;
1358 $key_cache = "courseModel/find/id/{$course_id}";
1359 $lp_course_cache = new LP_Course_Cache();
1360
1361 // Check cache
1362 if ( $check_cache ) {
1363 $course_model = $lp_course_cache->get_cache( $key_cache );
1364 if ( $course_model instanceof CourseModel ) {
1365 return $course_model;
1366 }
1367 }
1368
1369 // Query database no cache.
1370 $course_model = self::get_item_model_from_db( $filter_course );
1371 if ( false === $course_model ) { // Find on table posts
1372 $course_rs = CoursePostModel::find( $course_id );
1373 if ( $course_rs instanceof CoursePostModel ) {
1374 $course_model = new static( $course_rs );
1375 }
1376 }
1377
1378 // Set cache
1379 if ( $course_model instanceof CourseModel ) {
1380 $lp_course_cache->set_cache( $key_cache, $course_model );
1381 }
1382
1383 return $course_model;
1384 }
1385
1386 /**
1387 * Get course from table learnpress_courses
1388 *
1389 * @return array|object|stdClass|null
1390 * @throws Exception
1391 */
1392 private static function get_course_from_db( LP_Course_JSON_Filter $filter ) {
1393 $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1394 $lp_course_json_db->get_query_single_row( $filter );
1395 $query_single_row = $lp_course_json_db->get_courses( $filter );
1396
1397 return $lp_course_json_db->wpdb->get_row( $query_single_row );
1398 }
1399
1400 /**
1401 * Save course data to table learnpress_courses.
1402 *
1403 * @param bool $force_save
1404 *
1405 * @return CourseModel
1406 * @throws Exception
1407 * @since 4.2.6.9
1408 * @version 1.0.3
1409 */
1410 public function save( bool $force_save = false ): CourseModel {
1411 // Check permission
1412 if ( ! $force_save ) {
1413 $this->check_permission();
1414 }
1415
1416 $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1417
1418 $courseObjToJSON = clone $this;
1419 unset( $courseObjToJSON->post_content );
1420 unset( $courseObjToJSON->json );
1421 $this->json = json_encode( $courseObjToJSON, JSON_UNESCAPED_UNICODE );
1422 $data = get_object_vars( $this );
1423
1424 if ( ! isset( $data['ID'] ) ) {
1425 throw new Exception( 'Course ID is invalid!' );
1426 }
1427
1428 $filter = new LP_Course_JSON_Filter();
1429 $filter->ID = $this->ID;
1430 $filter->only_fields = [ 'ID' ];
1431 $course_rs = self::get_course_from_db( $filter );
1432 // Check if exists course id.
1433 if ( empty( $course_rs ) ) { // Insert data.
1434 $lp_course_json_db->insert_data( $data );
1435 } else { // Update data.
1436 $lp_course_json_db->update_data( $data );
1437 }
1438
1439 // Clear cache
1440 $this->clean_caches();
1441
1442 return $this;
1443 }
1444
1445 /**
1446 * Delete row
1447 *
1448 * @throws Exception
1449 */
1450 public function delete() {
1451 $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1452 $filter = new LP_Course_JSON_Filter();
1453 $filter->where[] = $lp_course_json_db->wpdb->prepare( 'AND ID = %d', $this->ID );
1454 $filter->collection = $lp_course_json_db->tb_lp_courses;
1455 $lp_course_json_db->delete_execute( $filter );
1456
1457 // Clear cache
1458 $this->clean_caches();
1459 }
1460
1461 /**
1462 * Clean caches
1463 *
1464 * @return void
1465 * @version 1.0.0
1466 * @since 4.2.7.4
1467 */
1468 public function clean_caches() {
1469 $key_cache = "courseModel/find/id/{$this->ID}";
1470 $lp_course_cache = new LP_Course_Cache();
1471 $lp_course_cache->clear( $key_cache );
1472
1473 // Clear cache image urls, store with many sizes
1474 $img_urls_key_cache = "image_urls/{$this->ID}";
1475 $lp_course_cache->clear_cache_on_group( $img_urls_key_cache );
1476 }
1477
1478 /**
1479 * Check permission to update/create course
1480 *
1481 * @throws Exception
1482 */
1483 public function check_permission() {
1484 $coursePostModel = new CoursePostModel( $this );
1485 if ( ! $coursePostModel->check_capabilities_update() ) {
1486 throw new Exception( 'You do not have permission to update this course!' );
1487 }
1488 }
1489
1490 /**
1491 * Return course's items support.
1492 * To replace learn_press_course_get_support_item_types()
1493 * Should add hook on addons before use this function.
1494 *
1495 * @return array
1496 * @since 4.2.7.4
1497 * @version 1.0.1
1498 */
1499 public static function item_types_support(): array {
1500 $item_types = [
1501 LP_LESSON_CPT,
1502 LP_QUIZ_CPT,
1503 ];
1504
1505 // Hook old
1506 if ( has_filter( 'learn-press/course-item-type' ) ) {
1507 $item_types = apply_filters( 'learn-press/course-item-type', $item_types );
1508 }
1509
1510 $item_types = apply_filters( 'learn-press/course/item-types-support', $item_types );
1511
1512 // set types unique
1513 return array_unique( $item_types );
1514 }
1515
1516 /**
1517 * Get label of item type
1518 *
1519 * @param string $item_type
1520 *
1521 * @return string
1522 * @since 4.2.8.6
1523 * @version 1.0.0
1524 */
1525 public static function item_types_label( string $item_type = '' ): string {
1526 switch ( $item_type ) {
1527 case LP_LESSON_CPT:
1528 $label = __( 'Lesson', 'learnpress' );
1529 break;
1530 case LP_QUIZ_CPT:
1531 $label = __( 'Quiz', 'learnpress' );
1532 break;
1533 default:
1534 $label = ucfirst( strtolower( str_replace( [ 'lp_', '_' ], '', $item_type ) ) );
1535 $label = apply_filters( 'learn-press/course/item-type-label', $label, $item_type );
1536 break;
1537 }
1538
1539 return $label;
1540 }
1541 }
1542