PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / trunk
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses vtrunk
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 4.2.1.1 All 137 releases
learnpress / inc / Models / CourseModel.php

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

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