Skip to content

Latest commit

 

History

History
339 lines (254 loc) · 10.8 KB

README.md

File metadata and controls

339 lines (254 loc) · 10.8 KB

Reflection Meta

PHP reflection in more reliable and deterministic way - for declarative engines

Content

What is it good for?

For declarative engines like orisai/object-mapper, defining what they do in form of annotations or attributes in a class (or in an external file). It makes PHP reflection easier to read and distinguish between callable context definition (class using trait or implementing interface) and the actual source definition (trait or interface).

This is an internal tool, and you likely do not need to use it directly.

Setup

Install with Composer

composer require orisai/reflection-meta

Building reflectors structure

To make PHP reflection usable for declarative engines we restructure reflectors in several phases:

  • Hierarchy - exact copy of classes structure as declared in code
  • List - flattened classes structure, removing duplicate interfaces and traits
  • Group - grouping overloaded definitions of public/protected definitions
use Orisai\ReflectionMeta\Structure\StructureBuilder;
use Orisai\ReflectionMeta\Structure\StructureFlattener;
use Orisai\ReflectionMeta\Structure\StructureGrouper;
use ReflectionClass;

$reflector = new ReflectionClass(ExampleClass::class);
$hierarchy = StructureBuilder::build($reflector);
$list = StructureFlattener::flatten($hierarchy);
$group = StructureGrouper::group($list);

var_dump($group);
/*
StructureGroup(
	classes: [
		ClassStructure(ParentInterface),
		ClassStructure(ParentTrait),
		ClassStructure(ParentClass),
		ClassStructure(ExampleInterface),
		ClassStructure(ExampleTrait),
		ClassStructure(ExampleClass),
	],
	constants: [
		'::publicConstName' => [
			ConstantStructure(ExampleInterface, 'publicConstName'),
		],
		'::protectedConstName' => [
			ConstantStructure(ParentClass, 'protectedConstName'),
			ConstantStructure(ExampleClass, 'protectedConstName'),
		],
		'ParentClass::privateConstName' => [
			ConstantStructure(ParentClass, 'privateConstName'),
		],
	],
	properties: [
		'::publicPropertyName' => [
			PropertyStructure(ExampleClass, 'publicPropertyName'),
		],
		// ...
	],
	methods: [
		// ...
	],
)
*/

Hierarchy

HierarchyClassStructure reflects the classes exactly how they were defined. Class with its parent, all interfaces and traits. Inside each class definition (including interfaces, traits and enums which are considered special types of classes by PHP reflection) are its constants, properties and methods.

Unlike in PHP reflection, public and protected definitions from parents are available only via parent definition, to avoid duplicates.

use Orisai\ReflectionMeta\Structure\StructureBuilder;
use ReflectionClass;

$reflector = new ReflectionClass(ExampleClass::class);
$hierarchy = StructureBuilder::build($reflector); // HierarchyClassStructure

$hierarchy->getParent(); // self|null
$hierarchy->getInterfaces(); // list<self>
$hierarchy->getTraits(); // list<self>

$hierarchy->getConstants(); // list<ConstantStructure>
$hierarchy->getProperties(); // list<PropertyStructure>
$hierarchy->getMethods(); // list<MethodStructure>

$hierarchy->getContextClass(); // ReflectionClass
$hierarchy->getSource(); // ClassSource

List

StructureList changes the class hierarchy into a flat list in following order:

  • parent
    • interfaces
    • traits
    • class
  • interfaces
  • traits
  • class

In case any interface or trait is used twice (e.g. by parent and child class), the later duplicate is removed.

Constants, properties and methods from all the classes are flattened as well.

use Orisai\ReflectionMeta\Structure\StructureFlattener;

$list = StructureFlattener::flatten($hierarchy); // StructureList

$list->getClasses(); // list<ClassStructure>
$list->getConstants(); // list<ConstantStructure>
$list->getProperties(); // list<PropertyStructure>
$list->getMethods(); // list<MethodStructure>

Group

StructureGroup groups all matching definitions of the same constants, properties and methods.

All public and protected properties with the same name are grouped together in an array with key ::propertyName. Private properties are alone in their own group, with key ClassName::propertyName. Same applies to constants and methods.

use Orisai\ReflectionMeta\Structure\StructureGrouper;

$group = StructureGrouper::group($list); // StructureGroup

$group->getClasses(); // list<ClassStructure>
$group->getGroupedConstants(); // array<string, list<ConstantStructure>>
$group->getGroupedProperties(); // array<string, list<PropertyStructure>>
$group->getGroupedMethods(); // array<string, list<MethodStructure>>

Reading metadata

To read metadata from a reflector (received in building phase), use the MetaReader

