Whoops! There was an error.
Whoops \ Exception \ ErrorException (E_WARNING)
Undefined variable $rating Whoops\Exception\ErrorException thrown with message "Undefined variable $rating" Stacktrace: #8 Whoops\Exception\ErrorException in /var/www/html/public/local/lernhive/classes/dashboard/course_catalog_provider.php:620 #7 default_error_handler in /var/www/html/public/local/lernhive/classes/dashboard/course_catalog_provider.php:620 #6 local_lernhive\dashboard\course_catalog_provider:course_to_offering in /var/www/html/public/local/lernhive/classes/dashboard/course_catalog_provider.php:127 #5 local_lernhive\dashboard\course_catalog_provider:get_offerings in /var/www/html/public/local/lernhive/classes/hook_callbacks.php:132 #4 local_lernhive\hook_callbacks:contribute_course_catalog_offerings in /var/www/html/public/lib/classes/hook/manager.php:323 #3 call_user_func in /var/www/html/public/lib/classes/hook/manager.php:323 #2 core\hook\manager:dispatch in /var/www/html/public/local/lernhive/classes/dashboard/catalog_registry.php:49 #1 local_lernhive\dashboard\catalog_registry:collect_offerings in /var/www/html/public/local/lernhive/classes/output/catalog_page.php:117 #0 local_lernhive\output\catalog_page:export_for_template in /var/www/html/public/local/lernhive/catalog.php:112
Stack frames (9)
8
Whoops\Exception\ErrorException
/public/local/lernhive/classes/dashboard/course_catalog_provider.php:620
7
default_error_handler
/public/local/lernhive/classes/dashboard/course_catalog_provider.php:620
6
local_lernhive\dashboard\course_catalog_provider course_to_offering
/public/local/lernhive/classes/dashboard/course_catalog_provider.php:127
5
local_lernhive\dashboard\course_catalog_provider get_offerings
/public/local/lernhive/classes/hook_callbacks.php:132
4
local_lernhive\hook_callbacks contribute_course_catalog_offerings
/public/lib/classes/hook/manager.php:323
3
call_user_func
/public/lib/classes/hook/manager.php:323
2
core\hook\manager dispatch
/public/local/lernhive/classes/dashboard/catalog_registry.php:49
1
local_lernhive\dashboard\catalog_registry collect_offerings
/public/local/lernhive/classes/output/catalog_page.php:117
0
local_lernhive\output\catalog_page export_for_template
/public/local/lernhive/catalog.php:112
/var/www/html/public/local/lernhive/classes/dashboard/course_catalog_provider.php
            timecreated: !empty($course->timecreated) ? (int) $course->timecreated : null,
            availability: $requiresapproval
                ? offering_data::AVAILABILITY_ON_REQUEST
                : offering_data::AVAILABILITY_BOOKABLE,
            score: !empty($markings['featured']) ? 40 : 10,
            metaitems: $meta,
            capacitystatus: $capacitystatus,
            capacitylabel: $capacitylabel,
            capacityfill: $capacityfill,
            capacityisprogress: $isenrolled,
            showcapacitybar: $isenrolled,
            trainers: self::trainers_data($context),
            bookmarkable: $userid > 0,
            bookmarkcomponent: 'core_course',
            bookmarkitemtype: 'course',
            bookmarkitemid: (string) $course->id,
            flags: $flags,
            featured: !empty($markings['featured']),
            ispublic: !empty($markings['publicvisible']),
            ratingaverage: $rating && $rating->count > 0 ? (float) $rating->aggregate : null,
            ratingcount: $rating ? (int) $rating->count : 0,
        );
    }
 
    /**
     * Build image-overlay flags (recommended / mandatory / paid+price) from a
     * course's markings.
     *
     * @param array{recommended:bool,mandatory:bool,paid:bool,price:string} $markings
     * @param bool $needsapproval
     * @return array<int,array{label:string,tone:string,icon:string}>
     */
    private static function flags_from_markings(array $markings, bool $needsapproval = false): array {
        $flags = [];
        if (!empty($markings['mandatory'])) {
            $flags[] = [
                'label' => get_string('flag_mandatory', 'local_lernhive'),
                'tone' => 'mandatory',
                'icon' => 'fa-exclamation',
            ];
/var/www/html/public/local/lernhive/classes/dashboard/course_catalog_provider.php
            timecreated: !empty($course->timecreated) ? (int) $course->timecreated : null,
            availability: $requiresapproval
                ? offering_data::AVAILABILITY_ON_REQUEST
                : offering_data::AVAILABILITY_BOOKABLE,
            score: !empty($markings['featured']) ? 40 : 10,
            metaitems: $meta,
            capacitystatus: $capacitystatus,
            capacitylabel: $capacitylabel,
            capacityfill: $capacityfill,
            capacityisprogress: $isenrolled,
            showcapacitybar: $isenrolled,
            trainers: self::trainers_data($context),
            bookmarkable: $userid > 0,
            bookmarkcomponent: 'core_course',
            bookmarkitemtype: 'course',
            bookmarkitemid: (string) $course->id,
            flags: $flags,
            featured: !empty($markings['featured']),
            ispublic: !empty($markings['publicvisible']),
            ratingaverage: $rating && $rating->count > 0 ? (float) $rating->aggregate : null,
            ratingcount: $rating ? (int) $rating->count : 0,
        );
    }
 
    /**
     * Build image-overlay flags (recommended / mandatory / paid+price) from a
     * course's markings.
     *
     * @param array{recommended:bool,mandatory:bool,paid:bool,price:string} $markings
     * @param bool $needsapproval
     * @return array<int,array{label:string,tone:string,icon:string}>
     */
    private static function flags_from_markings(array $markings, bool $needsapproval = false): array {
        $flags = [];
        if (!empty($markings['mandatory'])) {
            $flags[] = [
                'label' => get_string('flag_mandatory', 'local_lernhive'),
                'tone' => 'mandatory',
                'icon' => 'fa-exclamation',
            ];
/var/www/html/public/local/lernhive/classes/dashboard/course_catalog_provider.php
                continue;
            }
            // Date-range facet ("Termine von/bis"): a date window asks for
            // offerings *happening* inside it, so undated courses drop out
            // while the filter is active.
            if ($filter->from !== null || $filter->to !== null) {
                $startdate = (int) ($course->startdate ?? 0);
                if (
                    $startdate <= 0
                    || ($filter->from !== null && $startdate < $filter->from)
                    || ($filter->to !== null && $startdate > $filter->to)
                ) {
                    continue;
                }
            }
            if (!self::matches_access($course, $filter, $context, $userid)) {
                continue;
            }
 
            $offerings[] = self::course_to_offering($course, $context, $userid, $markings);
            if (count($offerings) >= self::RESULT_LIMIT) {
                break;
            }
        }
 
        return $offerings;
    }
 
    /**
     * Return course-specific catalogue facets.
     *
     * @param int $userid Viewer identifier.
     * @return facet[] Available facets.
     */
    public static function get_facets(int $userid): array {
        $facets = [
            new facet(
                id: 'catalog_focus',
                label: get_string('catalog_filter_group_focus', 'local_lernhive'),
                type: facet::TYPE_TOGGLE,
/var/www/html/public/local/lernhive/classes/hook_callbacks.php
            return;
        }
        if (($PAGE->theme->name ?? '') !== 'lernhive') {
            return;
        }
        if (!level_manager::get_level_record($USER->id)) {
            return;
        }
 
        $scope = level_manager::get_level($USER->id) <= 1 ? 'explorer' : 'lernhive';
        $hook->add_attribute('data-lh-ui', $scope);
    }
 
    /**
     * Contribute ordinary Moodle courses to the central LernHive catalogue.
     *
     * @param collect_offerings $hook
     */
    public static function contribute_course_catalog_offerings(collect_offerings $hook): void {
        $hook->add_offerings(course_catalog_provider::get_offerings($hook->userid, $hook->filter));
    }
 
    /**
     * Contribute core catalogue facets.
     *
     * @param collect_facets $hook
     */
    public static function contribute_course_catalog_facets(collect_facets $hook): void {
        $hook->add_facets(course_catalog_provider::get_facets($hook->userid));
    }
 
    /**
     * Register core catalogue sort options.
     *
     * @param collect_sort_options $hook Sort-option collection hook.
     */
    public static function contribute_course_catalog_sort_options(collect_sort_options $hook): void {
        // Core owns the built-in modes. Feature plugins may register more modes.
    }
 
/var/www/html/public/lib/classes/hook/manager.php
        if (empty($callbacks)) {
            // Nothing is interested in this hook.
            return $event;
        }
 
        foreach ($callbacks as $callback) {
            // Note: PSR-14 states:
            // If passed a Stoppable Event, a Dispatcher
            // MUST call isPropagationStopped() on the Event before each Listener has been called.
            // If that method returns true it MUST return the Event to the Emitter immediately and
            // MUST NOT call any further Listeners. This implies that if an Event is passed to the
            // Dispatcher that always returns true from isPropagationStopped(), zero listeners will be called.
            // Ergo, we check for a stopped event before calling each listener, not afterwards.
            if ($event instanceof StoppableEventInterface) {
                if ($event->isPropagationStopped()) {
                    return $event;
                }
            }
 
            call_user_func($callback, $event);
        }
 
        // Developers need to be careful to not create infinite loops in hook callbacks.
        return $event;
    }
 
    /**
     * Initialise list of all callbacks for each hook.
     */
    private function init_standard_callbacks(): void {
        global $CFG;
 
        $this->allcallbacks = [];
        $this->alldeprecations = [];
 
        // @codeCoverageIgnoreStart
        $shouldcache = $this->should_cache();
        if ($shouldcache) {
            $cache = $this->get_cache();
 
/var/www/html/public/lib/classes/hook/manager.php
        if (empty($callbacks)) {
            // Nothing is interested in this hook.
            return $event;
        }
 
        foreach ($callbacks as $callback) {
            // Note: PSR-14 states:
            // If passed a Stoppable Event, a Dispatcher
            // MUST call isPropagationStopped() on the Event before each Listener has been called.
            // If that method returns true it MUST return the Event to the Emitter immediately and
            // MUST NOT call any further Listeners. This implies that if an Event is passed to the
            // Dispatcher that always returns true from isPropagationStopped(), zero listeners will be called.
            // Ergo, we check for a stopped event before calling each listener, not afterwards.
            if ($event instanceof StoppableEventInterface) {
                if ($event->isPropagationStopped()) {
                    return $event;
                }
            }
 
            call_user_func($callback, $event);
        }
 
        // Developers need to be careful to not create infinite loops in hook callbacks.
        return $event;
    }
 
    /**
     * Initialise list of all callbacks for each hook.
     */
    private function init_standard_callbacks(): void {
        global $CFG;
 
        $this->allcallbacks = [];
        $this->alldeprecations = [];
 
        // @codeCoverageIgnoreStart
        $shouldcache = $this->should_cache();
        if ($shouldcache) {
            $cache = $this->get_cache();
 
/var/www/html/public/local/lernhive/classes/dashboard/catalog_registry.php
 * @package    local_lernhive
 * @copyright  2026 LernHive.de
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class catalog_registry {
    /**
     * Collect offerings for a user under the given filter.
     *
     * Sort order: relevance score descending, then start timestamp ascending
     * (offerings without a start date land last among same-score items), then
     * source-component for deterministic ordering.
     *
     * @param int $userid User id to render for; 0 renders the public
     *        (anonymous) catalogue and keeps only opted-in public offerings.
     * @param search_filter $filter Resolved filter values.
     * @return offering_data[] Sorted as documented above.
     */
    public static function collect_offerings(int $userid, search_filter $filter, string $sortid = 'relevance'): array {
        $hook = new collect_offerings($userid, $filter);
        di::get(hook_manager::class)->dispatch($hook);
 
        $offerings = $hook->get_offerings();
        $offerings = self::apply_visibility_filters($userid, $offerings, $filter);
 
        $option = self::sort_option($userid, $sortid);
        usort($offerings, static function (offering_data $a, offering_data $b) use ($option): int {
            $result = $option->compare($a, $b);
            return $result !== 0 ? $result : [$a->sourcecomponent, $a->id] <=> [$b->sourcecomponent, $b->id];
        });
 
        return $offerings;
    }
 
    /**
     * Collect built-in and provider-declared sort modes.
     *
     * @return sort_option[]
     */
    public static function collect_sort_options(int $userid): array {
        $hook = new collect_sort_options($userid);
/var/www/html/public/local/lernhive/classes/output/catalog_page.php
     */
    private function url(array $params = []): \moodle_url {
        $url = $this->baseurl
            ? new \moodle_url($this->baseurl)
            : new \moodle_url('/local/lernhive/catalog.php');
        foreach ($params as $name => $value) {
            $url->param($name, $value);
        }
        return $url;
    }
 
    /**
     * Export the catalogue template context.
     *
     * @param \renderer_base $output Renderer instance.
     * @return array<string,mixed> Template context.
     */
    public function export_for_template(\renderer_base $output): array {
        $sortid = (string) ($this->rawparams['sort'] ?? 'relevance');
        $offerings = catalog_registry::collect_offerings($this->userid, $this->filter, $sortid);
        $sortoptions = catalog_registry::collect_sort_options($this->userid);
        $selectedsort = catalog_registry::sort_option($this->userid, $sortid);
 
        // Preserve any non-search params (e.g. format=, category=) across the
        // search-submit so a click on the search button does not silently drop
        // an active tag-driven filter.
        $extras = [];
        foreach ($this->rawparams as $name => $value) {
            // Skip params that render as their own visible form fields.
            if (in_array($name, ['q', 'datefrom', 'dateto', 'sort'], true) || !is_string($value) || $value === '') {
                continue;
            }
            $extras[] = ['name' => $name, 'value' => $value];
        }
        // A GET form drops the query string of its action, so any param the
        // hosting page needs for itself (e.g. index.php's `redirect=0`) has to
        // ride along as a hidden field or the search submit would lose it.
        if ($this->baseurl) {
            foreach ($this->baseurl->params() as $name => $value) {
                if (array_key_exists($name, $this->rawparams) || $value === '') {
/var/www/html/public/local/lernhive/catalog.php
 
// View switcher (cards | month) — shared with the full-view catalogue block,
// see `catalog_view`.
$togglebase = new moodle_url('/local/lernhive/catalog.php', $rawparams);
echo catalog_view::view_toggle($togglebase, $view);
 
if ($view === catalog_view::VIEW_MONTH) {
    $monthhtml = catalog_view::month_html(
        $OUTPUT,
        $PAGE,
        $togglebase,
        $filter,
        $rawparams,
        $calendartime,
        (int) ($rawparams['expand'] ?? 0)
    );
    $renderable = new catalog_page($userid, $filter, $rawparams, $monthhtml);
    echo $OUTPUT->render_from_template(
        'local_lernhive/catalog_page',
        $renderable->export_for_template($OUTPUT)
    );
} else {
    $renderable = new catalog_page($userid, $filter, $rawparams);
    echo $OUTPUT->render_from_template(
        'local_lernhive/catalog_page',
        $renderable->export_for_template($OUTPUT)
    );
}
 
echo html_writer::end_div();
echo $OUTPUT->footer();
 

Environment & details:

Key Value
format online,hybrid
time 3450121200
view month
empty
empty
empty
Key Value
USER stdClass Object ( [id] => 0 [mnethostid] => 1 [access] => Array ( [ra] => Array ( [/1] => Array ( [6] => 6 ) ) [time] => 1789316012 [rsw] => Array ( ) ) [enrol] => Array ( [enrolled] => Array ( ) [tempguest] => Array ( ) ) [sesskey] => u43i33XBpF [preference] => Array ( ) )
SESSION stdClass Object ( [isnewsessioncookie] => 1 [cachestore_session] => Array ( [default_session-core/coursecat] => Array ( [__lastaccess__u0_d0f58e9345eaa0160cb589b657fa1587] => Array ( [0] => 1789316012 [1] => 1789316012 ) [u0_d0f58e9345eaa0160cb589b657fa1587_260b57ae1c3966b1910137be05eb286db0e0c867] => Array ( [0] => 1789316012.0886-6aa6cbac15a297.74084180 [1] => 1789316012 ) [u0_d0f58e9345eaa0160cb589b657fa1587_0878690aecf796a687f345a5cf7ce17a758a13e6] => Array ( [0] => Array ( [2] => Array ( [name] => Admin User [path] => /2 ) [58] => Array ( [name] => Praxisbeispiele [path] => /58 ) [59] => Array ( [name] => Einstieg und Pflichtwissen [path] => /58/59 ) [60] => Array ( [name] => Zusammenarbeit und Führung [path] => /58/60 ) [61] => Array ( [name] => Kunden, Produkt und Daten [path] => /58/61 ) [62] => Array ( [name] => Seminarveranstaltungen [path] => /62 ) ) [1] => 1789316012 ) [u0_d0f58e9345eaa0160cb589b657fa1587_eae4bdad609813f1131741916a481c2b05ca4ee3] => Array ( [0] => Array ( ) [1] => 1789316012 ) [u0_d0f58e9345eaa0160cb589b657fa1587_f94cf60b9b9490154c92b9fc367aa7e1af8368de] => Array ( [0] => Array ( [0] => 2 [1] => 58 [2] => 62 ) [1] => 1789316012 ) ) [default_session-core/navigation_cache] => Array ( [__lastaccess__u0_d0f58e9345eaa0160cb589b657fa1587] => Array ( [0] => 1789316012 [1] => 1789316012 ) ) [default_session-theme_boost_union/flavours] => Array ( [__lastaccess__u0_d0f58e9345eaa0160cb589b657fa1587] => Array ( [0] => 1789316012 [1] => 0 ) [u0_d0f58e9345eaa0160cb589b657fa1587_lastinvalidation-ab95758982f7eb97b18eb12d60b3d0cb] => Array ( [0] => 1789316012.0886-6aa6cbac15a297.74084180 [1] => 0 ) [u0_d0f58e9345eaa0160cb589b657fa1587_0-ab95758982f7eb97b18eb12d60b3d0cb] => Array ( [0] => [1] => 0 ) ) ) [theme_boost_union_menu_purgesessioncache] => 1 [theme_boost_union_prevlang] => en )
Key Value
HTTP_HOST dev.lernhive.de
HTTP_USER_AGENT Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
HTTP_ACCEPT */*
HTTP_ACCEPT_ENCODING gzip, br, zstd, deflate
HTTP_VIA 2.0 Caddy
HTTP_X_FORWARDED_FOR 216.73.216.75
HTTP_X_FORWARDED_HOST dev.lernhive.de
HTTP_X_FORWARDED_PROTO https
HTTP_X_REAL_IP 216.73.216.75
PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LD_LIBRARY_PATH /usr/local/instantclient
SERVER_SIGNATURE <address>Apache/2.4.66 (Debian) Server at dev.lernhive.de Port 80</address>
SERVER_SOFTWARE Apache/2.4.66 (Debian)
SERVER_NAME dev.lernhive.de
SERVER_ADDR 172.23.0.2
SERVER_PORT 443
REMOTE_ADDR 172.23.0.3
DOCUMENT_ROOT /var/www/html/public
REQUEST_SCHEME http
CONTEXT_PREFIX
CONTEXT_DOCUMENT_ROOT /var/www/html/public
SERVER_ADMIN webmaster@localhost
SCRIPT_FILENAME /var/www/html/public/local/lernhive/catalog.php
REMOTE_PORT 42958
GATEWAY_INTERFACE CGI/1.1
SERVER_PROTOCOL HTTP/1.1
REQUEST_METHOD GET
QUERY_STRING format=online%2Chybrid&time=3450121200&view=month
REQUEST_URI /local/lernhive/catalog.php?format=online%2Chybrid&time=3450121200&view=month
SCRIPT_NAME /local/lernhive/catalog.php
PHP_SELF /local/lernhive/catalog.php
REQUEST_TIME_FLOAT 1789316012.0401
REQUEST_TIME 1789316012
argv Array ( [0] => format=online%2Chybrid&time=3450121200&view=month )
argc 1
HTTPS on
Key Value
MOODLE_DOCKER_DBUSER moodle
MOODLE_DOCKER_DBNAME moodle
MOODLE_DOCKER_TIMEOUT_FACTOR 1
HOSTNAME f5ad226f3b16
PHP_VERSION 8.3.30
APACHE_CONFDIR /etc/apache2
PHP_INI_DIR /usr/local/etc/php
GPG_KEYS 1198C0117593497A5EC5C199286AF1F9897469DC C28D937575603EB4ABB725861C0779DC5C0A9DE4 AFD8691FDAEDF03BDF6E460563F15A9B715376CA
PHP_LDFLAGS -Wl,-O1 -pie
MOODLE_DOCKER_RUNNING 1
PWD /var/www/html
APACHE_DOCUMENT_ROOT /var/www/html/public
MOODLE_DOCKER_WEB_PORT 127.0.0.1:8000
APACHE_LOG_DIR /var/log/apache2
LANG C
PHP_SHA256 67f084d36852daab6809561a7c8023d130ca07fc6af8fb040684dd1414934d48
TARGETPLATFORM linux/amd64
MOODLE_DOCKER_DBTYPE pgsql
MOODLE_DOCKER_WEB_HOST localhost
APACHE_PID_FILE /var/run/apache2/apache2.pid
PHPIZE_DEPS autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c
MOODLE_DOCKER_BROWSER firefox
PHP_URL https://www.php.net/distributions/php-8.3.30.tar.xz
APACHE_RUN_GROUP www-data
APACHE_LOCK_DIR /var/lock/apache2
SHLVL 0
PHP_CFLAGS -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
MOODLE_DOCKER_DBPASS m@0dl3ing
LD_LIBRARY_PATH /usr/local/instantclient
APACHE_RUN_DIR /var/run/apache2
APACHE_ENVVARS /etc/apache2/envvars
APACHE_RUN_USER www-data
PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PHP_ASC_URL https://www.php.net/distributions/php-8.3.30.tar.xz.asc
PHP_CPPFLAGS -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
0. Whoops\Handler\PrettyPageHandler
1. Whoops\Handler\CallbackHandler