[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 * Abstract class for core_competency objects saved to the DB. 19 * 20 * @package core_competency 21 * @copyright 2015 Damyon Wiese 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace core_competency; 25 defined('MOODLE_INTERNAL') || die(); 26 27 use coding_exception; 28 use invalid_parameter_exception; 29 use lang_string; 30 use ReflectionMethod; 31 use stdClass; 32 use renderer_base; 33 34 /** 35 * Abstract class for core_competency objects saved to the DB. 36 * 37 * @copyright 2015 Damyon Wiese 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 abstract class persistent { 41 42 /** The table name. */ 43 const TABLE = null; 44 45 /** @var array The model data. */ 46 private $data = array(); 47 48 /** @var array The list of validation errors. */ 49 private $errors = array(); 50 51 /** @var boolean If the data was already validated. */ 52 private $validated = false; 53 54 /** 55 * Create an instance of this class. 56 * 57 * @param int $id If set, this is the id of an existing record, used to load the data. 58 * @param stdClass $record If set will be passed to {@link self::from_record()}. 59 */ 60 public function __construct($id = 0, stdClass $record = null) { 61 if ($id > 0) { 62 $this->set('id', $id); 63 $this->read(); 64 } 65 if (!empty($record)) { 66 $this->from_record($record); 67 } 68 } 69 70 /** 71 * Magic method to capture getters and setters. 72 * 73 * @param string $method Callee. 74 * @param array $arguments List of arguments. 75 * @return mixed 76 */ 77 final public function __call($method, $arguments) { 78 if (strpos($method, 'get_') === 0) { 79 return $this->get(substr($method, 4)); 80 } else if (strpos($method, 'set_') === 0) { 81 return $this->set(substr($method, 4), $arguments[0]); 82 } 83 throw new coding_exception('Unexpected method call: ' . $method); 84 } 85 86 /** 87 * Data getter. 88 * 89 * This is the main getter for all the properties. Developers can implement their own getters 90 * but they should be calling {@link self::get()} in order to retrieve the value. Essentially 91 * the getters defined by the developers would only ever be used as helper methods and will not 92 * be called internally at this stage. In other words, do not expect {@link self::to_record()} or 93 * {@link self::from_record()} to use them. 94 * 95 * This is protected because we wouldn't want the developers to get into the habit of 96 * using $persistent->get('property_name'), the lengthy getters must be used. 97 * 98 * @param string $property The property name. 99 * @return mixed 100 */ 101 final protected function get($property) { 102 if (!static::has_property($property)) { 103 throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.'); 104 } 105 if (!array_key_exists($property, $this->data) && !static::is_property_required($property)) { 106 $this->set($property, static::get_property_default_value($property)); 107 } 108 return isset($this->data[$property]) ? $this->data[$property] : null; 109 } 110 111 /** 112 * Data setter. 113 * 114 * This is the main setter for all the properties. Developers can implement their own setters 115 * but they should always be calling {@link self::set()} in order to set the value. Essentially 116 * the setters defined by the developers are helper methods and will not be called internally 117 * at this stage. In other words do not expect {@link self::to_record()} or 118 * {@link self::from_record()} to use them. 119 * 120 * This is protected because we wouldn't want the developers to get into the habit of 121 * using $persistent->set('property_name', ''), the lengthy setters must be used. 122 * 123 * @param string $property The property name. 124 * @param mixed $value The value. 125 * @return mixed 126 */ 127 final protected function set($property, $value) { 128 if (!static::has_property($property)) { 129 throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.'); 130 } 131 if (!array_key_exists($property, $this->data) || $this->data[$property] != $value) { 132 // If the value is changing, we invalidate the model. 133 $this->validated = false; 134 } 135 $this->data[$property] = $value; 136 } 137 138 /** 139 * Return the custom definition of the properties of this model. 140 * 141 * Each property MUST be listed here. 142 * 143 * The result of this method is cached internally for the whole request. 144 * 145 * The 'default' value can be a Closure when its value may change during a single request. 146 * For example if the default value is based on a $CFG property, then it should be wrapped in a closure 147 * to avoid running into scenarios where the true value of $CFG is not reflected in the definition. 148 * Do not abuse closures as they obviously add some overhead. 149 * 150 * Examples: 151 * 152 * array( 153 * 'property_name' => array( 154 * 'default' => 'Default value', // When not set, the property is considered as required. 155 * 'message' => new lang_string(...), // Defaults to invalid data error message. 156 * 'null' => NULL_ALLOWED, // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOW_ALLOWED or NULL_ALLOWED. 157 * 'type' => PARAM_TYPE, // Mandatory. 158 * 'choices' => array(1, 2, 3) // An array of accepted values. 159 * ) 160 * ) 161 * 162 * array( 163 * 'dynamic_property_name' => array( 164 * 'default' => function() { 165 * return $CFG->something; 166 * }, 167 * 'type' => PARAM_INT, 168 * ) 169 * ) 170 * 171 * @return array Where keys are the property names. 172 */ 173 protected static function define_properties() { 174 return array(); 175 } 176 177 /** 178 * Get the properties definition of this model.. 179 * 180 * @return array 181 */ 182 final public static function properties_definition() { 183 global $CFG; 184 185 static $def = null; 186 if ($def !== null) { 187 return $def; 188 } 189 190 $def = static::define_properties(); 191 $def['id'] = array( 192 'default' => 0, 193 'type' => PARAM_INT, 194 ); 195 $def['timecreated'] = array( 196 'default' => 0, 197 'type' => PARAM_INT, 198 ); 199 $def['timemodified'] = array( 200 'default' => 0, 201 'type' => PARAM_INT 202 ); 203 $def['usermodified'] = array( 204 'default' => 0, 205 'type' => PARAM_INT 206 ); 207 208 // List of reserved property names. Mostly because we have methods (getters/setters) which would confict with them. 209 // Think about backwards compability before adding new ones here! 210 $reserved = array('errors', 'formatted_properties', 'records', 'records_select', 'property_default_value', 211 'property_error_message', 'sql_fields'); 212 213 foreach ($def as $property => $definition) { 214 215 // Ensures that the null property is always set. 216 if (!array_key_exists('null', $definition)) { 217 $def[$property]['null'] = NULL_NOT_ALLOWED; 218 } 219 220 // Warn the developers when they are doing something wrong. 221 if ($CFG->debugdeveloper) { 222 if (!array_key_exists('type', $definition)) { 223 throw new coding_exception('Missing type for: ' . $property); 224 225 } else if (isset($definition['message']) && !($definition['message'] instanceof lang_string)) { 226 throw new coding_exception('Invalid error message for: ' . $property); 227 228 } else if (in_array($property, $reserved)) { 229 throw new coding_exception('This property cannot be defined: ' . $property); 230 231 } 232 } 233 } 234 235 return $def; 236 } 237 238 /** 239 * Gets all the formatted properties. 240 * 241 * Formatted properties are properties which have a format associated with them. 242 * 243 * @return array Keys are property names, values are property format names. 244 */ 245 final public static function get_formatted_properties() { 246 $properties = static::properties_definition(); 247 248 $formatted = array(); 249 foreach ($properties as $property => $definition) { 250 $propertyformat = $property . 'format'; 251 if ($definition['type'] == PARAM_RAW && array_key_exists($propertyformat, $properties) 252 && $properties[$propertyformat]['type'] == PARAM_INT) { 253 $formatted[$property] = $propertyformat; 254 } 255 } 256 257 return $formatted; 258 } 259 260 /** 261 * Gets the default value for a property. 262 * 263 * This assumes that the property exists. 264 * 265 * @param string $property The property name. 266 * @return mixed 267 */ 268 final protected static function get_property_default_value($property) { 269 $properties = static::properties_definition(); 270 if (!isset($properties[$property]['default'])) { 271 return null; 272 } 273 $value = $properties[$property]['default']; 274 if ($value instanceof \Closure) { 275 return $value(); 276 } 277 return $value; 278 } 279 280 /** 281 * Gets the error message for a property. 282 * 283 * This assumes that the property exists. 284 * 285 * @param string $property The property name. 286 * @return lang_string 287 */ 288 final protected static function get_property_error_message($property) { 289 $properties = static::properties_definition(); 290 if (!isset($properties[$property]['message'])) { 291 return new lang_string('invaliddata', 'error'); 292 } 293 return $properties[$property]['message']; 294 } 295 296 /** 297 * Returns whether or not a property was defined. 298 * 299 * @param string $property The property name. 300 * @return boolean 301 */ 302 final public static function has_property($property) { 303 $properties = static::properties_definition(); 304 return isset($properties[$property]); 305 } 306 307 /** 308 * Returns whether or not a property is required. 309 * 310 * By definition a property with a default value is not required. 311 * 312 * @param string $property The property name. 313 * @return boolean 314 */ 315 final public static function is_property_required($property) { 316 $properties = static::properties_definition(); 317 return !array_key_exists('default', $properties[$property]); 318 } 319 320 /** 321 * Populate this class with data from a DB record. 322 * 323 * Note that this does not use any custom setter because the data here is intended to 324 * represent what is stored in the database. 325 * 326 * @param \stdClass $record A DB record. 327 * @return persistent 328 */ 329 final public function from_record(stdClass $record) { 330 $record = (array) $record; 331 foreach ($record as $property => $value) { 332 $this->set($property, $value); 333 } 334 return $this; 335 } 336 337 /** 338 * Create a DB record from this class. 339 * 340 * Note that this does not use any custom getter because the data here is intended to 341 * represent what is stored in the database. 342 * 343 * @return \stdClass 344 */ 345 final public function to_record() { 346 $data = new stdClass(); 347 $properties = static::properties_definition(); 348 foreach ($properties as $property => $definition) { 349 $data->$property = $this->get($property); 350 } 351 return $data; 352 } 353 354 /** 355 * Load the data from the DB. 356 * 357 * @return persistent 358 */ 359 final public function read() { 360 global $DB; 361 362 if ($this->get_id() <= 0) { 363 throw new coding_exception('id is required to load'); 364 } 365 $record = $DB->get_record(static::TABLE, array('id' => $this->get_id()), '*', MUST_EXIST); 366 $this->from_record($record); 367 368 // Validate the data as it comes from the database. 369 $this->validated = true; 370 371 return $this; 372 } 373 374 /** 375 * Hook to execute before a create. 376 * 377 * Please note that at this stage the data has already been validated and therefore 378 * any new data being set will not be validated before it is sent to the database. 379 * 380 * This is only intended to be used by child classes, do not put any logic here! 381 * 382 * @return void 383 */ 384 protected function before_create() { 385 } 386 387 /** 388 * Insert a record in the DB. 389 * 390 * @return persistent 391 */ 392 final public function create() { 393 global $DB, $USER; 394 395 if ($this->get_id()) { 396 // The validation methods rely on the ID to know if we're updating or not, the ID should be 397 // falsy whenever we are creating an object. 398 throw new coding_exception('Cannot create an object that has an ID defined.'); 399 } 400 401 if (!$this->is_valid()) { 402 throw new invalid_persistent_exception($this->get_errors()); 403 } 404 405 // Before create hook. 406 $this->before_create(); 407 408 // We can safely set those values bypassing the validation because we know what we're doing. 409 $now = time(); 410 $this->set('timecreated', $now); 411 $this->set('timemodified', $now); 412 $this->set('usermodified', $USER->id); 413 414 $record = $this->to_record(); 415 unset($record->id); 416 417 $id = $DB->insert_record(static::TABLE, $record); 418 $this->set('id', $id); 419 420 // We ensure that this is flagged as validated. 421 $this->validated = true; 422 423 // After create hook. 424 $this->after_create(); 425 426 return $this; 427 } 428 429 /** 430 * Hook to execute after a create. 431 * 432 * This is only intended to be used by child classes, do not put any logic here! 433 * 434 * @return void 435 */ 436 protected function after_create() { 437 } 438 439 /** 440 * Hook to execute before an update. 441 * 442 * Please note that at this stage the data has already been validated and therefore 443 * any new data being set will not be validated before it is sent to the database. 444 * 445 * This is only intended to be used by child classes, do not put any logic here! 446 * 447 * @return void 448 */ 449 protected function before_update() { 450 } 451 452 /** 453 * Update the existing record in the DB. 454 * 455 * @return bool True on success. 456 */ 457 final public function update() { 458 global $DB, $USER; 459 460 if ($this->get_id() <= 0) { 461 throw new coding_exception('id is required to update'); 462 } else if (!$this->is_valid()) { 463 throw new invalid_persistent_exception($this->get_errors()); 464 } 465 466 // Before update hook. 467 $this->before_update(); 468 469 // We can safely set those values after the validation because we know what we're doing. 470 $this->set('timemodified', time()); 471 $this->set('usermodified', $USER->id); 472 473 $record = $this->to_record(); 474 unset($record->timecreated); 475 $record = (array) $record; 476 477 // Save the record. 478 $result = $DB->update_record(static::TABLE, $record); 479 480 // We ensure that this is flagged as validated. 481 $this->validated = true; 482 483 // After update hook. 484 $this->after_update($result); 485 486 return $result; 487 } 488 489 /** 490 * Hook to execute after an update. 491 * 492 * This is only intended to be used by child classes, do not put any logic here! 493 * 494 * @param bool $result Whether or not the update was successful. 495 * @return void 496 */ 497 protected function after_update($result) { 498 } 499 500 /** 501 * Hook to execute before a delete. 502 * 503 * This is only intended to be used by child classes, do not put any logic here! 504 * 505 * @return void 506 */ 507 protected function before_delete() { 508 } 509 510 /** 511 * Delete an entry from the database. 512 * 513 * @return bool True on success. 514 */ 515 final public function delete() { 516 global $DB; 517 518 if ($this->get_id() <= 0) { 519 throw new coding_exception('id is required to delete'); 520 } 521 522 // Hook before delete. 523 $this->before_delete(); 524 525 $result = $DB->delete_records(static::TABLE, array('id' => $this->get_id())); 526 527 // Hook after delete. 528 $this->after_delete($result); 529 530 // Reset the ID to avoid any confusion, this also invalidates the model's data. 531 if ($result) { 532 $this->set('id', 0); 533 } 534 535 return $result; 536 } 537 538 /** 539 * Hook to execute after a delete. 540 * 541 * This is only intended to be used by child classes, do not put any logic here! 542 * 543 * @param bool $result Whether or not the delete was successful. 544 * @return void 545 */ 546 protected function after_delete($result) { 547 } 548 549 /** 550 * Hook to execute before the validation. 551 * 552 * This hook will not affect the validation results in any way but is useful to 553 * internally set properties which will need to be validated. 554 * 555 * This is only intended to be used by child classes, do not put any logic here! 556 * 557 * @return void 558 */ 559 protected function before_validate() { 560 } 561 562 /** 563 * Validates the data. 564 * 565 * Developers can implement addition validation by defining a method as follows. Note that 566 * the method MUST return a lang_string() when there is an error, and true when the data is valid. 567 * 568 * protected function validate_propertyname($value) { 569 * if ($value !== 'My expected value') { 570 * return new lang_string('invaliddata', 'error'); 571 * } 572 * return true 573 * } 574 * 575 * It is OK to use other properties in your custom validation methods when you need to, however note 576 * they might not have been validated yet, so try not to rely on them too much. 577 * 578 * Note that the validation methods should be protected. Validating just one field is not 579 * recommended because of the possible dependencies between one field and another,also the 580 * field ID can be used to check whether the object is being updated or created. 581 * 582 * When validating foreign keys the persistent should only check that the associated model 583 * exists. The validation methods should not be used to check for a change in that relationship. 584 * The API method setting the attributes on the model should be responsible for that. 585 * E.g. On a course model, the method validate_categoryid will check that the category exists. 586 * However, if a course can never be moved outside of its category it would be up to the calling 587 * code to ensure that the category ID will not be altered. 588 * 589 * @return array|true Returns true when the validation passed, or an array of properties with errors. 590 */ 591 final public function validate() { 592 global $CFG; 593 594 // Before validate hook. 595 $this->before_validate(); 596 597 // If this object has not been validated yet. 598 if ($this->validated !== true) { 599 600 $errors = array(); 601 $properties = static::properties_definition(); 602 foreach ($properties as $property => $definition) { 603 604 // Get the data, bypassing the potential custom getter which could alter the data. 605 $value = $this->get($property); 606 607 // Check if the property is required. 608 if ($value === null && static::is_property_required($property)) { 609 $errors[$property] = new lang_string('requiredelement', 'form'); 610 continue; 611 } 612 613 // Check that type of value is respected. 614 try { 615 if ($definition['type'] === PARAM_BOOL && $value === false) { 616 // Validate_param() does not like false with PARAM_BOOL, better to convert it to int. 617 $value = 0; 618 } 619 validate_param($value, $definition['type'], $definition['null']); 620 } catch (invalid_parameter_exception $e) { 621 $errors[$property] = static::get_property_error_message($property); 622 continue; 623 } 624 625 // Check that the value is part of a list of allowed values. 626 if (isset($definition['choices']) && !in_array($value, $definition['choices'])) { 627 $errors[$property] = static::get_property_error_message($property); 628 continue; 629 } 630 631 // Call custom validation method. 632 $method = 'validate_' . $property; 633 if (method_exists($this, $method)) { 634 635 // Warn the developers when they are doing something wrong. 636 if ($CFG->debugdeveloper) { 637 $reflection = new ReflectionMethod($this, $method); 638 if (!$reflection->isProtected()) { 639 throw new coding_exception('The method ' . get_class($this) . '::'. $method . ' should be protected.'); 640 } 641 } 642 643 $valid = $this->{$method}($value); 644 if ($valid !== true) { 645 if (!($valid instanceof lang_string)) { 646 throw new coding_exception('Unexpected error message.'); 647 } 648 $errors[$property] = $valid; 649 continue; 650 } 651 } 652 } 653 654 $this->validated = true; 655 $this->errors = $errors; 656 } 657 658 return empty($this->errors) ? true : $this->errors; 659 } 660 661 /** 662 * Returns whether or not the model is valid. 663 * 664 * @return boolean True when it is. 665 */ 666 final public function is_valid() { 667 return $this->validate() === true; 668 } 669 670 /** 671 * Returns the validation errors. 672 * 673 * @return array 674 */ 675 final public function get_errors() { 676 $this->validate(); 677 return $this->errors; 678 } 679 680 /** 681 * Extract a record from a row of data. 682 * 683 * Most likely used in combination with {@link self::get_sql_fields()}. This method is 684 * simple enough to be used by non-persistent classes, keep that in mind when modifying it. 685 * 686 * e.g. persistent::extract_record($row, 'user'); should work. 687 * 688 * @param stdClass $row The row of data. 689 * @param string $prefix The prefix the data fields are prefixed with, defaults to the table name followed by underscore. 690 * @return stdClass The extracted data. 691 */ 692 public static function extract_record($row, $prefix = null) { 693 if ($prefix === null) { 694 $prefix = str_replace('_', '', static::TABLE) . '_'; 695 } 696 $prefixlength = strlen($prefix); 697 698 $data = new stdClass(); 699 foreach ($row as $property => $value) { 700 if (strpos($property, $prefix) === 0) { 701 $propertyname = substr($property, $prefixlength); 702 $data->$propertyname = $value; 703 } 704 } 705 706 return $data; 707 } 708 709 /** 710 * Load a list of records. 711 * 712 * @param array $filters Filters to apply. 713 * @param string $sort Field to sort by. 714 * @param string $order Sort order. 715 * @param int $skip Limitstart. 716 * @param int $limit Number of rows to return. 717 * 718 * @return \core_competency\persistent[] 719 */ 720 public static function get_records($filters = array(), $sort = '', $order = 'ASC', $skip = 0, $limit = 0) { 721 global $DB; 722 723 $orderby = ''; 724 if (!empty($sort)) { 725 $orderby = $sort . ' ' . $order; 726 } 727 728 $records = $DB->get_records(static::TABLE, $filters, $orderby, '*', $skip, $limit); 729 $instances = array(); 730 731 foreach ($records as $record) { 732 $newrecord = new static(0, $record); 733 array_push($instances, $newrecord); 734 } 735 return $instances; 736 } 737 738 /** 739 * Load a single record. 740 * 741 * @param array $filters Filters to apply. 742 * @return false|\core_competency\persistent 743 */ 744 public static function get_record($filters = array()) { 745 global $DB; 746 747 $record = $DB->get_record(static::TABLE, $filters); 748 return $record ? new static(0, $record) : false; 749 } 750 751 /** 752 * Load a list of records based on a select query. 753 * 754 * @param string $select 755 * @param array $params 756 * @param string $sort 757 * @param string $fields 758 * @param int $limitfrom 759 * @param int $limitnum 760 * @return \core_competency\persistent[] 761 */ 762 public static function get_records_select($select, $params = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) { 763 global $DB; 764 765 $records = $DB->get_records_select(static::TABLE, $select, $params, $sort, $fields, $limitfrom, $limitnum); 766 767 // We return class instances. 768 $instances = array(); 769 foreach ($records as $key => $record) { 770 $instances[$key] = new static(0, $record); 771 } 772 773 return $instances; 774 775 } 776 777 /** 778 * Return the list of fields for use in a SELECT clause. 779 * 780 * Having the complete list of fields prefixed allows for multiple persistents to be fetched 781 * in a single query. Use {@link self::extract_record()} to extract the records from the query result. 782 * 783 * @param string $alias The alias used for the table. 784 * @param string $prefix The prefix to use for each field, defaults to the table name followed by underscore. 785 * @return string The SQL fragment. 786 */ 787 public static function get_sql_fields($alias, $prefix = null) { 788 global $CFG; 789 $fields = array(); 790 791 if ($prefix === null) { 792 $prefix = str_replace('_', '', static::TABLE) . '_'; 793 } 794 795 // Get the properties and move ID to the top. 796 $properties = static::properties_definition(); 797 $id = $properties['id']; 798 unset($properties['id']); 799 $properties = array('id' => $id) + $properties; 800 801 foreach ($properties as $property => $definition) { 802 $as = $prefix . $property; 803 $fields[] = $alias . '.' . $property . ' AS ' . $as; 804 805 // Warn developers that the query will not always work. 806 if ($CFG->debugdeveloper && strlen($as) > 30) { 807 throw new coding_exception("The alias '$as' for column '$alias.$property' exceeds 30 characters" . 808 " and will therefore not work across all supported databases."); 809 } 810 } 811 812 return implode(', ', $fields); 813 } 814 815 /** 816 * Count a list of records. 817 * 818 * @param array $conditions An array of conditions. 819 * @return int 820 */ 821 public static function count_records(array $conditions = array()) { 822 global $DB; 823 824 $count = $DB->count_records(static::TABLE, $conditions); 825 return $count; 826 } 827 828 /** 829 * Count a list of records. 830 * 831 * @param string $select 832 * @param array $params 833 * @return int 834 */ 835 public static function count_records_select($select, $params = null) { 836 global $DB; 837 838 $count = $DB->count_records_select(static::TABLE, $select, $params); 839 return $count; 840 } 841 842 /** 843 * Check if a record exists by ID. 844 * 845 * @param int $id Record ID. 846 * @return bool 847 */ 848 public static function record_exists($id) { 849 global $DB; 850 return $DB->record_exists(static::TABLE, array('id' => $id)); 851 } 852 853 /** 854 * Check if a records exists. 855 * 856 * @param string $select 857 * @param array $params 858 * @return bool 859 */ 860 public static function record_exists_select($select, array $params = null) { 861 global $DB; 862 return $DB->record_exists_select(static::TABLE, $select, $params); 863 } 864 865 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |