Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
67 / 67 |
|
100.00% |
10 / 10 |
CRAP | |
100.00% |
1 / 1 |
TestingAccessWrapper | |
100.00% |
67 / 67 |
|
100.00% |
10 / 10 |
29 | |
100.00% |
1 / 1 |
newFromObject | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
newFromClass | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
constant | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
__call | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
__set | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
__get | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
isStatic | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMethod | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getProperty | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace Wikimedia; |
4 | |
5 | use DomainException; |
6 | use InvalidArgumentException; |
7 | use ReflectionClass; |
8 | use ReflectionException; |
9 | use ReflectionMethod; |
10 | use ReflectionProperty; |
11 | |
12 | /** |
13 | * Circumvent access restrictions on object internals |
14 | * |
15 | * This can be helpful for writing tests that can probe object internals, |
16 | * without having to modify the class under test to accommodate. |
17 | * |
18 | * Wrap an object with private methods as follows: |
19 | * $title = TestingAccessWrapper::newFromObject( Title::newFromDBkey( $key ) ); |
20 | * |
21 | * You can access private and protected instance methods and variables: |
22 | * $formatter = $title->getTitleFormatter(); |
23 | * |
24 | * You can access private and protected constants: |
25 | * $value = TestingAccessWrapper::constant( Foo::class, 'FOO_CONSTANT' ); |
26 | * |
27 | */ |
28 | class TestingAccessWrapper { |
29 | /** @var mixed The object, or the class name for static-only access */ |
30 | public $object; |
31 | |
32 | /** |
33 | * Return a proxy object which can be used the same way as the original, |
34 | * except that access restrictions can be ignored (protected and private methods and properties |
35 | * are available for any caller). |
36 | * @param object $object |
37 | * @return self |
38 | * @throws InvalidArgumentException |
39 | */ |
40 | public static function newFromObject( $object ) { |
41 | if ( !is_object( $object ) ) { |
42 | throw new InvalidArgumentException( __METHOD__ . ' must be called with an object' ); |
43 | } |
44 | $wrapper = new self(); |
45 | $wrapper->object = $object; |
46 | return $wrapper; |
47 | } |
48 | |
49 | /** |
50 | * Allow access to non-public static methods and properties of the class. |
51 | * Returns an object whose methods/properties will correspond to the |
52 | * static methods/properties of the given class. |
53 | * @param class-string $className |
54 | * @return self |
55 | * @throws InvalidArgumentException |
56 | */ |
57 | public static function newFromClass( $className ) { |
58 | if ( !is_string( $className ) ) { |
59 | throw new InvalidArgumentException( __METHOD__ . ' must be called with a class name' ); |
60 | } |
61 | $wrapper = new self(); |
62 | $wrapper->object = $className; |
63 | return $wrapper; |
64 | } |
65 | |
66 | /** |
67 | * Allow access to non-public constants of the class. |
68 | * @param class-string $className |
69 | * @param string $constantName |
70 | * @return mixed |
71 | */ |
72 | public static function constant( $className, $constantName ) { |
73 | $classReflection = new ReflectionClass( $className ); |
74 | // getConstant() returns `false` if the constant is defined in |
75 | // a parent class; this works more like ReflectionClass::getMethod() |
76 | while ( !$classReflection->hasConstant( $constantName ) ) { |
77 | $classReflection = $classReflection->getParentClass(); |
78 | if ( !$classReflection ) { |
79 | throw new ReflectionException( 'constant not present' ); |
80 | } |
81 | } |
82 | return $classReflection->getConstant( $constantName ); |
83 | } |
84 | |
85 | /** |
86 | * Allow constructing a class with a non-public constructor. |
87 | * @param class-string<T> $className |
88 | * @param mixed ...$args |
89 | * @return T |
90 | * @phan-template T |
91 | */ |
92 | public static function construct( string $className, ...$args ) { |
93 | $classReflection = new ReflectionClass( $className ); |
94 | $constructor = $classReflection->getConstructor(); |
95 | $constructor->setAccessible( true ); |
96 | $object = $classReflection->newInstanceWithoutConstructor(); |
97 | $constructor->invokeArgs( $object, $args ); |
98 | return $object; |
99 | } |
100 | |
101 | /** |
102 | * @param string $method |
103 | * @param array $args |
104 | * @return mixed |
105 | */ |
106 | public function __call( $method, $args ) { |
107 | $methodReflection = $this->getMethod( $method ); |
108 | |
109 | if ( $this->isStatic() && !$methodReflection->isStatic() ) { |
110 | throw new DomainException( __METHOD__ |
111 | . ': Cannot call non-static method when wrapping static class' ); |
112 | } |
113 | |
114 | return $methodReflection->invokeArgs( $methodReflection->isStatic() ? null : $this->object, |
115 | $args ); |
116 | } |
117 | |
118 | /** |
119 | * @param string $name |
120 | * @param mixed $value |
121 | */ |
122 | public function __set( $name, $value ) { |
123 | $propertyReflection = $this->getProperty( $name ); |
124 | |
125 | if ( $this->isStatic() && !$propertyReflection->isStatic() ) { |
126 | throw new DomainException( __METHOD__ |
127 | . ': Cannot set non-static property when wrapping static class' ); |
128 | } |
129 | |
130 | if ( $this->isStatic() ) { |
131 | $class = new ReflectionClass( $this->object ); |
132 | $class->setStaticPropertyValue( $name, $value ); |
133 | } else { |
134 | $propertyReflection->setValue( $this->object, $value ); |
135 | } |
136 | } |
137 | |
138 | /** |
139 | * @param string $name Field name |
140 | * @return mixed |
141 | */ |
142 | public function __get( $name ) { |
143 | $propertyReflection = $this->getProperty( $name ); |
144 | |
145 | if ( $this->isStatic() && !$propertyReflection->isStatic() ) { |
146 | throw new DomainException( __METHOD__ |
147 | . ': Cannot get non-static property when wrapping static class' ); |
148 | } |
149 | |
150 | if ( $propertyReflection->isStatic() ) { |
151 | // https://bugs.php.net/bug.php?id=69804 - can't use getStaticPropertyValue() on |
152 | // non-public properties |
153 | $class = new ReflectionClass( $this->object ); |
154 | $props = $class->getStaticProperties(); |
155 | |
156 | // Can't use isset() as it returns false for null values |
157 | if ( !array_key_exists( $name, $props ) ) { |
158 | throw new DomainException( __METHOD__ . ": class {$class->name} " |
159 | . "doesn't have static property '{$name}'" ); |
160 | } |
161 | return $props[$name]; |
162 | } |
163 | |
164 | return $propertyReflection->getValue( $this->object ); |
165 | } |
166 | |
167 | /** |
168 | * Tells whether this object was created for an object or a class. |
169 | * @return bool |
170 | */ |
171 | private function isStatic() { |
172 | return is_string( $this->object ); |
173 | } |
174 | |
175 | /** |
176 | * Return a method and make it accessible. |
177 | * @param string $name |
178 | * @return ReflectionMethod |
179 | * @throws ReflectionException |
180 | */ |
181 | private function getMethod( $name ) { |
182 | $classReflection = new ReflectionClass( $this->object ); |
183 | $methodReflection = $classReflection->getMethod( $name ); |
184 | $methodReflection->setAccessible( true ); |
185 | return $methodReflection; |
186 | } |
187 | |
188 | /** |
189 | * Return a property and make it accessible. |
190 | * |
191 | * ReflectionClass::getProperty() fails if the private property is defined |
192 | * in a parent class. This works more like ReflectionClass::getMethod(). |
193 | * |
194 | * @param string $name |
195 | * @return ReflectionProperty |
196 | * @throws ReflectionException |
197 | */ |
198 | private function getProperty( $name ) { |
199 | $classReflection = new ReflectionClass( $this->object ); |
200 | try { |
201 | $propertyReflection = $classReflection->getProperty( $name ); |
202 | } catch ( ReflectionException $ex ) { |
203 | while ( true ) { |
204 | $classReflection = $classReflection->getParentClass(); |
205 | if ( !$classReflection ) { |
206 | throw $ex; |
207 | } |
208 | try { |
209 | $propertyReflection = $classReflection->getProperty( $name ); |
210 | } catch ( ReflectionException $ex2 ) { |
211 | continue; |
212 | } |
213 | if ( $propertyReflection->isPrivate() ) { |
214 | break; |
215 | } else { |
216 | // @codeCoverageIgnoreStart |
217 | throw $ex; |
218 | // @codeCoverageIgnoreEnd |
219 | } |
220 | } |
221 | } |
222 | $propertyReflection->setAccessible( true ); |
223 | return $propertyReflection; |
224 | } |
225 | } |