MDL-81011 core: Add attribute alternative to hooks interfaces

This change replaces the requirement for:
- \core\hook\deprecated_callback_replacement
- \core\hook\described_hook

These are replaced by appropriate Attributes.
This commit is contained in:
Andrew Nicols 2024-02-21 22:48:05 +08:00
parent b2fa19f45d
commit 2b7754ccc2
No known key found for this signature in database
GPG Key ID: 6D1E3157C8CFBF14
16 changed files with 676 additions and 102 deletions

View File

@ -154,11 +154,7 @@ class hook_list_table extends flexible_table {
return '';
}
$rc = new \ReflectionClass($row->classname);
if (!$rc->implementsInterface(\core\hook\deprecated_callback_replacement::class)) {
return '';
}
$deprecates = call_user_func([$row->classname, 'get_deprecated_plugin_callbacks']);
$deprecates = \core\hook\manager::get_replaced_callbacks($row->classname);
if (count($deprecates) === 0) {
return '';
}

View File

@ -19,6 +19,11 @@
"allowedlevel2": true,
"allowedspread": true
},
"attribute": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"availability": {
"component": "core_availability",
"allowedlevel2": false,

View File

@ -0,0 +1,35 @@
<?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/>.
namespace core\attribute\hook;
/**
* A set of callbacks that this hook replaces.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\Attribute]
class replaces_callbacks {
/** @var string[] A list of callbacks that this hook replaces */
public readonly array $callbacks;
public function __construct(
...$callbacks,
) {
$this->callbacks = $callbacks;
}
}

View File

@ -0,0 +1,32 @@
<?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/>.
namespace core\attribute;
/**
* An unstranslated string attribute used to label an object.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\Attribute]
class label {
public function __construct(
public readonly string $label,
) {
}
}

View File

@ -0,0 +1,37 @@
<?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/>.
namespace core\attribute;
/**
* A set of string tags used to categorise an object.
*
* Note: These are not the same as the tags API.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\Attribute]
class tags {
/** @var string[] A list of tags */
public readonly array $tags;
public function __construct(
...$tags,
) {
$this->tags = $tags;
}
}

View File

