Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Contains the content_item_service class.
 *
 * @package    core
 * @subpackage course
 * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
namespace core_course\local\service;

defined('MOODLE_INTERNAL') || die();

use core_course\local\exporters\course_content_items_exporter;
use core_course\local\repository\content_item_readonly_repository_interface;

/**
 * The content_item_service class, providing the api for interacting with content items.
 *
 * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class content_item_service {

    /** @var content_item_readonly_repository_interface $repository a repository for content items. */
    private $repository;

    /** string the component for this favourite. */
    public const COMPONENT = 'core_course';
    /** string the favourite prefix itemtype in the favourites table. */
    public const FAVOURITE_PREFIX = 'contentitem_';
    /** string the recommendation prefix itemtype in the favourites table. */
    public const RECOMMENDATION_PREFIX = 'recommend_';
    /** string the cache name for recommendations. */
    public const RECOMMENDATION_CACHE = 'recommendation_favourite_course_content_items';

    /**
     * The content_item_service constructor.
     *
     * @param content_item_readonly_repository_interface $repository a content item repository.
     */
    public function __construct(content_item_readonly_repository_interface $repository) {
        $this->repository = $repository;
    }

    /**
     * Returns an array of objects representing favourited content items.
     *
     * Each object contains the following properties:
     * itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
     * ids[]: an array of ids, representing the content items within a component.
     *
     * Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
     *
     * @param \stdClass $user
     * @return array
     */
    private function get_favourite_content_items_for_user(\stdClass $user): array {
        $favcache = \cache::make('core', 'user_favourite_course_content_items');
        $key = $user->id;
        $favmods = $favcache->get($key);
        if ($favmods !== false) {
            return $favmods;
        }

        $favourites = $this->get_content_favourites(self::FAVOURITE_PREFIX, \context_user::instance($user->id));

        $favcache->set($key, $favourites);
        return $favourites;
    }

    /**
     * Returns an array of objects representing recommended content items.
     *
     * Each object contains the following properties:
     * itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
     * ids[]: an array of ids, representing the content items within a component.
     *
     * Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
     *
     * @return array
     */
    private function get_recommendations(): array {
        global $CFG;

        $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
        $key = $CFG->siteguest;
        $favmods = $recommendationcache->get($key);
        if ($favmods !== false) {
            return $favmods;
        }

        // Make sure the guest user exists in the database.
        if (!\core_user::get_user($CFG->siteguest)) {
            throw new \coding_exception('The guest user does not exist in the database.');
        }

        // Make sure the guest user context exists.
        if (!$guestusercontext = \context_user::instance($CFG->siteguest, false)) {
            throw new \coding_exception('The guest user context does not exist.');
        }

        $favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, $guestusercontext);

        $recommendationcache->set($CFG->siteguest, $favourites);
        return $favourites;
    }

    /**
     * Gets content favourites from the favourites system depending on the area.
     *
     * @param  string        $prefix      Prefix for the item type.
     * @param  \context_user $usercontext User context for the favourite
     * @return array An array of favourite objects.
     */
    private function get_content_favourites(string $prefix, \context_user $usercontext): array {
        // Get all modules and any submodules which implement get_course_content_items() hook.
        // This gives us the set of all itemtypes which we'll use to register favourite content items.
        // The ids that each plugin returns will be used together with the itemtype to uniquely identify
        // each content item for favouriting.
        $pluginmanager = \core_plugin_manager::instance();
        $plugins = $pluginmanager->get_plugins_of_type('mod');
        $itemtypes = [];
        foreach ($plugins as $plugin) {
            // Add the mod itself.
            $itemtypes[] = $prefix . 'mod_' . $plugin->name;

            // Add any subplugins to the list of item types.
            $subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name);
            foreach ($subplugins as $subpluginname => $subplugininfo) {
                try {
                    if (component_callback_exists($subpluginname, 'get_course_content_items')) {
                        $itemtypes[] = $prefix . $subpluginname;
                    }
                } catch (\moodle_exception $e) {
                    debugging('Cannot get_course_content_items: ' . $e->getMessage(), DEBUG_DEVELOPER);
                }
            }
        }

        $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
        $favourites = [];
        $favs = $ufservice->find_all_favourites(self::COMPONENT, $itemtypes);
        $favsreduced = array_reduce($favs, function($carry, $item) {
            $carry[$item->itemtype][$item->itemid] = 0;
            return $carry;
        }, []);

        foreach ($itemtypes as $type) {
            $favourites[] = (object) [
                'itemtype' => $type,
                'ids' => isset($favsreduced[$type]) ? array_keys($favsreduced[$type]) : []
            ];
        }
        return $favourites;
    }

    /**
     * Get all content items which may be added to courses, irrespective of course caps, for site admin views, etc.
     *
     * @param \stdClass $user the user object.
     * @return array the array of exported content items.
     */
    public function get_all_content_items(\stdClass $user): array {
        $allcontentitems = $this->repository->find_all();

        return $this->export_content_items($user, $allcontentitems);
    }

    /**
     * Get content items which name matches a certain pattern and may be added to courses,
     * irrespective of course caps, for site admin views, etc.
     *
     * @param \stdClass $user The user object.
     * @param string $pattern The search pattern.
     * @return array The array of exported content items.
     */
    public function get_content_items_by_name_pattern(\stdClass $user, string $pattern): array {
        $allcontentitems = $this->repository->find_all();

        $filteredcontentitems = array_filter($allcontentitems, function($contentitem) use ($pattern) {
            return preg_match("/$pattern/i", $contentitem->get_title()->get_value());
        });

        return $this->export_content_items($user, $filteredcontentitems);
    }

    /**
     * Export content items.
     *
     * @param \stdClass $user The user object.
     * @param array $contentitems The content items array.
     * @return array The array of exported content items.
     */
    private function export_content_items(\stdClass $user, $contentitems) {
        global $PAGE;

        // Export the objects to get the formatted objects for transfer/display.
        $favourites = $this->get_favourite_content_items_for_user($user);
        $recommendations = $this->get_recommendations();
        $ciexporter = new course_content_items_exporter(
            $contentitems,
            [
                'context' => \context_system::instance(),
                'favouriteitems' => $favourites,
                'recommended' => $recommendations
            ]
        );
        $exported = $ciexporter->export($PAGE->get_renderer('core'));

        // Sort by title for return.
< usort($exported->content_items, function($a, $b) { < return $a->title > $b->title; < }); < < return $exported->content_items;
> \core_collator::asort_objects_by_property($exported->content_items, 'title'); > return array_values($exported->content_items);
} /** * Return a representation of the available content items, for a user in a course. * * @param \stdClass $user the user to check access for. * @param \stdClass $course the course to scope the content items to. * @param array $linkparams the desired section to return to. * @return \stdClass[] the content items, scoped to a course. */ public function get_content_items_for_user_in_course(\stdClass $user, \stdClass $course, array $linkparams = []): array { global $PAGE; if (!has_capability('moodle/course:manageactivities', \context_course::instance($course->id), $user)) { return []; } // Get all the visible content items. $allcontentitems = $this->repository->find_all_for_course($course, $user); // Content items can only originate from modules or submodules. $pluginmanager = \core_plugin_manager::instance(); $components = \core_component::get_component_list(); $parents = []; foreach ($allcontentitems as $contentitem) { if (!in_array($contentitem->get_component_name(), array_keys($components['mod']))) { // It could be a subplugin. $info = $pluginmanager->get_plugin_info($contentitem->get_component_name()); if (!is_null($info)) { $parent = $info->get_parent_plugin(); if ($parent != false) { if (in_array($parent, array_keys($components['mod']))) { $parents[$contentitem->get_component_name()] = $parent; continue; } } } throw new \moodle_exception('Only modules and submodules can generate content items. \'' . $contentitem->get_component_name() . '\' is neither.'); } $parents[$contentitem->get_component_name()] = $contentitem->get_component_name(); } // Now, check access to these items for the user. $availablecontentitems = array_filter($allcontentitems, function($contentitem) use ($course, $user, $parents) { // Check the parent module access for the user. return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user); }); // Add the link params to the link, if any have been provided. if (!empty($linkparams)) { $availablecontentitems = array_map(function ($item) use ($linkparams) { $item->get_link()->params($linkparams); return $item; }, $availablecontentitems); } // Export the objects to get the formatted objects for transfer/display. $favourites = $this->get_favourite_content_items_for_user($user); $recommended = $this->get_recommendations(); $ciexporter = new course_content_items_exporter( $availablecontentitems, [ 'context' => \context_course::instance($course->id), 'favouriteitems' => $favourites, 'recommended' => $recommended ] ); $exported = $ciexporter->export($PAGE->get_renderer('course')); // Sort by title for return.
< usort($exported->content_items, function($a, $b) { < return $a->title > $b->title; < });
> \core_collator::asort_objects_by_property($exported->content_items, 'title');
< return $exported->content_items;
> return array_values($exported->content_items);
} /** * Add a content item to a user's favourites. * * @param \stdClass $user the user whose favourite this is. * @param string $componentname the name of the component from which the content item originates. * @param int $contentitemid the id of the content item. * @return \stdClass the exported content item. */ public function add_to_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass { $usercontext = \context_user::instance($user->id); $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); // Because each plugin decides its own ids for content items, a combination of // itemtype and id is used to guarantee uniqueness across all content items. $itemtype = self::FAVOURITE_PREFIX . $componentname; $ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext); $favcache = \cache::make('core', 'user_favourite_course_content_items'); $favcache->delete($user->id); $items = $this->get_all_content_items($user); return $items[array_search($contentitemid, array_column($items, 'id'))]; } /** * Remove the content item from a user's favourites. * * @param \stdClass $user the user whose favourite this is. * @param string $componentname the name of the component from which the content item originates. * @param int $contentitemid the id of the content item. * @return \stdClass the exported content item. */ public function remove_from_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass { $usercontext = \context_user::instance($user->id); $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); // Because each plugin decides its own ids for content items, a combination of // itemtype and id is used to guarantee uniqueness across all content items. $itemtype = self::FAVOURITE_PREFIX . $componentname; $ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext); $favcache = \cache::make('core', 'user_favourite_course_content_items'); $favcache->delete($user->id); $items = $this->get_all_content_items($user); return $items[array_search($contentitemid, array_column($items, 'id'))]; } /** * Toggle an activity to being recommended or not. * * @param string $itemtype The component such as mod_assign, or assignsubmission_file * @param int $itemid The id related to this component item. * @return bool True on creating a favourite, false on deleting it. */ public function toggle_recommendation(string $itemtype, int $itemid): bool { global $CFG; $context = \context_system::instance(); $itemtype = self::RECOMMENDATION_PREFIX . $itemtype; // Favourites are created using a user context. We'll use the site guest user ID as that should not change and there // can be only one. $usercontext = \context_user::instance($CFG->siteguest); $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE); $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext); if ($favouritefactory->favourite_exists(self::COMPONENT, $itemtype, $itemid, $context)) { $favouritefactory->delete_favourite(self::COMPONENT, $itemtype, $itemid, $context); $result = $recommendationcache->delete($CFG->siteguest); return false; } else { $favouritefactory->create_favourite(self::COMPONENT, $itemtype, $itemid, $context); $result = $recommendationcache->delete($CFG->siteguest); return true; } } }