Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Contains the user_favourite_service class, part of the service layer for the favourites subsystem.
  19   *
  20   * @package   core_favourites
  21   * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace core_favourites\local\service;
  25  use \core_favourites\local\entity\favourite;
  26  use \core_favourites\local\repository\favourite_repository_interface;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /**
  31   * Class service, providing an single API for interacting with the favourites subsystem for a SINGLE USER.
  32   *
  33   * This class is responsible for exposing key operations (add, remove, find) and enforces any business logic necessary to validate
  34   * authorization/data integrity for these operations.
  35   *
  36   * All object persistence is delegated to the favourite_repository_interface object.
  37   *
  38   * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
  39   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class user_favourite_service {
  42  
  43      /** @var favourite_repository_interface $repo the favourite repository object. */
  44      protected $repo;
  45  
  46      /** @var int $userid the id of the user to which this favourites service is scoped. */
  47      protected $userid;
  48  
  49      /**
  50       * The user_favourite_service constructor.
  51       *
  52       * @param \context_user $usercontext The context of the user to which this service operations are scoped.
  53       * @param \core_favourites\local\repository\favourite_repository_interface $repository a favourites repository.
  54       */
  55      public function __construct(\context_user $usercontext, favourite_repository_interface $repository) {
  56          $this->repo = $repository;
  57          $this->userid = $usercontext->instanceid;
  58      }
  59  
  60      /**
  61       * Favourite an item defined by itemid/context, in the area defined by component/itemtype.
  62       *
  63       * @param string $component the frankenstyle component name.
  64       * @param string $itemtype the type of the item being favourited.
  65       * @param int $itemid the id of the item which is to be favourited.
  66       * @param \context $context the context in which the item is to be favourited.
  67       * @param int|null $ordering optional ordering integer used for sorting the favourites in an area.
  68       * @return favourite the favourite, once created.
  69       * @throws \moodle_exception if the component name is invalid, or if the repository encounters any errors.
  70       */
  71      public function create_favourite(string $component, string $itemtype, int $itemid, \context $context,
  72              int $ordering = null) : favourite {
  73          // Access: Any component can ask to favourite something, we can't verify access to that 'something' here though.
  74  
  75          // Validate the component name.
  76          if (!in_array($component, \core_component::get_component_names())) {
  77              throw new \moodle_exception("Invalid component name '$component'");
  78          }
  79  
  80          $favourite = new favourite($component, $itemtype, $itemid, $context->id, $this->userid);
  81          $favourite->ordering = $ordering > 0 ? $ordering : null;
  82          return $this->repo->add($favourite);
  83      }
  84  
  85      /**
  86       * Find a list of favourites, by type, where type is the component/itemtype pair.
  87       *
  88       * E.g. "Find all favourite courses" might result in:
  89       * $favcourses = find_favourites_by_type('core_course', 'course');
  90       *
  91       * @param string $component the frankenstyle component name.
  92       * @param string $itemtype the type of the favourited item.
  93       * @param int $limitfrom optional pagination control for returning a subset of records, starting at this point.
  94       * @param int $limitnum optional pagination control for returning a subset comprising this many records.
  95       * @return array the list of favourites found.
  96       * @throws \moodle_exception if the component name is invalid, or if the repository encounters any errors.
  97       */
  98      public function find_favourites_by_type(string $component, string $itemtype, int $limitfrom = 0, int $limitnum = 0) : array {
  99          if (!in_array($component, \core_component::get_component_names())) {
 100              throw new \moodle_exception("Invalid component name '$component'");
 101          }
 102          return $this->repo->find_by(
 103              [
 104                  'userid' => $this->userid,
 105                  'component' => $component,
 106                  'itemtype' => $itemtype
 107              ],
 108              $limitfrom,
 109              $limitnum
 110          );
 111      }
 112  
 113      /**
 114       * Find a list of favourites, by multiple types within a component.
 115       *
 116       * E.g. "Find all favourites in the activity chooser" might result in:
 117       * $favcourses = find_all_favourites('core_course', ['contentitem_mod_assign');
 118       *
 119       * @param string $component the frankenstyle component name.
 120       * @param array $itemtypes optional the type of the favourited item.
 121       * @param int $limitfrom optional pagination control for returning a subset of records, starting at this point.
 122       * @param int $limitnum optional pagination control for returning a subset comprising this many records.
 123       * @return array the list of favourites found.
 124       * @throws \moodle_exception if the component name is invalid, or if the repository encounters any errors.
 125       */
 126      public function find_all_favourites(string $component, array $itemtypes = [], int $limitfrom = 0, int $limitnum = 0) : array {
 127          if (!in_array($component, \core_component::get_component_names())) {
 128              throw new \moodle_exception("Invalid component name '$component'");
 129          }
 130          $params = [
 131              'userid' => $this->userid,
 132              'component' => $component,
 133          ];
 134          if ($itemtypes) {
 135              $params['itemtype'] = $itemtypes;
 136          }
 137  
 138          return $this->repo->find_by(
 139              $params,
 140              $limitfrom,
 141              $limitnum
 142          );
 143      }
 144  
 145      /**
 146       * Returns the SQL required to include favourite information for a given component/itemtype combination.
 147       *
 148       * Generally, find_favourites_by_type() is the recommended way to fetch favourites.
 149       *
 150       * This method is used to include favourite information in external queries, for items identified by their
 151       * component and itemtype, matching itemid to the $joinitemid, and for the user to which this service is scoped.
 152       *
 153       * It uses a LEFT JOIN to preserve the original records. If you wish to restrict your records, please consider using a
 154       * "WHERE {$tablealias}.id IS NOT NULL" in your query.
 155       *
 156       * Example usage:
 157       *
 158       * list($sql, $params) = $service->get_join_sql_by_type('core_message', 'message_conversations', 'myfavouritetablealias',
 159       *                                                      'conv.id');
 160       * Results in $sql:
 161       *     "LEFT JOIN {favourite} fav
 162       *             ON fav.component = :favouritecomponent
 163       *            AND fav.itemtype = :favouriteitemtype
 164       *            AND fav.userid = 1234
 165       *            AND fav.itemid = conv.id"
 166       * and $params:
 167       *     ['favouritecomponent' => 'core_message', 'favouriteitemtype' => 'message_conversations']
 168       *
 169       * @param string $component the frankenstyle component name.
 170       * @param string $itemtype the type of the favourited item.
 171       * @param string $tablealias the desired alias for the favourites table.
 172       * @param string $joinitemid the table and column identifier which the itemid is joined to. E.g. conversation.id.
 173       * @return array the list of sql and params, in the format [$sql, $params].
 174       */
 175      public function get_join_sql_by_type(string $component, string $itemtype, string $tablealias, string $joinitemid) : array {
 176          $sql = " LEFT JOIN {favourite} {$tablealias}
 177                          ON {$tablealias}.component = :favouritecomponent
 178                         AND {$tablealias}.itemtype = :favouriteitemtype
 179                         AND {$tablealias}.userid = {$this->userid}
 180                         AND {$tablealias}.itemid = {$joinitemid} ";
 181  
 182          $params = [
 183              'favouritecomponent' => $component,
 184              'favouriteitemtype' => $itemtype,
 185          ];
 186  
 187          return [$sql, $params];
 188      }
 189  
 190      /**
 191       * Delete a favourite item from an area and from within a context.
 192       *
 193       * E.g. delete a favourite course from the area 'core_course', 'course' with itemid 3 and from within the CONTEXT_USER context.
 194       *
 195       * @param string $component the frankenstyle component name.
 196       * @param string $itemtype the type of the favourited item.
 197       * @param int $itemid the id of the item which was favourited (not the favourite's id).
 198       * @param \context $context the context of the item which was favourited.
 199       * @throws \moodle_exception if the user does not control the favourite, or it doesn't exist.
 200       */
 201      public function delete_favourite(string $component, string $itemtype, int $itemid, \context $context) {
 202          if (!in_array($component, \core_component::get_component_names())) {
 203              throw new \moodle_exception("Invalid component name '$component'");
 204          }
 205  
 206          // Business logic: check the user owns the favourite.
 207          try {
 208              $favourite = $this->repo->find_favourite($this->userid, $component, $itemtype, $itemid, $context->id);
 209          } catch (\moodle_exception $e) {
 210              throw new \moodle_exception("Favourite does not exist for the user. Cannot delete.");
 211          }
 212  
 213          $this->repo->delete($favourite->id);
 214      }
 215  
 216      /**
 217       * Check whether an item has been marked as a favourite in the respective area.
 218       *
 219       * @param string $component the frankenstyle component name.
 220       * @param string $itemtype the type of the favourited item.
 221       * @param int $itemid the id of the item which was favourited (not the favourite's id).
 222       * @param \context $context the context of the item which was favourited.
 223       * @return bool true if the item is favourited, false otherwise.
 224       */
 225      public function favourite_exists(string $component, string $itemtype, int $itemid, \context $context) : bool {
 226          return $this->repo->exists_by(
 227              [
 228                  'userid' => $this->userid,
 229                  'component' => $component,
 230                  'itemtype' => $itemtype,
 231                  'itemid' => $itemid,
 232                  'contextid' => $context->id
 233              ]
 234          );
 235      }
 236  
 237      /**
 238       * Get the favourite.
 239       *
 240       * @param string $component the frankenstyle component name.
 241       * @param string $itemtype the type of the favourited item.
 242       * @param int $itemid the id of the item which was favourited (not the favourite's id).
 243       * @param \context $context the context of the item which was favourited.
 244       * @return favourite|null
 245       */
 246      public function get_favourite(string $component, string $itemtype, int $itemid, \context $context) {
 247          try {
 248              return $this->repo->find_favourite(
 249                  $this->userid,
 250                  $component,
 251                  $itemtype,
 252                  $itemid,
 253                  $context->id
 254              );
 255          } catch (\dml_missing_record_exception $e) {
 256              return null;
 257          }
 258      }
 259  
 260      /**
 261       * Count the favourite by item type.
 262       *
 263       * @param string $component the frankenstyle component name.
 264       * @param string $itemtype the type of the favourited item.
 265       * @param \context|null $context the context of the item which was favourited.
 266       * @return int
 267       */
 268      public function count_favourites_by_type(string $component, string $itemtype, \context $context = null) {
 269          $criteria = [
 270              'userid' => $this->userid,
 271              'component' => $component,
 272              'itemtype' => $itemtype
 273          ];
 274  
 275          if ($context) {
 276              $criteria['contextid'] = $context->id;
 277          }
 278  
 279          return $this->repo->count_by($criteria);
 280      }
 281  }