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

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