Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.74% covered (success)
94.74%
36 / 38
90.91% covered (success)
90.91%
10 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
AttributeProcessor
94.74% covered (success)
94.74%
36 / 38
90.91% covered (success)
90.91%
10 / 11
26.10
0.00% covered (danger)
0.00%
0 / 1
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createHandlersPerTarget
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 processAll
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 isProcessAllowed
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 process
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 processClass
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 processProperties
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 processMethods
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 processSubject
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Dynart\Micro\Middleware;
4
5use Dynart\Micro\Micro;
6use Dynart\Micro\Middleware;
7use Dynart\Micro\AttributeHandler;
8use Dynart\Micro\MicroException;
9
10/**
11 * Processes PHP 8 attributes on registered classes
12 * @package Dynart\Micro
13 */
14class AttributeProcessor implements Middleware {
15
16    /** @var string[] */
17    protected $handlerClasses = [];
18
19    /** @var AttributeHandler[][] */
20    protected $handlers = [
21        AttributeHandler::TARGET_CLASS    => [],
22        AttributeHandler::TARGET_PROPERTY => [],
23        AttributeHandler::TARGET_METHOD   => []
24    ];
25
26    /** @var string[] */
27    protected $namespaces = [];
28
29    /**
30     * Adds an attribute handler for processing
31     *
32     * The given class name should implement the AttributeHandler interface, otherwise
33     * it will throw a MicroException.
34     *
35     * @throws MicroException if the given class does not implement AttributeHandler
36     * @param string $className The class name
37     */
38    public function add(string $className) {
39        if (!is_subclass_of($className, AttributeHandler::class)) {
40            throw new MicroException("$className doesn't implement the AttributeHandler interface");
41        }
42        $this->handlerClasses[] = $className;
43    }
44
45    /**
46     * Adds a namespace
47     *
48     * If one or more namespace added only those will be processed. The namespace should NOT start with a backslash!
49     *
50     * @param string $namespace
51     */
52    public function addNamespace(string $namespace) {
53        $this->namespaces[] = $namespace;
54    }
55
56    /**
57     * Creates the handlers then processes all interfaces in the App or those that are in the given namespaces.
58     */
59    public function run() {
60        $this->createHandlersPerTarget();
61        $this->processAll();
62    }
63
64    /**
65     * Creates the handler instances and puts them into the right `$handlers` array
66     */
67    protected function createHandlersPerTarget(): void {
68        foreach ($this->handlerClasses as $className) {
69            $handler = Micro::get($className);
70            foreach ($handler->targets() as $target) {
71                $this->handlers[$target][] = $handler;
72            }
73        }
74    }
75
76    /**
77     * Processes all interfaces in the App or those that are in the given namespaces
78     */
79    protected function processAll(): void {
80        foreach (Micro::interfaces() as $className) {
81            if ($this->isProcessAllowed($className)) {
82                $this->process($className);
83            }
84        }
85    }
86
87    /**
88     * If no namespace added returns true, otherwise checks the namespace and returns true if the interface is in it.
89     * @param string $className The name of the class
90     * @return bool Should we process this class?
91     */
92    protected function isProcessAllowed(string $className): bool {
93        if (empty($this->namespaces)) {
94            return true;
95        }
96        foreach ($this->namespaces as $namespace) {
97            if (substr($className, 0, strlen($namespace)) == $namespace) {
98                return true;
99            }
100        }
101        return false;
102    }
103
104    /**
105     * Processes one class with the given name
106     * @param string $className The name of the class
107     */
108    protected function process(string $className): void {
109        try {
110            $refClass = new \ReflectionClass($className);
111        } catch (\ReflectionException $ignore) {
112            throw new MicroException("Can't create reflection for: $className");
113        }
114        $this->processClass($refClass);
115        $this->processProperties($refClass);
116        $this->processMethods($refClass);
117    }
118
119    /**
120     * Processes all class-level attributes for the class
121     * @param \ReflectionClass $refClass
122     */
123    protected function processClass(\ReflectionClass $refClass): void {
124        foreach ($this->handlers[AttributeHandler::TARGET_CLASS] as $handler) {
125            $this->processSubject($handler, $refClass->getName(), $refClass);
126        }
127    }
128
129    /**
130     * Processes all property-level attributes for all the properties of a class
131     * @param \ReflectionClass $refClass
132     */
133    protected function processProperties(\ReflectionClass $refClass): void {
134        $refProperties = $refClass->getProperties();
135        foreach ($this->handlers[AttributeHandler::TARGET_PROPERTY] as $handler) {
136            foreach ($refProperties as $refProperty) {
137                $this->processSubject($handler, $refClass->getName(), $refProperty);
138            }
139        }
140    }
141
142    /**
143     * Processes all method-level attributes for all the methods of a class
144     * @param \ReflectionClass $refClass
145     */
146    protected function processMethods(\ReflectionClass $refClass): void {
147        $refMethods = $refClass->getMethods();
148        foreach ($this->handlers[AttributeHandler::TARGET_METHOD] as $handler) {
149            foreach ($refMethods as $refMethod) {
150                $this->processSubject($handler, $refClass->getName(), $refMethod);
151            }
152        }
153    }
154
155    /**
156     * Processes attributes on a class, property or method
157     *
158     * Gets the PHP 8 attributes from the subject that match the handler's attribute class,
159     * instantiates each and calls the handler's handle() method.
160     *
161     * @param AttributeHandler $handler The attribute handler
162     * @param string $className The class name
163     * @param \ReflectionClass|\ReflectionProperty|\ReflectionMethod $subject The reflection class, property or method
164     */
165    protected function processSubject(AttributeHandler $handler, string $className, $subject): void {
166        $attributes = $subject->getAttributes($handler->attributeClass());
167        foreach ($attributes as $refAttribute) {
168            $handler->handle($className, $subject, $refAttribute->newInstance());
169        }
170    }
171}