Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.71% covered (warning)
84.71%
72 / 85
76.47% covered (warning)
76.47%
13 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZObject
84.71% covered (warning)
84.71%
72 / 85
76.47% covered (warning)
76.47%
13 / 17
47.01
0.00% covered (danger)
0.00%
0 / 1
 getDefinition
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 __construct
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 isValid
30.00% covered (danger)
30.00%
3 / 10
0.00% covered (danger)
0.00%
0 / 1
13.57
 getValueByKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setValueByKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isBuiltin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isTypeReference
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isTypeFunctionCall
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getZTypeObject
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getZType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getZValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLinkedZObjects
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addLinkedZObject
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 extractLinkedZObjects
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
8.04
 getSerialized
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
7.64
 getHumanReadable
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
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
11namespace MediaWiki\Extension\WikiLambda\ZObjects;
12
13use FormatJson;
14use Language;
15use MediaWiki\Context\RequestContext;
16use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
17use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
18use MediaWiki\Extension\WikiLambda\ZObjectUtils;
19
20class 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}