@ -0,0 +1,245 @@
<?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/>.
namespace core;
use ReflectionAttribute;
/**
* Helper for loading attributes.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attribute_helper {
/**
* Get an instance of an attribute from a reference.
*
* The reference can be:
* - a string, in which case it will be checked for a function, class, method, property, constant, or enum.
* - an array
* - an instantiated object, in which case the object will be checked for a class, method, property, or constant.
*
* @param array|string|object $reference A reference of where to find the attribute
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return ?object
*/
public static function instance(
array|string|object $reference,
?string $attributename = null,
int $attributeflags = ReflectionAttribute::IS_INSTANCEOF,
): ?object {
return self::one_from($reference, $attributename, $attributeflags)?->newInstance();
}
/**
* Get all instance of an attribute from a reference.
*
* The reference can be:
* - a string, in which case it will be checked for a function, class, method, property, constant, or enum.
* - an array
* - an instantiated object, in which case the object will be checked for a class, method, property, or constant.
*
* @param array|string|object $reference A reference of where to find the attribute
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return ?object[]
*/
public static function instances(
array|string|object $reference,
?string $attributename = null,
int $attributeflags = ReflectionAttribute::IS_INSTANCEOF,
): ?array {
if ($attributes = self::from($reference, $attributename, $attributeflags)) {
return array_map(fn ($attribute) => $attribute->newInstance(), $attributes);
}
return null;
}
/**
* Get one attribute from a reference.
*
* The reference can be:
* - a string, in which case it will be checked for a function, class, method, property, constant, or enum.
* - an array
* - an instantiated object, in which case the object will be checked for a class, method, property, or constant.
*
* @param array|string|object $reference A reference of where to find the attribute
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return \ReflectionAttribute|null
*/
public static function one_from(
array|string|object $reference,
?string $attributename = null,
int $attributeflags = ReflectionAttribute::IS_INSTANCEOF,
): ?\ReflectionAttribute {
$attributes = self::from($reference, $attributename, $attributeflags);
if ($attributes && count($attributes) > 1) {
throw new \coding_exception('More than one attribute found');
}
return $attributes ? $attributes[0] : null;
}
/**
* Get the attribute from a reference.
*
* The reference can be:
* - a string, in which case it will be checked for a function, class, method, property, constant, or enum.
* - an array
* - an instantiated object, in which case the object will be checked for a class, method, property, or constant.
*
* @param array|string|object $reference A reference of where to find the attribute
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return \ReflectionAttribute[]|null
*/
public static function from(
array|string|object $reference,
?string $attributename = null,
int $attributeflags = ReflectionAttribute::IS_INSTANCEOF,
): ?array {
if (is_string($reference)) {
if (str_contains($reference, '::')) {
// The reference is a string but it looks to be in the format `object::item`.
return self::from(explode('::', $reference), $attributename, $attributeflags);;
}
if (class_exists($reference)) {
// The reference looks to be a class name.
return self::from([$reference], $attributename, $attributeflags);
}
if (function_exists($reference)) {
// The reference looks to be a global function.
$ref = new \ReflectionFunction($reference);
return $ref->getAttributes(
name: $attributename,
flags: $attributeflags,
);
}
return null;
}
if (is_object($reference)) {
// The reference is an object. Normalise and check again.
return self::from([$reference], $attributename, $attributeflags);
}
if (is_array($reference) && count($reference)) {
if (is_object($reference[0])) {
// The first array key is an instance of a class, enum, etc.
$rc = new \ReflectionObject($reference[0]);
if ($rc->isEnum() && $reference[0]->name) {
// Enums can be passed via ::from([enum::NAME]).
// In this case they will have a 'name', which must exist.
return self::from_reflected_object(
rc: $rc,
referenceproperty: $reference[0]->name,
attributename: $attributename,
flags: $attributeflags,
);
}
// The object is an instance of a class, or similar.
// That means that, if provided, the second array key is the name of the property, constant, method, etc.
return self::from_reflected_object(
rc: $rc,
referenceproperty: $reference[1] ?? null,
attributename: $attributename,
flags: $attributeflags,
);
}
if (is_string($reference[0]) && class_exists($reference[0])) {
// The first array key is a class name.
// That means that, if provided, the second array key is the name of the property, constant, method, etc.
$rc = new \ReflectionClass($reference[0]);
return self::from_reflected_object(
rc: $rc,
referenceproperty: $reference[1] ?? null,
attributename: $attributename,
flags: $attributeflags,
);
}
// The reference is an array, but it's not an object or a class that currently exists.
return null;
}
}
/**
* Fetch an attribute from a reflected object.
*
* @param \ReflectionClass $rc The reflected object
* @param null|string $referenceproperty The name of the thing to find attributes on
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return \ReflectionAttribute[]|null
*/
protected static function from_reflected_object(
\ReflectionClass $rc,
?string $referenceproperty,
?string $attributename = null,
int $flags = 0,
): ?array {
if ($referenceproperty === null) {
// No name specified - may be the whole class..
return $rc->getAttributes(
name: $attributename,
flags: $flags,
);
}
if ($rc->hasConstant($referenceproperty)) {
// This class has a constant with the specified name.
// Note: This also applies to enums.
$ref = $rc->getReflectionConstant($referenceproperty);
return $ref->getAttributes(
name: $attributename,
flags: $flags,
);
}
if ($rc->hasMethod($referenceproperty)) {
// This class has a method with the specified name.
$ref = $rc->getMethod($referenceproperty);
return $ref->getAttributes(
name: $attributename,
flags: $flags,
);
}
if ($rc->hasProperty($referenceproperty)) {
// This class has a property with the specified name.
$ref = $rc->getProperty($referenceproperty);
return $ref->getAttributes(
name: $attributename,
flags: $flags,
);
}
return null;
}
}

View File

