'GET', 'callback' => [$this, 'get_courses'], 'permission_callback' => '__return_true', ]); register_rest_route('bemalearn/v1', '/courses/(?P\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'get_course'], 'permission_callback' => '__return_true', 'args' => [ 'id' => [ 'required' => true, 'validate_callback' => function ($v) { return is_numeric($v) && (int) $v > 0; }, ], ], ]); } public function get_courses($request) { global $wpdb; $table = $wpdb->prefix . 'bl_courses'; $users = $wpdb->users; $rows = $wpdb->get_results( "SELECT c.id, c.title, c.price_minor, c.currency, c.enrolment_count, c.average_rating, c.is_published, c.published_at, u.display_name AS instructor_name FROM {$table} c LEFT JOIN {$users} u ON u.ID = c.instructor_id WHERE c.is_published = 1 ORDER BY c.published_at DESC, c.id DESC" ); $courses = []; foreach ((array) $rows as $row) { $courses[] = $this->shape($row); } return new WP_REST_Response([ 'courses' => $courses, 'previewExpiresInSeconds' => self::PREVIEW_TTL, ], 200); } public function get_course($request) { global $wpdb; $id = (int) $request->get_param('id'); $table = $wpdb->prefix . 'bl_courses'; $users = $wpdb->users; $row = $wpdb->get_row($wpdb->prepare( "SELECT c.*, u.display_name AS instructor_name FROM {$table} c LEFT JOIN {$users} u ON u.ID = c.instructor_id WHERE c.id = %d", $id )); // An unpublished course is treated as absent. A 403 here would confirm // that the course exists, which is itself a disclosure. if (!$row || !(int) $row->is_published) { return new WP_Error('not_found', 'Course not found.', ['status' => 404]); } $shaped = $this->shape($row); $shaped['description'] = $row->description; $shaped['lessonCount'] = (int) $row->lesson_count; return new WP_REST_Response($shaped, 200); } /** * Turn a database row into the contract shape. * * enrolmentCount and averageRating are nullable in the contract: null means * "not yet counted", which is not the same as a measured zero. They are * cast only when they carry a value. */ private function shape($row) { return [ 'id' => (int) $row->id, 'title' => $row->title, 'instructorName' => $row->instructor_name, 'priceMinor' => (int) $row->price_minor, 'currency' => $row->currency, 'enrolmentCount' => $row->enrolment_count === null ? null : (int) $row->enrolment_count, 'averageRating' => $row->average_rating === null ? null : (float) $row->average_rating, 'publishedAt' => $row->published_at ? gmdate('c', strtotime($row->published_at)) : null, 'isPublished' => (bool) $row->is_published, ]; } }