Previously, the lms_course group bundle was hard-coded throughout the LMS module. As of LMS 1.2.0, any group bundle whose bundle class implements \Drupal\lms\Entity\Bundle\CourseInterface is treated as a course, so a site can have multiple course bundles with different fields and configuration.
Registering an additional course bundle
Create a group type, then assign it a bundle class that implements CourseInterface (extending the provided Course class is the easiest way):
/**
* Implements hook_entity_bundle_info_alter().
*/
function mymodule_entity_bundle_info_alter(array &$bundles): void {
if (isset($bundles['group']['my_course_type'])) {
$bundles['group']['my_course_type']['class'] = \Drupal\mymodule\Entity\Bundle\MyCourse::class;
}
}
Course routes now restrict their {group} parameter to course bundles dynamically in route subscribers (previously static YAML), so a router rebuild is required after registering or removing a course bundle.
New API
\Drupal\lms\Entity\Bundle\CourseInterface— the contract for course bundle classes. TheCourse::LESSONSconstant now lives on the interface (still accessible asCourse::LESSONS).TrainingManager::getCourseBundleIds()— returns all group bundle IDs whose bundle class implementsCourseInterface(statically cached per request).TrainingManager::loadGroupRelationshipsByCourseBundles(array $properties)— wrapper aroundloadByProperties()that filters to course bundles. Use it instead of hard-coding'group_type' => 'lms_course'.
Backwards-incompatible changes
1. Type hints changed from Course to CourseInterface in public method signatures across the module, including TrainingManager, CourseController, ClassHelper, ClassNameGenerator and the hook_lms_course_link() hook. Code calling these methods keeps working; hook implementations and overrides must update their signatures:
// Before:
function mymodule_lms_course_link(Course $course, AccountInterface $current_user, CacheableMetadata $cacheability): array {}
// After:
function mymodule_lms_course_link(CourseInterface $course, AccountInterface $current_user, CacheableMetadata $cacheability): array {}
If your custom code checks $group instanceof Course, switch to $group instanceof CourseInterface so additional course bundles are recognized.
2. CourseStatusInterface::getCourse() no longer returns NULL. The return type changed from ?Course to CourseInterface. The implementation already threw \Drupal\lms\Exception\TrainingException when the course was missing, so the interface now matches reality — but custom code with NULL checks must switch to catching the exception:
// Before:
$course = $course_status->getCourse();
if ($course === NULL) {
// Orphaned status.
}
// After:
try {
$course = $course_status->getCourse();
}
catch (TrainingException $e) {
// Orphaned status.
}
3. Course navigation block classes renamed. \Drupal\lms\BlockBuilder is now \Drupal\lms\CourseNavBlockBuilder and the lms.block_builder service ID was removed — inject Drupal\lms\CourseNavBlockBuilder instead. The block plugin class StepsBlock is now CourseNavBlock; the plugin ID course_steps_block is unchanged, so existing block placements are unaffected.
4. Class-related Views integration moved to the lms_classes submodule. The ClassMemberCourseStatus views relationship plugin moved from Drupal\lms\Plugin\views\relationship to Drupal\lms_classes\Plugin\views\relationship, and the classes_filter / course_status views data definitions (with the lms_parent_class filter schema) moved from lms to lms_classes. Plugin IDs are unchanged; views using these handlers now require lms_classes to be enabled (they only ever worked with classes).
5. Bundle restrictions removed from routing YAML. The static bundle: [lms_course] parameter options were removed from lms.routing.yml and lms_classes.routing.yml and are now applied dynamically by RouteSubscriber and LmsClassesRouteSubscriber. If you altered these routes, review your alterations.