PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.3.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.3.8
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.3.8, at inc/Models/CourseModel.php

1,529 lines 40.6 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.0
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 return $methods[ $type ];
589 }
590
591 return $types[ $type ] ?? [];
592 }
593
594 /**
595 * Get course passing condition value.
596 *
597 * @return float
598 * @since 4.2.7.3
599 * @version 1.0.0
600 */
601 public function get_passing_condition(): float {
602 return (float) $this->get_meta_value_by_key( CoursePostModel::META_KEY_PASSING_CONDITION, 80 );
603 }
604
605 /**
606 * Get final quiz id
607 *
608 * @return int
609 */
610 public function get_final_quiz(): int {
611 $key = '_lp_final_quiz';
612 if ( ! empty( $this->meta_data->{$key} ) ) {
613 return $this->meta_data->$key;
614 }
615
616 $final_quiz = 0;
617
618 // Not use array_reverse, it's make change object
619 $section_items = $this->get_section_items();
620 $found = 0;
621 for ( $i = count( $section_items ); $i > 0; $i-- ) {
622 $section = $section_items[ $i - 1 ];
623 for ( $j = count( $section->items ); $j > 0; $j-- ) {
624 $item = $section->items[ $j - 1 ];
625 if ( learn_press_get_post_type( $item->id ) === LP_QUIZ_CPT ) {
626 $final_quiz = $item->id;
627 $found = 1;
628 break;
629 }
630 }
631
632 if ( $found ) {
633 break;
634 }
635 }
636
637 $evaluation_type = $this->get_evaluation_type();
638 if ( $evaluation_type === 'evaluate_final_quiz' ) {
639 if ( isset( $final_quiz ) ) {
640 update_post_meta( $this->ID, $key, $final_quiz );
641 } else {
642 delete_post_meta( $this->ID, $key );
643 }
644 }
645
646 $this->meta_data->{$key} = $final_quiz;
647
648 return $final_quiz;
649 }
650
651 /**
652 * Get all sections and items from database, then handle sort
653 * Only call when data change or not set
654 *
655 * @return array
656 * @since 4.1.6.9
657 * @version 1.0.4
658 * @author tungnx
659 */
660 private function get_sections_and_items_course_from_db_and_sort(): array {
661 $sections_items = [];
662 $course_id = $this->get_id();
663 $lp_course_db = LP_Course_DB::getInstance();
664
665 try {
666 $sections_results = $lp_course_db->get_sections( $course_id );
667 $sections_items_results = $lp_course_db->get_full_sections_and_items_course( $course_id );
668 $count_items = count( $sections_items_results );
669 $index_items_last = $count_items - 1;
670 $section_current = 0;
671
672 /**
673 * @var $section_order_plus int
674 * @var $item_order_plus int
675 * To fixed case: section order start from 0, item order start from 0
676 */
677 $section_order_plus = 0;
678 $item_order_plus = 0;
679 foreach ( $sections_items_results as $index => $sections_item ) {
680 $section_new = $sections_item->section_id;
681 $section_order = (int) $sections_item->section_order;
682 if ( $index === 0 && $section_order === 0 ) {
683 $section_order_plus = 1;
684 }
685
686 $section_order += $section_order_plus;
687 $item = new stdClass();
688 $item->id = $sections_item->item_id;
689 $item->item_id = $sections_item->item_id;
690 $item_order = (int) $sections_item->item_order;
691 if ( $index === 0 && $item_order === 0 ) {
692 $item_order_plus = 1;
693 }
694
695 $item_order += $item_order_plus;
696 $item->order = $item_order;
697 $item->item_order = $item_order;
698 $item->type = $sections_item->item_type;
699 $item->item_type = $sections_item->item_type;
700 $item_tmp = LP_Course_Item::get_item( $item->id );
701 if ( $item_tmp ) {
702 $item->title = html_entity_decode( $item_tmp->get_title() );
703 $item->preview = $item_tmp->is_preview();
704 }
705
706 if ( $section_new != $section_current ) {
707 $sections_items[ $section_new ] = new stdClass();
708 $sections_items[ $section_new ]->id = $section_new; // old field will be deprecated in future
709 $sections_items[ $section_new ]->section_id = $section_new; // new field
710 $sections_items[ $section_new ]->order = $section_order; // old field will be deprecated in future
711 $sections_items[ $section_new ]->section_order = $section_order; // new field
712 $sections_items[ $section_new ]->title = html_entity_decode( $sections_item->section_name ); // old field will be deprecated in future
713 $sections_items[ $section_new ]->section_name = html_entity_decode( $sections_item->section_name ); // new field
714 $sections_items[ $section_new ]->description = html_entity_decode( $sections_item->section_description ); // old field will be deprecated in future
715 $sections_items[ $section_new ]->section_description = html_entity_decode( $sections_item->section_description ); // new field
716 $sections_items[ $section_new ]->items = [];
717
718 // Sort item by item_order
719 if ( $section_current != 0 ) {
720 usort(
721 $sections_items[ $section_current ]->items,
722 function ( $item1, $item2 ) {
723 return $item1->item_order - $item2->item_order;
724 }
725 );
726 }
727
728 $section_current = $section_new;
729 }
730
731 $sections_items[ $section_new ]->items[ $item->item_id ] = $item;
732
733 if ( $index_items_last === $index ) {
734 usort(
735 $sections_items[ $section_current ]->items,
736 function ( $item1, $item2 ) {
737 return $item1->item_order - $item2->item_order;
738 }
739 );
740 }
741 }
742
743 // Check case if section empty items
744 foreach ( $sections_results as $section ) {
745 $section_id = $section->section_id;
746 if ( isset( $sections_items[ $section_id ] ) ) {
747 continue;
748 }
749
750 $section_obj = new stdClass();
751 $section_obj->id = $section_id;
752 $section_obj->section_id = $section_id;
753 $section_obj->order = $section->section_order;
754 $section_obj->section_order = $section->section_order;
755 $section_obj->title = html_entity_decode( $section->section_name );
756 $section_obj->section_name = html_entity_decode( $section->section_name );
757 $section_obj->description = html_entity_decode( $section->section_description );
758 $section_obj->section_description = html_entity_decode( $section->section_description );
759 $section_obj->items = [];
760 $sections_items[ $section_id ] = $section_obj;
761 }
762
763 // Sort section by section_order
764 usort(
765 $sections_items,
766 function ( $section1, $section2 ) {
767 return $section1->section_order - $section2->section_order;
768 }
769 );
770 } catch ( Throwable $e ) {
771 LP_Debug::error_log( $e );
772 }
773
774 return $sections_items;
775 }
776
777 /**
778 * Get permalink course
779 *
780 * @return string
781 */
782 public function get_permalink(): string {
783 if ( ! empty( $this->permalink ) ) {
784 return $this->permalink;
785 }
786
787 try {
788 $coursePostModel = new CoursePostModel( $this );
789 $this->permalink = $coursePostModel->get_permalink();
790 } catch ( Throwable $e ) {
791 $this->permalink = '';
792 }
793
794 return $this->permalink;
795 }
796
797 /**
798 * Get value option No enroll requirement
799 *
800 * @return mixed
801 */
802 public function get_no_enroll_requirement() {
803 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_NO_REQUIRED_ENROLL, 'no' );
804 }
805
806 /**
807 * Get description of Course
808 *
809 * @return string
810 */
811 public function get_description(): string {
812 $course_post = new CoursePostModel( $this );
813
814 return $course_post->get_the_content();
815 }
816
817 /**
818 * Get short description of Course
819 *
820 * @return string
821 */
822 public function get_short_description(): string {
823 $course_post = new CoursePostModel( $this );
824
825 return $course_post->get_the_excerpt();
826 }
827
828 /**
829 * Get value option No enroll requirement
830 *
831 * @return bool
832 */
833 public function has_no_enroll_requirement(): bool {
834 return $this->get_no_enroll_requirement() === 'yes';
835 }
836
837 /**
838 * Get value from meta data by key
839 *
840 * @param string $key
841 * @param mixed|false $default_value
842 * @param bool $single
843 *
844 * @return false|mixed
845 * @since 4.2.6.9
846 * @version 1.0.2
847 */
848 public function get_meta_value_by_key( string $key, $default_value = false, bool $single = true ) {
849 if ( $this->meta_data instanceof stdClass && isset( $this->meta_data->{$key} ) ) {
850 $value = maybe_unserialize( $this->meta_data->{$key} );
851 } else {
852 $coursePost = new CoursePostModel( $this );
853 $value = $coursePost->get_meta_value_by_key( $key, $default_value, $single );
854 }
855
856 $value = maybe_unserialize( $value );
857 $this->meta_data->{$key} = $value;
858
859 return $value;
860 }
861
862 /**
863 * Check course is in stock
864 * True is in stock, False is out of stock
865 *
866 * @return mixed
867 * @since 3.0.0
868 * @version 1.0.1
869 */
870 public function is_in_stock() {
871 $in_stock = true;
872 $max_allowed = (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_MAX_STUDENTS, 0 );
873
874 if ( $max_allowed ) {
875 $in_stock = $max_allowed > $this->get_total_user_enrolled_or_purchased();
876 }
877
878 return apply_filters( 'learn-press/is-in-stock', $in_stock, $this->get_id() );
879 }
880
881 /**
882 * Check course is enable repurchase
883 *
884 * @return bool
885 * @since 4.2.7.2
886 * @version 1.0.1
887 */
888 public function enable_allow_repurchase(): bool {
889 $enable = $this->get_meta_value_by_key( CoursePostModel::META_KEY_ALLOW_COURSE_REPURCHASE, 'no' );
890
891 return 'yes' === $enable;
892 }
893
894 /**
895 * Type repurchase
896 *
897 * @return string
898 * @since 4.2.7.3
899 * @version 1.0.0
900 */
901 public function get_type_repurchase(): string {
902 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_COURSE_REPURCHASE_OPTION, 'reset' );
903 }
904
905 /**
906 * Get external link
907 *
908 * @return string
909 */
910 public function get_external_link(): string {
911 return esc_url_raw(
912 $this->get_meta_value_by_key( CoursePostModel::META_KEY_EXTERNAL_LINK_BY_COURSE, '' )
913 );
914 }
915
916 /**
917 * Get item's link
918 * @move from LP_Abstract_Course
919 *
920 * @param int $item_id
921 * @param string $item_type
922 *
923 * @return string
924 * @since 3.0.0
925 * @version 1.0.2
926 */
927 public function get_item_link( int $item_id, string $item_type = '' ): string {
928 if ( empty( $item_type ) ) {
929 $item_type = get_post_type( $item_id );
930 }
931 $course_permalink = trailingslashit( $this->get_permalink() );
932 $item_slug = get_post_field( 'post_name', $item_id );
933
934 $slug_prefixes = apply_filters(
935 'learn-press/course/custom-item-prefixes',
936 array(
937 LP_QUIZ_CPT => sanitize_title_with_dashes( LP_Settings::get_option( 'quiz_slug', 'quizzes' ) ),
938 LP_LESSON_CPT => sanitize_title_with_dashes( LP_Settings::get_option( 'lesson_slug', 'lessons' ) ),
939 ),
940 $this->get_id()
941 );
942
943 $slug_prefix = trailingslashit( $slug_prefixes[ $item_type ] ?? '' );
944 $item_link = trailingslashit( $course_permalink . $slug_prefix . $item_slug );
945
946 return apply_filters( 'learn-press/course/item-link', $item_link, $item_id, $this );
947 }
948
949 /**
950 * Get total user enrolled, purchased or finished
951 *
952 * @move from LP_Abstract_Course
953 * @return int
954 * @version 1.0.1
955 * @since 4.1.4
956 */
957 public function get_total_user_enrolled_or_purchased(): int {
958 $total = 0;
959 $lp_course_cache = new LP_Course_Cache( true );
960
961 try {
962 $total = $lp_course_cache->get_total_students_enrolled_or_purchased( $this->get_id() );
963 if ( false !== $total ) {
964 return $total;
965 }
966
967 $lp_course_db = LP_Course_DB::getInstance();
968 $total = $lp_course_db->get_total_user_enrolled_or_purchased( $this->get_id() );
969 $lp_course_cache->set_total_students_enrolled_or_purchased( $this->get_id(), $total );
970 } catch ( Throwable $e ) {
971 LP_Debug::error_log( $e );
972 }
973
974 return $total;
975 }
976
977 /**
978 * Get fake students.
979 *
980 * @return int
981 */
982 public function get_fake_students(): int {
983 return (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_STUDENTS, 0 );
984 }
985
986 /**
987 * Count number of students enrolled course.
988 * Check global settings `enrolled_students_number`
989 * and add the fake value if both are set.
990 *
991 * @return int
992 * @move from LP_Abstract_Course
993 */
994 public function count_students(): int {
995 $total = $this->get_total_user_enrolled_or_purchased();
996 $total += $this->get_fake_students();
997
998 return $total;
999 }
1000
1001 /**
1002 * Count total items in Course
1003 * item_type empty will return all items if exists.
1004 *
1005 * @param string $item_type
1006 *
1007 * @return int
1008 * @since 4.2.7.3
1009 * @version 1.0.1
1010 */
1011 public function count_items( string $item_type = '' ): int {
1012 $count = 0;
1013
1014 $total_items = $this->get_total_items();
1015 if ( empty( $item_type ) ) {
1016 $count = $total_items->count_items ?? 0;
1017 } elseif ( isset( $total_items->{$item_type} ) ) {
1018 return $total_items->{$item_type};
1019 }
1020
1021 return $count;
1022 }
1023
1024 /**
1025 * Get Duration of course
1026 * Timestamp in second
1027 *
1028 * @return string
1029 */
1030 public function get_duration(): string {
1031 return $this->get_meta_value_by_key( CoursePostModel::META_KEY_DURATION, '10 week' );
1032 }
1033
1034 /**
1035 * Check user can enroll course.
1036 * @move from can_enroll_course method of LP_User class, since 4.1.1
1037 *
1038 * @param UserModel|false $user
1039 *
1040 * @return bool|WP_Error
1041 * @since 4.2.7.3
1042 * @version 1.0.2
1043 */
1044 public function can_enroll( $user ) {
1045 $can_enroll = true;
1046 $error_code = '';
1047
1048 $user_id = 0;
1049 if ( $user instanceof UserModel ) {
1050 $user_id = $user->get_id();
1051 }
1052
1053 try {
1054 if ( ! in_array( $this->post_status, [ 'publish', 'private' ] ) ) {
1055 $error_code = 'course_not_publish';
1056 throw new Exception( __( 'The course is not public', 'learnpress' ) );
1057 }
1058
1059 $userCourseModel = UserCourseModel::find( $user_id, $this->get_id(), true );
1060 $enable_no_required_enroll = $this->has_no_enroll_requirement();
1061 $out_of_stock = ! $this->is_in_stock();
1062
1063 // Case user can retake course.
1064 if ( $userCourseModel && $userCourseModel->can_retake() ) {
1065 $error_code = 'course_can_retry';
1066 throw new Exception( esc_html__( 'Course can retake.', 'learnpress' ) );
1067 }
1068
1069 // Case course is out of stock, show message when user is not login or user_item not exits
1070 if ( $out_of_stock &&
1071 ( ! $user || ! $userCourseModel || ! $userCourseModel->has_enrolled_or_finished() ) ) {
1072 $error_code = 'course_out_of_stock';
1073 throw new Exception( __( 'The course is full of students.', 'learnpress' ) );
1074 }
1075
1076 // Case user is logged in and user_item exists
1077 if ( $userCourseModel && $user ) {
1078 if ( $userCourseModel->has_enrolled() ) {
1079 $error_code = 'course_is_enrolled';
1080 throw new Exception( __( 'This course is already enrolled!', 'learnpress' ) );
1081 } elseif ( $userCourseModel->has_finished() ) {
1082 $error_code = 'course_is_finished';
1083 throw new Exception( __( 'The course is finished.', 'learnpress' ) );
1084 }
1085 }
1086
1087 if ( $enable_no_required_enroll ) {
1088 if ( ! $user ) {
1089 $error_code = 'course_is_no_required_enroll_not_login';
1090 throw new Exception(
1091 __(
1092 'Enrollment in the course is not mandatory. You can access course for learning now.',
1093 'learnpress'
1094 )
1095 );
1096 } else {
1097
1098 }
1099 } else {
1100 if ( ! empty( $this->get_external_link() )
1101 && ( ! $user || ! $userCourseModel
1102 || $userCourseModel->get_status() === UserItemModel::STATUS_CANCEL )
1103 && ! $this->is_offline() ) {
1104 $error_code = 'course_is_external';
1105 throw new Exception( __( 'The course is external', 'learnpress' ) );
1106 }
1107
1108 if ( ! $this->is_free() ) {
1109 if ( ! $user ) {
1110 $error_code = 'course_is_not_purchased_not_login';
1111 throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
1112 } elseif ( ! $userCourseModel || ! $userCourseModel->has_purchased() ) {
1113 $error_code = 'course_is_not_purchased';
1114 throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
1115 }
1116 }
1117 }
1118 } catch ( Throwable $e ) {
1119 if ( empty( $error_code ) ) {
1120 $error_code = 'course_can_not_enroll';
1121 }
1122 $can_enroll = new WP_Error( $error_code, $e->getMessage() );
1123 }
1124
1125 // Hook old
1126 if ( has_filter( 'learn-press/user/can-enroll-course' ) ) {
1127 $output = new stdClass();
1128 $output->check = true;
1129 $output->message = '';
1130 if ( $can_enroll instanceof WP_Error ) {
1131 $output->check = false;
1132 $output->message = $can_enroll->get_error_message();
1133 }
1134
1135 $course_old = learn_press_get_course( $this->get_id() );
1136 $user_old = learn_press_get_user( $user_id );
1137 $output = apply_filters( 'learn-press/user/can-enroll-course', $output, $course_old, false, $user_old );
1138 if ( $output === false ) {
1139 $can_enroll = new WP_Error( '', '' );
1140 } elseif ( ! $output->check && $output->message ) {
1141 $can_enroll = new WP_Error( 'error_custom', $output->message );
1142 }
1143 //_deprecated_function( 'The learn-press/user/can-enroll-course filter', '4.2.7.3', 'learn-press/user/can-enroll/course' );
1144 }
1145
1146 return apply_filters( 'learn-press/user/can-enroll/course', $can_enroll, $this, $user );
1147 }
1148
1149 /**
1150 * Check user can purchase course.
1151 * @move from can_purchase_course method of LP_User class, since 4.0.8
1152 * @use LP_User::can_purchase_course
1153 *
1154 * @param UserModel|false $user
1155 *
1156 * @return bool|WP_Error
1157 * @since 4.2.7.3
1158 * @version 1.0.0
1159 */
1160 public function can_purchase( $user ) {
1161 $can_purchase = true;
1162 $error_code = '';
1163
1164 $user_id = 0;
1165 if ( $user instanceof UserModel ) {
1166 $user_id = $user->get_id();
1167 }
1168
1169 try {
1170 $can_enroll = $this->can_enroll( $user );
1171 if ( $can_enroll instanceof WP_Error ) {
1172 $error_code_return = [
1173 'course_is_not_purchased_not_login',
1174 'course_is_not_purchased',
1175 'course_is_enrolled',
1176 'course_is_finished',
1177 ];
1178 if ( ! in_array( $can_enroll->get_error_code(), $error_code_return ) ) {
1179 $error_code = $can_enroll->get_error_code();
1180 throw new Exception( $can_enroll->get_error_message() );
1181 }
1182 }
1183
1184 if ( $this->is_free() ) {
1185 $error_code = 'course_is_free';
1186 throw new Exception( __( 'The course is free.', 'learnpress' ) );
1187 }
1188
1189 $enable_no_required_enroll = $this->has_no_enroll_requirement();
1190 if ( $enable_no_required_enroll ) {
1191 $error_code = 'course_is_no_required_enroll';
1192 throw new Exception(
1193 __( 'Enrollment in the course is not mandatory. You can access course for learning now.', 'learnpress' )
1194 );
1195 }
1196
1197 $userCourseModel = UserCourseModel::find( $user_id, $this->get_id(), true );
1198 if ( $user ) {
1199 if ( $userCourseModel ) {
1200 if ( $userCourseModel->has_purchased() ) {
1201 $error_code = 'course_purchased';
1202 throw new Exception( __( 'Course is purchased', 'learnpress' ) );
1203 }
1204
1205 if ( $this->enable_allow_repurchase() ) {
1206 if ( $userCourseModel->has_enrolled() && $userCourseModel->timestamp_remaining_duration() !== 0 ) {
1207 $error_code = 'course_is_enrolled';
1208 throw new Exception( 'This course is already enrolled!' );
1209 }
1210 } else {
1211 if ( $userCourseModel->has_enrolled_or_finished() ) {
1212 $error_code = 'course_is_enrolled_or_finished';
1213 throw new Exception( __( 'Course is enrolled or finished', 'learnpress' ) );
1214 }
1215 }
1216 }
1217 }
1218 } catch ( Throwable $e ) {
1219 if ( empty( $error_code ) ) {
1220 $error_code = 'course_can_not_purchase';
1221 }
1222 $can_purchase = new WP_Error( $error_code, $e->getMessage() );
1223 }
1224
1225 // Hook old
1226 if ( has_filter( 'learn-press/user/can-purchase-course' ) ) {
1227 $can_purchase = apply_filters( 'learn-press/user/can-purchase-course', $can_purchase, $user_id, $this->get_id() );
1228 if ( $can_purchase === false ) {
1229 $can_purchase = new WP_Error( '', '' );
1230 }
1231 //_deprecated_function( 'The learn-press/user/can-purchase-course filter', '4.2.7.3', 'learn-press/user/can-purchase/course' );
1232 }
1233
1234 return apply_filters( 'learn-press/user/can-purchase/course', $can_purchase, $this, $user );
1235 }
1236
1237 /**
1238 * Check user is author or co-in of course.
1239 *
1240 * @param UserModel|false $userModel
1241 *
1242 * @return bool
1243 * @since 4.2.7.6
1244 * @version 1.0.1
1245 */
1246 public function check_user_is_author( $userModel ): bool {
1247 $is_author = false;
1248
1249 if ( $userModel instanceof UserModel
1250 && $userModel->get_id() === $this->post_author ) {
1251 $is_author = true;
1252 }
1253
1254 return apply_filters( 'learn-press/course/is-author', $is_author, $this, $userModel );
1255 }
1256
1257 /**
1258 * Get item model assigned to this course
1259 *
1260 * @return mixed|false|null|WP_Post|PostModel
1261 * @since v4.2.7.6
1262 * @version 1.0.1
1263 */
1264 public function get_item_model( int $item_id, string $item_type ) {
1265 try {
1266 $item = false;
1267
1268 switch ( $item_type ) {
1269 case LP_LESSON_CPT:
1270 $item = LessonPostModel::find( $item_id, true );
1271 break;
1272 case LP_QUIZ_CPT:
1273 $item = QuizPostModel::find( $item_id, true );
1274 break;
1275 case LP_QUESTION_CPT:
1276 break;
1277 default:
1278 $item = apply_filters( 'learn-press/course/get-item-model', $item, $item_id, $item_type, $this );
1279 break;
1280 }
1281
1282 // If not defined class, get post default
1283 if ( ! $item ) {
1284 $filter = new PostFilter();
1285 $filter->ID = $item_id;
1286 $filter->post_type = $item_type;
1287 $item = PostModel::get_item_model_from_db( $filter );
1288 }
1289 } catch ( Exception $e ) {
1290 LP_Debug::error_log( $e );
1291 }
1292
1293 return $item;
1294 }
1295
1296 /**
1297 * Get item model if query success.
1298 * If not exists, return false.
1299 * If exists, return PostModel.
1300 *
1301 * @param LP_Course_JSON_Filter $filter
1302 *
1303 * @return CourseModel|false|static
1304 * @since 4.2.6.9
1305 * @version 1.0.2
1306 */
1307 public static function get_item_model_from_db( LP_Course_JSON_Filter $filter ) {
1308 $course_model = false;
1309
1310 try {
1311 $filter->only_fields = [ 'json', 'post_content' ];
1312
1313 $course_rs = self::get_course_from_db( $filter );
1314 if ( $course_rs instanceof stdClass && isset( $course_rs->json ) ) {
1315 $course_obj = LP_Helper::json_decode( $course_rs->json );
1316 $course_model = new static( $course_obj );
1317 //$course_model->json = $course_rs->json;
1318 $course_model->post_content = $course_rs->post_content;
1319 $course_model->get_author_model();
1320 }
1321 } catch ( Throwable $e ) {
1322 LP_Debug::error_log( $e );
1323 }
1324
1325 return $course_model;
1326 }
1327
1328 /**
1329 * Get course by ID
1330 *
1331 * @param int $course_id
1332 * @param bool $check_cache
1333 *
1334 * @return false|CourseModel|static
1335 * @since 4.2.6.9
1336 * @version 1.0.2
1337 */
1338 public static function find( int $course_id, bool $check_cache = false ) {
1339 if ( ! $course_id ) {
1340 return false;
1341 }
1342
1343 $filter_course = new LP_Course_JSON_Filter();
1344 $filter_course->ID = $course_id;
1345 $key_cache = "courseModel/find/id/{$course_id}";
1346 $lp_course_cache = new LP_Course_Cache();
1347
1348 // Check cache
1349 if ( $check_cache ) {
1350 $course_model = $lp_course_cache->get_cache( $key_cache );
1351 if ( $course_model instanceof CourseModel ) {
1352 return $course_model;
1353 }
1354 }
1355
1356 // Query database no cache.
1357 $course_model = self::get_item_model_from_db( $filter_course );
1358 if ( false === $course_model ) { // Find on table posts
1359 $course_rs = CoursePostModel::find( $course_id );
1360 if ( $course_rs instanceof CoursePostModel ) {
1361 $course_model = new static( $course_rs );
1362 }
1363 }
1364
1365 // Set cache
1366 if ( $course_model instanceof CourseModel ) {
1367 $lp_course_cache->set_cache( $key_cache, $course_model );
1368 }
1369
1370 return $course_model;
1371 }
1372
1373 /**
1374 * Get course from table learnpress_courses
1375 *
1376 * @return array|object|stdClass|null
1377 * @throws Exception
1378 */
1379 private static function get_course_from_db( LP_Course_JSON_Filter $filter ) {
1380 $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1381 $lp_course_json_db->get_query_single_row( $filter );
1382 $query_single_row = $lp_course_json_db->get_courses( $filter );
1383
1384 return $lp_course_json_db->wpdb->get_row( $query_single_row );
1385 }
1386
1387 /**
1388 * Save course data to table learnpress_courses.
1389 *
1390 * @param bool $force_save
1391 *
1392 * @return CourseModel
1393 * @throws Exception
1394 * @since 4.2.6.9
1395 * @version 1.0.3
1396 */
1397 public function save( bool $force_save = false ): CourseModel {
1398 // Check permission
1399 if ( ! $force_save ) {
1400 $this->check_permission();
1401 }
1402
1403 $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1404
1405 $courseObjToJSON = clone $this;
1406 unset( $courseObjToJSON->post_content );
1407 unset( $courseObjToJSON->json );
1408 $this->json = json_encode( $courseObjToJSON, JSON_UNESCAPED_UNICODE );
1409 $data = get_object_vars( $this );
1410
1411 if ( ! isset( $data['ID'] ) ) {
1412 throw new Exception( 'Course ID is invalid!' );
1413 }
1414
1415 $filter = new LP_Course_JSON_Filter();
1416 $filter->ID = $this->ID;
1417 $filter->only_fields = [ 'ID' ];
1418 $course_rs = self::get_course_from_db( $filter );
1419 // Check if exists course id.
1420 if ( empty( $course_rs ) ) { // Insert data.
1421 $lp_course_json_db->insert_data( $data );
1422 } else { // Update data.
1423 $lp_course_json_db->update_data( $data );
1424 }
1425
1426 // Clear cache
1427 $this->clean_caches();
1428
1429 return $this;
1430 }
1431
1432 /**
1433 * Delete row
1434 *
1435 * @throws Exception
1436 */
1437 public function delete() {
1438 $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1439 $filter = new LP_Course_JSON_Filter();
1440 $filter->where[] = $lp_course_json_db->wpdb->prepare( 'AND ID = %d', $this->ID );
1441 $filter->collection = $lp_course_json_db->tb_lp_courses;
1442 $lp_course_json_db->delete_execute( $filter );
1443
1444 // Clear cache
1445 $this->clean_caches();
1446 }
1447
1448 /**
1449 * Clean caches
1450 *
1451 * @return void
1452 * @version 1.0.0
1453 * @since 4.2.7.4
1454 */
1455 public function clean_caches() {
1456 $key_cache = "courseModel/find/id/{$this->ID}";
1457 $lp_course_cache = new LP_Course_Cache();
1458 $lp_course_cache->clear( $key_cache );
1459
1460 // Clear cache image urls, store with many sizes
1461 $img_urls_key_cache = "image_urls/{$this->ID}";
1462 $lp_course_cache->clear_cache_on_group( $img_urls_key_cache );
1463 }
1464
1465 /**
1466 * Check permission to update/create course
1467 *
1468 * @throws Exception
1469 */
1470 public function check_permission() {
1471 $coursePostModel = new CoursePostModel( $this );
1472 if ( ! $coursePostModel->check_capabilities_update() ) {
1473 throw new Exception( 'You do not have permission to update this course!' );
1474 }
1475 }
1476
1477 /**
1478 * Return course's items support.
1479 * To replace learn_press_course_get_support_item_types()
1480 * Should add hook on addons before use this function.
1481 *
1482 * @return array
1483 * @since 4.2.7.4
1484 * @version 1.0.1
1485 */
1486 public static function item_types_support(): array {
1487 $item_types = [
1488 LP_LESSON_CPT,
1489 LP_QUIZ_CPT,
1490 ];
1491
1492 // Hook old
1493 if ( has_filter( 'learn-press/course-item-type' ) ) {
1494 $item_types = apply_filters( 'learn-press/course-item-type', $item_types );
1495 }
1496
1497 $item_types = apply_filters( 'learn-press/course/item-types-support', $item_types );
1498
1499 // set types unique
1500 return array_unique( $item_types );
1501 }
1502
1503 /**
1504 * Get label of item type
1505 *
1506 * @param string $item_type
1507 *
1508 * @return string
1509 * @since 4.2.8.6
1510 * @version 1.0.0
1511 */
1512 public static function item_types_label( string $item_type = '' ): string {
1513 switch ( $item_type ) {
1514 case LP_LESSON_CPT:
1515 $label = __( 'Lesson', 'learnpress' );
1516 break;
1517 case LP_QUIZ_CPT:
1518 $label = __( 'Quiz', 'learnpress' );
1519 break;
1520 default:
1521 $label = ucfirst( strtolower( str_replace( [ 'lp_', '_' ], '', $item_type ) ) );
1522 $label = apply_filters( 'learn-press/course/item-type-label', $label, $item_type );
1523 break;
1524 }
1525
1526 return $label;
1527 }
1528 }
1529