forked from moodle/moodle
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MDL-65313 core_favourite: add component-scoped favourite service class
Added a new type of service which can be used to interact with the all favourites for a given component, not just those owned by a a specific user. As such, objects of this type are scoped to a component.
- Loading branch information
Showing
4 changed files
with
358 additions
and
0 deletions.
There are no files selected for viewing
75 changes: 75 additions & 0 deletions
75
favourites/classes/local/service/component_favourite_service.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
<?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 component_favourite_service class, part of the service layer for the favourites subsystem. | ||
* | ||
* @package core_favourites | ||
* @copyright 2019 Jake Dallimore <[email protected]> | ||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||
*/ | ||
namespace core_favourites\local\service; | ||
use \core_favourites\local\repository\favourite_repository_interface; | ||
|
||
defined('MOODLE_INTERNAL') || die(); | ||
|
||
/** | ||
* Class service, providing an single API for interacting with the favourites subsystem, for all favourites of a specific component. | ||
* | ||
* This class provides operations which can be applied to favourites within a component, based on type and context identifiers. | ||
* | ||
* All object persistence is delegated to the favourite_repository_interface object. | ||
* | ||
* @copyright 2019 Jake Dallimore <[email protected]> | ||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||
*/ | ||
class component_favourite_service { | ||
|
||
/** @var favourite_repository_interface $repo the favourite repository object. */ | ||
protected $repo; | ||
|
||
/** @var int $component the frankenstyle component name to which this favourites service is scoped. */ | ||
protected $component; | ||
|
||
/** | ||
* The component_favourite_service constructor. | ||
* | ||
* @param string $component The frankenstyle name of the component to which this service operations are scoped. | ||
* @param \core_favourites\local\repository\favourite_repository_interface $repository a favourites repository. | ||
* @throws \moodle_exception if the component name is invalid. | ||
*/ | ||
public function __construct(string $component, favourite_repository_interface $repository) { | ||
if (!in_array($component, \core_component::get_component_names())) { | ||
throw new \moodle_exception("Invalid component name '$component'"); | ||
} | ||
$this->repo = $repository; | ||
$this->component = $component; | ||
} | ||
|
||
|
||
/** | ||
* Delete a collection of favourites by type, and optionally for a given context. | ||
* | ||
* E.g. delete all favourites of type 'message_conversations' and for a specific CONTEXT_COURSE context. | ||
* | ||
* @param string $itemtype the type of the favourited items. | ||
* @param \context $context the context of the items which were favourited. | ||
*/ | ||
public function delete_favourites_by_type(string $itemtype, \context $context = null) { | ||
$criteria = ['component' => $this->component, 'itemtype' => $itemtype] + ($context ? ['contextid' => $context->id] : []); | ||
$this->repo->delete_by($criteria); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
<?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/>. | ||
|
||
/** | ||
* Testing the service layer within core_favourites. | ||
* | ||
* @package core_favourites | ||
* @category test | ||
* @copyright 2019 Jake Dallimore <[email protected]> | ||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||
*/ | ||
use \core_favourites\local\entity\favourite; | ||
defined('MOODLE_INTERNAL') || die(); | ||
|
||
/** | ||
* Test class covering the component_favourite_service within the service layer of favourites. | ||
* | ||
* @copyright 2019 Jake Dallimore <[email protected]> | ||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||
*/ | ||
class component_favourite_service_testcase extends advanced_testcase { | ||
|
||
public function setUp() { | ||
$this->resetAfterTest(); | ||
} | ||
|
||
// Basic setup stuff to be reused in most tests. | ||
protected function setup_users_and_courses() { | ||
$user1 = self::getDataGenerator()->create_user(); | ||
$user1context = \context_user::instance($user1->id); | ||
$user2 = self::getDataGenerator()->create_user(); | ||
$user2context = \context_user::instance($user2->id); | ||
$course1 = self::getDataGenerator()->create_course(); | ||
$course2 = self::getDataGenerator()->create_course(); | ||
$course1context = context_course::instance($course1->id); | ||
$course2context = context_course::instance($course2->id); | ||
return [$user1context, $user2context, $course1context, $course2context]; | ||
} | ||
|
||
/** | ||
* Generates an in-memory repository for testing, using an array store for CRUD stuff. | ||
* | ||
* @param array $mockstore | ||
* @return \PHPUnit\Framework\MockObject\MockObject | ||
*/ | ||
protected function get_mock_repository(array $mockstore) { | ||
// This mock will just store data in an array. | ||
$mockrepo = $this->getMockBuilder(\core_favourites\local\repository\favourite_repository_interface::class) | ||
->setMethods([]) | ||
->getMock(); | ||
$mockrepo->expects($this->any()) | ||
->method('add') | ||
->will($this->returnCallback(function(favourite $favourite) use (&$mockstore) { | ||
// Mock implementation of repository->add(), where an array is used instead of the DB. | ||
// Duplicates are confirmed via the unique key, and exceptions thrown just like a real repo. | ||
$key = $favourite->userid . $favourite->component . $favourite->itemtype . $favourite->itemid | ||
. $favourite->contextid; | ||
|
||
// Check the objects for the unique key. | ||
foreach ($mockstore as $item) { | ||
if ($item->uniquekey == $key) { | ||
throw new \moodle_exception('Favourite already exists'); | ||
} | ||
} | ||
$index = count($mockstore); // Integer index. | ||
$favourite->uniquekey = $key; // Simulate the unique key constraint. | ||
$favourite->id = $index; | ||
$mockstore[$index] = $favourite; | ||
return $mockstore[$index]; | ||
}) | ||
); | ||
$mockrepo->expects($this->any()) | ||
->method('find_by') | ||
->will($this->returnCallback(function(array $criteria, int $limitfrom = 0, int $limitnum = 0) use (&$mockstore) { | ||
// Check the mockstore for all objects with properties matching the key => val pairs in $criteria. | ||
foreach ($mockstore as $index => $mockrow) { | ||
$mockrowarr = (array)$mockrow; | ||
if (array_diff($criteria, $mockrowarr) == []) { | ||
$returns[$index] = $mockrow; | ||
} | ||
} | ||
// Return a subset of the records, according to the paging options, if set. | ||
if ($limitnum != 0) { | ||
return array_slice($returns, $limitfrom, $limitnum); | ||
} | ||
// Otherwise, just return the full set. | ||
return $returns; | ||
}) | ||
); | ||
$mockrepo->expects($this->any()) | ||
->method('find_favourite') | ||
->will($this->returnCallback(function(int $userid, string $comp, string $type, int $id, int $ctxid) use (&$mockstore) { | ||
// Check the mockstore for all objects with properties matching the key => val pairs in $criteria. | ||
$crit = ['userid' => $userid, 'component' => $comp, 'itemtype' => $type, 'itemid' => $id, 'contextid' => $ctxid]; | ||
foreach ($mockstore as $fakerow) { | ||
$fakerowarr = (array)$fakerow; | ||
if (array_diff($crit, $fakerowarr) == []) { | ||
return $fakerow; | ||
} | ||
} | ||
throw new \dml_missing_record_exception("Item not found"); | ||
}) | ||
); | ||
$mockrepo->expects($this->any()) | ||
->method('find') | ||
->will($this->returnCallback(function(int $id) use (&$mockstore) { | ||
return $mockstore[$id]; | ||
}) | ||
); | ||
$mockrepo->expects($this->any()) | ||
->method('exists') | ||
->will($this->returnCallback(function(int $id) use (&$mockstore) { | ||
return array_key_exists($id, $mockstore); | ||
}) | ||
); | ||
$mockrepo->expects($this->any()) | ||
->method('count_by') | ||
->will($this->returnCallback(function(array $criteria) use (&$mockstore) { | ||
$count = 0; | ||
// Check the mockstore for all objects with properties matching the key => val pairs in $criteria. | ||
foreach ($mockstore as $index => $mockrow) { | ||
$mockrowarr = (array)$mockrow; | ||
if (array_diff($criteria, $mockrowarr) == []) { | ||
$count++; | ||
} | ||
} | ||
return $count; | ||
}) | ||
); | ||
$mockrepo->expects($this->any()) | ||
->method('delete') | ||
->will($this->returnCallback(function(int $id) use (&$mockstore) { | ||
foreach ($mockstore as $mockrow) { | ||
if ($mockrow->id == $id) { | ||
unset($mockstore[$id]); | ||
} | ||
} | ||
}) | ||
); | ||
$mockrepo->expects($this->any()) | ||
->method('delete_by') | ||
->will($this->returnCallback(function(array $criteria) use (&$mockstore) { | ||
// Check the mockstore for all objects with properties matching the key => val pairs in $criteria. | ||
foreach ($mockstore as $index => $mockrow) { | ||
$mockrowarr = (array)$mockrow; | ||
if (array_diff($criteria, $mockrowarr) == []) { | ||
unset($mockstore[$index]); | ||
} | ||
} | ||
}) | ||
); | ||
$mockrepo->expects($this->any()) | ||
->method('exists_by') | ||
->will($this->returnCallback(function(array $criteria) use (&$mockstore) { | ||
// Check the mockstore for all objects with properties matching the key => val pairs in $criteria. | ||
foreach ($mockstore as $index => $mockrow) { | ||
$mockrowarr = (array)$mockrow; | ||
echo "Here"; | ||
if (array_diff($criteria, $mockrowarr) == []) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}) | ||
); | ||
return $mockrepo; | ||
} | ||
|
||
/** | ||
* Test confirming the deletion of favourites by type, but with no optional context filter provided. | ||
*/ | ||
public function test_delete_favourites_by_type() { | ||
list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses(); | ||
|
||
// Get a user_favourite_service for each user. | ||
$repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB. | ||
$user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo); | ||
$user2service = new \core_favourites\local\service\user_favourite_service($user2context, $repo); | ||
|
||
// Favourite both courses for both users. | ||
$fav1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context); | ||
$fav2 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context); | ||
$fav3 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context); | ||
$fav4 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context); | ||
$this->assertTrue($repo->exists($fav1->id)); | ||
$this->assertTrue($repo->exists($fav2->id)); | ||
$this->assertTrue($repo->exists($fav3->id)); | ||
$this->assertTrue($repo->exists($fav4->id)); | ||
|
||
// Favourite something else arbitrarily. | ||
$fav5 = $user2service->create_favourite('core_user', 'course', $course2context->instanceid, $course2context); | ||
$fav6 = $user2service->create_favourite('core_course', 'whatnow', $course2context->instanceid, $course2context); | ||
|
||
// Get a component_favourite_service to perform the type based deletion. | ||
$service = new \core_favourites\local\service\component_favourite_service('core_course', $repo); | ||
|
||
// Delete all 'course' type favourites (for all users at ANY context). | ||
$service->delete_favourites_by_type('course'); | ||
|
||
// Verify the favourites don't exist. | ||
$this->assertFalse($repo->exists($fav1->id)); | ||
$this->assertFalse($repo->exists($fav2->id)); | ||
$this->assertFalse($repo->exists($fav3->id)); | ||
$this->assertFalse($repo->exists($fav4->id)); | ||
|
||
// Verify favourites of other types or for other components are not affected. | ||
$this->assertTrue($repo->exists($fav5->id)); | ||
$this->assertTrue($repo->exists($fav6->id)); | ||
|
||
// Try to delete favourites for a type which we know doesn't exist. Verify no exception. | ||
$this->assertNull($service->delete_favourites_by_type('course')); | ||
} | ||
|
||
/** | ||
* Test confirming the deletion of favourites by type and with the optional context filter provided. | ||
*/ | ||
public function test_delete_favourites_by_type_with_context() { | ||
list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses(); | ||
|
||
// Get a user_favourite_service for each user. | ||
$repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB. | ||
$user1service = new \core_favourites\local\service\user_favourite_service($user1context, $repo); | ||
$user2service = new \core_favourites\local\service\user_favourite_service($user2context, $repo); | ||
|
||
// Favourite both courses for both users. | ||
$fav1 = $user1service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context); | ||
$fav2 = $user2service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context); | ||
$fav3 = $user1service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context); | ||
$fav4 = $user2service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context); | ||
$this->assertTrue($repo->exists($fav1->id)); | ||
$this->assertTrue($repo->exists($fav2->id)); | ||
$this->assertTrue($repo->exists($fav3->id)); | ||
$this->assertTrue($repo->exists($fav4->id)); | ||
|
||
// Favourite something else arbitrarily. | ||
$fav5 = $user2service->create_favourite('core_user', 'course', $course1context->instanceid, $course1context); | ||
$fav6 = $user2service->create_favourite('core_course', 'whatnow', $course1context->instanceid, $course1context); | ||
|
||
// Get a component_favourite_service to perform the type based deletion. | ||
$service = new \core_favourites\local\service\component_favourite_service('core_course', $repo); | ||
|
||
// Delete all 'course' type favourites (for all users at ONLY the course 1 context). | ||
$service->delete_favourites_by_type('course', $course1context); | ||
|
||
// Verify the favourites for course 1 context don't exist. | ||
$this->assertFalse($repo->exists($fav1->id)); | ||
$this->assertFalse($repo->exists($fav2->id)); | ||
|
||
// Verify the favourites for the same component and type, but NOT for the same contextid and unaffected. | ||
$this->assertTrue($repo->exists($fav3->id)); | ||
$this->assertTrue($repo->exists($fav4->id)); | ||
|
||
// Verify favourites of other types or for other components are not affected. | ||
$this->assertTrue($repo->exists($fav5->id)); | ||
$this->assertTrue($repo->exists($fav6->id)); | ||
|
||
// Try to delete favourites for a type which we know doesn't exist. Verify no exception. | ||
$this->assertNull($service->delete_favourites_by_type('course', $course1context)); | ||
} | ||
} |
File renamed without changes.