@ -16,8 +16,6 @@
namespace core\hook\backup;
use core\hook\described_hook;
/**
* Get a list of event names which are excluded to trigger from course changes in automated backup.
*
@ -25,31 +23,14 @@ use core\hook\described_hook;
* @copyright 2023 Tomo Tsuyuki <tomotsuyuki@catalyst-au.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class get_excluded_events implements described_hook {
#[\core\attribute\label('Get a list of event names which are excluded to trigger from course changes in automated backup.')]
#[\core\attribute\tags('backup')]
final class get_excluded_events {
/**
* @var string[] Array of event names.
*/
private $events = [];
/**
* Describes the hook purpose.
*
* @return string
*/
public static function get_hook_description(): string {
return 'Get a list of event names which are excluded to trigger from course changes in automated backup.';
}
/**
* List of tags that describe this hook.
*
* @return string[]
*/
public static function get_hook_tags(): array {
return ['backup'];
}
/**
* Add an array of event names which are excluded to trigger from course changes in automated backup.
* This is set from plugin hook.

View File

@ -19,6 +19,8 @@ namespace core\hook;
/**
* Interface for describing of lib.php callbacks that were deprecated by the hook.
*
* Please note that, from Moodle 4.4, you can instead use the \core\attribute\hook\replaces_callback attribute.
*
* @package core
* @author Petr Skoda
* @copyright 2022 Open LMS

View File

@ -17,6 +17,7 @@
namespace core\hook;
use DI\ContainerBuilder;
use core\attribute\label;
/**
* Allow for init-time configuration of the Dependency Injection container.
@ -25,7 +26,8 @@ use DI\ContainerBuilder;
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class di_configuration implements described_hook {
#[label('The DI container, which allows plugins to register any service requiring configuration or initialisation.')]
class di_configuration {
/**
* Create the Dependency Injection configuration hook instance.
*
@ -78,12 +80,4 @@ class di_configuration implements described_hook {
return $this;
}
public static function get_hook_description(): string {
return 'The DI container, which allows plugins to register any service requiring configuration or initialisation.';
}
public static function get_hook_tags(): array {
return [];
}
}

View File

@ -16,6 +16,7 @@
namespace core\hook;
use core\attribute_helper;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\EventDispatcher\StoppableEventInterface;
@ -171,6 +172,30 @@ final class manager implements
}
}
/**
* Get the list of callbacks that the given hook class replaces (if any).
*
* @param string $hookclassname
* @return array
*/
public static function get_replaced_callbacks(string $hookclassname): array {
if (!class_exists($hookclassname)) {
return [];
}
if (is_subclass_of($hookclassname, \core\hook\deprecated_callback_replacement::class)) {
return $hookclassname::get_deprecated_plugin_callbacks();
}
// Ensure that the replaces_callbacks attribute is loaded.
// TODO MDL-81134 Remove after LTS+1.
require_once(dirname(__DIR__) . '/attribute/hook/replaces_callbacks.php');
if ($replaces = attribute_helper::instance($hookclassname, \core\attribute\hook\replaces_callbacks::class)) {
return $replaces->callbacks;
}
return [];
}
/**
* Verify that callback is valid.
*
@ -450,20 +475,9 @@ final class manager implements
private function fetch_deprecated_callbacks(): void {
$candidates = self::discover_known_hooks();
/** @var class-string<deprecated_callback_replacement> $hookclassname */
foreach (array_keys($candidates) as $hookclassname) {
if (!class_exists($hookclassname)) {
continue;
}
if (!is_subclass_of($hookclassname, \core\hook\deprecated_callback_replacement::class)) {
continue;
}
$deprecations = $hookclassname::get_deprecated_plugin_callbacks();
if (!$deprecations) {
continue;
}
foreach ($deprecations as $deprecation) {
$this->alldeprecations[$deprecation][] = $hookclassname;
foreach (self::get_replaced_callbacks($hookclassname) as $replacedcallback) {
$this->alldeprecations[$replacedcallback][] = $hookclassname;
}
}
}

View File

