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