use Orisai\ReflectionMeta\Reader\AttributesMetaReader;

$definitionClass = ExampleAttribute::class;

$reader = new AttributesMetaReader(); // Or any other MetaReader
$reader->readClass($reflectionClass, $definitionClass); // list<$definitionClass>
$reader->readConstant($reflectionConstant, $definitionClass); // list<$definitionClass>
$reader->readProperty($reflectionProperty, $definitionClass); // list<$definitionClass>
$reader->readMethod($reflectionMethod, $definitionClass); // list<$definitionClass>
$reader->readParameter($reflectionParameter, $definitionClass); // list<$definitionClass>

e.g. loading metadata from structure group classes would look like this:

$metaByClass = [];
foreach ($group->getClasses() as $class) {
	$metaByClass[] = $reader->readClass($class, $definitionClass);
}

$meta = array_merge(...$metaByClass);

Annotations

Load metadata from doctrine/annotations

use Doctrine\Common\Annotations\AnnotationReader;
use Orisai\ReflectionMeta\Reader\AnnotationsMetaReader;

$reader = new AnnotationsMetaReader();
// or with specific doctrine/annotations reader
$reader = new AnnotationsMetaReader(new AnnotationReader());
/**
 * @Annotation
 */
class ExampleAnnotation {}

/**
 * @ExampleAnnotation()
 */
class ExampleClass {}

Filtering annotations

Sometimes you may want to write a multi-line text in an annotation, e.g. documentation. Doctrine annotations do not handle this case and the string contains asterisks (*) from phpdoc. To normalize multi-line string, use AnnotationFilter

use Orisai\ReflectionMeta\Filter\AnnotationFilter;

$docblock = AnnotationFilter::filterMultilineDocblock($docblock);

It will normalize the string in following way:

  • remove first line, if empty
  • remove last line, if empty
  • remove single asterisk and also following single space from each line, if they exist

All of the above is shown in this example:

/**
 * @Description("
 * Multi
 *  line
 *
 *   docblock
 * ")
 */
class ExampleClass{}
Multi
 line

  docblock

Attributes

Load metadata from PHP (8.0+) attributes

use Orisai\ReflectionMeta\Reader\AttributesMetaReader;

$reader = new AttributesMetaReader();
use Attribute;

#[Attribute]
class ExampleAttribute {}

#[ExampleAttribute]
class ExampleClass {}

Callable definition and source definition

Each structure has two ways of accessing reflection.

First is $structure->getContextReflector()->getDeclaringClass(). It returns the class which can be used to access properties and methods by reflection or by closure binding and therefore excludes interfaces and traits.

To make it work, the reflector given to StructureBuilder::build() must be a non-abstract class. Otherwise, context will be based on whatever reflector was given to builder. In case of abstract classes, interfaces and traits, the object will be not instantiable and neither non-static nor static properties and methods can be called.

For example assigning a property of any visibility to an instance would look like this:

$object = /* create instance of a root class (the one given to StructureBuilder */;

$contextProperty = $propertyStructure->getContextReflector();
$className = $contextProperty->getDeclaringClass()->getName();
$propertyName = $contextProperty->getName();
$value = 'anything';

(fn () => $object->$propertyName = $value)
			->bindTo($object, $className)();

Seconds way of accessing reflection is $structure->getSource()->getReflector(). It returns the actual source as defined in code, including interfaces and traits and therefore is useful to show in metadata validation errors.

Each source is an instance of ReflectorSource and their description is available in orisai/source-map

For each structure, different source is returned:

  • ClassStructure -> ClassSource
    • HierarchyClassStructure -> ClassSource
  • ConstantStructure -> ClassConstantSource
  • MethodStructure -> MethodSource
  • ParameterStructure -> ParameterSource
  • PropertyStructure -> PropertySource

What you should consider

These are just tips what you should consider in your own code when using this library as there is no general solution provided by us

  • Visibility - With Closure::bindTo() and $structure->getContextReflector() you can work with properties and methods with any visibility, but you may still want to choose whether public, protected and private should be all supported.
  • Static - With Closure::bindTo() and $structure->getContextReflector() you can work with properties and methods that are either static or non-static, but you will likely want to support only non-static properties and may want to support only non-static methods.
  • Source - Metadata can be defined on class or its subtypes - interface, trait or enum.
    • Check whether they are defined only by types that you want to support.
    • You will probably want to require non-abstract class as a root source and forbid enums for most cases.
  • Target - Metadata can be defined on class (or its subtypes), constant, property, method or method parameter.
    • Both annotations and attributes can individually define their allowed target
      • Alternatively (or in addition) you may also want to check all of them
    • Method parameters are not supported by annotations, only attributes