@ -16,7 +16,6 @@
namespace core\hook\navigation;
use core\hook\described_hook;
use core\hook\stoppable_trait;
use core\navigation\views\primary;
@ -27,8 +26,9 @@ use core\navigation\views\primary;
* @copyright 2023 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class primary_extend implements described_hook,
\Psr\EventDispatcher\StoppableEventInterface {
#[\core\attribute\label('Allows plugins to insert nodes into site primary navigation')]
#[\core\attribute\tags('navigation')]
class primary_extend implements \Psr\EventDispatcher\StoppableEventInterface {
use stoppable_trait;
/**
@ -36,7 +36,9 @@ class primary_extend implements described_hook,
*
* @param primary $primaryview Primary navigation view
*/
public function __construct(protected primary $primaryview) {
public function __construct(
public readonly primary $primaryview,
) {
}
/**
@ -47,22 +49,4 @@ class primary_extend implements described_hook,
public function get_primaryview(): primary {
return $this->primaryview;
}
/**
* Describes the hook purpose.
*
* @return string
*/
public static function get_hook_description(): string {
return 'Allows plugins to insert nodes into site primary navigation';
}
/**
* List of tags that describe this hook.
*
* @return string[]
*/
public static function get_hook_tags(): array {
return ['navigation'];
}
}

View File

@ -16,9 +16,6 @@
namespace core\hook\output;
use core\hook\described_hook;
use core\hook\deprecated_callback_replacement;
/**
* Allows plugins to add any elements to the page <head> html tag
*
@ -26,37 +23,13 @@ use core\hook\deprecated_callback_replacement;
* @copyright 2023 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class standard_head_html_prepend implements described_hook, deprecated_callback_replacement {
#[\core\attribute\tags('output')]
#[\core\attribute\label('Allows plugins to add any elements to the page &lt;head&gt; html tag.')]
#[\core\attribute\hook\replaces_callbacks('before_standard_html_head')]
class standard_head_html_prepend {
/** @var string $output Stores results from callbacks */
private $output = '';
/**
* Describes the hook purpose.
*
* @return string
*/
public static function get_hook_description(): string {
return 'Allows plugins to add any elements to the page &lt;head&gt; html tag.';
}
/**
* List of tags that describe this hook.
*
* @return string[]
*/
public static function get_hook_tags(): array {
return ['output'];
}
/**
* Returns list of lib.php plugin callbacks that were deprecated by the hook.
*
* @return array
*/
public static function get_deprecated_plugin_callbacks(): array {
return ['before_standard_html_head'];
}
/**
* Plugins implementing callback can add any HTML to the page.
*

View File

@ -81,6 +81,14 @@ final class hooks implements \core\hook\discovery_agent {
if (is_subclass_of($classname, \core\hook\described_hook::class)) {
$hooks[$classname]['description'] = $classname::get_hook_description();
$hooks[$classname]['tags'] = $classname::get_hook_tags();
} else {
if ($description = attribute_helper::instance($classname, \core\attribute\label::class)) {
$hooks[$classname]['description'] = (string) $description->label;
}
if ($tags = attribute_helper::instance($classname, \core\attribute\tags::class)) {
$hooks[$classname]['tags'][] = $tags->tags;
}
}
}

View File

@ -0,0 +1,170 @@
<?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/>.
namespace core;
/**
* Tests for the attribute_helper.
*
* @package core
* @category test
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core\attribute_helper
*/
final class attribute_helper_test extends \advanced_testcase {
public static function setUpBeforeClass(): void {
require_once(__DIR__ . '/fixtures/attribute_helper_example.php');
}
/**
* @dataProvider get_attributes_provider
*/
public function test_get_attributes(
int $expectedcount,
array $args,
): void {
$attributes = attribute_helper::from(...$args);
$instances = attribute_helper::instances(...$args);
if ($expectedcount) {
$this->assertNotEmpty($attributes);
$this->assertCount($expectedcount, $attributes);
$this->assertCount($expectedcount, $instances);
} else {
$this->assertEmpty($attributes);
$this->assertEmpty($instances);
}
}
public static function get_attributes_provider(): array {
return [
[3, [[attribute_helper_example::class]]],
[0, [[attribute_helper_example_without::class]]],
[3, [[attribute_helper_example::class, 'WITH_ATTRIBUTES']]],
[0, [[attribute_helper_example::class, 'WITHOUT_ATTRIBUTE']]],
[3, [[attribute_helper_example::class, 'withattributes']]],
[0, [[attribute_helper_example::class, 'withoutattributes']]],
[3, [[attribute_helper_example::class, 'with_attributes']]],
[0, [[attribute_helper_example::class, 'without_attributes']]],
[3, [[attribute_helper_enum::class, 'WITH_ATTRIBUTES']]],
[0, [[attribute_helper_enum::class, 'WITHOUT_ATTRIBUTE']]],
[3, [__NAMESPACE__ . '\\attribute_helper_method_with']],
[0, [__NAMESPACE__ . '\\attribute_helper_method_without']],
[0, [__NAMESPACE__ . '\\function_not_exists']],
[3, [attribute_helper_example::class]],
[0, [attribute_helper_example_without::class]],
[3, [attribute_helper_example::class . '::WITH_ATTRIBUTES']],
[0, [attribute_helper_example::class . '::WITHOUT_ATTRIBUTE']],
[3, [attribute_helper_example::class . '::withattributes']],
[0, [attribute_helper_example::class . '::withoutattributes']],
[3, [attribute_helper_example::class . '::with_attributes']],
[0, [attribute_helper_example::class . '::without_attributes']],
[3, [attribute_helper_enum::class . '::WITH_ATTRIBUTES']],
[0, [attribute_helper_enum::class . '::WITHOUT_ATTRIBUTE']],
[2, [[attribute_helper_example::class], attribute_helper_attribute_a::class]],
[0, [[attribute_helper_example_without::class], attribute_helper_attribute_a::class]],
[2, [[attribute_helper_example::class, 'WITH_ATTRIBUTES'], attribute_helper_attribute_a::class]],
[0, [[attribute_helper_example::class, 'WITHOUT_ATTRIBUTE'], attribute_helper_attribute_a::class]],
[2, [[attribute_helper_example::class, 'withattributes'], attribute_helper_attribute_a::class]],
[0, [[attribute_helper_example::class, 'withoutattributes'], attribute_helper_attribute_a::class]],
[2, [[attribute_helper_example::class, 'with_attributes'], attribute_helper_attribute_a::class]],
[0, [[attribute_helper_example::class, 'without_attributes'], attribute_helper_attribute_a::class]],
[2, [[attribute_helper_enum::class, 'WITH_ATTRIBUTES'], attribute_helper_attribute_a::class]],
[0, [[attribute_helper_enum::class, 'WITHOUT_ATTRIBUTE'], attribute_helper_attribute_a::class]],
[2, [__NAMESPACE__ . '\\attribute_helper_method_with', attribute_helper_attribute_a::class]],
[0, [__NAMESPACE__ . '\\attribute_helper_method_without', attribute_helper_attribute_a::class]],
];
}
public function test_get_attributes_references(): void {
$attributes = attribute_helper::from(new attribute_helper_example());
$this->assertCount(3, $attributes);
$attributes = attribute_helper::from(
new attribute_helper_example(),
attribute_helper_attribute_a::class,
);
$this->assertCount(2, $attributes);
$attributes = attribute_helper::from(attribute_helper_enum::WITH_ATTRIBUTES);
$this->assertCount(3, $attributes);
$instances = attribute_helper::instances(attribute_helper_enum::WITH_ATTRIBUTES);
$this->assertCount(3, $instances);
$attributes = attribute_helper::from(
attribute_helper_enum::WITH_ATTRIBUTES,
attribute_helper_attribute_a::class,
);
$this->assertCount(2, $attributes);
$instances = attribute_helper::instances(
attribute_helper_enum::WITH_ATTRIBUTES,
attribute_helper_attribute_a::class,
);
$this->assertCount(2, $instances);
array_map(
fn($instance) => $this->assertInstanceOf(attribute_helper_attribute_a::class, $instance),
$instances,
);
// Singular fetches.
$attribute = attribute_helper::one_from(
attribute_helper_enum::WITH_ATTRIBUTES,
attribute_helper_attribute_b::class,
);
$this->assertInstanceOf(\ReflectionAttribute::class, $attribute);
$instance = attribute_helper::instance(
attribute_helper_enum::WITH_ATTRIBUTES,
attribute_helper_attribute_b::class,
);
$this->assertInstanceOf(attribute_helper_attribute_b::class, $instance);
}
public function test_get_attributes_invalid(): void {
$this->assertNull(attribute_helper::from(non_existent_class::class));
$this->assertNull(attribute_helper::from([non_existent_class::class]));
$this->assertNull(attribute_helper::from([attribute_helper_example::class, 'non_existent']));
$this->assertNull(attribute_helper::from([non_existent_class::class, 'non_existent']));
$this->assertNull(attribute_helper::instances(non_existent_class::class));
$this->assertNull(attribute_helper::instances([non_existent_class::class]));
$this->assertNull(attribute_helper::instances([attribute_helper_example::class, 'non_existent']));
$this->assertNull(attribute_helper::instances([non_existent_class::class, 'non_existent']));
// Test singular fetches.
$this->assertNull(attribute_helper::one_from(non_existent_class::class));
$this->assertNull(attribute_helper::one_from([non_existent_class::class]));
$this->assertNull(attribute_helper::one_from([attribute_helper_example::class, 'non_existent']));
$this->assertNull(attribute_helper::one_from([non_existent_class::class, 'non_existent']));
$this->assertNull(attribute_helper::instance(non_existent_class::class));
$this->assertNull(attribute_helper::instance([non_existent_class::class]));
$this->assertNull(attribute_helper::instance([attribute_helper_example::class, 'non_existent']));
$this->assertNull(attribute_helper::instance([non_existent_class::class, 'non_existent']));
}
public function test_get_attribute_too_many(): void {
$this->expectException(\coding_exception::class);
$this->expectExceptionMessage('More than one attribute found');
attribute_helper::one_from(attribute_helper_example::class);
}
public function test_get_instance_too_many(): void {
$this->expectException(\coding_exception::class);
$this->expectExceptionMessage('More than one attribute found');
attribute_helper::instance(attribute_helper_example::class);
}
}

View File

@ -0,0 +1,88 @@
<?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/>.
namespace core;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_ALL)]
class attribute_helper_attribute_a {
public function __construct(
public readonly string $value,
) {
}
}
#[\Attribute]
class attribute_helper_attribute_b {
}
/**
* Helper for loading attributes.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[attribute_helper_attribute_a('a')]
#[attribute_helper_attribute_a('b')]
#[attribute_helper_attribute_b]
class attribute_helper_example {
#[attribute_helper_attribute_a('a')]
#[attribute_helper_attribute_a('b')]
#[attribute_helper_attribute_b]
public const WITH_ATTRIBUTES = 'examplevalue';
public const WITHOUT_ATTRIBUTE = 'examplevalue';
#[attribute_helper_attribute_a('a')]
#[attribute_helper_attribute_a('b')]
#[attribute_helper_attribute_b]
public string $withattributes = 'With attributes';
public string $withoutattributes = 'Without attributes';
#[attribute_helper_attribute_a('a')]
#[attribute_helper_attribute_a('b')]
#[attribute_helper_attribute_b]
public function with_attributes(): void {
}
public function without_attributes(): void {
}
}
class attribute_helper_example_without {
}
#[attribute_helper_attribute_a('a')]
#[attribute_helper_attribute_a('b')]
#[attribute_helper_attribute_b]
enum attribute_helper_enum: string {
#[attribute_helper_attribute_a('a')]
#[attribute_helper_attribute_a('b')]
#[attribute_helper_attribute_b]
case WITH_ATTRIBUTES = 'With attributes';
case WITHOUT_ATTRIBUTE = 'Without attributes';
}
#[attribute_helper_attribute_a('a')]
#[attribute_helper_attribute_a('b')]
#[attribute_helper_attribute_b]
function attribute_helper_method_with(): void {
}
function attribute_helper_method_without(): void {
}

View File

@ -66,6 +66,16 @@ information provided here is intended especially for developers.
* Removed \zip_writer::sanitise_filepath and \zipwriter::sanitise_filename as they are now automatically sanitised in the zipstream.
* Plugins implementing callback `bulk_user_actions()` should be aware that bulk user actions can be executed
from /admin/user.php as well as from the bulk actions page. The 'returnurl' parameter will be passed in the request.
* A new attribute helper has been created at \core\attribute_helper.
This helper contains methods to fetch a single \ReflectionAttribute, or an array of \ReflectionAttribute for an item,
or an instance, or a set of instances.
* New generic attributes have been added for:
- \core\attribute\label - An untranslated text label of an object
- \core\attribute\tags - A set of tags for an object
* The hook API now supports the use of \core\attribute\label and \core\attribute\tags
as an alternative to implementing the \core\hook\described_hook interface.
* The hook API now supports the use of the new \core\attribute\hook\replaces_callbacks() attribute
as an alternative to implementing the \core\hook\deprecated_callback_replacement interface.
=== 4.3 ===