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.

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

   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  namespace core;
  18  
  19  use advanced_testcase;
  20  use coding_exception;
  21  use dml_missing_record_exception;
  22  use lang_string;
  23  use xmldb_table;
  24  
  25  /**
  26   * Persistent testcase.
  27   *
  28   * @package    core
  29   * @copyright  2015 Frédéric Massart - FMCorz.net
  30   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   * @covers     \core\persistent
  32   */
  33  class persistent_test extends advanced_testcase {
  34  
  35      public function setUp(): void {
  36          $this->make_persistent_table();
  37          $this->make_second_persistent_table();
  38          $this->resetAfterTest();
  39      }
  40  
  41      /**
  42       * Make the table for the persistent.
  43       */
  44      protected function make_persistent_table() {
  45          global $DB;
  46          $dbman = $DB->get_manager();
  47  
  48          $table = new xmldb_table(core_testable_persistent::TABLE);
  49          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
  50          $table->add_field('shortname', XMLDB_TYPE_CHAR, '100', null, null, null, null);
  51          $table->add_field('idnumber', XMLDB_TYPE_CHAR, '100', null, null, null, null);
  52          $table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null);
  53          $table->add_field('descriptionformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0');
  54          $table->add_field('parentid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
  55          $table->add_field('path', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
  56          $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
  57          $table->add_field('scaleid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
  58          $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
  59          $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
  60          $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
  61  
  62          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
  63  
  64          if ($dbman->table_exists($table)) {
  65              $dbman->drop_table($table);
  66          }
  67  
  68          $dbman->create_table($table);
  69      }
  70  
  71      /**
  72       * Make the second table for the persistent.
  73       */
  74      protected function make_second_persistent_table() {
  75          global $DB;
  76          $dbman = $DB->get_manager();
  77  
  78          $table = new xmldb_table(core_testable_second_persistent::TABLE);
  79          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
  80          $table->add_field('someint', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
  81          $table->add_field('intnull', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
  82          $table->add_field('somefloat', XMLDB_TYPE_FLOAT, '10,5', null, null, null, null);
  83          $table->add_field('sometext', XMLDB_TYPE_TEXT, null, null, null, null, null);
  84          $table->add_field('someraw', XMLDB_TYPE_CHAR, '100', null, null, null, null);
  85          $table->add_field('booltrue', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
  86          $table->add_field('boolfalse', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
  87          $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
  88          $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
  89          $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
  90  
  91          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
  92  
  93          if ($dbman->table_exists($table)) {
  94              $dbman->drop_table($table);
  95          }
  96  
  97          $dbman->create_table($table);
  98      }
  99  
 100      public function test_properties_definition() {
 101          $expected = array(
 102              'shortname' => array(
 103                  'type' => PARAM_TEXT,
 104                  'default' => '',
 105                  'null' => NULL_NOT_ALLOWED
 106              ),
 107              'idnumber' => array(
 108                  'type' => PARAM_TEXT,
 109                  'null' => NULL_NOT_ALLOWED
 110              ),
 111              'description' => array(
 112                  'type' => PARAM_TEXT,
 113                  'default' => '',
 114                  'null' => NULL_NOT_ALLOWED
 115              ),
 116              'descriptionformat' => array(
 117                  'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN),
 118                  'type' => PARAM_INT,
 119                  'default' => FORMAT_HTML,
 120                  'null' => NULL_NOT_ALLOWED
 121              ),
 122              'parentid' => array(
 123                  'type' => PARAM_INT,
 124                  'default' => 0,
 125                  'null' => NULL_NOT_ALLOWED
 126              ),
 127              'path' => array(
 128                  'type' => PARAM_RAW,
 129                  'default' => '',
 130                  'null' => NULL_NOT_ALLOWED
 131              ),
 132              'sortorder' => array(
 133                  'type' => PARAM_INT,
 134                  'message' => new lang_string('invalidrequest', 'error'),
 135                  'null' => NULL_NOT_ALLOWED
 136              ),
 137              'scaleid' => array(
 138                  'default' => null,
 139                  'type' => PARAM_INT,
 140                  'null' => NULL_ALLOWED
 141              ),
 142              'id' => array(
 143                  'default' => 0,
 144                  'type' => PARAM_INT,
 145                  'null' => NULL_NOT_ALLOWED
 146              ),
 147              'timecreated' => array(
 148                  'default' => 0,
 149                  'type' => PARAM_INT,
 150                  'null' => NULL_NOT_ALLOWED
 151              ),
 152              'timemodified' => array(
 153                  'default' => 0,
 154                  'type' => PARAM_INT,
 155                  'null' => NULL_NOT_ALLOWED
 156              ),
 157              'usermodified' => array(
 158                  'default' => 0,
 159                  'type' => PARAM_INT,
 160                  'null' => NULL_NOT_ALLOWED
 161              ),
 162          );
 163          $this->assertEquals($expected, core_testable_persistent::properties_definition());
 164      }
 165  
 166      /**
 167       * Test filtering record properties returns only those defined by the persistent
 168       */
 169      public function test_properties_filter(): void {
 170          $result = core_testable_persistent::properties_filter((object) [
 171              'idnumber' => '123',
 172              'sortorder' => 1,
 173              'invalidparam' => 'abc',
 174          ]);
 175  
 176          // We should get back all data except invalid param.
 177          $this->assertEquals([
 178              'idnumber' => '123',
 179              'sortorder' => 1,
 180          ], $result);
 181      }
 182  
 183      /**
 184       * Test creating persistent instance by specifying record ID in constructor
 185       */
 186      public function test_constructor() : void {
 187          $persistent = (new core_testable_persistent(0, (object) [
 188              'idnumber' => '123',
 189              'sortorder' => 1,
 190          ]))->create();
 191  
 192          // Now create a new instance, passing the original instance ID in the constructor.
 193          $another = new core_testable_persistent($persistent->get('id'));
 194          $this->assertEquals($another->to_record(), $persistent->to_record());
 195      }
 196  
 197      /**
 198       * Test creating persistent instance by specifying non-existing record ID in constructor throws appropriate exception
 199       */
 200      public function test_constructor_invalid(): void {
 201          $this->expectException(dml_missing_record_exception::class);
 202          $this->expectExceptionMessage('Can\'t find data record in database table phpunit_persistent.');
 203          new core_testable_persistent(42);
 204      }
 205  
 206      public function test_to_record() {
 207          $p = new core_testable_persistent();
 208          $expected = (object) array(
 209              'shortname' => '',
 210              'idnumber' => null,
 211              'description' => '',
 212              'descriptionformat' => FORMAT_HTML,
 213              'parentid' => 0,
 214              'path' => '',
 215              'sortorder' => null,
 216              'id' => 0,
 217              'timecreated' => 0,
 218              'timemodified' => 0,
 219              'usermodified' => 0,
 220              'scaleid' => null,
 221          );
 222          $this->assertEquals($expected, $p->to_record());
 223      }
 224  
 225      public function test_from_record() {
 226          $p = new core_testable_persistent();
 227          $data = (object) array(
 228              'shortname' => 'ddd',
 229              'idnumber' => 'abc',
 230              'description' => 'xyz',
 231              'descriptionformat' => FORMAT_PLAIN,
 232              'parentid' => 999,
 233              'path' => '/a/b/c',
 234              'sortorder' => 12,
 235              'id' => 1,
 236              'timecreated' => 2,
 237              'timemodified' => 3,
 238              'usermodified' => 4,
 239              'scaleid' => null,
 240          );
 241          $p->from_record($data);
 242          $this->assertEquals($data, $p->to_record());
 243      }
 244  
 245      public function test_from_record_invalid_param() {
 246          $p = new core_testable_persistent();
 247          $data = (object) array(
 248              'shortname' => 'ddd',
 249              'idnumber' => 'abc',
 250              'description' => 'xyz',
 251              'descriptionformat' => FORMAT_PLAIN,
 252              'parentid' => 999,
 253              'path' => '/a/b/c',
 254              'sortorder' => 12,
 255              'id' => 1,
 256              'timecreated' => 2,
 257              'timemodified' => 3,
 258              'usermodified' => 4,
 259              'scaleid' => null,
 260              'invalidparam' => 'abc'
 261          );
 262  
 263          $p->from_record($data);
 264  
 265          // Previous call should succeed, assert we get back all data except invalid param.
 266          unset($data->invalidparam);
 267          $this->assertEquals($data, $p->to_record());
 268      }
 269  
 270      public function test_validate() {
 271          $data = (object) array(
 272              'idnumber' => 'abc',
 273              'sortorder' => 0
 274          );
 275          $p = new core_testable_persistent(0, $data);
 276          $this->assertFalse(isset($p->beforevalidate));
 277          $this->assertTrue($p->validate());
 278          $this->assertTrue(isset($p->beforevalidate));
 279          $this->assertTrue($p->is_valid());
 280          $this->assertEquals(array(), $p->get_errors());
 281          $p->set('descriptionformat', -100);
 282  
 283          $expected = array(
 284              'descriptionformat' => new lang_string('invaliddata', 'error'),
 285          );
 286          $this->assertEquals($expected, $p->validate());
 287          $this->assertFalse($p->is_valid());
 288          $this->assertEquals($expected, $p->get_errors());
 289      }
 290  
 291      public function test_validation_required() {
 292          $data = (object) array(
 293              'idnumber' => 'abc'
 294          );
 295          $p = new core_testable_persistent(0, $data);
 296          $expected = array(
 297              'sortorder' => new lang_string('requiredelement', 'form'),
 298          );
 299          $this->assertFalse($p->is_valid());
 300          $this->assertEquals($expected, $p->get_errors());
 301      }
 302  
 303      public function test_validation_custom() {
 304          $data = (object) array(
 305              'idnumber' => 'abc',
 306              'sortorder' => 10,
 307          );
 308          $p = new core_testable_persistent(0, $data);
 309          $expected = array(
 310              'sortorder' => new lang_string('invalidkey', 'error'),
 311          );
 312          $this->assertFalse($p->is_valid());
 313          $this->assertEquals($expected, $p->get_errors());
 314      }
 315  
 316      public function test_validation_custom_message() {
 317          $data = (object) array(
 318              'idnumber' => 'abc',
 319              'sortorder' => 'abc',
 320          );
 321          $p = new core_testable_persistent(0, $data);
 322          $expected = array(
 323              'sortorder' => new lang_string('invalidrequest', 'error'),
 324          );
 325          $this->assertFalse($p->is_valid());
 326          $this->assertEquals($expected, $p->get_errors());
 327      }
 328  
 329      public function test_validation_choices() {
 330          $data = (object) array(
 331              'idnumber' => 'abc',
 332              'sortorder' => 0,
 333              'descriptionformat' => -100
 334          );
 335          $p = new core_testable_persistent(0, $data);
 336          $expected = array(
 337              'descriptionformat' => new lang_string('invaliddata', 'error'),
 338          );
 339          $this->assertFalse($p->is_valid());
 340          $this->assertEquals($expected, $p->get_errors());
 341      }
 342  
 343      public function test_validation_type() {
 344          $data = (object) array(
 345              'idnumber' => 'abc',
 346              'sortorder' => 'NaN'
 347          );
 348          $p = new core_testable_persistent(0, $data);
 349          $this->assertFalse($p->is_valid());
 350          $this->assertArrayHasKey('sortorder', $p->get_errors());
 351      }
 352  
 353      public function test_validation_null() {
 354          $data = (object) array(
 355              'idnumber' => null,
 356              'sortorder' => 0,
 357              'scaleid' => 'bad!'
 358          );
 359          $p = new core_testable_persistent(0, $data);
 360          $this->assertFalse($p->is_valid());
 361          $this->assertArrayHasKey('idnumber', $p->get_errors());
 362          $this->assertArrayHasKey('scaleid', $p->get_errors());
 363          $p->set('idnumber', 'abc');
 364          $this->assertFalse($p->is_valid());
 365          $this->assertArrayNotHasKey('idnumber', $p->get_errors());
 366          $this->assertArrayHasKey('scaleid', $p->get_errors());
 367          $p->set('scaleid', null);
 368          $this->assertTrue($p->is_valid());
 369          $this->assertArrayNotHasKey('scaleid', $p->get_errors());
 370      }
 371  
 372      public function test_create() {
 373          global $DB;
 374          $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc'));
 375          $this->assertFalse(isset($p->beforecreate));
 376          $this->assertFalse(isset($p->aftercreate));
 377          $p->create();
 378          $record = $DB->get_record(core_testable_persistent::TABLE, array('id' => $p->get('id')), '*', MUST_EXIST);
 379          $expected = $p->to_record();
 380          $this->assertTrue(isset($p->beforecreate));
 381          $this->assertTrue(isset($p->aftercreate));
 382          $this->assertEquals($expected->sortorder, $record->sortorder);
 383          $this->assertEquals($expected->idnumber, $record->idnumber);
 384          $this->assertEquals($expected->id, $record->id);
 385          $this->assertTrue($p->is_valid()); // Should always be valid after a create.
 386      }
 387  
 388      public function test_update() {
 389          global $DB;
 390          $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc'));
 391          $p->create();
 392          $id = $p->get('id');
 393          $p->set('sortorder', 456);
 394          $p->from_record((object) array('idnumber' => 'def'));
 395          $this->assertFalse(isset($p->beforeupdate));
 396          $this->assertFalse(isset($p->afterupdate));
 397          $p->update();
 398  
 399          $expected = $p->to_record();
 400          $record = $DB->get_record(core_testable_persistent::TABLE, array('id' => $p->get('id')), '*', MUST_EXIST);
 401          $this->assertTrue(isset($p->beforeupdate));
 402          $this->assertTrue(isset($p->afterupdate));
 403          $this->assertEquals($id, $record->id);
 404          $this->assertEquals(456, $record->sortorder);
 405          $this->assertEquals('def', $record->idnumber);
 406          $this->assertTrue($p->is_valid()); // Should always be valid after an update.
 407      }
 408  
 409      /**
 410       * Test set_many prior to updating the persistent
 411       */
 412      public function test_set_many_update(): void {
 413          global $DB;
 414  
 415          $persistent = (new core_testable_persistent(0, (object) [
 416              'idnumber' => 'test',
 417              'sortorder' => 2
 418          ]))->create();
 419  
 420          // Set multiple properties, and update.
 421          $persistent->set_many([
 422              'idnumber' => 'test2',
 423              'sortorder' => 1,
 424          ])->update();
 425  
 426          // Confirm our persistent was updated.
 427          $record = $DB->get_record(core_testable_persistent::TABLE, ['id' => $persistent->get('id')], '*', MUST_EXIST);
 428          $this->assertEquals('test2', $record->idnumber);
 429          $this->assertEquals(1, $record->sortorder);
 430      }
 431  
 432      public function test_save() {
 433          global $DB;
 434          $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc'));
 435          $this->assertFalse(isset($p->beforecreate));
 436          $this->assertFalse(isset($p->aftercreate));
 437          $this->assertFalse(isset($p->beforeupdate));
 438          $this->assertFalse(isset($p->beforeupdate));
 439          $p->save();
 440          $record = $DB->get_record(core_testable_persistent::TABLE, array('id' => $p->get('id')), '*', MUST_EXIST);
 441          $expected = $p->to_record();
 442          $this->assertTrue(isset($p->beforecreate));
 443          $this->assertTrue(isset($p->aftercreate));
 444          $this->assertFalse(isset($p->beforeupdate));
 445          $this->assertFalse(isset($p->beforeupdate));
 446          $this->assertEquals($expected->sortorder, $record->sortorder);
 447          $this->assertEquals($expected->idnumber, $record->idnumber);
 448          $this->assertEquals($expected->id, $record->id);
 449          $this->assertTrue($p->is_valid()); // Should always be valid after a save/create.
 450  
 451          $p->set('idnumber', 'abcd');
 452          $p->save();
 453          $record = $DB->get_record(core_testable_persistent::TABLE, array('id' => $p->get('id')), '*', MUST_EXIST);
 454          $expected = $p->to_record();
 455          $this->assertTrue(isset($p->beforeupdate));
 456          $this->assertTrue(isset($p->beforeupdate));
 457          $this->assertEquals($expected->sortorder, $record->sortorder);
 458          $this->assertEquals($expected->idnumber, $record->idnumber);
 459          $this->assertEquals($expected->id, $record->id);
 460          $this->assertTrue($p->is_valid()); // Should always be valid after a save/update.
 461      }
 462  
 463      /**
 464       * Test set_many prior to saving the persistent
 465       */
 466      public function test_set_many_save(): void {
 467          global $DB;
 468  
 469          $persistent = (new core_testable_persistent(0, (object) [
 470              'idnumber' => 'test',
 471              'sortorder' => 2
 472          ]));
 473  
 474          // Set multiple properties, and save.
 475          $persistent->set_many([
 476              'idnumber' => 'test2',
 477              'sortorder' => 1,
 478          ])->save();
 479  
 480          // Confirm our persistent was saved.
 481          $record = $DB->get_record(core_testable_persistent::TABLE, ['id' => $persistent->get('id')], '*', MUST_EXIST);
 482          $this->assertEquals('test2', $record->idnumber);
 483          $this->assertEquals(1, $record->sortorder);
 484      }
 485  
 486      /**
 487       * Test set_many with empty array should not modify the persistent
 488       */
 489      public function test_set_many_empty(): void {
 490          global $DB;
 491  
 492          $persistent = (new core_testable_persistent(0, (object) [
 493              'idnumber' => 'test',
 494              'sortorder' => 2
 495          ]))->create();
 496  
 497          // Set empty properties, and update.
 498          $persistent->set_many([])->update();
 499  
 500          // Confirm our persistent was not updated.
 501          $record = $DB->get_record(core_testable_persistent::TABLE, ['id' => $persistent->get('id')], '*', MUST_EXIST);
 502          $this->assertEquals('test', $record->idnumber);
 503          $this->assertEquals(2, $record->sortorder);
 504      }
 505  
 506      /**
 507       * Test set with invalid property
 508       */
 509      public function test_set_invalid_property(): void {
 510          $persistent = (new core_testable_persistent(0, (object) [
 511              'idnumber' => 'test',
 512              'sortorder' => 2
 513          ]));
 514  
 515          $this->expectException(coding_exception::class);
 516          $this->expectExceptionMessage('Unexpected property \'invalid\' requested');
 517          $persistent->set('invalid', 'stuff');
 518      }
 519  
 520      /**
 521       * Test set_many with invalid property
 522       */
 523      public function test_set_many_invalid_property(): void {
 524          $persistent = (new core_testable_persistent(0, (object) [
 525              'idnumber' => 'test',
 526              'sortorder' => 2
 527          ]));
 528  
 529          $this->expectException(coding_exception::class);
 530          $this->expectExceptionMessage('Unexpected property \'invalid\' requested');
 531          $persistent->set_many(['invalid' => 'stuff']);
 532      }
 533  
 534      public function test_read() {
 535          $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc'));
 536          $p->create();
 537          unset($p->beforevalidate);
 538          unset($p->beforecreate);
 539          unset($p->aftercreate);
 540  
 541          $p2 = new core_testable_persistent($p->get('id'));
 542          $this->assertEquals($p, $p2);
 543  
 544          $p3 = new core_testable_persistent();
 545          $p3->set('id', $p->get('id'));
 546          $p3->read();
 547          $this->assertEquals($p, $p3);
 548      }
 549  
 550      public function test_delete() {
 551          global $DB;
 552  
 553          $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc'));
 554          $p->create();
 555          $this->assertNotEquals(0, $p->get('id'));
 556          $this->assertTrue($DB->record_exists_select(core_testable_persistent::TABLE, 'id = ?', array($p->get('id'))));
 557          $this->assertFalse(isset($p->beforedelete));
 558          $this->assertFalse(isset($p->afterdelete));
 559  
 560          $p->delete();
 561          $this->assertFalse($DB->record_exists_select(core_testable_persistent::TABLE, 'id = ?', array($p->get('id'))));
 562          $this->assertEquals(0, $p->get('id'));
 563          $this->assertEquals(true, $p->beforedelete);
 564          $this->assertEquals(true, $p->afterdelete);
 565      }
 566  
 567      public function test_has_property() {
 568          $this->assertFalse(core_testable_persistent::has_property('unknown'));
 569          $this->assertTrue(core_testable_persistent::has_property('idnumber'));
 570      }
 571  
 572      public function test_custom_setter_getter() {
 573          global $DB;
 574  
 575          $path = array(1, 2, 3);
 576          $json = json_encode($path);
 577  
 578          $p = new core_testable_persistent(0, (object) array('sortorder' => 0, 'idnumber' => 'abc'));
 579          $p->set('path', $path);
 580          $this->assertEquals($path, $p->get('path'));
 581          $this->assertEquals($json, $p->to_record()->path);
 582  
 583          $p->create();
 584          $record = $DB->get_record(core_testable_persistent::TABLE, array('id' => $p->get('id')), 'id, path', MUST_EXIST);
 585          $this->assertEquals($json, $record->path);
 586      }
 587  
 588      /**
 589       * Test get_record method for creating persistent instance
 590       */
 591      public function test_get_record(): void {
 592          $persistent = (new core_testable_persistent(0, (object) [
 593              'idnumber' => '123',
 594              'sortorder' => 1,
 595          ]))->create();
 596  
 597          $another = core_testable_persistent::get_record(['id' => $persistent->get('id')]);
 598  
 599          // Assert we got back a persistent instance, and it matches original.
 600          $this->assertInstanceOf(core_testable_persistent::class, $another);
 601          $this->assertEquals($another->to_record(), $persistent->to_record());
 602      }
 603  
 604      /**
 605       * Test get_record method for creating persistent instance, ignoring a non-existing record
 606       */
 607      public function test_get_record_ignore_missing(): void {
 608          $persistent = core_testable_persistent::get_record(['id' => 42]);
 609          $this->assertFalse($persistent);
 610      }
 611  
 612      /**
 613       * Test get_record method for creating persistent instance, throws appropriate exception for non-existing record
 614       */
 615      public function test_get_record_must_exist(): void {
 616          $this->expectException(dml_missing_record_exception::class);
 617          $this->expectExceptionMessage('Can\'t find data record in database table phpunit_persistent.');
 618          core_testable_persistent::get_record(['id' => 42], MUST_EXIST);
 619      }
 620  
 621      public function test_record_exists() {
 622          global $DB;
 623          $this->assertFalse($DB->record_exists(core_testable_persistent::TABLE, array('idnumber' => 'abc')));
 624          $p = new core_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc'));
 625          $p->create();
 626          $id = $p->get('id');
 627          $this->assertTrue(core_testable_persistent::record_exists($id));
 628          $this->assertTrue($DB->record_exists(core_testable_persistent::TABLE, array('idnumber' => 'abc')));
 629          $p->delete();
 630          $this->assertFalse(core_testable_persistent::record_exists($id));
 631      }
 632  
 633      public function test_get_sql_fields() {
 634          $expected = '' .
 635              'c.id AS prefix_id, ' .
 636              'c.shortname AS prefix_shortname, ' .
 637              'c.idnumber AS prefix_idnumber, ' .
 638              'c.description AS prefix_description, ' .
 639              'c.descriptionformat AS prefix_descriptionformat, ' .
 640              'c.parentid AS prefix_parentid, ' .
 641              'c.path AS prefix_path, ' .
 642              'c.sortorder AS prefix_sortorder, ' .
 643              'c.scaleid AS prefix_scaleid, ' .
 644              'c.timecreated AS prefix_timecreated, ' .
 645              'c.timemodified AS prefix_timemodified, ' .
 646              'c.usermodified AS prefix_usermodified';
 647          $this->assertEquals($expected, core_testable_persistent::get_sql_fields('c', 'prefix_'));
 648      }
 649  
 650      public function test_get_sql_fields_too_long() {
 651          $this->expectException(coding_exception::class);
 652          $this->expectExceptionMessageMatches('/The alias .+ exceeds 30 characters/');
 653          core_testable_persistent::get_sql_fields('c');
 654      }
 655  
 656      public function test_get(): void {
 657          $data = [
 658              'someint' => 123,
 659              'intnull' => null,
 660              'somefloat' => 33.44,
 661              'sometext' => 'Hello',
 662              'someraw' => '/dev/hello',
 663              'booltrue' => true,
 664              'boolfalse' => false,
 665          ];
 666          $p = new core_testable_second_persistent(0, (object)$data);
 667          $p->create();
 668  
 669          $this->assertSame($data['intnull'], $p->get('intnull'));
 670          $this->assertSame($data['someint'], $p->get('someint'));
 671          $this->assertIsFloat($p->get('somefloat')); // Avoid === comparisons on floats, verify type and value separated.
 672          $this->assertEqualsWithDelta($data['somefloat'], $p->get('somefloat'), 0.00001);
 673          $this->assertSame($data['sometext'], $p->get('sometext'));
 674          $this->assertSame($data['someraw'], $p->get('someraw'));
 675          $this->assertSame($data['booltrue'], $p->get('booltrue'));
 676          $this->assertSame($data['boolfalse'], $p->get('boolfalse'));
 677  
 678          // Ensure that types are correct after reloading data from database.
 679          $p->read();
 680  
 681          $this->assertSame($data['someint'], $p->get('someint'));
 682          $this->assertSame($data['intnull'], $p->get('intnull'));
 683          $this->assertIsFloat($p->get('somefloat')); // Avoid === comparisons on floats, verify type and value separated.
 684          $this->assertEqualsWithDelta($data['somefloat'], $p->get('somefloat'), 0.00001);
 685          $this->assertSame($data['sometext'], $p->get('sometext'));
 686          $this->assertSame($data['someraw'], $p->get('someraw'));
 687          $this->assertSame($data['booltrue'], $p->get('booltrue'));
 688          $this->assertSame($data['boolfalse'], $p->get('boolfalse'));
 689      }
 690  }
 691  
 692  /**
 693   * Example persistent class.
 694   *
 695   * @package    core
 696   * @copyright  2015 Frédéric Massart - FMCorz.net
 697   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 698   */
 699  class core_testable_persistent extends persistent {
 700  
 701      const TABLE = 'phpunit_persistent';
 702  
 703      /** @var bool before validate status. */
 704      public ?bool $beforevalidate;
 705  
 706      /** @var bool before create status. */
 707      public ?bool $beforecreate;
 708  
 709      /** @var bool before update status. */
 710      public ?bool $beforeupdate;
 711  
 712      /** @var bool before delete status. */
 713      public ?bool $beforedelete;
 714  
 715      /** @var bool after create status. */
 716      public ?bool $aftercreate;
 717  
 718      /** @var bool after update status. */
 719      public ?bool $afterupdate;
 720  
 721      /** @var bool after delete status. */
 722      public ?bool $afterdelete;
 723  
 724      protected static function define_properties() {
 725          return array(
 726              'shortname' => array(
 727                  'type' => PARAM_TEXT,
 728                  'default' => ''
 729              ),
 730              'idnumber' => array(
 731                  'type' => PARAM_TEXT,
 732              ),
 733              'description' => array(
 734                  'type' => PARAM_TEXT,
 735                  'default' => ''
 736              ),
 737              'descriptionformat' => array(
 738                  'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN),
 739                  'type' => PARAM_INT,
 740                  'default' => FORMAT_HTML
 741              ),
 742              'parentid' => array(
 743                  'type' => PARAM_INT,
 744                  'default' => 0
 745              ),
 746              'path' => array(
 747                  'type' => PARAM_RAW,
 748                  'default' => ''
 749              ),
 750              'sortorder' => array(
 751                  'type' => PARAM_INT,
 752                  'message' => new lang_string('invalidrequest', 'error')
 753              ),
 754              'scaleid' => array(
 755                  'type' => PARAM_INT,
 756                  'default' => null,
 757                  'null' => NULL_ALLOWED
 758              )
 759          );
 760      }
 761  
 762      protected function before_validate() {
 763          $this->beforevalidate = true;
 764      }
 765  
 766      protected function before_create() {
 767          $this->beforecreate = true;
 768      }
 769  
 770      protected function before_update() {
 771          $this->beforeupdate = true;
 772      }
 773  
 774      protected function before_delete() {
 775          $this->beforedelete = true;
 776      }
 777  
 778      protected function after_create() {
 779          $this->aftercreate = true;
 780      }
 781  
 782      protected function after_update($result) {
 783          $this->afterupdate = true;
 784      }
 785  
 786      protected function after_delete($result) {
 787          $this->afterdelete = true;
 788      }
 789  
 790      protected function get_path() {
 791          $value = $this->raw_get('path');
 792          if (!empty($value)) {
 793              $value = json_decode($value);
 794          }
 795          return $value;
 796      }
 797  
 798      protected function set_path($value) {
 799          if (!empty($value)) {
 800              $value = json_encode($value);
 801          }
 802          $this->raw_set('path', $value);
 803      }
 804  
 805      protected function validate_sortorder($value) {
 806          if ($value == 10) {
 807              return new lang_string('invalidkey', 'error');
 808          }
 809          return true;
 810      }
 811  
 812  }
 813  
 814  /**
 815   * Example persistent class to test types.
 816   *
 817   * @package    core
 818   * @copyright  2021 David Matamoros <davidmc@moodle.com>
 819   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 820   */
 821  class core_testable_second_persistent extends persistent {
 822  
 823      /** Table name for the persistent. */
 824      const TABLE = 'phpunit_second_persistent';
 825  
 826      /**
 827       * Return the list of properties.
 828       *
 829       * @return array
 830       */
 831      protected static function define_properties(): array {
 832          return [
 833              'someint' => [
 834                  'type' => PARAM_INT,
 835              ],
 836              'intnull' => [
 837                  'type' => PARAM_INT,
 838                  'null' => NULL_ALLOWED,
 839                  'default' => null,
 840              ],
 841              'somefloat' => [
 842                  'type' => PARAM_FLOAT,
 843              ],
 844              'sometext' => [
 845                  'type' => PARAM_TEXT,
 846                  'default' => ''
 847              ],
 848              'someraw' => [
 849                  'type' => PARAM_RAW,
 850                  'default' => ''
 851              ],
 852              'booltrue' => [
 853                  'type' => PARAM_BOOL,
 854              ],
 855              'boolfalse' => [
 856                  'type' => PARAM_BOOL,
 857              ]
 858          ];
 859      }
 860  }