MDL-64057 core_favourites: add get_join_sql_by_type() to service layer
[moodle.git] / favourites / classes / local / service / user_favourite_service.php
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/>.
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;
28 defined('MOODLE_INTERNAL') || die();
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 {
43     /** @var favourite_repository_interface $repo the favourite repository object. */
44     protected $repo;
46     /** @var int $userid the id of the user to which this favourites service is scoped. */
47     protected $userid;
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     }
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.
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         }
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     }
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     }
113     /**
114      * Returns the SQL required to include favourite information for a given component/itemtype combination.
115      *
116      * Generally, find_favourites_by_type() is the recommended way to fetch favourites.
117      *
118      * This method is used to include favourite information in external queries, for items identified by their
119      * component and itemtype, matching itemid to the $joinitemid, and for the user to which this service is scoped.
120      *
121      * It uses a LEFT JOIN to preserve the original records. If you wish to restrict your records, please consider using a
122      * "WHERE {$tablealias}.id IS NOT NULL" in your query.
123      *
124      * Example usage:
125      *
126      * list($sql, $params) = $service->get_join_sql_by_type('core_message', 'message_conversations', 'myfavouritetablealias',
127      *                                                      'conv.id');
128      * Results in $sql:
129      *     "LEFT JOIN {favourite} fav
130      *             ON fav.component = :favouritecomponent
131      *            AND fav.itemtype = :favouriteitemtype
132      *            AND fav.userid = 1234
133      *            AND fav.itemid = conv.id"
134      * and $params:
135      *     ['favouritecomponent' => 'core_message', 'favouriteitemtype' => 'message_conversations']
136      *
137      * @param string $component the frankenstyle component name.
138      * @param string $itemtype the type of the favourited item.
139      * @param string $tablealias the desired alias for the favourites table.
140      * @param string $joinitemid the table and column identifier which the itemid is joined to. E.g. conversation.id.
141      * @return array the list of sql and params, in the format [$sql, $params].
142      */
143     public function get_join_sql_by_type(string $component, string $itemtype, string $tablealias, string $joinitemid) : array {
144         $sql = " LEFT JOIN {favourite} {$tablealias}
145                         ON {$tablealias}.component = :favouritecomponent
146                        AND {$tablealias}.itemtype = :favouriteitemtype
147                        AND {$tablealias}.userid = {$this->userid}
148                        AND {$tablealias}.itemid = {$joinitemid} ";
150         $params = [
151             'favouritecomponent' => $component,
152             'favouriteitemtype' => $itemtype,
153         ];
155         return [$sql, $params];
156     }
158     /**
159      * Delete a favourite item from an area and from within a context.
160      *
161      * E.g. delete a favourite course from the area 'core_course', 'course' with itemid 3 and from within the CONTEXT_USER context.
162      *
163      * @param string $component the frankenstyle component name.
164      * @param string $itemtype the type of the favourited item.
165      * @param int $itemid the id of the item which was favourited (not the favourite's id).
166      * @param \context $context the context of the item which was favourited.
167      * @throws \moodle_exception if the user does not control the favourite, or it doesn't exist.
168      */
169     public function delete_favourite(string $component, string $itemtype, int $itemid, \context $context) {
170         if (!in_array($component, \core_component::get_component_names())) {
171             throw new \moodle_exception("Invalid component name '$component'");
172         }
174         // Business logic: check the user owns the favourite.
175         try {
176             $favourite = $this->repo->find_favourite($this->userid, $component, $itemtype, $itemid, $context->id);
177         } catch (\moodle_exception $e) {
178             throw new \moodle_exception("Favourite does not exist for the user. Cannot delete.");
179         }
181         $this->repo->delete($favourite->id);
182     }
184     /**
185      * Check whether an item has been marked as a favourite in the respective area.
186      *
187      * @param string $component the frankenstyle component name.
188      * @param string $itemtype the type of the favourited item.
189      * @param int $itemid the id of the item which was favourited (not the favourite's id).
190      * @param \context $context the context of the item which was favourited.
191      * @return bool true if the item is favourited, false otherwise.
192      */
193     public function favourite_exists(string $component, string $itemtype, int $itemid, \context $context) : bool {
194         return $this->repo->exists_by(
195             [
196                 'userid' => $this->userid,
197                 'component' => $component,
198                 'itemtype' => $itemtype,
199                 'itemid' => $itemid,
200                 'contextid' => $context->id
201             ]
202         );
203     }
205     /**
206      * Get the favourite.
207      *
208      * @param string $component the frankenstyle component name.
209      * @param string $itemtype the type of the favourited item.
210      * @param int $itemid the id of the item which was favourited (not the favourite's id).
211      * @param \context $context the context of the item which was favourited.
212      * @return favourite|null
213      */
214     public function get_favourite(string $component, string $itemtype, int $itemid, \context $context) {
215         try {
216             return $this->repo->find_favourite(
217                 $this->userid,
218                 $component,
219                 $itemtype,
220                 $itemid,
221                 $context->id
222             );
223         } catch (\dml_missing_record_exception $e) {
224             return null;
225         }
226     }
228     /**
229      * Count the favourite by item type.
230      *
231      * @param string $component the frankenstyle component name.
232      * @param string $itemtype the type of the favourited item.
233      * @param \context|null $context the context of the item which was favourited.
234      * @return int
235      */
236     public function count_favourites_by_type(string $component, string $itemtype, \context $context = null) {
237         $criteria = [
238             'userid' => $this->userid,
239             'component' => $component,
240             'itemtype' => $itemtype
241         ];
243         if ($context) {
244             $criteria['contextid'] = $context->id;
245         }
247         return $this->repo->count_by($criteria);
248     }