PluginProbe
Edwiser Bridge – WordPress Moodle Integration / trunk
Edwiser Bridge – WordPress Moodle Integration vtrunk
trunk 1.0.0 1.0.1 1.0.2 1.1 1.1.1 1.1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 All 74 releases
edwiser-bridge / includes / api / class-eb-blocks-course-api.php

class-eb-blocks-course-api.php in Edwiser Bridge – WordPress Moodle Integration trunk, at includes/api/class-eb-blocks-course-api.php

693 lines 27.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use app\wisdmlabs\edwiserBridge\Eb_Enrollment_Manager;
4 use app\wisdmlabs\edwiserBridge\Eb_Payment_Manager;
5
6 use function app\wisdmlabs\edwiserBridge\edwiser_bridge_instance;
7
8 if (!defined('ABSPATH')) {
9 exit;
10 }
11
12 class EdwiserBridge_Blocks_Course_API
13 {
14
15 // API namespace
16 private const API_NAMESPACE = 'eb/api/v1';
17
18 public function __construct()
19 {
20 add_action('rest_api_init', array($this, 'eb_register_course_routes'));
21 }
22
23 /**
24 * Register API routes.
25 */
26 public function eb_register_course_routes()
27 {
28 register_rest_route(self::API_NAMESPACE, '/courses', array(
29 'methods' => WP_REST_Server::READABLE,
30 'callback' => array($this, 'eb_get_courses'),
31 'permission_callback' => '__return_true',
32 'args' => array(
33 'page' => array(
34 'description' => __('Page number for pagination', 'edwiser-bridge'),
35 'type' => 'integer',
36 'default' => 1,
37 'sanitize_callback' => 'absint',
38 ),
39 'per_page' => array(
40 'description' => __('Number of courses per page', 'edwiser-bridge'),
41 'type' => 'integer',
42 'default' => 9,
43 'sanitize_callback' => function( $value ) {
44 return intval( $value );
45 },
46 ),
47 'sort_order' => array(
48 'description' => __('Sort order (latest, oldest, a-z, z-a)', 'edwiser-bridge'),
49 'type' => 'string',
50 'enum' => array('latest', 'oldest', 'a-z', 'z-a'),
51 'default' => 'latest',
52 'sanitize_callback' => 'sanitize_text_field',
53 ),
54 'search' => array(
55 'description' => __('Search term to filter courses', 'edwiser-bridge'),
56 'type' => 'string',
57 'default' => '',
58 'sanitize_callback' => 'sanitize_text_field',
59 ),
60 'group_by_category' => array(
61 'description' => __('Group courses by category', 'edwiser-bridge'),
62 'type' => 'boolean',
63 'default' => false,
64 ),
65 'category_per_page' => array(
66 'description' => __('Number of categories per page when grouping by category', 'edwiser-bridge'),
67 'type' => 'integer',
68 'default' => 0,
69 'sanitize_callback' => 'absint',
70 ),
71 'categories' => array(
72 'description' => __('Comma-separated list of category slugs to filter', 'edwiser-bridge'),
73 'type' => 'string',
74 'default' => '',
75 'sanitize_callback' => 'sanitize_text_field',
76 )
77 )
78 ));
79
80 register_rest_route(self::API_NAMESPACE, '/courses/(?P<course_id>\d+)', array(
81 'methods' => WP_REST_Server::READABLE,
82 'callback' => array($this, 'eb_get_single_course'),
83 'permission_callback' => '__return_true',
84 'args' => array(
85 'course_id' => array(
86 'description' => __('Course ID', 'edwiser-bridge'),
87 'type' => 'integer',
88 'required' => true,
89 'sanitize_callback' => 'absint',
90 ),
91 )
92 ));
93 }
94
95 /**
96 * Get all courses with pagination and filtering
97 *
98 * @param WP_REST_Request $request Request object
99 * @return WP_REST_Response Response with courses data
100 */
101 public function eb_get_courses($request)
102 {
103 // get query parameters
104 $page = max(1, $request['page']);
105 $per_page = $request['per_page'] === -1 ? -1 : max(1, $request['per_page']);
106 $sort_order = strtolower($request['sort_order']);
107 $search_query = strtolower($request['search']);
108 $category = isset($request['category']) ? sanitize_text_field($request['category']) : '';
109 $group_by_category = isset($request['group_by_category']) ? filter_var($request['group_by_category'], FILTER_VALIDATE_BOOLEAN) : false;
110 $category_per_page = isset($request['category_per_page']) ? absint($request['category_per_page']) : 0;
111 $categories_filter = isset($request['categories']) ? sanitize_text_field($request['categories']) : '';
112
113 $user_id = apply_filters('determine_current_user', false);
114 wp_set_current_user($user_id);
115
116 // get the taxonomy used for course categories
117 $taxonomy_names = get_object_taxonomies('eb_course');
118 $category_taxonomy = '';
119
120 // find the category taxonomy
121 foreach ($taxonomy_names as $taxonomy) {
122 if (strpos($taxonomy, 'category') !== false || strpos($taxonomy, 'cat') !== false) {
123 $category_taxonomy = $taxonomy;
124 break;
125 }
126 }
127
128 // Parse the comma-separated categories
129 $selected_categories_slugs = [];
130 if (!empty($categories_filter)) {
131 $selected_categories_slugs = array_map('trim', explode(',', $categories_filter));
132 }
133
134 // get all available categories for eb_course post type
135 $categories = [];
136
137 if (!empty($category_taxonomy)) {
138 $terms_args = [
139 'taxonomy' => $category_taxonomy,
140 'hide_empty' => true,
141 ];
142
143 // Filter categories by slug if specified
144 if (!empty($selected_categories_slugs)) {
145 $terms_args['slug'] = $selected_categories_slugs;
146 }
147
148 $terms = get_terms($terms_args);
149
150 if (!is_wp_error($terms) && !empty($terms)) {
151 foreach ($terms as $term) {
152 $categories[] = [
153 'id' => $term->term_id,
154 'name' => html_entity_decode($term->name, ENT_QUOTES, 'UTF-8'),
155 'slug' => $term->slug,
156 'count' => $term->count
157 ];
158 }
159 }
160 }
161
162 if ($group_by_category) {
163 return $this->get_courses_grouped_by_category(
164 $page,
165 $sort_order,
166 $search_query,
167 $category,
168 $category_taxonomy,
169 $categories,
170 $category_per_page,
171 $selected_categories_slugs
172 );
173 }
174
175 $args = array(
176 'post_type' => 'eb_course',
177 'post_status' => 'publish',
178 'posts_per_page' => $per_page,
179 'paged' => $page
180 );
181
182 if (!empty($search_query)) {
183 $args['s'] = $search_query;
184 }
185
186 // Tax query for filtering
187 $tax_query = [];
188
189 // add category filter if provided
190 if (!empty($category) && !empty($category_taxonomy)) {
191 $tax_query[] = array(
192 'taxonomy' => $category_taxonomy,
193 'field' => 'slug',
194 'terms' => $category,
195 );
196 }
197
198 // Add selected categories filter
199 if (!empty($selected_categories_slugs) && !empty($category_taxonomy)) {
200 $tax_query[] = array(
201 'taxonomy' => $category_taxonomy,
202 'field' => 'slug',
203 'terms' => $selected_categories_slugs,
204 'operator' => 'IN',
205 );
206 }
207
208 // Apply tax query if we have conditions
209 if (!empty($tax_query)) {
210 if (count($tax_query) > 1) {
211 $tax_query['relation'] = 'AND';
212 }
213 $args['tax_query'] = $tax_query;
214 }
215
216 // sort courses
217 switch ($sort_order) {
218 case 'latest':
219 $args['orderby'] = 'post_date';
220 $args['order'] = 'DESC';
221 break;
222 case 'oldest':
223 $args['orderby'] = 'post_date';
224 $args['order'] = 'ASC';
225 break;
226 case 'a-z':
227 $args['orderby'] = 'post_title';
228 $args['order'] = 'ASC';
229 break;
230 case 'z-a':
231 $args['orderby'] = 'post_title';
232 $args['order'] = 'DESC';
233 break;
234 }
235
236 // get courses
237 $courses = get_posts($args);
238 $formatted_courses = [];
239 $enrolled_courses = \app\wisdmlabs\edwiserBridge\eb_get_user_enrolled_courses();
240
241 foreach ($courses as &$course) {
242 $is_enrolled = in_array($course->ID, $enrolled_courses);
243 $course_data = apply_filters('eb_content_course_before', $course->ID, array(), $is_enrolled);
244
245 $course_categories = [];
246
247 foreach ($course_data['categories'] as $id => $name) {
248 $course_categories[] = [
249 'id' => (int)$id,
250 'name' => $name
251 ];
252 }
253
254 // extract price and currency
255 $price_info = $this->extract_price_info($course_data['course_price_formatted'] ?? '$ 0');
256
257 $formatted_courses[] = array(
258 'id' => $course->ID,
259 'title' => $course->post_title,
260 'link' => get_permalink($course->ID),
261 'excerpt' => !empty($course_data['short_description']) ? $course_data['short_description'] : wp_strip_all_tags(html_entity_decode($course->post_content)),
262 'category' => !empty($course_data['categories']) ? html_entity_decode(reset($course_data['categories']), ENT_QUOTES, 'UTF-8') : __('Uncategorized', 'edwiser-bridge'),
263 'thumbnail' => $course_data['thumb_url'],
264 'suspended' => \app\wisdmlabs\edwiserBridge\wdm_eb_get_user_suspended_status($user_id, $course->ID) == true,
265 'price' => [
266 'amount' => $price_info['amount'],
267 'currency' => $price_info['currency'],
268 'type' => $course_data['course_price_type'],
269 'enrolled' => $course_data['is_eb_my_courses'],
270 'originalAmount' => null,
271 ],
272 'createdAt' => $course->post_date,
273 'categories' => $course_categories,
274 );
275 }
276
277 // get total courses (without pagination)
278 $total_args = $args;
279 $total_args['posts_per_page'] = -1;
280 unset($total_args['paged']);
281 $total_courses = count(get_posts($total_args));
282
283 return new WP_REST_Response(array(
284 'total_courses' => $total_courses,
285 'total_pages' => $per_page === -1 ? 1 : ceil($total_courses / $per_page),
286 'current_page' => $page,
287 'per_page' => $per_page,
288 'courses' => $formatted_courses,
289 'categories' => $categories,
290 ), 200);
291 }
292
293 /**
294 * Get courses grouped by category with pagination
295 *
296 * @param int $page Current page number
297 * @param int $per_page Courses per page
298 * @param string $sort_order Sort order
299 * @param string $search_query Search query
300 * @param string $selected_category Selected category slug
301 * @param string $category_taxonomy Category taxonomy name
302 * @param array $categories Array of category data
303 * @param array $all_category_terms Array of WP_Term objects
304 * @param int $category_per_page Number of categories per page
305 * @return WP_REST_Response Response with courses grouped by category
306 */
307 private function get_courses_grouped_by_category(
308 $page,
309 $sort_order,
310 $search_query,
311 $selected_category,
312 $category_taxonomy,
313 $categories,
314 $category_per_page,
315 $selected_categories_slugs = []
316 ) {
317 $categorized_courses = [];
318 $selected_categories = [];
319 $enrolled_courses = \app\wisdmlabs\edwiserBridge\eb_get_user_enrolled_courses();
320
321 $user_id = apply_filters('determine_current_user', false);
322 wp_set_current_user($user_id);
323
324 // Determine which categories to show
325 if (!empty($selected_category)) {
326 // If a specific category is selected, only show that category
327 foreach ($categories as $cat) {
328 if ($cat['slug'] === $selected_category) {
329 $selected_categories[] = $cat;
330 break;
331 }
332 }
333 } else {
334 // Show all categories (or paginated if category_per_page is set)
335 if ($category_per_page > 0) {
336 $offset = ($page - 1) * $category_per_page;
337 $selected_categories = array_slice($categories, $offset, $category_per_page);
338 } else {
339 $selected_categories = $categories;
340 }
341 }
342
343 // Get courses for each selected category
344 foreach ($selected_categories as $category) {
345 $args = array(
346 'post_type' => 'eb_course',
347 'post_status' => 'publish',
348 'posts_per_page' => -1, // Get all courses for this category
349 'tax_query' => array(
350 array(
351 'taxonomy' => $category_taxonomy,
352 'field' => 'slug',
353 'terms' => $category['slug'],
354 ),
355 )
356 );
357
358 // Add search query if provided
359 if (!empty($search_query)) {
360 $args['s'] = $search_query;
361 }
362
363 // Apply additional category filtering if specified
364 if (!empty($selected_categories_slugs) && $category_taxonomy) {
365 $args['tax_query'] = array(
366 'relation' => 'AND',
367 array(
368 'taxonomy' => $category_taxonomy,
369 'field' => 'slug',
370 'terms' => $category['slug'],
371 ),
372 array(
373 'taxonomy' => $category_taxonomy,
374 'field' => 'slug',
375 'terms' => $selected_categories_slugs,
376 'operator' => 'IN',
377 )
378 );
379 }
380
381 // Sort courses
382 switch ($sort_order) {
383 case 'latest':
384 $args['orderby'] = 'post_date';
385 $args['order'] = 'DESC';
386 break;
387 case 'oldest':
388 $args['orderby'] = 'post_date';
389 $args['order'] = 'ASC';
390 break;
391 case 'a-z':
392 $args['orderby'] = 'post_title';
393 $args['order'] = 'ASC';
394 break;
395 case 'z-a':
396 $args['orderby'] = 'post_title';
397 $args['order'] = 'DESC';
398 break;
399 }
400
401 $courses = get_posts($args);
402 $formatted_cat_courses = [];
403
404 foreach ($courses as $course) {
405 $is_enrolled = in_array($course->ID, $enrolled_courses);
406 $course_data = apply_filters('eb_content_course_before', $course->ID, array(), $is_enrolled);
407
408 $course_categories = [];
409
410 foreach ($course_data['categories'] as $id => $name) {
411 $course_categories[] = [
412 'id' => (int)$id,
413 'name' => $name
414 ];
415 }
416
417 // Extract price and currency
418 $price_info = $this->extract_price_info($course_data['course_price_formatted'] ?? '$ 0');
419
420 $formatted_cat_courses[] = array(
421 'id' => $course->ID,
422 'title' => $course->post_title,
423 'link' => get_permalink($course->ID),
424 'excerpt' => !empty($course_data['short_description']) ? $course_data['short_description'] : wp_strip_all_tags(html_entity_decode($course->post_content)),
425 'category' => $category['name'],
426 'thumbnail' => $course_data['thumb_url'],
427 'suspended' => \app\wisdmlabs\edwiserBridge\wdm_eb_get_user_suspended_status($user_id, $course->ID) == true,
428 'price' => [
429 'amount' => $price_info['amount'],
430 'currency' => $price_info['currency'],
431 'type' => $course_data['course_price_type'],
432 'enrolled' => $course_data['is_eb_my_courses'],
433 'originalAmount' => null,
434 ],
435 'createdAt' => $course->post_date,
436 'categories' => $course_categories,
437 );
438 }
439
440 if (!empty($formatted_cat_courses)) {
441 $categorized_courses[$category['slug']] = $formatted_cat_courses;
442 }
443 }
444
445 // Calculate total pages for category pagination
446 $total_pages = 1;
447 if ($category_per_page > 0 && count($categories) > 0) {
448 if (!empty($selected_category)) {
449 $total_pages = 1;
450 } else {
451 $total_pages = ceil(count($categories) / $category_per_page);
452 }
453 }
454
455 return new WP_REST_Response(array(
456 'total_pages' => $total_pages,
457 'current_page' => $page,
458 'categories' => $categories,
459 'displayed_categories' => $selected_categories,
460 'categorized_courses' => $categorized_courses,
461 ), 200);
462 }
463
464 /**
465 * Get single course by ID
466 *
467 * @param WP_REST_Request $request Request object
468 * @return WP_REST_Response|WP_Error Response with course data or error
469 */
470 public function eb_get_single_course($request)
471 {
472 // Start output buffering to capture any unwanted output from filters/hooks
473 if (ob_get_level() === 0) {
474 ob_start();
475 }
476
477 $course_id = $request['course_id'];
478 $course = get_post($course_id);
479
480 $user_id = apply_filters('determine_current_user', false);
481 wp_set_current_user($user_id);
482
483 if (!$course || $course->post_type !== 'eb_course') {
484 // Clean buffer only if one exists
485 if (ob_get_level() > 0) {
486 ob_end_clean();
487 }
488 return new WP_Error('no_course', __('Course not found', 'edwiser-bridge'), ['status' => 404]);
489 }
490
491 $enrolled_courses = \app\wisdmlabs\edwiserBridge\eb_get_user_enrolled_courses();
492 $is_enrolled = in_array($course->ID, $enrolled_courses);
493 $course_data = apply_filters('eb_content_course_before', $course->ID, [], $is_enrolled);
494
495 // Extract price information
496 $price_info = $this->extract_price_info($course_data['course_price_formatted'] ?? '$ 0');
497
498 // Get course options
499 $course_options = get_post_meta($course->ID, 'eb_course_options', true);
500
501 // Get recommended courses if enabled
502 $recommended_courses = [];
503 $show_recommended = isset($course_options['enable_recmnd_courses']) && $course_options['enable_recmnd_courses'] === 'yes';
504
505 if ($show_recommended) {
506 $recommended_courses = $this->get_recommended_courses($course, $course_options, $course_data);
507 }
508
509 // Get course CTA details
510 $has_course_access = edwiser_bridge_instance()->enrollment_manager()->user_has_course_access($user_id, $course_id);
511 $user_is_suspended = \app\wisdmlabs\edwiserBridge\wdm_eb_get_user_suspended_status($user_id, $course_id);
512
513 $show_take_course_button = !$has_course_access || $user_is_suspended || !is_user_logged_in();
514
515 $course_cta_html = $show_take_course_button
516 ? Eb_Payment_Manager::take_course_button($course_id)
517 : Eb_Payment_Manager::access_course_button($course_id);
518
519 $course_cta = wp_kses($course_cta_html, \app\wisdmlabs\edwiserBridge\wdm_eb_sinlge_course_get_allowed_html_tags());
520
521 $is_user_suspended = \app\wisdmlabs\edwiserBridge\wdm_eb_get_user_suspended_status($user_id, $course->ID);
522 $status = null;
523 if ($is_user_suspended) {
524 $status = 'suspended';
525 } elseif ($is_enrolled) {
526 $status = 'enrolled';
527 }
528
529 $remaining_access = Eb_Enrollment_Manager::access_remianing($user_id, $course->ID);
530
531 $categories = [];
532
533 foreach ($course_data['categories'] as $id => $name) {
534 $categories[] = [
535 'id' => (int)$id,
536 'name' => $name
537 ];
538 }
539
540 $response_data = [
541 'id' => $course->ID,
542 'title' => $course->post_title,
543 'content' => apply_filters('the_content', $course->post_content),
544 'category' => !empty($course_data['categories']) ? reset($course_data['categories']) : __('Uncategorized', 'edwiser-bridge'),
545 'permalink' => get_permalink($course->ID),
546 'thumbnail' => $course_data['thumb_url'],
547 'course_expiry' => isset($course_options['course_expirey']) && $course_options['course_expirey'] === 'yes',
548 'remaining_access' => $remaining_access,
549 'course_closed_url' => $course_options['course_closed_url'] ?? '',
550 'status' => $status,
551 'price' => [
552 'amount' => $price_info['amount'],
553 'currency' => $price_info['currency'],
554 'type' => $course_data['course_price_type'] ?? '',
555 'originalAmount' => null,
556 ],
557 'course_cta' => $course_cta,
558 'moodle_course_id' => $course_options['moodle_course_id'] ?? 0,
559 'show_recommended_courses' => $show_recommended,
560 'recommended_courses' => $recommended_courses,
561 'categories' => $categories
562 ];
563
564 if (isset($course_options['course_expirey']) && $course_options['course_expirey'] === 'yes') {
565 $response_data['course_expires_after_days'] = is_user_logged_in() && $is_enrolled && '0000-00-00 00:00:00' !== $remaining_access ? $remaining_access : $course_options['num_days_course_access'];
566 }
567
568 // Clean buffer only if one exists to avoid PHP notices
569 if (ob_get_level() > 0) {
570 ob_end_clean();
571 }
572 return new WP_REST_Response($response_data, 200);
573 }
574
575 /**
576 * Extract currency symbol and amount from formatted price string
577 *
578 * @param string $formatted_price The formatted price string
579 * @return array Array with currency symbol and amount
580 */
581 private function extract_price_info($formatted_price)
582 {
583 preg_match('/^(\D+)\s*(\d+(\.\d+)?)/', $formatted_price, $matches);
584
585 return [
586 'currency' => isset($matches[1]) ? trim($matches[1]) : '$',
587 'amount' => isset($matches[2]) ? floatval($matches[2]) : 0
588 ];
589 }
590
591 /**
592 * Get recommended courses for a specific course
593 *
594 * @param WP_Post $course The course post object
595 * @param array $course_options Course options metadata
596 * @param array $course_data Course data from filters
597 * @return array Array of recommended course post objects
598 */
599 private function get_recommended_courses($course, $course_options, $course_data)
600 {
601 // If recommended courses are not enabled, return empty array
602 if (!isset($course_options['enable_recmnd_courses']) || $course_options['enable_recmnd_courses'] !== 'yes') {
603 return [];
604 }
605
606 $user_id = apply_filters('determine_current_user', false);
607 wp_set_current_user($user_id);
608
609 $query_args = [];
610
611 // Default recommendation based on category
612 if (isset($course_options['show_default_recmnd_course']) && $course_options['show_default_recmnd_course'] === 'yes') {
613 $query_args = [
614 'post_type' => 'eb_course',
615 'post_status' => 'publish',
616 'orderby' => 'date',
617 'order' => 'DESC',
618 'numberposts' => 3,
619 'post__not_in' => [$course->ID], // Exclude current course
620 'tax_query' => [
621 [
622 'taxonomy' => 'eb_course_cat',
623 'field' => 'tag_ID',
624 'terms' => array_keys($course_data['categories'] ?? []),
625 ],
626 ],
627 ];
628 }
629 // Specific recommended courses
630 elseif (!empty($course_options['enable_recmnd_courses_single_course'])) {
631 $query_args = [
632 'post_type' => 'eb_course',
633 'post_status' => 'publish',
634 'orderby' => 'date',
635 'order' => 'DESC',
636 'post__in' => $course_options['enable_recmnd_courses_single_course'],
637 'post__not_in' => [$course->ID], // Exclude current course
638 ];
639 }
640
641 // If no valid query args, return empty array
642 if (empty($query_args)) {
643 return [];
644 }
645
646 // Get recommended courses
647 $courses = get_posts($query_args);
648 $formatted_courses = [];
649 $enrolled_courses = \app\wisdmlabs\edwiserBridge\eb_get_user_enrolled_courses();
650
651 // Format each course
652 foreach ($courses as $rec_course) {
653 $is_enrolled = in_array($rec_course->ID, $enrolled_courses);
654 $rec_course_data = apply_filters('eb_content_course_before', $rec_course->ID, [], $is_enrolled);
655
656 $rec_course_categories = [];
657
658 foreach ($rec_course_data['categories'] as $id => $name) {
659 $rec_course_categories[] = [
660 'id' => (int)$id,
661 'name' => $name
662 ];
663 }
664
665 // Extract price information
666 $price_info = $this->extract_price_info($rec_course_data['course_price_formatted'] ?? '$ 0');
667
668 $formatted_courses[] = [
669 'id' => $rec_course->ID,
670 'title' => $rec_course->post_title,
671 'link' => get_permalink($rec_course->ID),
672 'excerpt' => !empty($rec_course_data['short_description']) ? $rec_course_data['short_description'] : wp_strip_all_tags(html_entity_decode($rec_course->post_content)),
673 'category' => !empty($rec_course_data['categories']) ? reset($rec_course_data['categories']) : __('Uncategorized', 'edwiser-bridge'),
674 'thumbnail' => $rec_course_data['thumb_url'] ?? '',
675 'suspended' => \app\wisdmlabs\edwiserBridge\wdm_eb_get_user_suspended_status($user_id, $rec_course->ID) == true,
676 'price' => [
677 'amount' => $price_info['amount'],
678 'currency' => $price_info['currency'],
679 'type' => $rec_course_data['course_price_type'] ?? '',
680 'enrolled' => $rec_course_data['is_eb_my_courses'] ?? false,
681 'originalAmount' => null,
682 ],
683 'createdAt' => $rec_course->post_date,
684 'categories' => $rec_course_categories,
685 ];
686 }
687
688 return $formatted_courses;
689 }
690 }
691
692 new EdwiserBridge_Blocks_Course_API();
693