Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
84.71% |
72 / 85 |
|
76.47% |
13 / 17 |
CRAP | |
0.00% |
0 / 1 |
ZObject | |
84.71% |
72 / 85 |
|
76.47% |
13 / 17 |
47.01 | |
0.00% |
0 / 1 |
getDefinition | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
isValid | |
30.00% |
3 / 10 |
|
0.00% |
0 / 1 |
13.57 | |||
getValueByKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setValueByKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isBuiltin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isTypeReference | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isTypeFunctionCall | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getZTypeObject | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getZType | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getZValue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLinkedZObjects | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
addLinkedZObject | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
extractLinkedZObjects | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
8.04 | |||
getSerialized | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
7.64 | |||
getHumanReadable | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * WikiLambda generic ZObject class |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt |
8 | * @license MIT |
9 | */ |
10 | |
11 | namespace MediaWiki\Extension\WikiLambda\ZObjects; |
12 | |
13 | use FormatJson; |
14 | use Language; |
15 | use MediaWiki\Context\RequestContext; |
16 | use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry; |
17 | use MediaWiki\Extension\WikiLambda\WikiLambdaServices; |
18 | use MediaWiki\Extension\WikiLambda\ZObjectUtils; |
19 | |
20 | class ZObject { |
21 | |
22 | public const FORM_CANONICAL = 1; |
23 | public const FORM_NORMAL = 2; |
24 | |
25 | /** @var ZReference|ZFunctionCall|null */ |
26 | protected $type = null; |
27 | |
28 | /** @var array */ |
29 | protected $data = []; |
30 | |
31 | /** @var array */ |
32 | protected $linkedZObjects = []; |
33 | |
34 | /** |
35 | * Provide this ZObject's schema. |
36 | * |
37 | * @return array It's complicated. |
38 | */ |
39 | public static function getDefinition(): array { |
40 | return [ |
41 | 'type' => [ |
42 | 'type' => ZTypeRegistry::Z_REFERENCE, |
43 | 'value' => ZTypeRegistry::Z_OBJECT, |
44 | ], |
45 | 'keys' => [ |
46 | ZTypeRegistry::Z_OBJECT_TYPE => [ |
47 | 'type' => ZTypeRegistry::HACK_REFERENCE_TYPE, |
48 | ], |
49 | ], |
50 | 'additionalKeys' => true |
51 | ]; |
52 | } |
53 | |
54 | /** |
55 | * Construct a new ZObject instance. This top-level class has a number of Type-specific sub- |
56 | * classes for built-in representations, and is mostly intended to represent instances of |
57 | * wiki-defined types. |
58 | * |
59 | * This constructor should only be called by ZObjectFactory (and test code), and not directly. |
60 | * Validation of inputs to this and all other ZObject constructors is left to ZObjectFactory. |
61 | * |
62 | * @param ZObject $type ZReference or ZFunctionCall that resolves to the type of this ZObject |
63 | */ |
64 | public function __construct( $type ) { |
65 | // Fetch the extra arguments passed and affix them to the $data representation. |
66 | $args = func_get_args(); |
67 | if ( count( $args ) === 1 ) { |
68 | $this->data[ ZTypeRegistry::Z_OBJECT_TYPE ] = $type; |
69 | } else { |
70 | $this->data = [ ZTypeRegistry::Z_OBJECT_TYPE => $type ] + $args[ 1 ]; |
71 | } |
72 | } |
73 | |
74 | /** |
75 | * Validate this ZObject against our schema, to prevent creation and saving of invalid items. |
76 | * |
77 | * @return bool Whether content is valid |
78 | */ |
79 | public function isValid(): bool { |
80 | // A generic ZObject just needs a type key (Z1K1) to be present and valid. |
81 | if ( !isset( $this->data[ ZTypeRegistry::Z_OBJECT_TYPE ] ) ) { |
82 | return false; |
83 | } |
84 | |
85 | // Validate if type is a Reference |
86 | if ( self::isTypeReference() ) { |
87 | return ZObjectUtils::isValidZObjectReference( $this->data[ ZTypeRegistry::Z_OBJECT_TYPE ]->getZValue() ); |
88 | } |
89 | |
90 | // Validate if type is a Function Call |
91 | if ( self::isTypeFunctionCall() ) { |
92 | $functionCallInner = $this->data[ ZTypeRegistry::Z_OBJECT_TYPE ]; |
93 | '@phan-var \MediaWiki\Extension\WikiLambda\ZObjects\ZFunctionCall $functionCallInner'; |
94 | if ( $functionCallInner->getReturnType() !== ZTypeRegistry::Z_TYPE ) { |
95 | return false; |
96 | } |
97 | return ZObjectUtils::isValidZObjectReference( $functionCallInner->getZValue() ); |
98 | } |
99 | |
100 | // If type is neither reference or function call, not valid |
101 | return false; |
102 | } |
103 | |
104 | /** |
105 | * Fetch value of given key from the current ZObject. |
106 | * |
107 | * @param string $key The key to search for. |
108 | * @return ZObject|null The value of the supplied key as a ZObject, null if key is undefined. |
109 | */ |
110 | public function getValueByKey( string $key ) { |
111 | return $this->data[ $key ] ?? null; |
112 | } |
113 | |
114 | /** |
115 | * Set a value of given key in the current ZObject. |
116 | * |
117 | * @param string $key The key to set. |
118 | * @param ZObject $value The value to set. |
119 | */ |
120 | public function setValueByKey( string $key, ZObject $value ) { |
121 | $this->data[ $key ] = $value; |
122 | } |
123 | |
124 | /** |
125 | * Returns whether this ZObject is a builtin class. |
126 | * |
127 | * @return bool Whether this object is a built-in type or generic, or it's a direct instance of ZObject |
128 | */ |
129 | public function isBuiltin() { |
130 | return ( get_class( $this ) !== self::class ); |
131 | } |
132 | |
133 | /** |
134 | * Returns whether the object type is a ZReference that points to a type |
135 | * |
136 | * @return bool Whether the object type is a reference to a type |
137 | */ |
138 | public function isTypeReference(): bool { |
139 | $type = $this->isBuiltin() |
140 | ? $this->getDefinition()['type']['type'] |
141 | : $this->data[ ZTypeRegistry::Z_OBJECT_TYPE ]->getZType(); |
142 | return ( $type === ZTypeRegistry::Z_REFERENCE ); |
143 | } |
144 | |
145 | /** |
146 | * Returns whether the object type is a ZFunctionCall that resolves to a type |
147 | * |
148 | * @return bool Whether the object type is a function call to a type |
149 | */ |
150 | public function isTypeFunctionCall(): bool { |
151 | $type = $this->isBuiltin() |
152 | ? $this->getDefinition()['type']['type'] |
153 | : $this->data[ ZTypeRegistry::Z_OBJECT_TYPE ]->getZType(); |
154 | return ( $type === ZTypeRegistry::Z_FUNCTIONCALL ); |
155 | } |
156 | |
157 | /** |
158 | * Returns either the ZReference or the ZFunctionCall that contain the type of this ZObject (Z1K1) |
159 | * |
160 | * @return ZReference|ZFunctionCall The ZObject representing the type of this ZObject |
161 | */ |
162 | public function getZTypeObject() { |
163 | if ( $this->isBuiltin() ) { |
164 | return $this->type ?? new ZReference( $this->getDefinition()['type']['value'] ); |
165 | } |
166 | return $this->data[ ZTypeRegistry::Z_OBJECT_TYPE ]; |
167 | } |
168 | |
169 | /** |
170 | * Returns a string with the Zid representing the type of this ZObject. If it has an anonymous type |
171 | * given by a ZFunctionCall, this method returns the Function Zid |
172 | * |
173 | * TODO (T301553): Return the output type of the Function instead of its identifier |
174 | * |
175 | * @return string The type of this ZObject |
176 | */ |
177 | public function getZType(): string { |
178 | if ( $this->isBuiltin() ) { |
179 | return $this->getDefinition()['type']['value']; |
180 | } |
181 | return $this->getZTypeObject()->getZValue(); |
182 | } |
183 | |
184 | /** |
185 | * Return the untyped content of this ZObject. |
186 | * |
187 | * @return mixed The basic content of this ZObject; most ZObject types will implement specific |
188 | * accessors specific to that type. |
189 | */ |
190 | public function getZValue() { |
191 | return $this->data; |
192 | } |
193 | |
194 | /** |
195 | * Return all ZObject Zids that are linked to the current ZObject. |
196 | * |
197 | * @return string[] An array of other ZObjects to which this ZObject links |
198 | * for injection into the MediaWiki system as if they were wiki links. |
199 | */ |
200 | public function getLinkedZObjects(): array { |
201 | foreach ( array_values( $this->data ) as $value ) { |
202 | self::extractLinkedZObjects( $value, $this ); |
203 | } |
204 | return array_keys( $this->linkedZObjects ); |
205 | } |
206 | |
207 | /** |
208 | * Register in the linkedZObjects array a reference to a Zid to which |
209 | * this ZObject is linked. |
210 | * |
211 | * @param string $zReference for the linked ZObject |
212 | */ |
213 | private function addLinkedZObject( string $zReference ) { |
214 | $this->linkedZObjects[ $zReference ] = 1; |
215 | } |
216 | |
217 | /** |
218 | * Iterate through ZObject values to find reference links and register them |
219 | * locally. |
220 | * |
221 | * @param ZObject $value value to check for reference links |
222 | * @param ZObject $zobject original ZObject to add links |
223 | */ |
224 | private static function extractLinkedZObjects( $value, $zobject ) { |
225 | if ( is_array( $value ) ) { |
226 | foreach ( array_values( $value ) as $arrayItem ) { |
227 | self::extractLinkedZObjects( $arrayItem, $zobject ); |
228 | } |
229 | } elseif ( is_object( $value ) ) { |
230 | if ( $value instanceof ZReference ) { |
231 | $zobject->addLinkedZObject( $value->getZValue() ); |
232 | } else { |
233 | $objectVars = get_object_vars( $value ); |
234 | foreach ( array_values( $objectVars ) as $objectItem ) { |
235 | self::extractLinkedZObjects( $objectItem, $zobject ); |
236 | } |
237 | } |
238 | } elseif ( is_string( $value ) ) { |
239 | // TODO (T296925): Revisit this (probably not needed) when |
240 | // ZReferences are preserved/created correctly |
241 | if ( ZObjectUtils::isValidZObjectReference( $value ) ) { |
242 | $zobject->addLinkedZObject( $value ); |
243 | } |
244 | } |
245 | } |
246 | |
247 | /** |
248 | * Convert this ZObject into its serialized canonical representation |
249 | * |
250 | * @param int $form |
251 | * @return \stdClass|array|string |
252 | */ |
253 | public function getSerialized( $form = self::FORM_CANONICAL ) { |
254 | $serialized = [ |
255 | ZTypeRegistry::Z_OBJECT_TYPE => $this->getZTypeObject()->getSerialized( $form ) |
256 | ]; |
257 | |
258 | foreach ( $this->data as $key => $value ) { |
259 | if ( $key == ZTypeRegistry::Z_OBJECT_TYPE ) { |
260 | continue; |
261 | } |
262 | |
263 | if ( is_string( $value ) ) { |
264 | $serialized[ $key ] = $value; |
265 | continue; |
266 | } |
267 | |
268 | if ( is_array( $value ) ) { |
269 | $serialized[ $key ] = array_map( static function ( $element ) use ( $form ) { |
270 | return ( $element instanceof ZObject ) ? $element->getSerialized( $form ) : $element; |
271 | }, $value ); |
272 | continue; |
273 | } |
274 | |
275 | if ( $value instanceof ZObject ) { |
276 | $serialized[ $key ] = $value->getSerialized( $form ); |
277 | } |
278 | } |
279 | return (object)$serialized; |
280 | } |
281 | |
282 | /** |
283 | * Convert this ZObject into human readable object by translating all keys and |
284 | * references into the preferred language or its fallbacks |
285 | * |
286 | * @param Language|null $language |
287 | * @return \stdClass|array|string |
288 | */ |
289 | public function getHumanReadable( $language = null ) { |
290 | $serialized = $this->getSerialized(); |
291 | |
292 | // Walk the ZObject tree to get all ZIDs that need to be fetched from the database |
293 | // TODO (T296741): currently fetchBatchZObjects doesn't fetch them in batch, must fix or reconsider |
294 | $zids = ZObjectUtils::getRequiredZids( $serialized ); |
295 | $zObjectStore = WikiLambdaServices::getZObjectStore(); |
296 | $contents = $zObjectStore->fetchBatchZObjects( $zids ); |
297 | |
298 | if ( $language === null ) { |
299 | $language = RequestContext::getMain()->getLanguage(); |
300 | } |
301 | |
302 | return ZObjectUtils::extractHumanReadableZObject( $serialized, $contents, $language ); |
303 | } |
304 | |
305 | /** |
306 | * Over-ride the default __toString() method to serialise ZObjects into a JSON representation. |
307 | * |
308 | * @return string |
309 | */ |
310 | public function __toString() { |
311 | return FormatJson::encode( $this->getSerialized( self::FORM_CANONICAL ), true, FormatJson::UTF8_OK ); |
312 | } |
313 | } |